ppx_factory

PPX to derive factories and default values
README

ppx_factory

ppx_factory is an OCaml preprocessor to derive factory methods and default values from type
definitions.

Overview

Factories are functions that let you build test data while only specifying the parts that are
relevant to your tests.

ppx_factory alows you to derive such functions from type definitions.

Let's take a very basic example. Consider the following type and function:

type person =
  { first_name : string
  ; middle_name : string option
  ; last_name : string
  ; age : int
  ; hobbies : Hobby.t list
  }

let full_name {first_name; last_name; middle_name; _} =
  match middle_name with
  | None -> Printf.sprintf "%s %s" first_name last_name
  | Some m_name -> Printf.sprintf "%s \"%s\" %s" first_name m_name last_name

When writing tests for the full_name function, you don't want to bother defining age and
hobbies every time so you use a factory method that looks like:

val person_factory :
  ?first_name: string ->
  ?middle_name: string ->
  ?last_name: string ->
  ?age: int ->
  ?hobbies: Hobby.t list ->
  unit ->
  person

and let's you write your test in a more concise manner:

test
  ~input:(person_factory ~first_name:"John" ?middle_name:None ~last_name:"Doe" ())
  ~expected:"John Doe";
test
  ~input:(person_factory ~first_name:"Robyn" ~middle_name:"Rihanna" ~last_name:"Fenty" ())
  ~expected:"Robyn \"Rihanna\" Fenty"

By adding the [@@deriving factory] attribute to your type definitions, ppx_factory will generate
a single factory function for record types or one per constructor for variant types.

You can also use [@@deriving default] to generate a single default value per type to use in your
tests.

See the Detailed usage section for
further information.

Installation and usage

You can install ppx_factory using opam:

$ opam install ppx_factory

If you're building your library or app with dune, add the following field to your library,
executable or test stanza:

(preprocess (pps ppx_factory))

or simply add ppx_factory to your preprocess field if it's already there.

You can now add the factory and/or default plugins to [@@deriving ...] attributes on type
definitions to derive the corresponding values.

Detailed usage

factory

You can use [@@deriving factory] to derive factory functions from type definitions both in module
and module signatures eg both in your .ml and .mli files given that it's an explicit record or
variant type definition.

Record types

You can derive factory functions from record type definitions. This will derive a single factory
function that has an optional argument per field. The name of the factory function depends on the
name of the type, factory will be derived from type t and <type_name>_factory for any other
type.

Each optional parameter will expect the same type as the one declared for the corresponding field,
except for option types. A field which has type a option will be turned into a optional argument
expecting an a.

Because examples are worth a thousand words:

type t =
  { a : int
  ; b : string
  }
[@@deriving factory]

type 'a u =
  { c : 'a list
  ; d : 'a option
  }
[@@deriving factory]

will derive the following factory functions:

val factory : ?a: int -> ?b: string -> unit -> t

val u_factory : ?c: 'a list -> ?d: 'a -> unit -> 'a t
Variant types

You can also derive factory functions from variant type definitions. This will derive one of them
per constructor. Those functions will be named based on the type and the constructor name and have
a <type_name>_<lowercased_constructor_name>_ prefix.

Constant constructor factories will have a single unit argument, while for constructors with tuple
arguments, including 1-element tuples, they will have ?tup<element_tuple_index> arguments starting
at ?tup0 and for constructors with record arguments they will have optional arguments named as the
corresponding record field.

type t =
  | A
  | B of string
[@@deriving factory]

type 'a u =
  | C of int * 'a option
  | D of
    { some_int : int
    ; some_list : 'a list
    }
[@@deriving factory]

will derive the following factory functions:

val a_factory : unit -> t

val b_factory : ?tup0: string -> unit -> t

val u_c_factory : ?tup0: int -> ?tup1: 'a -> 'a u

val u_d_factory : ?some_int: int -> ?some_list: 'a list -> 'a u

default

To derive factory functions, ppx_factory relies on the fact that it's able to derive a default
value from a record field or constructor argument's type. For most core types we derive one of our
own whereas for custom types we expect to find one in the right place. Eg for a type t we expect a
default value to be available in the current scope, for a type u we expect default_u, for
A.B.t, A.B.default and so on.

ppx_factory also exposes a deriver to spare you the need to write and update those yourself. You
can use it by attaching [@@deriving default] to type definitions.

It can be used with most core types, record and variant types. It will derive a single value
which name will be based on the type name, ie default for type t or default_<type_name>
otherwise.

In some cases it's impossible to derive a default from a parametrized type. For example you can't
derive a default value from the following type:

type 'a t = 'a * int

because we can't derive a value of type 'a for any type 'a.

It will only derive a default value if it can derive a generic one. For instance it's possible to
derive a value that as type 'a option, 'a list or 'a array for any type 'a (obviously
None, [] and [||]). The default deriver will be able to figure that from your explicit type
definitions. If you consider the following two types:

type ('a, 'b) t1 =
  | A of 'a
  | B of 'b

type ('a, 'b) t2 =
  | C of 'a
  | D of 'b
  | E of int

[@@deriving default] can't derive a default value that as type ('a, 'b) t1 so you will get a
compile error if you try to use it with type t1.
On the other hand it can derive a default value that as type ('a, 'b) t2 such as E 0.

It is worth noting that you should never rely on what the actual value of such a default is as
it is susceptible to change in minor or even patch releases. If your tests depend on a default value
it means you're not using factories right!

Install
Sources
ppx_factory-0.2.0.tbz
sha256=23a90da63c9ab5078b0582805bbdaabf440cd8a8ac32abd1ad16d2dbac27b891
sha512=5d1e9f14d7cecc6617a9bc4d4438648230e05c167b7fd7f88449d94d5ab237561c0fc1f62fd628a6af0d77094c7f167d18910676e20ce3659a5383c38f2d4806
Dependencies
ppx_deriving
with-test
ppxlib
>= "0.14.0"
ounit
with-test & >= "2.0.0"
ocaml
>= "4.07.0"
dune
>= "1.1"
Reverse Dependencies