Skip to content

Generate clients to go with cppgraphqlgen services #165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 66 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
2f3cf47
Clone SchemaGenerator.* into ClientGenerator.* and bump the version
wravery May 18, 2021
a98690b
Refactor common logic into GeneratorUtil.*
wravery May 18, 2021
529abc4
Validate requests against the schema
wravery May 19, 2021
3af63f6
Cleanup leftover initializer_list reference
wravery May 19, 2021
5e45c3e
Export functions from DLLs for clientgen
wravery May 19, 2021
fb54030
Move converter export declarations to graphql::service
wravery May 19, 2021
84d1d7d
Fix the GRAPHQLINTROSPECTION_EXPORT macro
wravery May 19, 2021
c8b4e37
Refactor common schema code into SchemaLoader
wravery May 19, 2021
c73b05e
Minor fixes to clientgen
wravery May 19, 2021
eb5906a
Fix NON_NULL wrappers in clientgen runtime schema
wravery May 19, 2021
870edf3
Add sample request documents to test clientgen
wravery May 19, 2021
bd4c980
Fix update_samples dependencies
wravery May 20, 2021
3eb7768
Refactor GraphQLService.cpp visitors into RequestLoader.*
wravery May 20, 2021
b44df99
Generate placeholder client samples with CMake
wravery May 20, 2021
589b104
Fix update_version dependencies so source dirs are not outputs
wravery May 20, 2021
dd9dad5
Put resolver visitors back in GraphQLService.cpp
wravery May 20, 2021
d6b6e97
Add an operation name argument to clientgen
wravery May 20, 2021
f0dde15
Move visitors back to GraphQLService because of variables
wravery May 20, 2021
a7c90f6
Move request parsing and validation to RequestLoader
wravery May 20, 2021
8931442
Simplify RequestLoader initialization
wravery May 21, 2021
1129477
Output the original request in a header comment block
wravery May 21, 2021
4fa3e7b
Get the operation type and name from the request doc
wravery May 21, 2021
efce1b0
Finish RequestLoader
wravery May 22, 2021
8bd60de
Start replacing content from schemagen in clientgen
wravery May 22, 2021
b8f5208
Add namespaces to clientgen output
wravery May 22, 2021
c42e09c
Start building client samples
wravery May 22, 2021
98c6f1b
Keep track of field positions in RequestLoader
wravery May 23, 2021
79db4d1
Build header path from prefix instead of full path from command line
wravery May 23, 2021
09ce9b8
Cache stable heap allocations in getSafeCppName
wravery May 23, 2021
e3033c4
Store key with value allocation to index on std::string_view
wravery May 23, 2021
21ec432
Store the unwrapped types with a TypeModifierStack
wravery May 23, 2021
9b68d65
Use the SchemaLoader instance to get cppType names in the schema
wravery May 23, 2021
2b8505b
Start emitting some of the Variables struct
wravery May 23, 2021
e220447
Collect referenced enum and input types
wravery May 24, 2021
7355979
Fill in RequestLoader versions of getInputCppType and getOutputCppType
wravery May 24, 2021
323e5ac
Merge branch 'main' of https://github.com/wravery/cppgraphqlgen into …
wravery May 24, 2021
9069f08
Port reorderInputTypeDependencies to RequestLoader
wravery May 24, 2021
b27d5d3
Unwrap referenced enum and input types
wravery May 24, 2021
09c967a
Output the Response structs
wravery May 25, 2021
eaa0619
Add an enum type to the Query to test clientgen
wravery May 25, 2021
68d0812
Add enum serialize/parse and start on input serialize
wravery May 25, 2021
4114b38
Move Base64 conversions from GraphQLService to GraphQLResponse
wravery May 26, 2021
44b0629
Replace missing service::schema_exception with std::logic_error
wravery May 26, 2021
d5fdf0a
Switch the closing namespace comments to C++ style
wravery May 26, 2021
310e845
Use doc comment syntax for request comment blocks
wravery May 26, 2021
04d8225
Add ModifiedVariable and ModifiedResponse in GraphQLClient
wravery May 26, 2021
f4c99d2
Implement serializeVariables
wravery May 26, 2021
f8e5431
Fix #161 implement parseResponse
wravery May 26, 2021
c682643
Fix build break
wravery May 26, 2021
d8c0d61
Fix DLL build on Windows
wravery May 26, 2021
60e6481
Add template <> to specializations
wravery May 26, 2021
6347a7e
Add tests and fix found bugs
wravery May 27, 2021
d432a8f
Copy graphqlclient.dll to the test directory
wravery May 27, 2021
faaa2cd
Update README describing clientgen and graphqlclient
wravery May 27, 2021
a38f304
Test and fix union types
wravery May 27, 2021
c092c11
Default initialize Variables members
wravery May 27, 2021
634e01f
Make testTaskStatus non-nullable so test default enum init
wravery May 27, 2021
dfecf03
Declare embedded structs first for better layout
wravery May 27, 2021
66cbe33
Remove extra blank line after includes
wravery May 27, 2021
774c28f
Remove duplicate copyright and generated file warning comments
wravery May 27, 2021
d92d8f6
Minor edits to the README
wravery May 27, 2021
78513c6
Add a client_benchmark utility to compare with dynamic parse/serialize
wravery May 27, 2021
bd7bcbf
Split PegtlTests.cpp into 3 grammar variants to speeed up build
wravery May 27, 2021
a224fea
Add new files to install targets
wravery May 27, 2021
063dd54
Move more headers into graphqlservice/internal
wravery May 27, 2021
cef14e1
Simplify splitting service response into data and errors
wravery May 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ else()
set(GRAPHQL_UPDATE_SAMPLES OFF CACHE BOOL "Disable regenerating samples." FORCE)
endif()

option(GRAPHQL_BUILD_CLIENTGEN "Build the clientgen tool." ON)

option(GRAPHQL_UPDATE_VERSION "Regenerate graphqlservice/internal/Version.h and all of the version info rc files for Windows." ON)

add_subdirectory(cmake)
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ service, you can use the same GraphQL client code to access your native data sou
service online. You might even be able to share some more of that code between a progressive web
app and your native/hybrid app.

If what you're after is a way to consume a GraphQL service from C++, as of
[v3.6.0](https://github.com/microsoft/cppgraphqlgen/releases/tag/v3.6.0) this project also includes
a `graphqlclient` library and a `clientgen` utility to generate types matching a GraphQL request
document, its variables, and all of the serialization code you need to talk to a `graphqlservice`
implementation. If you want to consume another service, you will need access to the schema definition
(rather than the Introspection query results), and you will need be able to send requests along with
any variables to the service and parse its responses into a `graphql::response::Value` (e.g. with the
`graphqljson` library) in your code.

# Getting Started

## Related projects
Expand Down Expand Up @@ -116,6 +125,41 @@ adds a runtime dependency on a Boost DLL. The `schemagen` tool won't run without
the install of Boost, vcpkg also takes care of installing the dependencies next to `schemagen.exe` when building the
Windows and UWP shared library targets (the platform triplets which don't end in `-static`).

### clientgen

The `clientgen` utility is based on `schemagen` and shares the same external dependencies. The command line arguments
are almost the same, except it takes an extra file for the request document and there is no equivalent to `--no-stubs` or
`--separate-files`:
```
Usage: clientgen [options] <schema file> <request file> <output filename prefix> <output namespace>
Command line options:
--version Print the version number
-? [ --help ] Print the command line options
-v [ --verbose ] Verbose output including generated header names as
well as sources
-s [ --schema ] arg Schema definition file path
-r [ --request ] arg Request document file path
-o [ --operation ] arg Operation name if the request document contains more
than one
-p [ --prefix ] arg Prefix to use for the generated C++ filenames
-n [ --namespace ] arg C++ sub-namespace for the generated types
--source-dir arg Target path for the <prefix>Client.cpp source file
--header-dir arg Target path for the <prefix>Client.h header file
--no-introspection Do not expect support for Introspection
```

This utility should output one header and one source file for each request document. A request document may contain
more than one operation, in which case you must specify the `--operation` (or `-o`) parameter to indicate which one
should be used to generate the files. If you want to generate client code for more than one operation in the same
document, you will need to run `clientgen` more than once and specify another operation name each time.

The generated code depends on the `graphqlclient` library for serialization of built-in types. If you link the generated
code, you'll also need to link `graphqlclient`, `graphqlpeg` for the pre-parsed, pre-validated request AST, and
`graphqlresponse` for the `graphql::response::Value` implementation.

Sample output for `clientgen` is in the [samples/client](samples/client) directory, and each sample is consumed by
a unit test in [test/ClientTests.cpp](test/ClientTests.cpp).

### tests (`GRAPHQL_BUILD_TESTS=ON`)

- Unit testing: [Google Test](https://github.com/google/googletest) for the unit testing framework. If you don't want to
Expand Down
43 changes: 43 additions & 0 deletions cmake/ClientGen.rc.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include <winver.h>

#define CLIENTGEN_RC_VERSION @CLIENTGEN_RC_VERSION@
#define CLIENTGEN_RC_VERSION_STR "@CLIENTGEN_RC_VERSION_STR@"

#ifndef DEBUG
#define VER_DEBUG 0
#else
#define VER_DEBUG VS_FF_DEBUG
#endif

VS_VERSION_INFO VERSIONINFO
FILEVERSION CLIENTGEN_RC_VERSION
PRODUCTVERSION CLIENTGEN_RC_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS VER_DEBUG
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "Microsoft Corporation"
VALUE "FileDescription", "Code generator for https://github.com/microsoft/cppgraphqlgen"
VALUE "FileVersion", CLIENTGEN_RC_VERSION_STR
VALUE "InternalName", "clientgen"
VALUE "LegalCopyright", "Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License."
VALUE "OriginalFilename", "clientgen.exe"
VALUE "ProductName", "CppGraphQLGen"
VALUE "ProductVersion", CLIENTGEN_RC_VERSION_STR
END
END

BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
2 changes: 1 addition & 1 deletion cmake/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.0
3.6.0
2 changes: 1 addition & 1 deletion doc/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ JSONRESPONSE_EXPORT std::string toJSON(Value&& response);

JSONRESPONSE_EXPORT Value parseJSON(const std::string& json);

} /* namespace graphql::response */
} // namespace graphql::response
```

You will also need to update the [CMakeLists.txt](../src/CMakeLists.txt) file
Expand Down
4 changes: 2 additions & 2 deletions doc/parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ you have another use for a GraphQL parser you could probably make a few small
tweaks to include additional information in the rules or in the resulting AST.
You could also use the grammar without the AST module if you want to handle
the parsing callbacks another way. The grammar itself is defined in
[GraphQLGrammar.h](../include/graphqlservice/GraphQLGrammar.h), and the AST
selector callbacks are all defined in [GraphQLTree.cpp](../src/GraphQLTree.cpp).
[Grammar.h](../include/graphqlservice/internal/Grammar.h), and the AST
selector callbacks are all defined in [SyntaxTree.cpp](../src/SyntaxTree.cpp).
The grammar handles both the schema definition syntax which is used in
`schemagen`, and the query/mutation/subscription operation syntax used in
`Request::resolve` and `Request::subscribe`.
Expand Down
70 changes: 70 additions & 0 deletions include/ClientGenerator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#ifndef CLIENTGENERATOR_H
#define CLIENTGENERATOR_H

#include "RequestLoader.h"
#include "SchemaLoader.h"

namespace graphql::generator::client {

struct GeneratorPaths
{
const std::string headerPath;
const std::string sourcePath;
};

struct GeneratorOptions
{
const std::optional<GeneratorPaths> paths;
const bool verbose = false;
};

class Generator
{
public:
// Initialize the generator with the introspection client or a custom GraphQL client.
explicit Generator(
SchemaOptions&& schemaOptions, RequestOptions&& requestOptions, GeneratorOptions&& options);

// Run the generator and return a list of filenames that were output.
std::vector<std::string> Build() const noexcept;

private:
std::string getHeaderDir() const noexcept;
std::string getSourceDir() const noexcept;
std::string getHeaderPath() const noexcept;
std::string getSourcePath() const noexcept;
const std::string& getClientNamespace() const noexcept;
const std::string& getRequestNamespace() const noexcept;
const std::string& getFullNamespace() const noexcept;
std::string getResponseFieldCppType(
const ResponseField& responseField, std::string_view currentScope = {}) const noexcept;

bool outputHeader() const noexcept;
void outputRequestComment(std::ostream& headerFile) const noexcept;
void outputGetRequestDeclaration(std::ostream& headerFile) const noexcept;
bool outputResponseFieldType(std::ostream& headerFile, const ResponseField& responseField,
size_t indent = 0) const noexcept;

bool outputSource() const noexcept;
void outputGetRequestImplementation(std::ostream& sourceFile) const noexcept;
bool outputModifiedResponseImplementation(std::ostream& sourceFile,
const std::string& outerScope, const ResponseField& responseField) const noexcept;
static std::string getTypeModifierList(const TypeModifierStack& modifiers) noexcept;

const SchemaLoader _schemaLoader;
const RequestLoader _requestLoader;
const GeneratorOptions _options;
const std::string _headerDir;
const std::string _sourceDir;
const std::string _headerPath;
const std::string _sourcePath;
};

} // namespace graphql::generator::client

#endif // CLIENTGENERATOR_H
75 changes: 75 additions & 0 deletions include/GeneratorLoader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#ifndef GENERATORLOADER_H
#define GENERATORLOADER_H

#include "graphqlservice/GraphQLService.h"

namespace graphql::generator {

// Types that we understand and use to generate the skeleton of a service.
enum class SchemaType
{
Scalar,
Enum,
Input,
Union,
Interface,
Object,
Operation,
};

using SchemaTypeMap = std::map<std::string_view, SchemaType>;

// Any type can also have a list and/or non-nullable wrapper, and those can be nested.
// Since it's easier to express nullability than non-nullability in C++, we'll invert
// the presence of NonNull modifiers.
using TypeModifierStack = std::vector<service::TypeModifier>;

// Recursively visit a Type node until we reach a NamedType and we've
// taken stock of all of the modifier wrappers.
class TypeVisitor
{
public:
std::pair<std::string_view, TypeModifierStack> getType();

void visit(const peg::ast_node& typeName);

private:
void visitNamedType(const peg::ast_node& namedType);
void visitListType(const peg::ast_node& listType);
void visitNonNullType(const peg::ast_node& nonNullType);

std::string_view _type;
TypeModifierStack _modifiers;
bool _nonNull = false;
};

// Recursively visit a Value node representing the default value on an input field
// and build a JSON representation of the hardcoded value.
class DefaultValueVisitor
{
public:
response::Value getValue();

void visit(const peg::ast_node& value);

private:
void visitIntValue(const peg::ast_node& intValue);
void visitFloatValue(const peg::ast_node& floatValue);
void visitStringValue(const peg::ast_node& stringValue);
void visitBooleanValue(const peg::ast_node& booleanValue);
void visitNullValue(const peg::ast_node& nullValue);
void visitEnumValue(const peg::ast_node& enumValue);
void visitListValue(const peg::ast_node& listValue);
void visitObjectValue(const peg::ast_node& objectValue);

response::Value _value;
};

} // namespace graphql::generator

#endif // GENERATORLOADER_H
62 changes: 62 additions & 0 deletions include/GeneratorUtil.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#ifndef GENERATORUTIL_H
#define GENERATORUTIL_H

#include <iostream>
#include <string>
#include <string_view>

namespace graphql::generator {

// RAII object to help with emitting matching include guard begin and end statements
class IncludeGuardScope
{
public:
explicit IncludeGuardScope(std::ostream& outputFile, std::string_view headerFileName) noexcept;
~IncludeGuardScope() noexcept;

private:
std::ostream& _outputFile;
std::string _includeGuardName;
};

// RAII object to help with emitting matching namespace begin and end statements
class NamespaceScope
{
public:
explicit NamespaceScope(
std::ostream& outputFile, std::string_view cppNamespace, bool deferred = false) noexcept;
NamespaceScope(NamespaceScope&& other) noexcept;
~NamespaceScope() noexcept;

bool enter() noexcept;
bool exit() noexcept;

private:
bool _inside = false;
std::ostream& _outputFile;
std::string_view _cppNamespace;
};

// Keep track of whether we want to add a blank separator line once some additional content is about
// to be output.
class PendingBlankLine
{
public:
explicit PendingBlankLine(std::ostream& outputFile) noexcept;

void add() noexcept;
bool reset() noexcept;

private:
bool _pending = true;
std::ostream& _outputFile;
};

} // namespace graphql::generator

#endif // GENERATORUTIL_H
Loading