// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "ClientGenerator.h"
#include "GeneratorUtil.h"
#include "graphqlservice/internal/Version.h"
#include "graphqlservice/introspection/IntrospectionSchema.h"
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 26495)
#pragma warning(disable : 26812)
#endif // _MSC_VER
#include
#ifdef _MSC_VER
#pragma warning(pop)
#endif // _MSC_VER
// clang-format off
#ifdef USE_STD_FILESYSTEM
#include
namespace fs = std::filesystem;
#else
#ifdef USE_STD_EXPERIMENTAL_FILESYSTEM
#include
namespace fs = std::experimental::filesystem;
#else
#ifdef USE_BOOST_FILESYSTEM
#include
namespace fs = boost::filesystem;
#else
#error "No std::filesystem implementation defined"
#endif
#endif
#endif
// clang-format on
#include
#include
#include
#include
#include
#include
using namespace std::literals;
namespace graphql::generator::client {
Generator::Generator(
SchemaOptions&& schemaOptions, RequestOptions&& requestOptions, GeneratorOptions&& options)
: _schemaLoader(std::make_optional(std::move(schemaOptions)))
, _requestLoader(std::move(requestOptions), _schemaLoader)
, _options(std::move(options))
, _headerDir(getHeaderDir())
, _sourceDir(getSourceDir())
, _headerPath(getHeaderPath())
, _sourcePath(getSourcePath())
{
}
std::string Generator::getHeaderDir() const noexcept
{
if (_options.paths)
{
return fs::path { _options.paths->headerPath }.string();
}
else
{
return {};
}
}
std::string Generator::getSourceDir() const noexcept
{
if (_options.paths)
{
return fs::path(_options.paths->sourcePath).string();
}
else
{
return {};
}
}
std::string Generator::getHeaderPath() const noexcept
{
fs::path fullPath { _headerDir };
fullPath /= (std::string { _schemaLoader.getFilenamePrefix() } + "Client.h");
return fullPath.string();
}
std::string Generator::getSourcePath() const noexcept
{
fs::path fullPath { _sourceDir };
fullPath /= (std::string { _schemaLoader.getFilenamePrefix() } + "Client.cpp");
return fullPath.string();
}
const std::string& Generator::getClientNamespace() const noexcept
{
static const auto s_namespace = R"cpp(graphql::client)cpp"s;
return s_namespace;
}
const std::string& Generator::getRequestNamespace() const noexcept
{
static const auto s_namespace = [this]() noexcept {
std::ostringstream oss;
oss << _requestLoader.getOperationType() << R"cpp(::)cpp"
<< _requestLoader.getOperationNamespace();
return oss.str();
}();
return s_namespace;
}
const std::string& Generator::getFullNamespace() const noexcept
{
static const auto s_namespace = [this]() noexcept {
std::ostringstream oss;
oss << getClientNamespace() << R"cpp(::)cpp" << getRequestNamespace();
return oss.str();
}();
return s_namespace;
}
std::string Generator::getResponseFieldCppType(
const ResponseField& responseField, std::string_view currentScope /* = {} */) const noexcept
{
std::string result;
switch (responseField.type->kind())
{
case introspection::TypeKind::OBJECT:
case introspection::TypeKind::INTERFACE:
case introspection::TypeKind::UNION:
{
std::ostringstream oss;
if (!currentScope.empty())
{
oss << currentScope << R"cpp(::)cpp";
}
oss << responseField.cppName << R"cpp(_)cpp" << responseField.type->name();
result = SchemaLoader::getSafeCppName(oss.str());
break;
}
default:
result = _schemaLoader.getCppType(responseField.type->name());
}
return result;
}
std::vector<:string> Generator::Build() const noexcept
{
std::vector<:string> builtFiles;
if (outputHeader() && _options.verbose)
{
builtFiles.push_back(_headerPath);
}
if (outputSource())
{
builtFiles.push_back(_sourcePath);
}
return builtFiles;
}
bool Generator::outputHeader() const noexcept
{
std::ofstream headerFile(_headerPath, std::ios_base::trunc);
IncludeGuardScope includeGuard { headerFile, fs::path(_headerPath).filename().string() };
headerFile << R"cpp(#include "graphqlservice/GraphQLClient.h"
#include "graphqlservice/GraphQLParse.h"
#include "graphqlservice/GraphQLResponse.h"
#include "graphqlservice/internal/Version.h"
// Check if the library version is compatible with clientgen )cpp"
<< graphql::internal::MajorVersion << R"cpp(.)cpp" << graphql::internal::MinorVersion
<< R"cpp(.0
static_assert(graphql::internal::MajorVersion == )cpp"
<< graphql::internal::MajorVersion
<< R"cpp(, "regenerate with clientgen: major version mismatch");
static_assert(graphql::internal::MinorVersion == )cpp"
<< graphql::internal::MinorVersion
<< R"cpp(, "regenerate with clientgen: minor version mismatch");
#include
#include
#include
)cpp";
outputRequestComment(headerFile);
NamespaceScope fullNamespaceScope { headerFile, getFullNamespace() };
PendingBlankLine pendingSeparator { headerFile };
outputGetRequestDeclaration(headerFile);
// Define all of the enums referenced either in variables or the response.
for (const auto& enumType : _requestLoader.getReferencedEnums())
{
pendingSeparator.reset();
headerFile << R"cpp(enum class )cpp" << _schemaLoader.getCppType(enumType->name()) << R"cpp(
{
)cpp";
for (const auto& enumValue : enumType->enumValues())
{
headerFile << R"cpp( )cpp" << SchemaLoader::getSafeCppName(enumValue->name())
<< R"cpp(,
)cpp";
}
headerFile << R"cpp(};
)cpp";
pendingSeparator.add();
}
pendingSeparator.reset();
const auto& variables = _requestLoader.getVariables();
if (!variables.empty())
{
headerFile << R"cpp(struct Variables
{
)cpp";
// Define all of the input object structs referenced in variables.
for (const auto& inputType : _requestLoader.getReferencedInputTypes())
{
pendingSeparator.reset();
headerFile << R"cpp( struct )cpp" << _schemaLoader.getCppType(inputType->name())
<< R"cpp(
{
)cpp";
for (const auto& inputField : inputType->inputFields())
{
headerFile << R"cpp( )cpp"
<< _requestLoader.getInputCppType(inputField->type().lock())
<< R"cpp( )cpp" << SchemaLoader::getSafeCppName(inputField->name())
<< R"cpp( {};
)cpp";
}
headerFile << R"cpp( };
)cpp";
pendingSeparator.add();
}
pendingSeparator.reset();
for (const auto& variable : variables)
{
headerFile << R"cpp( )cpp" << _schemaLoader.getCppType(variable.type->name())
<< R"cpp( )cpp" << variable.cppName << R"cpp( {};
)cpp";
}
headerFile << R"cpp(};
response::Value serializeVariables(Variables&& variables);
)cpp";
pendingSeparator.add();
}
pendingSeparator.reset();
const auto& responseType = _requestLoader.getResponseType();
headerFile << R"cpp(struct Response
{)cpp";
pendingSeparator.add();
for (const auto& responseField : responseType.fields)
{
pendingSeparator.reset();
if (outputResponseFieldType(headerFile, responseField, 1))
{
pendingSeparator.add();
}
}
for (const auto& responseField : responseType.fields)
{
pendingSeparator.reset();
headerFile << R"cpp( )cpp"
<< RequestLoader::getOutputCppType(getResponseFieldCppType(responseField),
responseField.modifiers)
<< R"cpp( )cpp" << responseField.cppName << R"cpp( {};
)cpp";
}
headerFile << R"cpp(};
Response parseResponse(response::Value&& response);
)cpp";
pendingSeparator.add();
pendingSeparator.reset();
return true;
}
void Generator::outputRequestComment(std::ostream& headerFile) const noexcept
{
headerFile << R"cpp(
///
/// Operation: )cpp"
<< _requestLoader.getOperationType() << ' '
<< _requestLoader.getOperationDisplayName() << R"cpp(
///
///
)cpp";
std::istringstream request { std::string { _requestLoader.getRequestText() } };
for (std::string line; std::getline(request, line);)
{
headerFile << R"cpp(/// )cpp" << line << std::endl;
}
headerFile << R"cpp(///
)cpp";
}
void Generator::outputGetRequestDeclaration(std::ostream& headerFile) const noexcept
{
headerFile << R"cpp(
// Return the original text of the request document.
const std::string& GetRequestText() noexcept;
// Return a pre-parsed, pre-validated request object.
const peg::ast& GetRequestObject() noexcept;
)cpp";
}
bool Generator::outputResponseFieldType(std::ostream& headerFile,
const ResponseField& responseField, size_t indent /* = 0 */) const noexcept
{
switch (responseField.type->kind())
{
case introspection::TypeKind::OBJECT:
case introspection::TypeKind::INTERFACE:
case introspection::TypeKind::UNION:
// This is a complex type that requires a custom struct declaration.
break;
default:
// This is a scalar type, it doesn't require a type declaration.
return false;
}
const std::string indentTabs(indent, '\t');
std::unordered_set<:string_view> fieldNames;
PendingBlankLine pendingSeparator { headerFile };
headerFile << indentTabs << R"cpp(struct )cpp" << getResponseFieldCppType(responseField)
<< R"cpp(
)cpp" << indentTabs
<< R"cpp({)cpp";
switch (responseField.type->kind())
{
case introspection::TypeKind::OBJECT:
case introspection::TypeKind::INTERFACE:
case introspection::TypeKind::UNION:
{
for (const auto& field : responseField.children)
{
pendingSeparator.reset();
if (fieldNames.emplace(field.name).second
&& outputResponseFieldType(headerFile, field, indent + 1))
{
pendingSeparator.add();
}
}
fieldNames.clear();
for (const auto& field : responseField.children)
{
pendingSeparator.reset();
if (fieldNames.emplace(field.name).second)
{
headerFile << indentTabs << R"cpp( )cpp"
<< RequestLoader::getOutputCppType(getResponseFieldCppType(field),
field.modifiers)
<< R"cpp( )cpp" << field.cppName << R"cpp( {};
)cpp";
}
}
break;
}
default:
break;
}
headerFile << indentTabs << R"cpp(};
)cpp";
return true;
}
bool Generator::outputSource() const noexcept
{
std::ofstream sourceFile(_sourcePath, std::ios_base::trunc);
sourceFile << R"cpp(// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// WARNING! Do not edit this file manually, your changes will be overwritten.
#include ")cpp" << _schemaLoader.getFilenamePrefix()
<< R"cpp(Client.h"
#include
#include
#include
#include
#include
using namespace std::literals;
)cpp";
NamespaceScope clientNamespaceScope { sourceFile, getClientNamespace() };
PendingBlankLine pendingSeparator { sourceFile };
sourceFile << R"cpp(
using namespace )cpp"
<< getRequestNamespace() << R"cpp(;
)cpp";
const auto& variables = _requestLoader.getVariables();
for (const auto& enumType : _requestLoader.getReferencedEnums())
{
pendingSeparator.reset();
const auto& enumValues = enumType->enumValues();
const auto cppType = _schemaLoader.getCppType(enumType->name());
bool firstValue = true;
sourceFile << R"cpp(static const std::array<:string_view enumvalues.size r> s_names)cpp" << cppType << R"cpp( = {
)cpp";
for (const auto& enumValue : enumValues)
{
firstValue = false;
sourceFile << R"cpp( ")cpp" << SchemaLoader::getSafeCppName(enumValue->name())
<< R"cpp("sv,
)cpp";
}
sourceFile << R"cpp(};
)cpp";
pendingSeparator.add();
}
if (!variables.empty())
{
for (const auto& enumType : _requestLoader.getReferencedEnums())
{
pendingSeparator.reset();
const auto& enumValues = enumType->enumValues();
const auto cppType = _schemaLoader.getCppType(enumType->name());
if (!variables.empty())
{
sourceFile << R"cpp(template <>
response::Value ModifiedVariable<)cpp"
<< cppType << R"cpp(>::serialize()cpp" << cppType << R"cpp(&& value)
{
response::Value result { response::Type::EnumValue };
result.set<:stringtype>(response::StringType { s_names)cpp"
<< cppType << R"cpp([static_cast(value)] });
return result;
}
)cpp";
}
pendingSeparator.add();
}
for (const auto& inputType : _requestLoader.getReferencedInputTypes())
{
pendingSeparator.reset();
const auto cppType = _schemaLoader.getCppType(inputType->name());
sourceFile << R"cpp(template <>
response::Value ModifiedVariable<: cpptype r>::serialize(Variables::)cpp" << cppType
<< R"cpp(&& inputValue)
{
response::Value result { response::Type::Map };
)cpp";
for (const auto& inputField : inputType->inputFields())
{
const auto [type, modifiers] =
RequestLoader::unwrapSchemaType(inputField->type().lock());
sourceFile << R"cpp( result.emplace_back(R"js()cpp" << inputField->name()
<< R"cpp()js"s, ModifiedVariable<)cpp"
<< _schemaLoader.getCppType(type->name()) << R"cpp(>::serialize)cpp"
<< getTypeModifierList(modifiers) << R"cpp((std::move(inputValue.)cpp"
<< SchemaLoader::getSafeCppName(inputField->name()) << R"cpp()));
)cpp";
}
sourceFile << R"cpp(
return result;
}
)cpp";
pendingSeparator.add();
}
}
for (const auto& enumType : _requestLoader.getReferencedEnums())
{
pendingSeparator.reset();
const auto cppType = _schemaLoader.getCppType(enumType->name());
sourceFile << R"cpp(template <>
)cpp" << cppType << R"cpp( ModifiedResponse<)cpp"
<< cppType << R"cpp(>::parse(response::Value&& value)
{
if (!value.maybe_enum())
{
throw std::logic_error { "not a valid )cpp"
<< cppType << R"cpp( value" };
}
const auto itr = std::find(s_names)cpp"
<< cppType << R"cpp(.cbegin(), s_names)cpp" << cppType
<< R"cpp(.cend(), value.release<:stringtype>());
if (itr == s_names)cpp"
<< cppType << R"cpp(.cend())
{
throw std::logic_error { "not a valid )cpp"
<< cppType << R"cpp( value" };
}
return static_cast<)cpp"
<< cppType << R"cpp(>(itr - s_names)cpp" << cppType << R"cpp(.cbegin());
}
)cpp";
pendingSeparator.add();
}
const auto& responseType = _requestLoader.getResponseType();
const auto currentScope = R"cpp(Response)cpp"s;
for (const auto& responseField : responseType.fields)
{
if (outputModifiedResponseImplementation(sourceFile, currentScope, responseField))
{
pendingSeparator.add();
}
}
pendingSeparator.reset();
NamespaceScope requestNamespaceScope { sourceFile, getRequestNamespace() };
pendingSeparator.add();
outputGetRequestImplementation(sourceFile);
if (!variables.empty())
{
sourceFile << R"cpp(
response::Value serializeVariables(Variables&& variables)
{
response::Value result { response::Type::Map };
)cpp";
for (const auto& variable : variables)
{
sourceFile << R"cpp( result.emplace_back(R"js()cpp" << variable.name
<< R"cpp()js"s, ModifiedVariable<: _schemaloader.getcpptype>name()) << R"cpp(>::serialize)cpp"
<< getTypeModifierList(variable.modifiers)
<< R"cpp((std::move(variables.)cpp" << variable.cppName << R"cpp()));
)cpp";
}
sourceFile << R"cpp(
return result;
}
)cpp";
}
sourceFile << R"cpp(
Response parseResponse(response::Value&& response)
{
Response result;
if (response.type() == response::Type::Map)
{
auto members = response.release<:maptype>();
for (auto& member : members)
{
)cpp";
std::unordered_set<:string_view> fieldNames;
for (const auto& responseField : responseType.fields)
{
if (fieldNames.emplace(responseField.name).second)
{
sourceFile << R"cpp( if (member.first == R"js()cpp" << responseField.name
<< R"cpp()js"sv)
{
result.)cpp"
<< responseField.cppName << R"cpp( = ModifiedResponse<)cpp"
<< getResponseFieldCppType(responseField, currentScope)
<< R"cpp(>::parse)cpp" << getTypeModifierList(responseField.modifiers)
<< R"cpp((std::move(member.second));
continue;
}
)cpp";
}
}
sourceFile << R"cpp( }
}
return result;
}
)cpp";
pendingSeparator.reset();
return true;
}
void Generator::outputGetRequestImplementation(std::ostream& sourceFile) const noexcept
{
sourceFile << R"cpp(
const std::string& GetRequestText() noexcept
{
static const auto s_request = R"gql(
)cpp";
std::istringstream request { std::string { _requestLoader.getRequestText() } };
for (std::string line; std::getline(request, line);)
{
sourceFile << R"cpp( )cpp" << line << std::endl;
}
sourceFile << R"cpp( )gql"s;
return s_request;
}
const peg::ast& GetRequestObject() noexcept
{
static const auto s_request = []() noexcept {
auto ast = peg::parseString(GetRequestText());
// This has already been validated against the schema by clientgen.
ast.validated = true;
return ast;
}();
return s_request;
}
)cpp";
}
bool Generator::outputModifiedResponseImplementation(std::ostream& sourceFile,
const std::string& outerScope, const ResponseField& responseField) const noexcept
{
std::ostringstream oss;
oss << outerScope << R"cpp(::)cpp" << getResponseFieldCppType(responseField);
const auto cppType = oss.str();
std::unordered_set<:string_view> fieldNames;
switch (responseField.type->kind())
{
case introspection::TypeKind::OBJECT:
case introspection::TypeKind::INTERFACE:
case introspection::TypeKind::UNION:
{
for (const auto& field : responseField.children)
{
if (fieldNames.emplace(field.name).second)
{
outputModifiedResponseImplementation(sourceFile, cppType, field);
}
}
break;
}
default:
// This is a scalar type, it doesn't require a type declaration.
return false;
}
fieldNames.clear();
// This is a complex type that requires a custom ModifiedResponse implementation.
sourceFile << R"cpp(
template <>
)cpp" << cppType
<< R"cpp( ModifiedResponse<)cpp" << cppType
<< R"cpp(>::parse(response::Value&& response)
{
)cpp" << cppType
<< R"cpp( result;
if (response.type() == response::Type::Map)
{
auto members = response.release<:maptype>();
for (auto& member : members)
{
)cpp";
switch (responseField.type->kind())
{
case introspection::TypeKind::OBJECT:
case introspection::TypeKind::INTERFACE:
case introspection::TypeKind::UNION:
{
for (const auto& field : responseField.children)
{
if (fieldNames.emplace(field.name).second)
{
sourceFile << R"cpp( if (member.first == R"js()cpp" << field.name
<< R"cpp()js"sv)
{
result.)cpp" << field.cppName
<< R"cpp( = ModifiedResponse<)cpp"
<< getResponseFieldCppType(field, cppType) << R"cpp(>::parse)cpp"
<< getTypeModifierList(field.modifiers)
<< R"cpp((std::move(member.second));
continue;
}
)cpp";
}
}
break;
}
default:
break;
}
sourceFile << R"cpp( }
}
return result;
}
)cpp";
return true;
}
std::string Generator::getTypeModifierList(const TypeModifierStack& modifiers) noexcept
{
if (modifiers.empty())
{
return {};
}
bool firstModifier = true;
std::ostringstream oss;
oss << '<';
for (auto modifier : modifiers)
{
if (!firstModifier)
{
oss << R"cpp(, )cpp";
}
firstModifier = false;
switch (modifier)
{
case service::TypeModifier::None:
oss << R"cpp(TypeModifier::None)cpp";
break;
case service::TypeModifier::Nullable:
oss << R"cpp(TypeModifier::Nullable)cpp";
break;
case service::TypeModifier::List:
oss << R"cpp(TypeModifier::List)cpp";
break;
}
}
oss << '>';
return oss.str();
}
} // namespace graphql::generator::client
namespace po = boost::program_options;
void outputVersion(std::ostream& ostm)
{
ostm << graphql::internal::FullVersion << std::endl;
}
void outputUsage(std::ostream& ostm, const po::options_description& options)
{
ostm << "Usage:\tclientgen [options]