Skip to content

XFFS/OooapI

Repository files navigation

OooapI: OCaml of OpenAPI

OooapI is OCaml of OpenAPI (v3): it is an executable that tries to generate OCaml clients from OpenAPI specs.

Table of Contents

Overview

OooapI[fn:1] is OCaml of (and for) OpenAPI 3.0 specs.

OooapI currently supports:

  • Generating OCaml clients for a (substantial) fragment of the OpenAPI specification schema.
  • Generating a family of OCaml types encoding the supported OpenAPI fragment’s request and response data.

Aims

  • Given an OpenAPI server spec, OooapI should produce a completes and correct OCaml client library. It should not generate boilerplate for an OCaml client that requires manual revision.
  • If any extension, revision, or alteration of the generated client library is necessary, it should be achievable by shadowing OCaml values, types, or modules, rather than requiring editing the generated source code.
  • As little code should be dynamically generated as possible. Instead, put all reusable logic into the =Oooapi_lib= library. Abstraction is a better form of automation than boilerplate generation.

Generated Client Library Principles

  • Each specified operation is taken to define a function.
  • Everything that ends up the body of a request is passed together as a single ~data argument to an operation function, whether multipart form, JSON, or binary data.
  • Anything that goes in the path, query parameters, or headers, is passed via arguments to an operation function.
  • Try to fail gracefully: JSON Schemas that are too complicated for us to support currently should degrade into untyped json that can be decoded manually.

Caveat

The oooapi tool, and its associated library, is meant to assist OCaml programmers who need to interface with servers that (correctly) specify their API via OpenAPI v3. However, if an alternative machine-readable specification is available, we would recommend considering using it. We would also discourage server authors from using OpenAPI to specify their systems.

Our work on this project has led us to conclude that OpenAPI and JSON Schema (upon which the former depends) are bad specification formats. Both formats adopt and propagate accidental complexity and inconsistent structures, and we believe that their widespread adoption in web development is a dangerous and costly proliferation of systemic technical debt.

We have compiled some notes explaining the rationale for this assessment.

Please not this assessment is purely technical, and not a judgment about the intentions or abilities of those who have worked so hard on these efforts.

Known Limitations

YAML is not supported

YAML may include references and all kinds of other junk. It is an overly complicated format, and the available OCaml libraries for parsing it don’t cover all of its baroque girth. We bypass this problem by expecting the OpenAPI spec to be in JSON. Many tools are available to normalize YAML into JSON.

JSON Schema (#9)

Limited support for JSON Schema’s oneOf, anyOf, allOf, and not. Only uniform simple types (string, numerics, monomorphic) arrays are supported. Anything else is treated as untyped JSON.

Media types (#11)

  • Proper serialization support is only provided for JSON and multi-part forms, all other media types are passed along as unserialized binary strings.
  • Proper deserialization support is only provided for JSON, all other media types are passed along as unserialized binary strings.

Recursive scheme definitions are not supported (#6)

This is used, for instance, in Stripe’s API (this is currently the only known hard blocker for generating code for Stripe’s API).

Responses (#12)

Deserialization is only automated for the first ResponseObject handling a success code, any other responses are returned by an operation function as raw data in an error result.

Parameters (#10)

OpenAPI “parameters” are unruly and the spec makes it hard to work with them correctly, as a result we only have basic support for most “parameters”. In particular, we do not have full support for complex, custom data-structures in parameters. Instead, these are just treated as untyped json. Custom defined shadowing functions may be needed to serialize them correctly.

File structure (#14)

All client code currently goes into one file. This can drag down build times.

Depends on Cohttp and Lwt (#15)

The current implementation is hard coded to depend on Cohttp and Lwt. These libraries are great, but it should be easy to remove this requirement, as the generateed API client code is functorized on the HTTP client library.

Alternatives

  • OpenAPI Generator includes a generator that is meant to produce OCaml client boilerplate.
  • OCaml-Swagger is a code generator that implements Swagger 2.0 API clients in OCaml.

Motivation

Why not OCaml-Swagger

We did not find OCaml-Swagger until most initial work had been completed on this project, because we were only looking for OpenAPI generation, and did not think to look for “swagger”. That said, OCaml-Swagger only supports (part of) Swagger 2.0, which was released in 2014. OooapI supports (part of) OpenAPI version 3, released in 2017.

Why not OpenAPI Generator

At the time work on this library was initiated, the OCaml client generation supplied by OpenAPI Generator had the following documented errors:

To see the current known errors with OCaml generation, see https://github.com/OpenAPITools/openapi-generator/issues?q=is%3Aissue+is%3Aopen+ocaml

However, the tool has broader stability and correctness issues. For a lengthy discussion of the tools copious shortcomings and rough spots, see Do people successfully use this? #7490?

Our own attempt to use the tool reflected the struggles discussed in issue #7490, and we encountered numerous generation errors resulting in generation of syntactically invalid OCaml programs. We ended up generating (a nontrivial amount of) broken code that required significant manual fixes, and the quality and quantity of which was not up to our standards.

We explored contributing fixes to the generator, but after investigating the implementation, we came to the conclusion that the approach to generation via mustache templates was too fragile and ad hoc to be worth the invested time.

We hope that facing this problem through a principled metaprogramming approach, leveraging OCaml’s ppx system and AST libraries, will enable pursuing a more modular, maintainable, and correct implementation.

Why OpenAPI?

All that said, we have come to the conclusion that the main problems troubling OpenAPI-Generator are probably inheritence from the copious accidental complexity permeating JSON Schema and OpenaAPI. But since there are a lot of APIs that use OpenAPI, we hope that this project may be of some value to the OCaml ecosystem, even if only as a cautionary tale or as a place to start when building something better.

Usage

Install

opam pin git@github.com:XFFS/OooapI.git

Generate OCaml code from an OpenAPI spec

$ oooapi some-server-api.json > Some_server_api.ml

Use oooapi in a dune project

Set up some dune rules to build the client and put it in your source tree

; In case only a YAML version of the spec is available,
; it needs to be converted to JSON.
; This rule uses https://github.com/mikefarah/yq
(rule
 (target spec.json)
 (deps (:spec spec.yaml))
 (action
  (with-stdout-to %{target}
   (with-stdin-from %{spec}
    (run yq --output-format json)))))

; Generate the client
(rule
 (alias generate)
 (target api.ml)
 (deps %{bin:oooapi}
       (:spec spec.json))
 (action
  (progn

   ; Generate the client code
   (with-stdout-to api.gen.ml
    (run oooapi %{spec}))

   ; (optional) Format the code
   (run ocamlformat --inplace api.gen.ml)

   ; Move the code into the source tree
   (diff? %{target} api.gen.ml))))

; In case you want the code as its own libary component
(library
 (public_name api)
 (libraries oooapi_lib) ; The oooapi_lib is requires for oooapi generated code to work
 (preprocess (pps ppx_deriving_yojson
                  ppx_deriving.make))) ; These derivers are also required

Then use it in your code (here’s an example adapted from our GitHub API Test):

module Ooo = Oooapi_lib
module Config : Ooo.Config = struct
  let bearer_token = None (* Supply this if needed, reading from the env and NOT IN YOUR SOURCE CODE :) *)
  let default_headers = None
end

module Api = Github_api.Make (Ooo.Cohttp_client) (Config)

let main =
  let open Lwt_result.Syntax in
  let+ readme_file = Api.repos_get_readme ~owner:"shonfeder" ~repo:"nomad" () in
  readme_file.name

let () =
  match Lwt_main.run main with
  | Ok file_name ->
    print_endline file_name
  | Error (`Deserialization (_data, err))
  | Error (`Request (_code, err)) ->
    Printf.eprintf "Error %s\n%!" err;
    exit 1

Examples

Footnotes

[fn:1] Pronounced variously “ooo-ah-pea”, “ooo-ah-pie”, “oh-oh-oh-ay-pee-eye”, or any other way you like.

About

OCaml of OpenAPI

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages