A code generator for RAML files as used by the NMOS specifications (https://specs.amwa.tv/nmos/, https://github.com/AMWA-TV/nmos).
The idea:
<RAML file> --> codegenerator ---> C++ code
The codegenerator is written in Python, so you'll need python 3.12+ See 'requirements.txt' for requirements.
You'll need a logging library and a C++ capable httpserver to bind the generated code to. I'm using a web server based on iuring.
python raml-cpp-codegenerator/main.py --input ConnectionAPI.ramlwill generate a bunch of files in the working directory.
To integrate the code generator in your build system, I suggest something like the following (assuming you've added this as a git submodule):
add_custom_command(
OUTPUT gen/all_endpoints-ConnectionAPI.hpp
COMMAND mkdir -p gen
COMMAND python ${PROJECT_SOURCE_DIR}/raml-cpp-codegenerator /main.py --input ${PROJECT_SOURCE_DIR}/is-05/APIs/ConnectionAPI.raml
DEPENDS is-05/APIs/ConnectionAPI.raml
DEPENDS ${PROJECT_SOURCE_DIR}/raml-cpp-codegenerator/main.py
DEPENDS ${PROJECT_SOURCE_DIR}/raml-cpp-codegenerator/TypeAST.py
)For each RAML endpoint we
- generate a C++ class that represents the endpoint
- generate a serializer/deserializer from/to JSON to access the arguments and/or the return values from and endpoint.
For example, given the following exerpt:
types:
Sources: !include schemas/sources.json
/sources:
displayName: Sources
get:
description: List Sources
responses:
200:
body:
type: "object"
properties:
foo:
type: "string"
400:
body:
type: "object"
properties:
bar:
type: "string"we generate a class and supporting code. Below an excerpt of the relevant parts:
namespace sources {
class Endpoint {
class reply_get_200 {
std::string foo;
};
class reply_get_400 {
std::string bar;
};
using response_get = std::variant<std::monostate,
reply_get_200, reply_get_400>;
using reply_func_get_t = std::function<void(response_get)>;
void handle_get([[maybe_unused]] const http::URLParameters& params, reply_func_get_t reply);
// ... more support methods generated...
};
}To create a complete program the 'handle_get(reply_handler_t)' function is then to be implemented by you. The code generator takes care of packing the objects returned to from JSON.
For example, you would do sth like:
namespace sources {
void Endpoint::handle_get([[maybe_unused]] const http::URLParameters& params, reply_func_get_t reply) {
reply_get_200 ok {
.foo = 123;
};
reply(ok);
}
}To integrate the above into a http-server, we generate an extra class that holds all endpoint classes we've generated:
#include <all_endpoints-NodeAPI.hpp>contains:
class AllEndpoints {
public:
sources::Endpoint endpoint_sources;
...
void register_endpoints(http::HttpServer& http_server) {
http_server.register_endpoint_handler("<endpoint/path>",
http::HttpMethod::GET,
[this](const std::string& endpoint,
const std::string& payload, const http::URLParameters& params,
http::reply_handler_t reply_handler) {
// code generated here that calls
// generated endpoint handler
// from above
});
// and the same for all other endpoints...
};- OpenAPI/RAML patterns like:
type: object
properties:
...translate to a class
- OpenAPI/RAML 'oneOf' translates to a std::variant:
"oneOf": [
{
"type": "object",
"description": "Obj1",
"title": "object 1 schema",
"properties": { ... },
}, {
"type": "object",
"description": "Obj2",
"title": "object 2 schema",
"properties": { ... },
}
]becomes:
class obj1 { ... };
class obj2 { ... }
using oneof = std::variant<obj1, obj2>- anyof/allof translate to classes with all properties merged.
- pattern rules becomes a map of variants:
{
"type": "object",
"additionalPropertes": false,
"patternProperties": {
"^[a-zA-Z0-9\\-_]+$": {
...obj 1...
...obj 2...
}
}using patterntype = std::map<std::string, std::vector<std::variant<obj1,obj2>>>;again, the code generator takes care of serialation to/from json. You, the user of the code generator have to add the business logic of adding/retrieving data from the patterntype above.