Skip to content

Enhancing the REST API

tzach edited this page Oct 13, 2014 · 1 revision

For using the REST API, look here Using-OSv-REST-API

Adding an API implementation

The httpserver located under the mgmt sub-module handle the different API request. To add an implementation it is required to add a mapping between a URL and an implementation.

handler - An Implementation

An implementation is a class inherit from handler_base (see handlers.hh) that implements the handle method. There are two options to create such an implementation by inherit directly or by using lambda function

Implementation by inherit handler_base

For the most cases, this is over complication, use it only when you actually need an object. Unless you have a good reason, skip this section and use a lambda expression (next section)

The following handler, uses two parameters a path parameter called str and a query parameters called str1 and return the string concatenated. If a string is missing or empty an error will be returned.

class my_handler : public handler_base {
public:
    bool handle(const std::string& path, parameters* parts,
                const http::server::request& req, http::server::reply& rep)
    override {
        auto str = req.param["str"]
        auto str1 = req.get_query_param("str1");
        if (str1 == "") {
           throw missing_param_exception("str1");
        }
        rep.content.append(str + str1);
        set_headers(rep, "txt");
        return true;
    }
};

Note the use of path parameter in req.param["str"] query parameter in req.get_query_param("str1"); and the use of exception to return an error.

Exception that inherits from httpserver::base_exception mapped to their own error code, for example: throwing not_found_exception will result in a 404 reply. And throwing missing_param_exception will result in 400 bad request.

All unhandled exception will be caught by the server and will result in a 500 result.

Using a lambda function

C++11 adds an anonymous function creation. For simple implementation the function_handler can be use which get as a parameter to its constructor a lambda function

   getOSversion.set_handler("json", [](const_req req)
    {
        return formatter::to_json(osv::version());
    });

Note that the first parameter set the return type, json format is the default and can be emitted:

    getOSversion.set_handler([](const_req req)
    {
        return formatter::to_json(osv::version());
    }); 

A few things to note in the example:

  1. getOSversion is created by the code generation according to the swagger definition file. You can pass a handler or a lambda expression to it.
  2. The optional type "json" This should be equivalent to the file extension which would have been used to return the result and mark the content type that will be return, see the previous example for returning text.
  3. See the use of the formatter::to_json method, it can be used with all primitive types to be represented in a json format and with json objects that are created by the code generation based on the swagger definition file.

Handling and Reporting Errors

The http protocol defines few standard error codes to reflect errors while handling a request. Though you can set the error result yourself (when you have the reply object) this is typically over complication. The error handling in the server is based on exceptions. look at the various http server predefined exception, found at exception.hh. Any general exception will be caught by the server and a 500 (Internal server error) will be returned. The nice part, is that for most of the exception, you don't need to do anything. The other two widely used error are 404 (not found) that will be return for all url that are not mapped, but can also return for a missing resource (e.g. call to the jvm when the jvm is not available) The other is 400 (bad request) this one is used for example when there is something is wrong with the given parameters. The httpserver return a 400 when a parameter that is define as mandatory is missing.

Mapping a handler to a URL

The previous section explained how to create an implementation, this section will explain how to do the mapping between a URL e.g. /os/version or /file/{path} to a handler. Note that path parameter is part of the url definition.

There are two options of adding the mapping, do it explicitly in the code or taken it from the swagger definition.

Adding the mapping is done on the route object and is typically done in an init function that is called from the global_server set_route.

In all the following example we assume that the handler implementation is handler

Using a route

If there is no swagger definition file for the handler, like in the case of files, adding a url mapping without path parameter is done by calling put on the routes object.

routes.put(GET,"/hello",handler);

The first parameter is the http command (GET, PUT, POST or DELETE) the second is the URL to map and the third is the handler. When set a call to http://192.168.122.89:8000/hello will execute the handler implementation.

If the handler uses path parameter use the add method, routes.add(GET, url("/api").remainder("path"), api); Note here the use of the url object that can be use to add additional parameters to the URL, remainder catches the remaining URL to its end.

Note, if you find yourself using the above method, you are most likely doing something wrong. In most cases you should have a swagger definition for your API and you should use the method below. Writing your routes by yourself is error pron.

Starting from a Swagger definition

In most cases, adding an implementation will start from a Swagger definition file. In those cases the httpserver compilation will auto generate related code and mapping for you.

By default the httpserver makefile will auto generate code from all json files found under mgmt/api-doc/listings/.

The code generation would create a parameter based on the command nickname declare in the swagger file that can be used to set the handler.

The use of the generated command is the prefer way, it would enforce compatibility of the code to the definition. It also enforces mandatory parameters, so it is not required to check them in the handler, if they are missing the caller would get a 400 stating that the parameter is missing.

Add a Swagger definition file

look at the files under mgmt/api/listings/ for examples. You should also add a link in mgmt/api/api-docs.json to the new file.

After you add a Swagger definition file it is best to run the httpserver compilation so the code generation would create the files for you and your IDE could use them.

Add an implementation

The implementation should be placed under the api directory in the httpserver, the convention is that if the json file is called os.json the implementation is os.cc and os.hh under the api directory.

Include the header file of the auto generated file. For the os.json the hh file will be #include "autogen/os.json.hh"

From the init method call the auto generated init method for the os.json it will be os_json_init_path();

Now you can use the set_handler method that uses the nickname of the operation, getOSversion.set_handler(handler);

Update the global_server init routes function global_server::set_routes and include a call to api::os::init(_routes);

Using the Json objects

The code generation also define any object that is placed under the models section in the swagger definition file.

For example if you have the following definition in your swagger file:

    "Balloon": {
        "properties": {
            "size": {
                "type": "long"
            }
        }
    }

You can use in the code

httpserver::json::Balloon b;
b.size = 3;
return formatter::to_json(b);

Testing

The API test suite if found under tests directory. Tests are split into files according to the APIs, so for every file under httpserver/api there should be a python test file located under httpserver/tests/api.

The test suite will start the httpserver for you and would call all tests under the api directory. When adding tests, inherit your test class from basetest.Basetest it would add some useful functionality to your testing to ease describing the API and test their result.

Avoid specifying the url path when possible and relay on the path defining in the swagger definition file.

Look at httpserver/tests/api/testenv.py for a simple test that set and get an environment variable via the API.

To run tests do:

cd module/httpserver
make check

Adding an API Check List

  1. add a swagger definition file (compile and see that the auto-generated code is created)
  2. under the httpserver/api directory add the equivelent cc and hh files
  3. include the auto-generated header file in the cc file
  4. add an init method
  5. Inside the init method add a call to the auto-generated code init
  6. add the implementation and mapping
  7. add an include in global_server.cc to the new header file (under api)
  8. add a call in global_server::set_routes to call the init function Note that file mapping init should be last, don't put anything after it
  9. add test for the new API
Clone this wiki locally