FastCGI implementation in OCaml

This repository exists to implement an OCaml specification for the FastCGI interface.

The spec for the FastCGI protocol is in spec/FastCGI_Specification.html. The Go lang implementation of FastCGI is in spec/fcgi.go. An OCaml specification exists in spec/OCAML.md.

These are intended to act as a reference implementation for us to figure out what the ideal OCaml interface should look like for FastCGI.

Our target language is OCaml, using the Eio library. The README for Eio is in OCaml-EIO-README.md to give you a reference. The parsing and serialisation should be done using Eio's Buf_read and Buf_write modules.

You can run commands with:

  • clean: opam exec -- dune clean
  • build: opam exec -- dune build @check
  • docs: opam exec -- dune build @doc
  • build while ignoring warnings: add --profile=release to the CLI to activate the profile that ignores warnings

Tips on fixing bugs#

If you see errors like this:

File "../../.jmap.objs/byte/jmap.odoc":
Warning: Hidden fields in type 'Jmap.Email.Identity.identity_create'

Then examine the HTML docs built for that module. You will see that there are module references with __ in them, e.g. "Jmap__.Jmap_email_types.Email_address.t" which indicate that the module is being accessed directly instead of via the module aliases defined.

Documentation Comments#

When adding OCaml documentation comments, be careful about ambiguous documentation comments. If you see errors like:

Error (warning 50 [unexpected-docstring]): ambiguous documentation comment

This usually means there isn't enough whitespace between the documentation comment and the code element it's documenting. Always:

  1. Add blank lines between consecutive documentation comments
  2. Add a blank line before a documentation comment for a module/type/value declaration
  3. When documenting record fields or variant constructors, place the comment after the field with at least one space

Example of correct documentation spacing:

(** Module documentation. *)

(** Value documentation. *)
val some_value : int

(** Type documentation. *)
type t = 
  | First    (** First constructor *)
  | Second   (** Second constructor *)

(** Record documentation. *)
type record = {
  field1 : int;   (** Field1 documentation *)
  field2 : string (** Field2 documentation *)
}

If in doubt, add more whitespace lines than needed - you can always clean this up later with dune build @fmt to get ocamlformat to sort out the whitespace properly.

When adding ocamldoc comments, use the information in the protocol specifications available to also add relevant explanations to the OCamldoc. Do not refer to the specification in the third-person, but directly include enough information in the OCamldoc for a reader to understand the protocol flow directly.

Module Structure Guidelines#

IMPORTANT: For all modules, use a nested module structure with a canonical type t inside each submodule. This approach ensures consistent type naming and logical grouping of related functionality.

  1. Top-level files should define their main types directly (e.g., jmap_identity.mli should define identity-related types at the top level).

  2. Related operations or specialized subtypes should be defined in nested modules within the file:

    module Create : sig
      type t  (* NOT 'type create' or any other name *)
      (* Functions operating on creation requests *)
    
      module Response : sig
        type t
        (* Functions for creation responses *)
      end
    end
    
  3. Consistently use type t for the main type in each module and submodule.

  4. Functions operating on a type should be placed in the same module as the type.

  5. When a file is named after a concept (e.g., jmap_identity.mli), there's no need to have a matching nested module inside the file (e.g., module Identity : sig...), as the file itself represents that namespace.

This structured approach promotes encapsulation, consistent type naming, and clearer organization of related functionality.

Software engineering#

We will go through a multi step process to build this library. We are currently at STEP 2.

  1. we will generate OCaml interface files only, and no module implementations. The purpose here is to write and document the necessary type signatures. Once we generate these, we can check that they work with "dune build @check". Once that succeeds, we will build HTML documentation with "dune build @doc" in order to ensure the interfaces are reasonable.

  2. once these interface files exist, we will build a series of sample binaries that will attempt to implement the library for some sample usecases. This binary will not fully link, but it should type check. The only linking error that we get should be from the missing library implementation that we are currently buildign.

  3. we will calculate the dependency order for each module in our library, and work through an implementation of each one in increasing dependency order (that is, the module with the fewest dependencies should be handled first). For each module interface, we will generate a corresponding module implementation. We will also add test cases for this specific module, and update the dune files. Before proceeding to the next module, a build should be done to ensure the implementation builds and type checks as far as is possible.