GithubHelp home page GithubHelp logo

jaredly / milk Goto Github PK

View Code? Open in Web Editor NEW
200.0 5.0 6.0 309 KB

Milk ๐Ÿฅ› Stress-free serialization & deserialization for Reason/OCaml

Shell 0.03% JavaScript 1.77% C++ 13.54% Reason 84.65%

milk's Introduction

Milk ๐Ÿฅ› : Stress-free serialization & deserialization for Reason/OCaml

๐Ÿšง Stability: settling ๐Ÿšง This is recently released, still settling down API n stuff, but I plan to maintain backwards compatability. Also I'm still working on documentation, so some things might be missing or unclear.

Usage: milk [config file] [options]

If no config file is provided, `types.json` will be used if it exists,
otherwise the `milk` section of esy.json or package.json will be used if
it exists.

Options:

- --init     : create a new config file for the current project

(rarely used)

- --override : Ignore current lockfile version, overriding conflicts.
               NOTE: This should only be passed if you have not stored any
               data using the currently generated serde, as it may now be
               unparseable.
- --upvert   : Convert a legacy config file (generated with a previous
               version of milk) to the current schema.

Configuration

This goes either in a separate JSON file (e.g. types.json), or in a "milk" section of your esy.json or package.json.

Example types.json:

{
  "milkSchemaVersion": 2,
  "version": 1,
  "engines": {
    "bs.json": {
      "output": "./src/Serde.re"
    }
  },
  "entries": [
    {
      "file": "./src/Types.re",
      "type": "config"
    },
    {
      "file": "./src/Node.re",
      "type": "t",
      "publicName": "node"
    }
  ]
}

See the types.json in this repo for another example, or this one over here.

Full spec:

  • milkSchemaVersion: int this is the milk config file schema version. The latest version is 2 (the one documented here).
  • version: int : this is the version of your types. When you serialize anything, it will have this version attached to it, so that the data can be deserialized in the future & migrated through any subsequent versions. When you make a "breaking change" (see below for details), you'll need to increment this version number, and provide any necessary migrator functions (see below for migration details).
  • lockedTypes: string : If you have multiple engines defined, then this is required, otherwise it's optional. This file is where all of the types for all of the versions that exist in your lockfile will be generated, for use by deserialization and migration functions.
  • engines: {[key: engine_name]: engineConfig} : a mapping of engine_name to engineConfig, where engine_name is one of rex_json, bs_json, ezjsonm, and yojson.
    • engineConfig: {output: string, helpers: option(string)}. Output is the file where the serializer & deserializer functions should be output to, and helpers is the name of the module that contains any necessary TransformHelpers (see below)
  • entries: list(entry) : the list of your "entry point" types. These are the types that you want to be able to serialize & deserialize.
    • file: string the source file where this type is defined
    • type: string the name of the type within the file, including containing submodules. e.g. someType if it's at the top level, or SomeModule.InnerModule.t if it's nested.
    • publicName: string the name that will be used in the externally usable serialization & deserialization functions. e.g. if your type name is t, having a function called deserializeT won't be super helpful, so you can put publicName: "animal" and you'll get deserializeAnimal
  • custom: list(custom). "Custom" types are types for which you want to be treated as opaque -- milk will not generate ser/de functions for them, and you will provide those in the helpers module(s).
    • module: string the module name the contains the type you want to override. e.g. Animals
    • path: list(string) the path within the module, if there's nesting. If the type is Animals.Dogs.t, this would be ["Dogs"]
    • name: string the name of the type to override. e.g. t
    • args: int the number of type arguments that the type has.

How does it work?

Milk has two phases. 1) generate/update lockfile. 2) generate serialization, migration, and deserialization code for all known types & versions.

Generate/update lockfile

A lockfile consists of an array of "locked type maps", each corresponding to a "version" of your types. A "locked type map" is a map from a "module path" (like MyModule.SubModule.typename) to a serialization of the type declaration, including any @attributes.

Milk first creates a "locked type map" for the current state of your types, starting with your "entry types", and recursively following any type references down to their definitions. Then, if there's a current lockfile & the version in types.json has not been incremented, it checks for incompatible changes & errors out in that case. If the changes are compatible (see below), it overwrites the locked type map, and if the version number has been incremented since the last type Milk was run, it appends the type map to the array. The whole array is then written out to the lockfile.

Generate code!

First, the "locked types" are generated. For each version, a TypesN module is created that contains the type definition for every type needed to fully define all of the entry types for that version. The final (current) version also aliases those type definitions to the definitions used in the rest of your app. Also, migration functions are auto-generated (if possible), or referenced (if defined as decorators, see below). If a migration function cannot be auto-generated, and has not been provided, Milk errors out with a message indicating the migration function that's missing.

With the "locked types" modules, Milk is able to create type-safe deserializers for all previous versions of your types, after you have made changes to the types used in the rest of your app.

Next, deserialization functions are created (recursively) for all versions in the lockfile.

Then, serialization functions are created for the latest version.

Finally, "entry point" serializeThing and deserializeThing functions are created, with the deserialize function checking the schema version of the data passed in, and performing any necessary migrations to get it up to the current version of the type.

Migrations!

When you make a backwards-incompatible change (see below) to a type, you must provide functions to migrate from the previous version to the current version, in the form of [@ decorators.

For a "whole type" migration, provide a function as a [@migrate ] decorator that takes data of the old type and returns data of the new type.

// previous type definition
type person = {name: string, age: int};
// new type, with decorator
[@migrate ({name, age}) => {name, age: float_of_int(age), favoriteColor: None}]
type person = {name: string, age: float, favoriteColor: option(string)};

Records

You can also provide migrator functions on an attribute basis, which is especially helpful if the type is large.

// previous type definition
type person = {name: string, age: int};
// new type, with decorator
[@migrate.age person => float_of_int(person.age)]
[@migrate.favoriteColor (_) => None]
type person = {name: string, age: float, favoriteColor: option(string)};

Note that the per-attribute migration function takes the whole previous record as the argument, so that you can provide migrators for newly added attributes as well.

Variants

If you remove a constructor from a variant, or modify a constuctor, you can provide a per-constructor migrator. The "function" that you're passing in actually gets dissected and turned into a case of a switch block, so keep that in mind (which is why I'm deconstructing the individual case in the function argument, which would usually cause problems).

// previous type definition
type animal = Dog(string) | Cat | Mouse;
// new type definition
[@migrate.Dog (Dog(breed)) => Dog(breed, None)]
[@migrate.Mouse (Mouse) => Cat] // yeah this doesn't make sense as a migration to me either
type animal = Dog(string, option(int)) | Cat;

Abstract Types

For abstract types that you want to support, add manual serialization and deserialization functions.

Make sure to list the module that provides the serialize and deserialize functions in the "helpers" section of your types.json file:

   "version": 1,
   "engines": {
     "Js.Json": {
-      "output": "src/TypesEncoder.re"
+      "output": "src/TypesEncoder.re",
+      "helpers": "EncoderHelpers"
     }
   },
   "entries": [

Then define the serialize and deserialize functions. Here is an example of the function signatures to support a StringMap.t('a):

/* EncoderHelpers.re */
let serialize_StringMap____t = map => /* ... */
let deserialize_StringMap____t = json => /* ... */
let deserialize_StringMap____t = (migrator, json) => /* ... */

Here are some real-world examples:

What type changes are "compatible"

(e.g. don't require a version bump)

  • adding a constructor to a variant type
  • adding a row to a polymorphic variant type (TODO not yet supported)
  • adding a new "entry" type
  • adding an optional attribute for a record (will default to None)
  • removing an attribute from a record

milk's People

Contributors

jaredly avatar jsiebern avatar kevinsimper avatar wpcarro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

milk's Issues

Cant find exported type when ppx is used

Hi @jaredly! I was trying to use milk and https://github.com/ELLIOTTCABLE/bs-deriving

when i have ppx in bsconfig.json milk throws error that it can't find desired type.

Fatal error: exception Failure("No exported type t in module file:///~/demo/src/Config.re ")
Raised at file "stdlib.ml", line 33, characters 17-33
Called from file "belt/belt_List.ml", line 172, characters 29-34
Called from file "src/Milk.re", line 66, characters 4-675
Called from file "src/Milk.re", line 139, characters 47-66
Called from file "bin/Main.re", line 104, characters 4-47

removing ppx from bsconfig helps

syntax error with variants that have record parameters

This code:

type bar = {x: int};
type event =
  | Foo(int)
  | Bar(bar);

Generates this output, which has a syntax error:

module Types1 = {
  type _Lib__Events__event =
    Lib__Events.event = | Foo(int) | Bar(_lib__Events__bar)
  and _lib__Events__bar = lib__Events.bar = {x: int};
};

Error with [email protected]

I attempted to upgrade to bs-platform 5.1.0 and received this error after running `milk v1.0.0-alpha3``

Fatal error: exception Failure("Failed to load <project-path>/node_modules/.lsp/Types.cmt as a cmt w/ ocaml version 402, error: <project-path>/node_modules/.lsp/Types.cmt\nis not a compiled interface")
Raised at file "stdlib.ml", line 33, characters 17-33
Called from file "belt/belt_List.ml", line 172, characters 29-34
Called from file "src/Milk.re", line 66, characters 4-675
Called from file "src/Milk.re", line 139, characters 47-66
Called from file "bin/Main.re", line 104, characters **4-47**

After re-installing 5.0.6, milk ran successfully. I'd love to help with this but have no idea where to start.

Abstract type support?

First of all - thank you for making this and open sourcing it. I'm loving it so far. I'm new to Reason and OCaml so forgive me if the way I phrase this question is vague.

I added a new field to my record that I'm encoding of type StringMap.t(list(string)). When I run milk, I get this error:

Fatal error: exception Failure("Abstract type found, but no 'helpers' module specified for this engine")
Raised at file "stdlib.ml", line 33, characters 17-33
Called from file "src/serde/MakeDeserializer.re", line 131, characters 16-98
Called from file "src/serde/MakeDeserializer.re", line 206, characters 10-20
Called from file "src/serde/MakeDeserializer.re", line 302, characters 16-82
Called from file "belt/belt_List.ml", line 172, characters 29-34
Called from file "src/SerdeFile.re", line 232, characters 12-94
Called from file "src/Milk.re", line 190, characters 12-271
Called from file "src/Milk.re", line 212, characters 4-11
Called from file "src/Milk.re", line 241, characters 10-44
Called from file "bin/Main.re", line 104, characters 4-47

This error goes away when I remove the StringMap.t(..). I'm inferring from the error that the "abstract type" is the StringMap.t and that I need to provide my own serialize and deserialize functions. I think this makes sense. Can you point me to a place in the documentation where I can learn more about doing this? If there isn't any, I'd be happy to write some if you give me a hand.

Thanks!

"Unsupported OCaml version"

Hello,
Trying to use this project as it sounds like the best alternative for me.
but when trying to execute milk I get -
Fatal error: exception Failure("Unsupported OCaml version: 4.14.0") error.

  1. What are the supported versions?
  2. Why 4.14.0 is not supported? I mean, what should I change to make it be supported?

No error when engine.t.output invalid

env

(โœ“) 20:26:52 [~/code/milktest]
$ uname -a
Linux **** 5.0.0-31-generic #33~18.04.1-Ubuntu SMP Tue Oct 1 10:20:39 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
(โœ“) 20:27:00 [~/code/milktest]
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Pop!_OS 18.04 LTS
Release:        18.04
Codename:       bionic

Recreate

  1. Make a "bad" path as types.json.engine.t.output (I typo'd ",/src/Serde.re"), with a leading comma.
  2. Run milk
  3. milk exits with no error code or message

What Happened

No Serde.re file created

What I expected to happen

milk would exit with non-zero code or print an error as a result of invalid file system operation

Error with inline record definitions

I have a type like this:

type event = Foo{bar: int}

but the translation doesn't contain the inline record anymore. If I change to regular records it works.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.