Atdgen - это проект по созданию типов и структур данных, которые можно сериализовать в JSON. Это очень удобно при взаимодействии между несколькими процессами, создании REST API или использовании объектов JSON из других инструментов. Его можно сравнить с схемой JSON или буферами протокола, но с более богатыми типами и большим количеством функций.

Идея состоит в том, чтобы записать список типов в файл спецификации, файл .atd. Затем, запустив atdgen, можно сгенерировать код OCaml или Java для сериализации / десериализации значений этих типов в / из соответствующего json.

До недавнего времени atdgen мог генерировать код только для собственного OCaml. Но добавлена ​​поддержка bucklescript! atdgen инструмент cli по-прежнему является встроенным двоичным кодом OCaml. Но он может выводить некоторый код OCaml, который можно скомпилировать с помощью bucklescript.

Работу по внедрению этой новой функции atdgen профинансировал Ahrefs. Мы высоко ценим инструменты с открытым исходным кодом. И, насколько это возможно, мы предпочитаем вносить свой вклад в существующие проекты с открытым исходным кодом, а не изобретать колесо самостоятельно.

Установка

Чтобы установить atdgen, нам сначала нужно установить opam (менеджер пакетов OCaml), поскольку atdgen не предоставляет готовых к использованию двоичных файлов и распространяется только как исходный пакет через opam. Процедура проста и задокументирована здесь: https://opam.ocaml.org/doc/2.0/Install.html

Затем нам нужно инициализировать opam и создать коммутатор. Подойдет любая версия ocaml выше или равная 4.03.0.

opam init -a
opam switch create . 4.07.1 -y

Как только это будет сделано, мы должны установить разрабатываемую версию atdgen. Поддержка Bucklescript официально не реализована.

opam pin add atd --dev-repo   
opam pin add atdgen --dev-repo

Убедитесь, что atdgen доступен.

$ which atdgen                 
(current $PWD)/_opam/bin/atdgen

Конечно, нам понадобится Bucklescript.

yarn init                 
yarn add bs-platform --dev

Нам также нужна среда выполнения bucklescript для atdgen, поскольку она в настоящее время не предоставляется самим atdgen. Итак, мы написали и открыли исходный код нашей версии среды выполнения: https://github.com/ahrefs/bs-atdgen-codec-runtime.

Эта среда выполнения отвечает за преобразование между значениями JSON и значениями OCaml. Значения JSON основаны на стандартном типе Js.Json.t, предоставляемом bucklescript, чтобы гарантировать простоту взаимодействия с остальной экосистемой.

Он опубликован на npm для легкой интеграции в проекты Bucklescript.

yarn add @ahrefs/bs-atdgen-codec-runtime

Конфигурация проекта

После предыдущего раздела package.json должен быть почти готов. Мы можем добавить несколько скриптов, чтобы было удобнее компилировать проект. Вот как это должно выглядеть после завершения.

{
  "name": "demo-bs-atdgen",
  "version": "0.0.1",
  "description": "demo of atdgen with bucklescript",
  "scripts": {
    "clean": "bsb -clean-world",
    "build": "bsb -make-world",
    "watch": "bsb -make-world -w",
    "atdgen": "atdgen -t meetup.atd && atdgen -bs meetup.atd"
  },
  "devDependencies": {
    "bs-platform": "^4.0.5"
  },
  "peerDependencies": {
    "bs-platform": "^4.0.5"
  },
  "dependencies": {
    "@ahrefs/bs-atdgen-codec-runtime": "^1.0.4"
  }
}

Конфигурация Bucklescript очень проста. Мы используем базовую конфигурацию, которую можно найти в любом проекте Bucklescript. За исключением того, что нам нужно добавить одну зависимость к bsconfig.json:

{
  "name": "demo-bs-atdgen",
  "version": "0.0.1",
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "package-specs": {
    "module": "commonjs",
    "in-source": true
  },
  "suffix": ".bs.js",
  "bs-dependencies": [
    "@ahrefs/bs-atdgen-codec-runtime"
  ],
  "warnings": {
    "error": "+101"
  },
  "generate-merlin": true,
  "namespace": true,
  "refmt": 3
}

Первые определения ATD

Пришло время создать первый файл .atd, содержащий наши типы. Эта часть также задокументирована на https://atd.readthedocs.io/en/latest/tutorial.html#getting-started

В этом примере я решил провести встречу. Поместите определения типов в src / meetup.atd.

(* This is a comment. Same syntax as in ocaml. *)
type access = [ Private | Public ]
(* the date will be a float in the json and a Js.Date.t in ocaml *)
type date = float wrap <ocaml module="Js.Date" wrap="Js.Date.fromFloat" unwrap="Js.Date.valueOf">
(* Some people don't want to provide a phone number, make it optional *)
type person = {
  name: string;
  email: string;
  ?phone: string nullable;
}
type event = {
  access: access;
  name: string;
  host: person;
  date: date;
  guests: person list;
}
type events = event list

Мы используем двоичный файл atdgen (скомпилированный ранее) для генерации типов ocaml и кода для сериализации / десериализации этих типов.

atdgen -t meetup.atd # generates an ocaml file containing the types
atdgen -bs meetup.atd # generates the code to (de)serialize

Сгенерированные файлы:

  • meetup_t.ml (i), которые содержат типы ocaml, соответствующие нашим определениям ATD.
  • meetup_bs.ml (i), который содержит код ocaml для преобразования значений json и обратно.

На этом этапе мы можем скомпилировать наш проект.

yarn build

Если все работает правильно, теперь у нас есть два файла .bs.js в каталоге src.

$ tree src
src
├── meetup.atd
├── meetup_bs.bs.js
├── meetup_bs.ml
├── meetup_bs.mli
├── meetup_t.bs.js
├── meetup_t.ml
└── meetup_t.mli
0 directories, 7 files

На этом этапе мы можем создать новые файлы OCaml / Reason в каталоге src и использовать весь сгенерированный для нас код atdgen. Два примера, чтобы проиллюстрировать это.

Запросить REST API

Обычно atdgen используется для декодирования JSON, возвращаемого REST API. Вот краткий пример с использованием синтаксиса причины и bs-fetch.

let get = (url, decode) =>
  Js.Promise.(
    Fetch.fetchWithInit(
      url,
      Fetch.RequestInit.make(~method_=Get, ()),
    )
    |> then_(Fetch.Response.json)
    |> then_(json => json |> decode |> resolve)
  );
let v: Meetup_t.events =
  get(
    "http://localhost:8000/events",
    Atdgen_codec_runtime.Decode.decode(Meetup_bs.read_events),
  );

Чтение и запись файла JSON

Atdgen for bucklescript не занимается преобразованием строки в объект JSON. Это позволяет нам использовать эффективный парсер json, включенный в nodejs или браузер.

let read_events filename =
  (* Read and parse the json file from disk, this doesn't involve atdgen. *)
  let json =
    Node_fs.readFileAsUtf8Sync filename
    |> Js.Json.parseExn
  in
  (* Turn it into a proper record. The annotation is of course optional. *)
  let events: Meetup_t.events =
    Atdgen_codec_runtime.Decode.decode Meetup_bs.read_events json
  in
  events

Обратная операция, преобразование записи в объект JSON и запись его в файл, также проста.

let write_events filename events =
  Atdgen_codec_runtime.Encode.encode Meetup_bs.write_events events (* turn a list of records into json *)
  |. Js.Json.stringifyWithSpace 2   (* convert the json to a pretty string *)
  |> Node_fs.writeFileAsUtf8Sync filename  (* write the json in our file *)

Полный пример

Теперь, когда у нас есть функции для чтения и записи событий, мы можем создать небольшой клиентский интерфейс, чтобы распечатать список событий и добавить новые события.

Исходный код полного примера доступен на github.

Вы можете запустить его так:

$ echo "[]" > events.json
$ nodejs src/cli.bs.js add louis [email protected]
$ nodejs src/cli.bs.js add bob [email protected]
$ nodejs src/cli.bs.js print
=== OCaml/Reason Meetup! summary ===
date: Tue, 11 Sep 2018 15:04:16 GMT
access: public
host: bob <[email protected]>
guests: 1
=== OCaml/Reason Meetup! summary ===
date: Tue, 11 Sep 2018 15:04:13 GMT
access: public
host: louis <[email protected]>
guests: 1
$ cat events.json
[
  {
    "guests": [
      {
        "email": "[email protected]",
        "name": "bob"
      }
    ],
    "date": 1536678256177,
    "host": {
      "email": "[email protected]",
      "name": "bob"
    },
    "name": "OCaml/Reason Meetup!",
    "access": "Public"
  },
  {
    "guests": [
      {
        "email": "[email protected]",
        "name": "louis"
      }
    ],
    "date": 1536678253790,
    "host": {
      "email": "[email protected]",
      "name": "louis"
    },
    "name": "OCaml/Reason Meetup!",
    "access": "Public"
  }
]