Skip to content

Added getting started tutorial. #349

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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
266 changes: 266 additions & 0 deletions doc/getting_started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# Getting Started

# About
This simple tutorial aims at getting you started using `cppgraphqlgen`, even though it will not cover all the feature provided by both GraphQL and cppgraphqlgen; it will get you started, allowing you to understand the basic usage and concepts behind this library.

The final goal is to write a basic system, which given an input *query string* will return an *output in Json format*.

# The Schema
The first step is to create a GraphQL schema; said schema will be provided to *schemagen* to produce the equivalent *C++ Schema Representation*, more details about how to use this will be shown later through the tutorial.

For this getting started guide the schema provided will be fairly simple, it will contain only a query which will return a list of strings and a list of object, given a certain optional input parameter.

Below the schema in question:

``` GraphQL Schema
type Thing {
description: String!
id: Int!
}

type Query {
names: [String!]!
stuff(id: String = null): [Thing!]!
}
```

## Generating the C++ Schema Code
In order to generate the schema code you need to use the previously mentioned schemagen. This can be done either by leveraging the CMake helper function or by invoking schemagen from the command line directly, for example:

```
schemagen mySchema.graphql prefix gsm
```

*Arguments (invoke schemagen with `--help` for a full list of instructions)*:
1. File name of the GraphQL Schema (code above)
2. String to prefix on the name of the generated files
3. Custom namespace to add *(it will be wrapped between `graphql::` and `::object`)*

If you provide the same schema as this tutorial, the generated files will be (each with a `.h` and `.cpp`):
- *prefixSchema*
- *prefixQueryObject*
- *prefixThingObject*

*Note: you will not need to edit these files, as indicated by the warning at the top of each generated file; however this tutorial will guide you towards reading them to understand how to implement your code.*

With the C++ Schema code generated, you can move on to the next section.

# Implementation
The basic premise of cppgraphqlgen is to provide an `std::shared_ptr` containing a custom class that you will be writing, as an input to the various classes generated by schemagen.

In order for your classes to be *"compatible"* they need to contain certain methods which have to satisfy a specific function signature in terms of name, return value and arguments.

The code generated by schemagen will provide a compile time error (unless the `--stubs` option was provided to schemagen, in which case they will be runtime exceptions) in case it doesn't find the correct function signature by stating that the *"`Function XYZ is not implemented`"*.

The best way to check the required function signature is to open the *header file* of the corresponding object generated by schemagen and search for the word `static_assert`, each match will be referred to a function that needs to be implemented in a certain way, but more on that later.

## The Query Object & the Thing Object
The query object is the entry point of your GraphQL query, every query starts from the query object you defined, therefore that will also be the entry point that cppgraphqlgen will leverage to execute your query.

This object will be provided to the `Operations` object, which once initialized will return a service to execute your queries and return an output, more on that later.

Your implementation of the query object itself, like for other objects, *is a class which does not inherit the schemagen code nor does it implement some virtual functions*.
Strictly speaking, the only requirements are the function that the generated schemagen code checks for, therefore, in the case of our sample schema, it will be two methods: one to retrieve *names* and one to retrieve *stuff*.

By exploring the schemagen `QueryObject.h` file and searching `static_assert` you can notice how there will be two matches, reported below:

``` cpp
[[nodiscard("unnecessary call")]] service::AwaitableScalar<std::vector<std::string>> getNames(service::FieldParams&& params) const override
{
if constexpr (methods::QueryHas::getNamesWithParams<T>)
{
return { _pimpl->getNames(std::move(params)) };
}
else
{
static_assert(methods::QueryHas::getNames<T>, R"msg(Query::getNames is not implemented)msg");
return { _pimpl->getNames() };
}
}
[[nodiscard("unnecessary call")]] service::AwaitableObject<std::vector<std::shared_ptr<Thing>>> getStuff(service::FieldParams&& params, std::optional<std::string>&& idArg) const override
{
if constexpr (methods::QueryHas::getStuffWithParams<T>)
{
return { _pimpl->getStuff(std::move(params), std::move(idArg)) };
}
else
{
static_assert(methods::QueryHas::getStuff<T>, R"msg(Query::getStuff is not implemented)msg");
return { _pimpl->getStuff(std::move(idArg)) };
}
}
```

these two blocks of code are responsible for checking whether you class, of which instance you will be providing to this object, satisfies the requirement of having certain methods.

For example, in this case you can see how two methods are required, one is `getStuff` and one is `getName`, and they belong to the class `Query`.
The name `Query` comes from the fact that it's a query, while `getNames` and `getStuff` are made by prefixing `get` to `name` and `stuff`.
*This can be useful to remember, but it's not important since the compiler error, and this code, will remind you.*

Other useful information are the *arguments* and *return type.*

***Get Names Signature***
If we take as an example `getNames` we can see how the return type is indicated in the function signature, it returns a `service::AwaitableScalar<std::vector<std::string>>`, meaning that our code will have to return a `std::vector<std::string>>`; which makes sense, considering how schema returns an array of string that is *mandatory*, as in, the query *must return the field with something*.
The method itself is called like this `_pimpl->getNames()`, without any parameters; which makes sense since we didn't specify any on our original GraphQL schema.

***Get Stuff Signature***
Another, slightly different example is `getStuff`, from our own specification this GraphQL field:
- Can take an *optional string argument called id*
- *Must return a vector of Things*
As we can see our requirements have been reflected in the function signature.
The return value is `service::AwaitableObject<std::vector<std::shared_ptr<Thing>>>`, this means that we *must* to return a `<std::vector<std::shared_ptr<Thing>>`; once again it's a vector since the GraphQL schema declared a list.
You can also note how this non scalar value (a custom class) is wrapped in a shared pointer.

Next we can notice how an argument is passed to the function call itself: `_pimpl->getStuff(std::move(idArg))`; if we read the definition of `idArg` (which takes it's name by the `id` declaration on GraphQL) we can see it as follows: `std::optional<std::string>&& idArg`.
Notice how it's a string wrapped in an `optional` object, since the parameter itself was declared as optional on the GraphQL schema.

***Full Class Implementation***
In order to therefore satisfy the cppgraphqlgen Query requirements, we need to provide a class with the methods specified above; the implementation itself is yours to decide depends on your need.

Here is a sample class declaration and implementation
``` cpp
#include "QueryObject.h"

namespace mod_graphql::mock ...
using namespace graphql;
// Declaration
class Query {
public:
explicit Query() noexcept;

std::vector<std::string> getNames() const noexcept;
std::vector<std::shared_ptr<::graphql::gsm::object::Thing>>
getStuff(std::optional<std::string> &&idArg) const noexcept;
};

// Definition
Query::Query() noexcept {}

std::vector<std::string> Query::getNames() const noexcept {
// Some mock code
auto names = std::vector<std::string>();
names.push_back("Name 1");
names.push_back("Name 2");
names.push_back("Name 3");
return names;
}

std::vector<std::shared_ptr<::graphql::gsm::object::Thing>>
Query::getStuff(std::optional<std::string> &&idArg) const noexcept {
// Some mock code
auto stuff = std::vector<std::shared_ptr<::graphql::gsm::object::Thing>>();
auto thing_ptr = std::make_shared<Thing>(0, "Sample Description!");
stuff.push_back(std::make_shared<::graphql::gsm::object::Thing>(thing_ptr));
return stuff;
}
```

*Note: this is more very basic code to show you the bare minimum to implement a compatible class, for more examples you can check the `samples/` directory.*

As you may have noticed, the *getStuff* method returns an instance of `object::Thing`, which is the class defined by cppgraphqlgen; and by the method definition you can see how `object::Thing` is initialized by passing a `shared_ptr` of type `Thing` (*the class we will be defining shortly*).
This is basically the logic behind the Query class we have just implemented, and you can see the same `static_assert` guidance on the relative header file.

In this case, the `Thing` class has been implemented as such:
``` cpp
#include "ThingObject.h"

namespace mod_graphql::mock ...
using namespace graphql;
// Declaration
class Thing {
public:
explicit Thing(int id, std::string description) noexcept;

const int getId() const noexcept;
const std::string getDescription() const noexcept;

private:
const int id;
const std::string description;
};

// Definition
Thing::Thing(int id, std::string description) noexcept
: id{id}, description{description} {};

const int Thing::getId() const noexcept { return id; }

const std::string Thing::getDescription() const noexcept { return description; }
```

As you can imagine, `getId` and `getDescription` match the requirements of `ThingObject.h`; though in this case they can just return a *string* and an *int* since they do not return a complex class.

## Initializing the Service
Once you have defined all the necessary classes you can create a service.

To do this you need to provide a `shared_ptr` to an instance of your custom `Query` class to `Operations`, for example:
```cpp
#include "gsmSchema.h"
#include <graphqlservice/GraphQLService.h>
#include <graphqlservice/JSONResponse.h>

auto query = std::make_shared<mod_graphql::mock::Query>();
auto service = std::make_shared<::graphql::gsm::Operations>(query);
```

The `Operations` class may require more parameters, depending for example, on whether you specified a mutation on the schema or not.
The best way to check the required input is by reading constructor signature in the `gsmSchema.h` equivalent file.

## Executing Queries
Once the service is ready you can execute queries.

A query however needs to be parsed by the `graphql::peg::parseString` (or equivalent file, etc. functions) function into a `graphql::peg::ast` object.

This object can then be given to the service `resolve` method, which will return an object that can be parsed by the `graphql::response::toJSON` method, to provide a final result string.

These methods may throw an exception, therefore you need to wrap them in a `try/catch` block.
Below an example:

```cpp
// Previous service init code...

std::string final_output = "Empty Result!";
try {
::graphql::peg::ast query_ast = ::graphql::peg::parseString(query_input);
final_output =
::graphql::response::toJSON(service->resolve({query_ast, ""}).get());
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
}
return std::string(final_output);
```

In this case `final_output` will be the final `GraphQL` response which can be given to a client.

If you followed this tutorial you should be able to provide as input the following query:
``` GraphQL
query GetShit{
names
stuff(id: "0") {
description
}
}
```

and get the following result inside `final_output`
```cpp
{
"data":
{
"names":
[
"Name 1",
"Name 2",
"Name 3"
],
"stuff":
[
{ "description" : "Sample Description!" }
]
}
}
```
which reflects the test data and hardcoded strings we placed in the code.

## Next Steps
With a now clearer image of the cppgraphqlgen library, you can try exploring the `samples` directory for a practical implementation of more features, such as mutations.