[⬅ Back to highlights](/README.md)

# Sury PPX

ReScript PPX to generate **Sury** schema from types.

> 🧠 It's 100% opt-in. You can use **Sury** without ppx.

## Table of contents

- [Table of contents](#table-of-contents)
- [Install](#install)
- [Basic usage](#basic-usage)
- [API reference](#api-reference)

## Install

```sh
npm install sury sury-ppx
```

Then update your `rescript.json` config:

```diff
{
  ...
+ "bs-dependencies": ["sury"],
+ "ppx-flags": ["sury-ppx/bin"],
}
```

## Basic usage

```rescript
// 1. Define a type and add @schema attribute
@schema
type rating =
  | @as("G") GeneralAudiences
  | @as("PG") ParentalGuidanceSuggested
  | @as("PG13") ParentalStronglyCautioned
  | @as("R") Restricted
@schema
type film = {
  @as("Id")
  id: float,
  @as("Title")
  title: string,
  @as("Tags")
  tags: @s.default([]) array<string>,
  @as("Rating")
  rating: rating,
  @as("Age")
  deprecatedAgeRestriction: @s.meta({deprecated: true}) option<int>,
}

// 2. Generated by PPX ⬇️
let ratingSchema = S.union([
  S.literal(GeneralAudiences),
  S.literal(ParentalGuidanceSuggested),
  S.literal(ParentalStronglyCautioned),
  S.literal(Restricted),
])
let filmSchema = S.object(s => {
  id: s.field("Id", S.float),
  title: s.field("Title", S.string),
  tags: s.fieldOr("Tags", S.array(S.string), []),
  rating: s.field("Rating", ratingSchema),
  deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.meta({deprecated: true})),
})

// 3. Parse data using the schema
// The data is validated and transformed to a convenient format
%raw(`{
  "Id": 1,
  "Title": "My first film",
  "Rating": "R",
  "Age": 17
}`)->S.parseOrThrow(filmSchema)
// Ok({
//   id: 1.,
//   title: "My first film",
//   tags: [],
//   rating: Restricted,
//   deprecatedAgeRestriction: Some(17),
// })

// 4. Transform data back using the same schema
{
  id: 2.,
  tags: ["Loved"],
  title: "Sad & sed",
  rating: ParentalStronglyCautioned,
  deprecatedAgeRestriction: None,
}->S.decodeOrThrow(~from=filmSchema, ~to=S.unknown)
// Ok(%raw(`{
//   "Id": 2,
//   "Title": "Sad & sed",
//   "Rating": "PG13",
//   "Tags": ["Loved"],
//   "Age": undefined,
// }`))

// 5. Use schema as a building block for other tools
// For example, create a JSON schema and use it for OpenAPI generation
let filmJSONSchema = filmSchema->S.toJSONSchema
```

> 🧠 Read more about schema usage in the _[ReScript Schema for ReScript users](/docs/rescript-usage.md)_ documentation.

## API reference

### `@schema`

**Applies to**: type declarations, type signatures

Indicates that a schema should be generated for the given type.

### `@s.matches(S.t<'value>)`

**Applies to**: type expressions

Specifies custom schema for the type.

```rescript
@schema
type t = @s.matches(S.string->S.url) string

// Generated by PPX ⬇️
let schema = S.string->S.url
```

### `@s.null`

**Applies to**: option type expressions

Tells to use `S.nullAsOption` for the option schema constructor.

```rescript
@schema
type t = @s.null option<string>

// Generated by PPX ⬇️
let schema = S.nullAsOption(S.string)
```

### `@s.nullable`

**Applies to**: option type expressions

Tells to use `S.nullableAsOption` for the option schema constructor.

```rescript
@schema
type t = @s.nullable option<string>

// Generated by PPX ⬇️
let schema = S.nullableAsOption(S.string)
```

### `@s.default('value)`

**Applies to**: type expressions

Wraps the type expression schema into an option with the provided default value.

```rescript
@schema
type t = @s.default("Unknown") string

// Generated by PPX ⬇️
let schema = S.option(S.string)->S.Option.getOr("Unknown")
```

It might also be used together with `@s.null`:

```rescript
@schema
type t = @s.null @s.default("Unknown") string

// Generated by PPX ⬇️
let schema = S.nullAsOption(S.string)->S.Option.getOr("Unknown")
```

### `@s.defaultWith(unit => 'value)`

**Applies to**: type expressions

Wraps the type expression schema into an option with callback to get the default value.

```rescript
@schema
type t = @s.defaultWith(() => []) array<string>

// Generated by PPX ⬇️
let schema = S.option(S.array(S.string))->S.Option.getOrWith(() => [])
```

> 🧠 The same as `@s.default` it might be used together with `@s.null`

### `@s.meta(S.meta)`

**Applies to**: type expressions

Adds metadata to the generated schema.

```rescript
@schema
type t = @s.meta({description: "A useful bit of text, if you know what to do with it."}) string

// Generated by PPX ⬇️
let schema = S.string->S.meta({description: "A useful bit of text, if you know what to do with it."}})
```

This can be useful for documenting, JSON Schema creation, etc.

```rescript
schema->S.toJSONSchema
// {
//   "type": "string",
//   "description": "A useful bit of text, if you know what to do with it."
// }
```

Read more about `S.meta` in the [Sury documentation](../../docs/js-usage.md#meta).

### Other expression attributes

There's also support for `@s.noValidation`, `@s.strict`, `@s.strip`, `@s.deepStrict` and `@s.deepStrip` attributes. Create a PR if you need more.
