@greenlabs/ppx-spice
Version:
ReScript PPX which generate JSON (de)serializer
233 lines (180 loc) • 6.13 kB
Markdown
# AGENTS.md
This document provides guidelines for AI coding agents working on the ppx_spice project.
## Project Overview
ppx_spice is a ReScript PPX that generates JSON (de)serializers. It is written in OCaml
and uses ppxlib for AST manipulation. The PPX processes ReScript type definitions annotated
with `@spice` and generates corresponding `_encode` and `_decode` functions.
## Build Commands
All OCaml build commands must be run from the `src/` directory:
```bash
# Navigate to source directory
cd src
# Create opam switch (first time setup)
opam switch create spice 4.14.0
# Install dependencies
opam install . --deps-only
# Build the PPX
dune build
# Build with static linking (for releases)
dune build --profile static
# Clean build artifacts
dune clean
```
## Test Commands
Tests are written in ReScript and located in the `test/` directory:
```bash
# Navigate to test directory
cd test
# Install dependencies
pnpm install
# Build ReScript code
pnpm res:build
# Clean and rebuild
pnpm res:clean && pnpm res:build
# Run all tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run a single test file
pta './test/__tests__/spec/<test_name>_test.mjs' | tap-difflet
# Example: Run only records tests
pta './test/__tests__/spec/records_test.mjs' | tap-difflet
```
### Development Workflow
1. Make changes to OCaml code in `src/ppx/`
2. Run `dune build` in `src/`
3. Navigate to `test/` and run `pnpm res:clean && pnpm res:build`
4. Run `pnpm test` to verify changes
## Project Structure
```
ppx_spice/
├── src/ppx/ # OCaml PPX implementation
│ ├── ppx_spice.ml # Entry point, registers transformation
│ ├── codecs.ml # Primitive type codec generation
│ ├── records.ml # Record encoder/decoder generation
│ ├── variants.ml # Variant type handling
│ ├── polyvariants.ml # Polymorphic variant handling
│ ├── structure.ml # Implementation (.ml) processing
│ ├── signature.ml # Interface (.mli) processing
│ └── utils.ml # Shared utilities
├── src/bin/bin.ml # Executable entry point
├── test/src/ # ReScript test source types
└── test/test/__tests__/ # Test specifications
```
### PPX Integration (rescript.json)
```json
{
"ppx-flags": ["@greenlabs/ppx-spice/ppx"],
"warnings": { "error": true, "number": "-48" }
}
```
## Code Style Guidelines
### OCaml Code Style
#### Formatting
- Use 2 spaces for indentation
- Use LF line endings
- Insert final newline at end of files
- Maximum line length: ~100 characters (soft limit)
#### Imports
Always open modules in this order at the top of each file:
```ocaml
open Ppxlib
open Parsetree
open Ast_helper
open Utils (* Local utilities last *)
```
#### Type Definitions
- Define types at the top of the module, after opens
- Use record types for complex data structures:
```ocaml
type parsed_decl = {
name : string;
key : expression;
field : expression;
codecs : expression option * expression option;
}
```
#### Naming Conventions
- Module names: PascalCase (`Records`, `Variants`, `Polyvariants`)
- Function names: snake_case (`generate_encoder`, `parse_decl`)
- Type names: snake_case (`parsed_decl`, `generator_settings`)
- Variables: snake_case (`type_name`, `param_names`)
- Constants/suffixes: snake_case with descriptive names
- `encoder_func_suffix = "_encode"`
- `decoder_func_suffix = "_decode"`
#### Error Handling
- Use `Result` type for operations that can fail
- Use `fail` function from `Utils` to raise location-aware errors:
```ocaml
let fail loc message = Location.raise_errorf ~loc "%s" message
(* Usage *)
| Ptyp_any -> fail ptyp_loc "Can't generate codecs for `any` type"
```
#### Pattern Matching
- Prefer exhaustive pattern matching
- Use `_` prefix for intentionally unused variables
- Group related cases together:
```ocaml
match ptype_kind with
| Ptype_abstract -> (* handle abstract *)
| Ptype_variant decls -> (* handle variant *)
| Ptype_record decls -> (* handle record *)
| _ -> fail ptype_loc "This type is not handled by spice"
```
#### PPX-Specific Patterns
- Use ppxlib metaquot for AST construction: `[%expr ...]`, `[%pat? ...]`, `[%type: ...]`
- Always attach `res.arity` attribute for ReScript function compatibility
- Use `Utils.expr_func ~arity:1` for single-argument functions
### ReScript Test Code Style
#### Test Structure
```rescript
open Zora
zoraBlock("descriptive test block name", t => {
// Setup
let sampleJson = ...
let expected = ...
// Execute
let result = SomeModule.function(input)
// Assert
t->test("assertion description", async t => {
t->equal(result, expected, "message")
})
})
```
#### Naming
- Test files: `<module>_test.res` (e.g., `records_test.res`)
- Test blocks: Descriptive phrases in quotes
## Dependencies
### OCaml Dependencies (from opam)
- `ocaml` >= 4.14.0, <= 4.14.2
- `dune` >= 2.8
- `ppxlib` = 0.28.0
### ReScript Dependencies (test only)
- `rescript` ^12.0.0
- `@dusty-phillips/rescript-zora` ^4.0.0
## Common Patterns
### Generated Function Names
The PPX generates functions with these naming patterns:
- Encoder: `<type_name>_encode`
- Decoder: `<type_name>_decode`
### Attribute Handling
```ocaml
(* Check for attribute *)
match get_attribute_by_name attributes "spice.key" with
| Ok (Some attr) -> (* attribute present *)
| Ok None -> (* attribute absent *)
| Error s -> fail loc s
```
### Codec Generation Flow
1. `ppx_spice.ml` registers the transformation
2. `structure.ml` / `signature.ml` process type declarations
3. `codecs.ml` handles primitive types
4. `records.ml`, `variants.ml`, `polyvariants.ml` handle complex types
5. Generated AST is returned to the compiler
## CI/CD
GitHub Actions workflows are in `.github/workflows/`:
- `build_linux.yml` - Linux build with Alpine container
- `build_macos.yml` - macOS build
- `build_windows.yml` - Windows build
- `publish.yml` - NPM package publishing
Builds use OCaml 4.14.2 with static linking for portable binaries.