diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..8a6a31b26 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; if [[ \"$f\" == *.res ]]; then npx rescript format \"$f\" > /dev/null; fi; } 2>/dev/null || true", + "statusMessage": "Formatting ReScript..." + } + ] + } + ] + } +} diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 000000000..cddf8c8c0 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "wallaby": { + "command": "node", + "args": ["~/.wallaby/mcp/"] + } + } +} diff --git a/.cursor/skills/testing-wallaby/SKILL.md b/.cursor/skills/testing-wallaby/SKILL.md new file mode 100644 index 000000000..07f072ce5 --- /dev/null +++ b/.cursor/skills/testing-wallaby/SKILL.md @@ -0,0 +1,63 @@ +--- +name: testing-wallaby +description: Debug and fix failing tests using Wallaby.js MCP tools. Use when investigating test failures, debugging test errors, inspecting runtime values, checking code coverage, or updating snapshots. +--- + +# Testing with Wallaby + +Use Wallaby MCP tools as the primary way to work with tests. Fall back to terminal only if Wallaby is unavailable. + +## Debug Workflow + +### 1. Identify failures + +- **Broad**: `wallaby_failingTests` — all failing tests in the project +- **By file**: `wallaby_failingTestsForFile` — failures related to a specific source or test file +- **By line**: `wallaby_failingTestsForFileAndLine` — failures covering a specific line + +Each returns test name, test ID, errors with stack traces, runtime logs, and coverage percentage. + +### 2. Narrow down with coverage + +- `wallaby_coveredLinesForFile` — which lines a file has covered, optionally filtered by test ID +- `wallaby_coveredLinesForTest` — all files/lines a specific test covers + +Use coverage to find the implementation code a failing test actually executes. + +### 3. Inspect runtime values + +- `wallaby_runtimeValues` — value of an expression at a file/line across all tests +- `wallaby_runtimeValuesByTest` — value of an expression at a file/line for a specific test + +Required params: `file`, `line` (1-based, count blank lines), `lineContent`, `expression`. For `runtimeValuesByTest`, also `testId`. + +### 4. Fix the code + +Apply the fix based on evidence from steps 1-3. + +### 5. Verify the fix + +- `wallaby_testById` — re-check a specific test by its ID +- `wallaby_failingTests` — confirm no remaining failures + +Iterate steps 3-5 until the test passes. + +### 6. Update snapshots (if needed) + +- `wallaby_updateTestSnapshots` — update snapshots for one test (by test ID) +- `wallaby_updateFileSnapshots` — update all snapshots in/covering a file +- `wallaby_updateProjectSnapshots` — update every snapshot in the project + +## Discovery Tools + +Use these when exploring tests, not just debugging failures: + +- `wallaby_allTests` — list every test in the project +- `wallaby_allTestsForFile` — tests related to a specific file +- `wallaby_allTestsForFileAndLine` — tests covering a specific file and line + +## Principles + +- Always cite runtime values and coverage data to justify conclusions. +- Explain reasoning step by step. +- Keep iterating with updated Wallaby data until the test passes. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..12d28b449 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# Sury Architecture + +## Schema Input and Output Types + +A schema represents two types: Input and Output. + +### Example 1: Same Input and Output + +```typescript +S.string +// Input: string +// Output: string +``` + +### Example 2: Different Input and Output + +```typescript +S.schema({ + foo: S.string.with(S.to, S.number) +}) +// Input: { foo: string } +// Output: { foo: number } +``` + +The input and output differ because nested items have transformations, even though the schema itself does not have a `.to` property. + +## Modifying a Schema + +When modifying a schema, the modification applies to the output type. + +```typescript +S.schema({ + foo: S.string.with(S.to, S.number) +}).with(S.refine, () => {...}) +``` + +Since this schema does not have `.to`, `inputRefiner` and `refiner` must be stored separately to support `S.reverse`. Every schema should be reversible from Input→Output to Output→Input, unless explicitly prevented. + +For modifications like `name` or built-in refinements that do not affect nested items, they apply to both input and output without differentiation. + +## Decode Function + +The decode function is created from a single schema and transforms the schema's Input to Output. When multiple schemas are joined by the `.to` property, they are automatically combined into a single transformation pipeline. + +## Schema Properties and Execution Order + +Schema properties are executed in the following order: + +1. **decoder** - If input val differs from the schema, decode it to the schema's input type. May skip directly to schema output if there is no inputRefiner. + +2. **inputRefiner** - Custom validations on the input part of the schema value. + +3. **decoder** - Decodes input to output for the current schema. Typically required to decode nested items such as object fields. + +4. **refiner** - Custom validations on the output part of the schema value. + +### If Schema Has `.to` Property + +5. **parser** - Custom transformation logic to the `.to` schema. The serializer is the reverse of parser. + +### If There Is No Parser + +5. **encoder** - Transformation logic from the current schema's output to the `.to` schema's input. + +6. **.to.decoder** - Starts the cycle from the beginning with the `.to` schema. + +## Reversal with S.reverse + +`S.reverse` swaps: + +- `inputRefiner` ↔ `refiner` +- `parser` ↔ `serializer` +- Reverses the `.to` chain direction + +## Async Support + +Every transformation may return an async value. To continue the transformation chain: + +1. Append `.then()` and continue the logic in the callback function. +2. For nested items (e.g., object fields, array items), create a promise that collects all inner items with `Promise.all()`. + +## Val + +The `val` represents a value at a specific point in time during compilation. Each `val` reflects a specific value type at that moment. + +Key properties: + +- `schema` - The actual type of the value at this point +- `expected` - The schema to build decoder for +- `var` - Returns the variable name in generated code +- `inline` - The value as an inline code expression +- `path` - Current location in the data structure (for error messages) + +Transformation tracking (relative to `.prev`): + +- `prev` - The previous val in the chain, indicating where this value originated from +- `code` - Generated code describing the transformation from `.prev` to this val +- `validation` - Type check condition from `.prev` (e.g., `typeof x === "string"`). Different from custom refiners. + +This design allows tracing back through the transformation history, where each step records what code was generated and what validations were applied to get from the previous state to the current one. + +- `B.refine` allows to modify the value, by cloning, while keeping the var allocation link. + +- `skipTo` is used to abort the parse after finishing current decoder. Ideally to get rid of it and use `val.expected` instead. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4869e9f88..c16d56bb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,127 @@ The following steps will get you setup to contribute changes to this repo: 5. Run `pnpm res` to run ReScript compiler 6. Run `pnpm test` for tests or use Wallaby.js +## Architecture + +This section describes the internal architecture of Sury to help with understanding and contributing to the codebase. + +### Core Concepts + +#### Schema (internal type) + +The internal representation of a type schema, containing: + +- `tag`: Type identifier (e.g., `stringTag`, `objectTag`, `arrayTag`) +- `decoder`: Builder function for input validation (type checking) +- `encoder`: Builder function for converting from different schema types +- `parser`: Builder function for transformations after decoding (used by `S.shape`, `S.to`) +- `serializer`: Builder function for reverse transformations +- `to`: Target schema for transformations (set by `S.shape`, `S.to`) +- `from`: Path array indicating where this value comes from in shaped schemas +- `properties`: For object schemas, a dict of field name to schema +- `items`: For array/tuple schemas, an array of item schemas + +#### Builder + +A builder is a function with signature `(~input: val, ~selfSchema: internal) => val`. Builders generate JavaScript code at compile time by manipulating `val` objects. They are created using `Builder.make`: + +```rescript +let myBuilder = Builder.make((~input, ~selfSchema) => { + // Generate code and return output val + let output = input->B.val(`someTransform(${input.var()})`, ~schema=selfSchema) + output +}) +``` + +#### Val (Value) + +A compilation-time representation of a value being processed. Key fields: + +- `inline`: The generated code expression (e.g., `i["foo"]`, `v0`) +- `var()`: Function to allocate/retrieve a variable name (use when value is referenced multiple times) +- `schema`: The schema of the current value +- `expected`: The schema we're trying to parse/convert into +- `from`: Link to the input val (for code merging) +- `code`: Generated code statements +- `validation`: Optional validation function to generate type checks +- `skipTo`: When `Some(true)`, prevents `parse` from following the `.to` chain +- `global`: Shared compilation context containing: + - `embeded`: Array of embedded values (functions, constants) accessible as `e[n]` + - `varCounter`: Counter for generating unique variable names + +### Compilation Flow + +When a schema operation is compiled (e.g., `parseOrThrow`), the following happens: + +``` +Input Schema + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ parse(val) function │ +│ │ +│ 1. Encoder (if input.schema !== expected) │ +│ - Converts between different schema types │ +│ │ +│ 2. Decoder (always runs) │ +│ - Validates input type (e.g., typeof === "string") │ +│ - Generates validation code │ +│ │ +│ 3. Parser (if expected.parser exists) │ +│ - Applies transformations (S.transform, S.shape) │ +│ │ +│ 4. Recursive parse (if expected.to exists) │ +│ - Follows transformation chain │ +│ - Skipped if val.skipTo === Some(true) │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +Output Val with merged code + │ + ▼ +B.merge() → JavaScript function string +``` + +### Code Generation Example + +For `S.object(s => s.field("foo", S.string))`: + +```javascript +// Generated parse function: +(i) => { + if (typeof i !== "object" || !i) { + e[0](i); + } // Object validation + let v0 = i["foo"]; // Field access + if (typeof v0 !== "string") { + e[1](v0); + } // String validation + return v0; // Return parsed value +}; +``` + +Where: + +- `i` is the input argument +- `e` is the embedded values array (error throwers, transformers) +- `v0`, `v1`, etc. are allocated variables + +### Key Functions + +- `parse(val)`: Main compilation function that walks through encoder → decoder → parser → to chain +- `B.merge(val)`: Collects all generated code from the val chain into a single string +- `B.Val.cleanValFrom(val)`: Creates a clean copy of val for new code generation while preserving variable binding +- `B.embed(val, value)`: Embeds a runtime value (function, object) and returns reference like `e[0]` + +### Shaped Schemas (S.shape, S.object with definer) + +Shaped schemas use a proxy-based approach to track how values are used: + +1. During schema definition, field accesses are tracked via `proxifyShapedSchema` +2. Each accessed field gets `from` set to its path (e.g., `["foo"]` for `s.field("foo", ...)`) +3. During parsing, `shapedParser` traverses the target structure and maps values from input +4. During serialization, `shapedSerializer` builds an accumulator (`acc`) that maps output paths to input vals, then `getShapedSerializerOutput` reconstructs the original structure + ## PPX ### With Dune diff --git a/IDEAS.md b/IDEAS.md index 488fa35c4..57d8645f8 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -1,12 +1,107 @@ # Ideas draft -## Alpha.4 - -- Use built-in JSON String transformation for JSON String output in `S.compile` -- Fix https://github.com/DZakh/sury/issues/150 -- Add `S.brand` for TS API -- Update Standard Schema error message to only include reason part -- Fix refinement on union schema which also uses `S.to` +## Alpha.5 + +- Add `S.date` — standalone Date instance schema. Validates `instanceof Date` and rejects Invalid Date. Unlike `S.isoDateTime` (which validates ISO 8601 UTC strings) and `S.string->S.to(S.date)` (which decodes ISO strings into Date objects), `S.date` directly validates existing Date instances. +- Add `S.isoDateTime` and `S.enableIsoDateTime` — standalone string schema that validates ISO 8601 UTC datetime strings (no timezone offsets, arbitrary sub-second precision). Reuses the built-in string decoder; the regex lives inside `enableIsoDateTime` so it is tree-shaken from the bundle when unused. Replaces the removed `S.datetime` for the "validate an ISO string" use case — for string↔Date conversion use `S.string->S.to(S.date)`. +- Added `S.compactColumns` - transforms columnar data (`[[a1,a2], [b1,b2]]`) to/from row objects (`[{foo:a1,bar:b1}, {foo:a2,bar:b2}]`) +- TypeScript: Use `S.encoder(schema)` for encoding (replaces internal `reverseConvertOrThrow`) +- `S.compactColumns` type is `Schema` +- `S.toExpression` now shows proper type for `S.compactColumns` without `S.to` (e.g., `"string[][]"`) +- Changed `S.refine` from callback-based to boolean-returning. TS/JS: `(value) => boolean` with optional `{ error?: string, path?: string[] }` options. ReScript: `'value => bool` with optional `~error: string=?` and `~path: array=?` labeled arguments. The check returns `true` when valid, `false` when invalid. +- TS/JS API: Renamed `S.asyncParserRefine` to `S.asyncDecoderAssert`. Removed `EffectCtx` (`s`) parameter — throw directly to signal failure instead of calling `s.fail()` +- TS API: Removed `S.transform` in favor of `S.to` +- Add `S.uint8Array` and `S.enableUint8Array` +- Updated `InvalidType` error code to include the received schema +- Updated internal representation of object schema - removed `items` fields. Updated internalt representation of tuple schema - `items` field is now an array of schemas instead of array of items. The `item` type is removed. +- Removed `Failed parsing/converting/asserting` when the error is at root +- Renamed `Failed parsing/converting/asserting at path` to `Failed at path` +- ReScript: Removed `schema` from `S.transform` and `S.refine` context +- ReScript: + - `S.ErrorClass.constructor` -> `S.Error.make` - now accepts full error details and doesn't require `flag` parameter + - `S.ErrorClass.t` -> `S.Error.class` + - `S.ErrorClass.value` -> `S.Error.class` + - Reworked error code and added `S.Error.classify` to turn error into a variant of all possible error codes +- All errors thrown in transform/refine are wrapped in `SuryError` +- TS: Updated `S.Error` type to use variants instead of code property +- ReScript: `S.null` -> `S.nullAsOption` +- Updated union conversion logic - it now always performs exhaustive validation +- Encoding to JSON now strips undefined fields +- JSON decoder now automatically decodes non-JSON types (e.g. `S.date`, `S.bigint`) to string via their encoder, instead of special-casing each type. Schemas with a string encoder (like `S.date` → `toISOString()`) work with `S.json`/`S.jsonString` out of the box. +- Renamed error code `unsupported_conversion` → `unsupported_decode` and variant `UnsupportedConversion` → `UnsupportedDecode` +- Error message for unsupported decode now reads: `"Can't decode X to Y. Use S.to to define a custom decoder"` +- `S.port`, `S.email`, `S.uuid`, `S.cuid`, `S.url` are now standalone tree-shakeable schemas (like `S.isoDateTime`), each requiring an `enable*()` call. +- Added `Email`, `Uuid`, `Cuid`, `Url` to `StringFormat` type. +- Removed `S.String.Refinement` module and `S.String.refinements` accessor. Use schema `pattern`/`format`/`errorMessage` fields directly. +- Added `errorMessage` field to `S.meta` for overriding validation messages per-use: `S.email.with(S.meta, { errorMessage: { format: "Custom" } })`. Supports `_` as a catch-all key. Empty `{}` deletes the field. +- Added typed `SchemaErrorMessage` type with fields for each constraint key (`format`, `type`, `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`, `maxItems`, `pattern`, `_`). +- Renamed `errorMessages` → `errorMessage` (singular) on schema types. + +### Union coercion + +When compiling `source -> targetUnion` (via `S.to` or reversal), matching uses a three-tier algorithm based on the source's derived tag. The derived tag is the tag at compile time, which may be narrower than the tag set at schema creation (upstream transformations can narrow it). When the source is itself a union, the algorithm is applied independently for each source variant. + +1. **Same-tag group.** Collect target variants sharing the source's tag. If non-empty, match only within this group: target variants with a matching `const`/`format` (string literals, `Int32`, etc.) are attempted first in target-union order, then remaining catch-all same-tag variants in target-union order. Variants with a different tag are never tried from here — if all branches in the group fail, the match errors. +2. **Nullish bridge.** Applied only when tier 1's group is empty. If the source tag is `null` or `undefined`, use the opposite nullish target variant if present, exclusively. +3. **Fallback.** Applied only when tiers 1 and 2 are both empty. Attempt to build a decoder for every target variant in target-union order. This is where cross-type coercions live: `number`/`bigint` → `string` via `""+i`, `string` → `number` via `+i`, `string` → `bigint` via `BigInt(i)`, stringified-const matches like `"null" → null`, etc. + +Worked example — `S.union([S.bigint, S.float, S.nullLiteral])->S.to(S.union([S.string, S.unit]))`: + +Forward: + +- `123n` → `"123"` (tier 3: bigint → string) +- `123.12` → `"123.12"` (tier 3: float → string) +- `null` → `undefined` (tier 2: nullish bridge) + +Reverse: + +- `"null"` → `null` (tier 3: stringified-const literal match) +- `undefined` → `null` (tier 2: nullish bridge) +- `"123"` → `123n` (tier 3: bigint attempted first by target order; parse succeeds) +- `"123.12"` → `123.12` (tier 3: bigint parse throws, falls through to float) +- `"abc"` → error (tier 3: no variant's decoder succeeds) + +Identity is strictly preferred over available coercion. For `S.union([S.string, S.bigint])->S.to(S.union([S.float, S.string]))`: + +- `"123"` → `"123"` (tier 1: `string` matches `string`, never coerced to `float` even though a `float` target exists) +- `123n` → `"123"` (tier 3: no `bigint` target, falls through to the `string` variant via `""+i`) + +To opt into `string → float` when a `string` target also exists, the user writes the transform into a variant explicitly: `S.union([S.string->S.to(S.float), S.string])` as the target. The transform variant is const/format-refined relative to the catch-all `string` and matches first within tier 1. + +### TS + +- `S.parseOrThrow` -> `S.parser(schema)(data)` +- `S.parseJsonOrThrow` -> `S.decoder(S.json, schema)(data)` +- `S.parseJsonStringOrThrow` -> `S.decoder(S.jsonString, schema)(data)` +- `S.parseAsyncOrThrow` -> `S.asyncParser(schema)(data)` +- `S.convertOrThrow` -> `S.decoder(schema)(data)` +- `S.convertToJsonOrThrow` -> `S.decoder(schema, S.json)(data)` +- `S.convertToJsonStringOrThrow` -> `S.decoder(schema, S.jsonString)(data)` +- `S.reverseConvertOrThrow` -> `S.encoder(schema)(data)` +- `S.reverseConvertToJsonOrThrow` -> `S.encoder(schema, S.json)(data)` +- `S.reverseConvertToJsonStringOrThrow` -> `S.encoder(schema, S.jsonString)(data)` +- `S.assertOrThrow` -> `S.assert(schema, data)` +- `S.compile` -> `S.decoder` or `S.encoder` or `S.parser` + +### ReScript + +| Before | After | +|---|---| +| `S.parseOrThrow(data, schema)` | `S.parseOrThrow(data, ~to=schema)` | +| `S.parseAsyncOrThrow(data, schema)` | `S.parseAsyncOrThrow(data, ~to=schema)` | +| `S.parseJsonOrThrow(data, schema)` | `S.decodeOrThrow(data, ~from=S.json, ~to=schema)` | +| `S.parseJsonStringOrThrow(data, schema)` | `S.decodeOrThrow(data, ~from=S.jsonString, ~to=schema)` | +| `S.assertOrThrow(data, schema)` | `S.assertOrThrow(data, ~to=schema)` | +| — | `S.assertAsyncOrThrow(data, ~to=schema)` | +| `S.reverseConvertOrThrow(data, schema)` | `S.decodeOrThrow(data, ~from=schema, ~to=S.unknown)` | +| `S.reverseConvertToJsonOrThrow(data, schema)` | `S.decodeOrThrow(data, ~from=schema, ~to=S.json)` | +| `S.reverseConvertToJsonStringOrThrow(data, schema)` | `S.decodeOrThrow(data, ~from=schema, ~to=S.jsonString)` | +| `S.reverseConvertToJsonStringOrThrow(data, schema, ~space=2)` | `S.decodeOrThrow(data, ~from=schema, ~to=S.jsonStringWithSpace(2))` | +| `S.convertOrThrow(data, schema)` | `S.decoder1(schema)(data)` | +| `S.compile(schema, ~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=true)` | `S.parser(~to=schema)` | +| `S.compile(schema, ~input=Any, ~output=Value, ~mode=Async, ~typeValidation=true)` | `S.asyncParser(~to=schema)` | +| `S.compile(schema, ~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=false)` | `S.decoder(~from=schema, ~to=S.unknown)` | +| `S.compile(schema, ~input=Value, ~output=Unknown, ~mode=Async, ~typeValidation=false)` | `S.asyncDecoder(~from=schema, ~to=S.unknown)` | ## v11 @@ -14,46 +109,24 @@ - Add `promise` type and `S.promise` (instead of async flag internally) -```diff -const userSchema = S.schema({ - id: S.string, - name: S.string -}) - -S.parseOrThrow(data, userSchema) - -- S.parseJsonOrThrow(data, userSchema) -+ S.convertFromOrThrow(data, S.json, userSchema) - -- S.parseJsonStringOrThrow(data, userSchema) -+ S.convertFromOrThrow(data, S.jsonString, userSchema) - -- S.reverseConvertOrThrow(user, userSchema) -+ S.convertOrThrow(user, S.reverse(userSchema)) - -- S.reverseConvertToJsonOrThrow(user, userSchema) -+ S.convertFromOrThrow(user, userSchema, S.json) +TODO: -- S.reverseConvertToJsonStringOrThrow(user, userSchema) -+ S.convertFromOrThrow(user, userSchema, S.jsonString) +Test null<> in ppx -- S.reverseConvertToJsonStringOrThrow(user, userSchema, 2) -+ S.convertFromOrThrow(user, userSchema, S.jsonStringWithSpace(2)) - -S.convertOrThrow(data, userSchema) - -- S.convertToJsonOrThrow(data, userSchema) -+ S.convertOrThrow(data, userSchema.with(S.to, S.json)) +``` +// Test that refinement works correctly with reverse -- S.convertToJsonStringOrThrow(data, userSchema) -+ S.convertOrThrow(data, userSchema.with(S.to, S.jsonString)) +S.reverse(S.schema({ + foo: S.string->S.to(S.number) +})->S.refine(value => value.foo > 0)) ``` +### TS operation functions + - rename `serializer` to reverse parser ? - Make `foo->S.to(S.unknown)` stricter ?? - Add `S.to(from, target, parser, serializer)` instead of `S.transform`? -- Remove `s.fail` with `throw new Error` - Make built-in refinements not work with `unknown`. Use `S.to` (manually & automatically) to deside the type first - Better inline empty recursive schema operations (union convert) - Don't iterate over JSON value when it's `S.json` convert without parsing @@ -66,19 +139,53 @@ S.convertOrThrow(data, userSchema) - Make `S.record` accept two args - Update docs +### Known bugs left over from the validation refactor (`val.validation: array`) + +- **Union discriminant hoists refinement checks with `&&` instead of `;`.** + Now that refinements are structured checks, the union item merge loop + hoists all checks on a val via `andJoinChecks`, fusing type checks and + refinement checks into one `&&`-joined condition with a single error throw. + This causes two problems: (1) `typeof==="string"&&length===N` shares one + error instead of separate type/refinement errors, and (2) same-type items + with different refinements (e.g. `S.union([S.string->S.email, S.string->S.url])`) + lose per-item error messages. Fix: split hoisted checks by `fail` reference — + first group (type checks) → discriminant condition, remaining groups + (refinement checks) → body code as `cond||fail;`. For same-type items with + different refinements, use if/else if dispatch on the refinement cond instead + of try/catch. Failing regression tests in `S_union_test.res`. +- **`noValidation` on a literal inside a union silently breaks dispatch.** + `literalDecoder` short-circuits when `expectedSchema.noValidation` is set + and emits no check at all, so there's nothing for the union discriminant + hoister to lift — that case becomes a catch-all. Fix: either emit the + equality check regardless of `noValidation` when the val ends up inside a + union, or reject `S.noValidation` on a literal-in-union at schema + construction time. Failing regression test: `S_noValidation_test.res › + Union dispatch still works when a case has noValidation`. +- **`err.received` is wrong for refine-chain vals on type failures.** Because + `B.refine` sets `~schema=prev.expected`, `val.schema` on a refined val + equals the target schema, and `failInvalidType` reads `val.schema` for + `received`. So `err.received === err.expected` on a primitive type failure. + User-visible reason text is unaffected (it uses `input->stringify`) but + programmatic consumers reading `err.received` get the target schema instead + of the source type. Fix: either have the fail function reach through + `val.prev.schema` (with a comment on the invariant that validation-owning + vals always have a prev) or stop mutating `val.schema` to the target in + `refine` and walk the chain differently for "Expected X" messages. + FIXME is tagged at `Sury.res:failInvalidType`. + ## v11 initial - Add `s.parseChild` to EffectContext ??? -- Start using rescript v12 (Fix unboxed types in JSONSchema module) - Support arrays for `S.to` - Remove fieldOr in favor of optionOr? - Allow to pass custom error message via `.with` - Make S.to extensible -- Add S.Date (S.instanceof) and remove S.datetime +- ~~Add S.Date (S.instanceof) and remove S.datetime~~ (S.date added; S.datetime kept for backward compat) - Add refinement info to the tagged type ## v??? +- `S.promise: S.t<'value> => S.t>` and `S.await: S.t> => S.t<'value>` - Remove `S.deepStrict` and `S.deepStrip` in favor of `S.deep` (if it works) - Make S.serializeToJsonString super fast - Somehow determine whether transformed or not (including shape) diff --git a/docs/js-usage.md b/docs/js-usage.md index 91873f730..55931afd4 100644 --- a/docs/js-usage.md +++ b/docs/js-usage.md @@ -16,13 +16,14 @@ - [Defining schemas](#defining-schemas) - [Advanced schemas](#advanced-schemas) - [Strings](#strings) + - [Custom error messages](#custom-error-messages) - [ISO datetimes](#iso-datetimes) - [Numbers](#numbers) - [Optionals](#optionals) - [Nullables](#nullables) - [Nullish](#nullish) - [Objects](#objects) - - [Literal shorthand](#literal-shorthand) + - [Literal fields](#literal-fields) - [Advanced object schema](#advanced-object-schema) - [`strict`](#strict) - [`strip`](#strip) @@ -37,6 +38,8 @@ - [Records](#records) - [JSON](#json) - [JSON string](#json-string) +- [Date](#date) +- [ISO DateTime](#iso-datetime) - [Instance](#instance) - [Meta](#meta) - [Custom schema](#custom-schema) @@ -76,7 +79,7 @@ Let's start with a simple object schema for the purpose of this guide. I use the ```ts import * as S from "sury"; // 4.3 kB (min + gzip) -const Player = S.schema({ +const playerSchema = S.schema({ username: S.string, xp: S.number, }); @@ -89,50 +92,50 @@ const Player = S.schema({ The most basic use-case for a schema is to parse unknown data. If the data is valid, the function will return a strongly-typed deep clone of the input. (With stripped fields by default) ```ts -S.parseOrThrow({ username: "billie", xp: 100 }, Player); +S.parser(playerSchema)({ username: "billie", xp: 100 }); // => returns { username: "billie", xp: 100 } ``` If the data is invalid, the function will throw an error. ```ts -S.parseOrThrow({ username: "billie", xp: "not a number" }, Player); -// => throws S.Error: Failed parsing at ["xp"]: Expected number, got string +S.parser(playerSchema)({ username: "billie", xp: "not a number" }); +// => throws S.Error: Failed at ["xp"]: Expected number, got string ``` **Sury** API explicitly tells you that it might throw an error. If you need you can catch it and perform `err instanceof S.Error` check. But **Sury** provides a convenient API which does it for you: ```ts const result = S.safe(() => - S.parseOrThrow({ username: "billie", xp: "not a number" }, Player) + S.parser(playerSchema)({ username: "billie", xp: "not a number" }) ); // Or for async operations: const result = await S.safeAsync(() => - S.parseAsyncOrThrow({ username: "billie", xp: "not a number" }, Player) + S.asyncParser(playerSchema)({ username: "billie", xp: "not a number" }) ); // The result type is a discriminated union, so you can handle both cases conveniently: if (!result.success) { result.error; // handle error } else { - result.data; // do stuff + result.value; // do stuff } ``` -> 🧠 Besides `parseOrThrow` there are also built-in operations to transform the data without validation, assert without allocating output, serialize back to the initial format and more. If somebody is missing in built-in operations, you can use `S.compile` to create a custom one. +> 🧠 Besides `parser` there are also built-in operations to transform the data without validation, assert without allocating output, serialize back to the initial format and more. If somebody is missing in built-in operations, you can use `S.compile` to create a custom one. ### Inferred types **Sury** automatically infers the static type from the schema definition. It has a really nice type on hover, which you can extract by using `S.Infer`, `S.Output`, or `S.Input`. ```ts -const Player = S.schema({ +const playerSchema = S.schema({ username: S.string, xp: S.number, }); //? S.Schema<{ username: string; xp: number }, { username: string; xp: number }> -type Player = S.Infer; +type Player = S.Infer; // Use it in your code const player: Player = { username: "billie", xp: 100 }; @@ -143,7 +146,7 @@ const player: Player = { username: "billie", xp: 100 }; If you wonder why the schema needs an `Input` type, it's because **Sury** supports serializing data back to the initial format. ```ts -S.reverseConvertOrThrow({ username: "billie", xp: 100 }, Player); +S.encoder(playerSchema)({ username: "billie", xp: 100 }); // => returns { username: "billie", xp: 100 } ``` @@ -154,7 +157,7 @@ Doesn't look like a big deal, with the example above. But if you have a more com // S.to - for easy & fast coercion // S.shape - for fields transformation // S.meta - with examples in Output format -const User = S.schema({ +const userSchema = S.schema({ USER_ID: S.string.with(S.to, S.bigint), USER_NAME: S.string, }) @@ -181,27 +184,21 @@ const User = S.schema({ // }> // 2. You can use it for parsing Input to Output -S.parseOrThrow( - { - USER_ID: "0", - USER_NAME: "Dmitry", - }, - userSchema -); +S.parser(userSchema)({ + USER_ID: "0", + USER_NAME: "Dmitry", +}); // { id: 0n, name: "Dmitry" } // See how "0" is turned into 0n and fields are renamed // 3. And reverse the schema and use it for parsing Output to Input -S.parseOrThrow( - { - id: 0n, - name: "Dmitry", - }, - S.reverse(userSchema) -); +S.parser(S.reverse(userSchema))({ + id: 0n, + name: "Dmitry", +}); // { USER_ID: "0", USER_NAME: "Dmitry" } // Just use `S.reverse` and get a full-featured schema with switched `Output` and `Input` types -// Note: You can use `S.reverseConvertOrThrow(data, schema)` if you don't need to perform validation +// Note: You can use `S.encoder(schema)(data)` if you don't need to perform validation ``` ### Performance @@ -209,7 +206,7 @@ S.parseOrThrow( This is not really about usage, but what you should be aware of is that **Sury** will most likely outperform not only other libraries, but also your own hand-rolled validation logic. ```ts -// This is how S.parseOrThrow(data, userSchema) is compiled +// This is how S.parser(userSchema)(data) is compiled (i) => { if (typeof i !== "object" || !i) { e[3](i); @@ -233,7 +230,7 @@ This is not really about usage, but what you should be aware of is that **Sury** ``` ```ts -// This is how S.reverseConvertOrThrow(data, userSchema) is compiled +// This is how S.encoder(userSchema)(data) is compiled (i) => { let v0 = i["id"]; return { USER_ID: "" + v0, USER_NAME: i["name"] }; @@ -261,7 +258,7 @@ console.log( But for better interoperability, you can convert it to the official JSON Schema specification. Let's take the `User` schema from the example above and convert it: ```ts -S.toJSONSchema(User); +S.toJSONSchema(userSchema); // { // type: "object", // additionalProperties: true, @@ -289,14 +286,14 @@ See how all the properties and examples are in the Input format. It's just askin If that's not cool enough for you, you can also turn a JSON Schema into a **Sury** schema: ```ts -S.assertOrThrow( - "example.com", +S.assert( S.fromJSONSchema({ type: "string", format: "email", - }) + }), + "example.com" ); -// Throws S.Error: Failed asserting: Invalid email address +// Throws S.Error: Invalid email address ``` ### Standard Schema @@ -380,6 +377,7 @@ Enable the schemas you need at the project root: ```ts S.enableJson(); S.enableJsonString(); +S.enableUint8Array(); ``` And use them as usual: @@ -392,18 +390,24 @@ And use them as usual: S.json; // JSON string - // Asserts that the input is a valid JSON string S.jsonString; S.jsonStringWithSpace(2); - // Parses JSON string and validates that it's a number // JSON string -> number S.jsonString.with(S.to, S.number); - // Serializes number to JSON string -// number -> JSON string S.number.with(S.to, S.jsonString); + +// Asserts that the input is a Date instance and not Invalid Date +S.date; + +// Asserts that the input is an instance of Uint8Array +S.uint8Array; +// Decodes Uint8Array to utf-8 string +S.uint8Array.with(S.to, S.string); +// Encodes utf-8 string to Uint8Array +S.string.with(S.to, S.uint8Array); ``` ## Strings @@ -414,16 +418,29 @@ S.number.with(S.to, S.jsonString); S.max(S.string, 5); // String must be 5 or fewer characters long S.min(S.string, 5); // String must be 5 or more characters long S.length(S.string, 5); // String must be exactly 5 characters long -S.email(S.string); // Invalid email address -S.url(S.string); // Invalid url -S.uuid(S.string); // Invalid UUID -S.cuid(S.string); // Invalid CUID -S.pattern(S.string, %re(`/[0-9]/`)); // Invalid -S.datetime(S.string); // Invalid datetime string! Expected UTC +S.string.with(S.pattern, /[0-9]/); // Invalid pattern S.trim(S.string); // trim whitespaces ``` +For format-specific string validation, use the standalone schemas: + +```ts +S.enableEmail(); +S.email; // Standalone email schema + +S.enableUrl(); +S.url; // Standalone URL schema + +S.enableUuid(); +S.uuid; // Standalone UUID schema + +S.enableCuid(); +S.cuid; // Standalone CUID schema +``` + +> For ISO 8601 UTC datetime strings use the dedicated standalone `S.isoDateTime` schema — see [ISO datetimes](#iso-datetimes) below. + > ⚠️ Validating email addresses is nearly impossible with just code. Different clients and servers accept different things and many diverge from the various specs defining "valid" emails. The ONLY real way to validate an email address is to send a verification email to it and check that the user got it. With that in mind, Sury picks a relatively simple regex that does not cover all cases. When using built-in refinements, you can provide a custom error message. @@ -433,19 +450,51 @@ S.min(S.string, 1, "String can't be empty"); S.length(S.string, 5, "SMS code should be 5 digits long"); ``` +### Custom error messages + +Built-in refinements accept an optional last argument for a custom error message: + +```ts +S.min(S.string, 5, "Too short"); +S.pattern(S.string, /^\d+$/, "Must be numeric"); +``` + +For standalone schemas or more control, use `S.meta` with the `errorMessage` field: + +```ts +// Override a specific constraint message +S.email.with(S.meta, { errorMessage: { format: "Must be a valid email" } }); + +// Use "_" as a catch-all for any constraint +S.email.with(S.meta, { errorMessage: { _: "Invalid input" } }); + +// Reset error messages (removes all overrides) +schema.with(S.meta, { errorMessage: {} }); +``` + +Available keys: `format`, `type`, `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`, `maxItems`, `pattern`, `_` (catch-all). + ### ISO datetimes -The `S.datetime(S.string)` function has following UTC validation: no timezone offsets with arbitrary sub-second decimal precision. +`S.isoDateTime` is a **standalone** string schema (`S.Schema`) that validates ISO 8601 UTC datetime strings: no timezone offsets allowed, with arbitrary sub-second decimal precision. Because the regex used to validate the input lives inside `S.enableIsoDateTime`, it is tree-shaken from your bundle unless you opt in — call `S.enableIsoDateTime()` once at your project root before using the schema. ```ts -const datetimeSchema = S.datetime(S.string); -// The datetimeSchema has the type S.Schema -// String is transformed to the Date instance +S.enableIsoDateTime(); // ❕ Call at the project root. -S.parseOrThrow("2020-01-01T00:00:00Z", datetimeSchema); // pass -S.parseOrThrow("2020-01-01T00:00:00.123Z", datetimeSchema); // pass -S.parseOrThrow("2020-01-01T00:00:00.123456Z", datetimeSchema); // pass (arbitrary precision) -S.parseOrThrow("2020-01-01T00:00:00+02:00", datetimeSchema); // fail (no offsets allowed) +const schema = S.isoDateTime; +// schema has the type S.Schema + +S.parser(schema)("2020-01-01T00:00:00Z"); // pass +S.parser(schema)("2020-01-01T00:00:00.123Z"); // pass +S.parser(schema)("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision) +S.parser(schema)("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed) +``` + +To decode an ISO datetime string into a `Date`, combine it with `S.to(S.date)`: + +```ts +const schema = S.to(S.string, S.date); +// schema has the type S.Schema ``` ## Numbers @@ -454,7 +503,7 @@ S.parseOrThrow("2020-01-01T00:00:00+02:00", datetimeSchema); // fail (no offsets ```ts S.max(S.number, 5); // Number must be lower than or equal to 5 -S.min(S.number 5); // Number must be greater than or equal to 5 +S.min(S.number, 5); // Number must be greater than or equal to 5 ``` Optionally, you can pass in a second argument to provide a custom error message. @@ -470,7 +519,7 @@ You can make any schema optional with `S.optional`. ```ts const schema = S.optional(S.string); -S.parseOrThrow(undefined, schema); // => returns undefined +S.parser(schema)(undefined); // => returns undefined type A = S.Infer; // string | undefined ``` @@ -479,7 +528,7 @@ You can pass a default value to the second argument of `S.optional`. ```ts const stringWithDefaultSchema = S.optional(S.string, "tuna"); -S.parseOrThrow(undefined, stringWithDefaultSchema); // => returns "tuna" +S.parser(stringWithDefaultSchema)(undefined); // => returns "tuna" type A = S.Infer; // string ``` @@ -488,9 +537,9 @@ Optionally, you can pass a function as a default value that will be re-executed ```ts const numberWithRandomDefault = S.optional(S.number, Math.random); -S.parseOrThrow(undefined, numberWithRandomDefault); // => 0.4413456736055323 -S.parseOrThrow(undefined, numberWithRandomDefault); // => 0.1871840107401901 -S.parseOrThrow(undefined, numberWithRandomDefault); // => 0.7223408162401552 +S.parser(numberWithRandomDefault)(undefined); // => 0.4413456736055323 +S.parser(numberWithRandomDefault)(undefined); // => 0.1871840107401901 +S.parser(numberWithRandomDefault)(undefined); // => 0.7223408162401552 ``` Conceptually, this is how **Sury** processes default values: @@ -504,8 +553,8 @@ Similarly, you can create nullable types with `S.nullable`. ```ts const nullableStringSchema = S.nullable(S.string); -S.parseOrThrow("asdf", nullableStringSchema); // => "asdf" -S.parseOrThrow(null, nullableStringSchema); // => undefined +S.parser(nullableStringSchema)("asdf"); // => "asdf" +S.parser(nullableStringSchema)(null); // => undefined ``` Notice how the `null` input transformed to `undefined`. @@ -516,9 +565,9 @@ A convenience method that returns a "nullish" version of a schema. Nullish schem ```ts const nullishStringSchema = S.nullish(S.string); -S.parseOrThrow("asdf", nullishStringSchema); // => "asdf" -S.parseOrThrow(null, nullishStringSchema); // => null -S.parseOrThrow(undefined, nullishStringSchema); // => undefined +S.parser(nullishStringSchema)("asdf"); // => "asdf" +S.parser(nullishStringSchema)(null); // => null +S.parser(nullishStringSchema)(undefined); // => undefined ``` ## Objects @@ -548,7 +597,7 @@ Besides passing schemas for values in `S.schema`, you can also pass **any** Js v const meSchema = S.schema({ id: S.number, name: "Dmitry Zakharov", - age: 23 + age: 23, kind: "human", metadata: { description: "What?? Even an object with NaN works! Yes 🔥", @@ -579,29 +628,23 @@ const userSchema = S.object((s) => ({ name: s.field("USER_NAME", S.string), })); -S.parseOrThrow( - { - USER_ID: 1, - USER_NAME: "John", - }, - userSchema -); +S.parser(userSchema)({ + USER_ID: 1, + USER_NAME: "John", +}); // => returns { id: 1, name: "John" } // Infer output TypeScript type of the userSchema type User = S.Infer; // { id: number; name: string } ``` -Compared to using `S.transform`, the approach has 0 performance overhead. Also, you can use the same schema to convert the parsed data back to the initial format: +Compared to using custom transformation functions, the approach has 0 performance overhead. Also, you can use the same schema to convert the parsed data back to the initial format: ```ts -S.reverseConvertOrThrow( - { - id: 1, - name: "John", - }, - userSchema -); +S.encoder(userSchema)({ + id: 1, + name: "John", +}); // => returns { USER_ID: 1, USER_NAME: "John" } ``` @@ -616,13 +659,10 @@ const personSchema = S.strict( }) ); -S.parseOrThrow( - { - name: "bob dylan", - extraKey: 61, - }, - personSchema -); +S.parser(personSchema)({ + name: "bob dylan", + extraKey: 61, +}); // => throws S.Error ``` @@ -677,14 +717,14 @@ const stringArraySchema = S.array(S.string); ```ts S.max(S.array(S.string), 5); // Array must be 5 or fewer items long -S.min(S.array(S.string) 5); // Array must be 5 or more items long -S.length(S.array(S.string) 5); // Array must be exactly 5 items long +S.min(S.array(S.string), 5); // Array must be 5 or more items long +S.length(S.array(S.string), 5); // Array must be exactly 5 items long ``` -### Unnest +### Compact Columns ```ts -const schema = S.unnest( +const schema = S.compactColumns( S.schema({ id: S.string, name: S.nullable(S.string), @@ -692,19 +732,16 @@ const schema = S.unnest( }) ); -const value = S.reverseConvertOrThrow( - [ - { id: "0", name: "Hello", deleted: false }, - { id: "1", name: undefined, deleted: true }, - ], - schema -); +const value = S.encoder(schema)([ + { id: "0", name: "Hello", deleted: false }, + { id: "1", name: undefined, deleted: true }, +]); // [["0", "1"], ["Hello", null], [false, true]] ``` The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field. -The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`. +The main concern of the approach described in the article is usability. And **Sury** completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.
@@ -797,8 +834,8 @@ The schema function `union` creates an OR relationship between any number of sch const stringOrNumberSchema = S.union([S.string, S.number]); -S.parseOrThrow("foo", stringOrNumberSchema); // passes -S.parseOrThrow(14, stringOrNumberSchema); // passes +S.parser(stringOrNumberSchema)("foo"); // passes +S.parser(stringOrNumberSchema)(14); // passes ``` ### Discriminated unions @@ -834,7 +871,7 @@ Creating a schema for a enum-like union was never so easy: ```ts const schema = S.union(["Win", "Draw", "Loss"]); -typeof S.Infer; // Win | Draw | Loss +type Schema = S.Infer; // "Win" | "Draw" | "Loss" ``` ## Records @@ -850,6 +887,44 @@ type NumberCache = S.Infer; // => { [k: string]: number } ``` +## Date + +`S.date` validates that the input is a `Date` instance and rejects Invalid Date. + +```ts +S.parser(S.date)(new Date()); // passes +S.parser(S.date)(new Date("2024-01-01T00:00:00Z")); // passes +S.parser(S.date)(new Date("invalid")); // throws +S.parser(S.date)("2024-01-01"); // throws - not a Date instance +``` + +> Unlike `S.isoDateTime` (which validates ISO datetime strings) and `S.to(S.string, S.date)` (which decodes ISO strings into Date objects), `S.date` validates existing Date instances directly. + +You can use `S.decoder` with multiple arguments to decode between strings and dates: + +```ts +// Decode ISO string to Date +S.decoder(S.string, S.date)("2024-01-01T00:00:00.000Z"); // Date + +// Decode Date to ISO string +S.decoder(S.date, S.string)(new Date("2024-01-01T00:00:00.000Z")); // "2024-01-01T00:00:00.000Z" +``` + +## ISO DateTime + +`S.Schema` + +```ts +S.enableIsoDateTime(); // ❕ Call at the project root. + +const schema = S.isoDateTime; + +S.parser(schema)("2020-01-01T00:00:00Z"); // "2020-01-01T00:00:00Z" +S.parser(schema)("not-a-date"); // throws +``` + +Standalone string schema that validates ISO 8601 UTC datetime strings. The regex is tree-shaken from the bundle unless you call `S.enableIsoDateTime()`. See also [ISO datetimes](#iso-datetimes) under Strings for more details and examples. + ## Instance You can use `S.instance` to check that the input is an instance of a class. This is useful to validate inputs against classes that are exported from third-party libraries. @@ -859,11 +934,11 @@ class Test { name: string; } -const TestSchema = S.instance(Test); +const testSchema = S.instance(Test); const blob: any = "whatever"; -S.parseOrThrow(new Test(), TestSchema); // passes -S.parseOrThrow(blob, TestSchema); // throws S.Error: Failed parsing: Expected Test, received "whatever" +S.parser(testSchema)(new Test()); // passes +S.parser(testSchema)(blob); // throws S.Error: Expected Test, received "whatever" ``` ## Meta @@ -896,10 +971,10 @@ Use `S.brand` to attach a nominal brand to a schema's output. This is a TypeScri ```ts // Brand a string as a UserId -const UserId = S.string.with(S.brand, "UserId"); -type UserId = S.Infer; // S.Brand +const userIdSchema = S.string.with(S.brand, "UserId"); +type UserId = S.Infer; // S.Brand -const id: UserId = S.parseOrThrow("u_123", UserId); // OK +const id: UserId = S.parser(userIdSchema)("u_123"); // OK const asString: string = id; // OK: branded value is assignable to string // @ts-expect-error - A plain string is not assignable to a branded string const notId: UserId = "u_123"; @@ -908,15 +983,15 @@ const notId: UserId = "u_123"; You can define brands for refined constraints, like even numbers: ```ts -const even = S.number - .with(S.refine, (value, s) => { - if (value % 2 !== 0) s.fail("Expected an even number"); +const evenSchema = S.number + .with(S.refine, (value) => value % 2 === 0, { + error: "Expected an even number", }) .with(S.brand, "even"); -type Even = S.Infer; // S.Brand +type Even = S.Infer; // S.Brand -const good: Even = S.parseOrThrow(2, even); // OK +const good: Even = S.parser(evenSchema)(2); // OK // @ts-expect-error - number is not assignable to brand "even" const bad: Even = 5; ``` @@ -928,16 +1003,23 @@ For more information on branding in general, check out [this excellent article]( **Sury** might not have many built-in schemas for your use case. In this case you can create a custom schema for any TypeScript type. 1. Choose a base schema which is the closest to your type. Most likely it'll be `S.instance`. -2. Use `S.transform` to add a custom parser and serializer. +2. Use `S.to` to add a custom decode and encode logic. 3. Optionally, use `S.meta` to add customize the name of the schema and additional metadata. ```ts const mySet = (itemSchema: S.Schema): S.Schema> => - S.instance(Set) - .with(S.transform, (input) => { + S.instance(Set) + .with(S.to, S.instance(Set), (input) => { const output = new Set(); - input.forEach((item) => { - output.add(S.parseOrThrow(item, itemSchema)); + input.forEach((item, index) => { + try { + output.add(S.parser(itemSchema)(item)); + } catch (e) { + if (e instanceof S.Error) { + throw new Error(`At item ${index} - ${e.reason}`); + } + throw e; + } }); return output; }) @@ -948,9 +1030,9 @@ const mySet = (itemSchema: S.Schema): S.Schema> => const numberSetSchema = mySet(S.number); type NumberSet = S.Infer; // Set -S.parseOrThrow(new Set([1, 2, 3]), numberSetSchema); // passes -S.parseOrThrow(new Set([1, 2, "3"]), numberSetSchema); // throws S.Error: Failed parsing: Expected number, received "3" -S.parseOrThrow([1, 2, 3], numberSetSchema); // throws S.Error: Failed parsing: Expected Set, received [1, 2, 3] +S.parser(numberSetSchema)(new Set([1, 2, 3])); // passes +S.parser(numberSetSchema)(new Set([1, 2, "3"])); // throws S.Error: At item 3 - Expected number, received "3" +S.parser(numberSetSchema)([1, 2, 3]); // throws S.Error: Expected Set, received [1, 2, 3] ``` ## Recursive schemas @@ -975,26 +1057,58 @@ const nodeSchema = S.recursive("Node", (nodeSchema) => ## Refinements -**Sury** lets you provide custom validation logic via refinements. It's useful to add checks that's not possible to cover with type system. For instance: checking that a number is an integer or that a string is a valid email address. +**Sury** lets you provide custom validation logic via refinements. Refinements let you define checks that are not expressible in the type system alone — for example, checking that a number is positive or that a string is a valid URL. ```ts -const shortStringSchema = S.string.with(S.refine, (value, s) => { - if (value.length > 255) { - s.fail("String can't be more than 255 characters"); - } +const positiveNumberSchema = S.number.with(S.refine, (value) => value > 0); +``` + +Refinement functions should return `true` to indicate success or `false` to signal failure. By default, a failed refinement throws with the message `"Refinement failed"`. + +#### Custom error message + +Provide a custom error message via the `error` option: + +```ts +const shortStringSchema = S.string.with(S.refine, (value) => value.length <= 255, { + error: "String can't be more than 255 characters", +}); +``` + +#### Custom error path + +When refining an object schema, you can use the `path` option to attach the error to a specific field: + +```ts +const passwordFormSchema = S.schema({ + password: S.string, + confirm: S.string, +}).with(S.refine, (data) => data.password === data.confirm, { + error: "Passwords don't match", + path: ["confirm"], }); ``` -The refine function is applied for both parser and serializer. +#### Chaining refinements -Also, you can have an asynchronous refinement (for parser only): +Refinements can be chained. Each refinement is applied in order: + +```ts +const evenPositiveSchema = S.number + .with(S.refine, (val) => val > 0, { error: "Must be positive" }) + .with(S.refine, (val) => val % 2 === 0, { error: "Must be even" }); +``` + +The refine function is applied for both parsing and serializing. + +Also, you can have an asynchronous assertion (for decoder only): ```ts const userSchema = S.schema({ - id: S.string.with(S.uuid).with(S.asyncParserRefine, async (id, s) => { + id: S.uuid.with(S.asyncDecoderAssert, async (id) => { const isActiveUser = await checkIsActiveUser(id); if (!isActiveUser) { - s.fail(`The user ${id} is inactive.`); + throw new Error(`The user ${id} is inactive.`); } }), name: S.string, @@ -1002,42 +1116,18 @@ const userSchema = S.schema({ type User = S.Infer; // { id: string, name: string } -// Need to use parseAsync which will return a promise with S.Result -await S.parseAsyncOrThrow( - { - id: "1", - name: "John", - }, - userSchema -); -``` - -## Transforms - -**Sury** allows to augment schema with transformation logic, letting you transform value during parsing and serializing. This is most commonly used when you need to access the value in runtime and perform some transformation logic. For cases when you only want to change the shape of the data, it's better to use `S.shape` instead. - -```ts -const intToString = (schema) => - S.transform( - schema, - (int) => int.toString(), - (string, s) => { - const int = parseInt(string, 10); - if (isNaN(int)) { - s.fail("Can't convert string to int"); - } - return int; - } - ); +// Need to use asyncParser for schemas with async transformations +await S.asyncParser(userSchema)({ + id: "1", + name: "John", +}); ``` -> 🧠 You can use `S.int32.with(S.to, S.string)` which is a better version of the above example. - ### **`shape`** The `S.shape` schema is a helper function that allows you to transform the value to a desired shape. It'll statically derive required data transformations to perform the change in the most optimal way. -> ⚠️ Even though it looks like you operate with a real value, it's actually a dummy proxy object. So conditions or any other runtime logic won't work. Please use `S.transform` for such cases. +> ⚠️ Even though it looks like you operate with a real value, it's actually a dummy proxy object. So conditions or any other runtime logic won't work. Please use `S.to` for such cases. ```typescript const circleSchema = S.number.with(S.shape, (radius) => ({ @@ -1045,10 +1135,10 @@ const circleSchema = S.number.with(S.shape, (radius) => ({ radius: radius, })); -S.parseOrThrow(1, circleSchema); //? { kind: "circle", radius: 1 } +S.parser(circleSchema)(1); //? { kind: "circle", radius: 1 } // Also works in reverse 🔄 -S.reverseConvertOrThrow({ kind: "circle", radius: 1 }, circleSchema); //? 1 +S.encoder(circleSchema)({ kind: "circle", radius: 1 }); //? 1 ``` ## Functions on schema @@ -1059,20 +1149,17 @@ The library provides a bunch of built-in operations that can be used to parse, c Parsing means that the input value is validated against the schema and transformed to the expected output type. You can use the following operations to parse values: -| Operation | Interface | Description | -| ------------------------ | ----------------------------------------------------- | ------------------------------------------------------------- | -| S.parseOrThrow | `(unknown, Schema) => Output` | Parses any value with the schema | -| S.parseJsonOrThrow | `(Json, Schema) => Output` | Parses JSON value with the schema | -| S.parseJsonStringOrThrow | `(string, Schema) => Output` | Parses JSON string with the schema | -| S.parseAsyncOrThrow | `(unknown, Schema) => Promise` | Parses any value with the schema having async transformations | +| Operation | Interface | Description | +| -------------- | --------------------------------------------------------------- | ------------------------------------------------------------- | +| S.parser | `(Schema) => (data: unknown) => Output` | Parses any value with the schema | +| S.asyncParser | `(Schema) => (data: unknown) => Promise` | Parses any value with the schema having async transformations | For advanced users you can only transform to the output type without type validations. But be careful, since the input type is not checked: -| Operation | Interface | Description | -| ---------------------------- | ------------------------------------------ | --------------------------------------- | -| S.convertOrThrow | `(Input, Schema) => Output` | Converts input value to the output type | -| S.convertToJsonOrThrow | `(Input, Schema) => Json` | Converts input value to JSON | -| S.convertToJsonStringOrThrow | `(Input, Schema) => string` | Converts input value to JSON string | +| Operation | Interface | Description | +| --------------- | -------------------------------------------------------- | ---------------------------------------------------------------- | +| S.decoder | `(Schema) => (Input) => Output` | Converts input value to the output type | +| S.asyncDecoder | `(Schema) => (Input) => Promise` | Converts input value to the output type with async transforms | Note, that in this case only type validations are skipped. If your schema has refinements or transforms, they will be applied. @@ -1080,25 +1167,23 @@ Also, you can use `S.noValidation(schema, true)` helper to turn off type validat More often than converting input to output, you'll need to perform the reversed operation. It's usually called "serializing" or "decoding". The ReScript Schema has a unique mental model and provides an ability to reverse any schema with `S.reverse` which you can later use with all possible kinds of operations. But for convinence, there's a few helper functions that can be used to convert output values to the initial format: -| Operation | Interface | Description | -| ----------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------- | -| S.reverseConvertOrThrow | `(Output, Schema) => Input` | Converts schema value to the output type | -| S.reverseConvertToJsonOrThrow | `(Output, Schema) => Json` | Converts schema value to JSON | -| S.reverseConvertToJsonStringOrThrow | `(Output, Schema) => string` | Converts schema value to JSON string | -| S.reverseConvertAsyncOrThrow | `(Output, Schema) => promise` | Converts schema value to the output type having async transformations | +| Operation | Interface | Description | +| --------------- | ------------------------------------------------------ | --------------------------------------------------------------------- | +| S.encoder | `(Schema) => (Output) => Input` | Converts schema value to the input type | +| S.asyncEncoder | `(Schema) => (Output) => Promise`| Converts schema value to the input type with async transformations | This is literally the same as convert operations applied to the reversed schema. -For some cases you might want to simply assert the input value is valid. For this there's `S.assertOrThrow` operation: +For some cases you might want to simply assert the input value is valid. For this there's `S.assert` operation: -| Operation | Interface | Description | -| --------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| S.assertOrThrow | `(data: unknown, Schema) asserts data is Input` | Asserts that the input value is valid. Since the operation doesn't return a value, it's 2-3 times faster than `parseOrThrow` depending on the schema | +| Operation | Interface | Description | +| --------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| S.assert | `(Schema, data: unknown) asserts data is Input` | Asserts that the input value is valid. Since the operation doesn't return a value, it's 2-3 times faster than `parser` depending on the schema | All operations either return the output value or throw an error. For convinient error handling you can use the `S.safe` and `S.safeAsync` helpers, which would catch the error an wrap it into a `Result` type: ```ts -const result = S.safe(() => S.parseOrThrow(123, S.string)); +const result = S.safe(() => S.parser(S.string)(123)); ``` ### **`compile`** @@ -1150,16 +1235,16 @@ S.reverse(S.nullable(S.string)); ```ts const schema = S.object((s) => s.field("foo", S.string)); -S.parseOrThrow({ foo: "bar" }, schema); +S.parser(schema)({ foo: "bar" }); // "bar" const reversed = S.reverse(schema); -S.parseOrThrow("bar", reversed); +S.parser(reversed)("bar"); // {"foo": "bar"} -S.parseOrThrow(123, reversed); -// throws S.error with the message: `Failed parsing: Expected string, received 123` +S.parser(reversed)(123); +// throws S.error with the message: `Expected string, received 123` ``` Reverses the schema. This gets especially magical for schemas with transformations 🪄 @@ -1171,17 +1256,47 @@ This very powerful API allows you to coerce another data type in a declarative w ```ts const schema = S.string.with(S.to, S.number); -S.parseOrThrow("123", schema); //? 123. -S.parseOrThrow("abc", schema); //? throws: Failed parsing: Expected number, received "abc" +S.parser(schema)("123"); //? 123. +S.parser(schema)("abc"); //? throws: Expected number, received "abc" // Reverse works correctly as well 🔥 -S.reverseConvertOrThrow(123, schema); //? "123" +S.encoder(schema)(123); //? "123" +``` + +#### Custom transformations + +You can also provide a custom transformation function to the `S.to` operation. This is useful when you need to perform a more complex transformation than the built-in ones. + +```ts +const schema = S.string.with( + S.to, + S.number, + // Custom decode function + (string) => { + const number = parseInt(string, 10); + if (Number.isNaN(number)) { + throw new Error("Invalid number"); + } + return number; + }, + // Custom encode function + (number) => { + return number.toString(); + } +); + +S.parser(schema)("123"); //? 123 +S.parser(schema)("abc"); //? throws: Invalid number + +S.encoder(schema)(123); //? "123" ``` +> 🧠 Prefer to use built-in `S.string.with(S.to, S.number)` instead of custom transformation functions when possible. + ### **`name`** ```ts -const schema = S.schema({ abc: 123 }.with(S.meta, { name: "Abc" })); +const schema = S.schema({ abc: 123 }).with(S.meta, { name: "Abc" }); schema.name; // "Abc" ``` @@ -1207,14 +1322,14 @@ Used internally for readable error messages. **Sury** throws `S.Error` which is a subclass of Error class. It contains detailed information about the operation problem. ```ts -S.parseOrThrow(true, S.schema(false)); -// => Throws S.Error with the following message: Failed parsing: Expected false, received true". +S.parser(S.schema(false))(true); +// => Throws S.Error with the following message: Expected false, received true". ``` You can catch the error using `S.safe` and `S.safeAsync` helpers: ```ts -const result = S.safe(() => S.parseOrThrow(true, S.schema(false))); +const result = S.safe(() => S.parser(S.schema(false))(true)); if (result.success) { console.log(result.value); @@ -1227,7 +1342,7 @@ Or the async version: ```ts const result = await S.safeAsync(async () => { - const passed = await S.parseAsyncOrThrow(data, S.schema(S.boolean)); + const passed = await S.asyncParser(S.boolean)(data); return passed ? 1 : 0; }); ``` diff --git a/docs/rescript-usage.md b/docs/rescript-usage.md index 10068ddb2..ec4948235 100644 --- a/docs/rescript-usage.md +++ b/docs/rescript-usage.md @@ -10,6 +10,7 @@ - [Real-world examples](#real-world-examples) - [API reference](#api-reference) - [`string`](#string) + - [Custom error messages](#custom-error-messages) - [ISO datetimes](#iso-datetimes) - [`bool`](#bool) - [`int`](#int) @@ -20,6 +21,7 @@ - [`Option.getOr`](#optiongetor) - [`Option.getOrWith`](#optiongetorwith) - [`null`](#null) + - [`nullAsOption`](#nullasoption) - [`nullable`](#nullable) - [`nullableAsOption`](#nullableasoption) - [`unit`](#unit) @@ -42,11 +44,13 @@ - [Enums](#enums) - [`array`](#array) - [`list`](#list) - - [`unnest`](#unnest) + - [`compactColumns`](#compactcolumns) - [`tuple`](#tuple) - [`tuple1` - `tuple3`](#tuple1---tuple3) - [`dict`](#dict) - [`unknown`](#unknown) + - [`date`](#date) + - [`isoDateTime`](#isodatetime) - [`instance`](#instance) - [`never`](#never) - [`json`](#json) @@ -59,7 +63,12 @@ - [Functions on schema](#functions-on-schema) - [`Built-in operations`](#built-in-operations) - - [`compile`](#compile) + - [`parseOrThrow`](#parseorthrow) + - [`decodeOrThrow`](#decodeorthrow) + - [`assertOrThrow`](#assertorthrow) + - [`parser`](#parser) + - [`decoder`](#decoder) + - [`decoder1`](#decoder1) - [`reverse`](#reverse) - [`to`](#to) - [`isAsync`](#isasync) @@ -118,7 +127,7 @@ let filmSchema = S.object(s => { S.literal(Restricted), ]), ), - deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.deprecated("Use rating instead")), + deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.meta({deprecated: true})), }) // 3. Parse data using the schema @@ -128,7 +137,7 @@ let filmSchema = S.object(s => { "Title": "My first film", "Rating": "R", "Age": 17 -}->S.parseOrThrow(filmSchema) +}->S.parseOrThrow(~to=filmSchema) // { // id: 1., // title: "My first film", @@ -144,7 +153,7 @@ let filmSchema = S.object(s => { title: "Sad & sed", rating: ParentalStronglyCautioned, deprecatedAgeRestriction: None, -}->S.reverseConvertOrThrow(filmSchema) +}->S.decodeOrThrow(~from=filmSchema, ~to=S.unknown) // { // "Id": 2, // "Title": "Sad & sed", @@ -263,7 +272,7 @@ Compiled serializer code ```rescript let schema = S.string -"Hello World!"->S.parseOrThrow(schema) +"Hello World!"->S.parseOrThrow(~to=schema) // "Hello World!" ``` @@ -275,38 +284,77 @@ The `S.string` schema represents a data that is a string. It can be further cons S.string->S.max(5) // String must be 5 or fewer characters long S.string->S.min(5) // String must be 5 or more characters long S.string->S.length(5) // String must be exactly 5 characters long -S.string->S.email // Invalid email address -S.string->S.url // Invalid url -S.string->S.uuid // Invalid UUID -S.string->S.cuid // Invalid CUID -S.string->S.pattern(%re(`/[0-9]/`)) // Invalid -S.string->S.datetime // Invalid datetime string! Expected UTC +S.string->S.pattern(%re(`/[0-9]/`)) // Invalid pattern S.string->S.trim // trim whitespaces ``` +For format-specific string validation, use the standalone schemas: + +```rescript +S.enableEmail() +S.email // Standalone email schema + +S.enableUrl() +S.url // Standalone URL schema + +S.enableUuid() +S.uuid // Standalone UUID schema + +S.enableCuid() +S.cuid // Standalone CUID schema +``` + +> For ISO 8601 UTC datetime strings use the dedicated standalone `S.isoDateTime` schema — see [ISO datetimes](#iso-datetimes) below. + > ⚠️ Validating email addresses is nearly impossible with just code. Different clients and servers accept different things and many diverge from the various specs defining "valid" emails. The ONLY real way to validate an email address is to send a verification email to it and check that the user got it. With that in mind, Sury picks a relatively simple regex that does not cover all cases. -When using built-in refinements, you can provide a custom error message. +#### Custom error messages + +Built-in refinements accept an optional `~message` argument for a custom error message: ```rescript S.string->S.min(1, ~message="String can't be empty") S.string->S.length(5, ~message="SMS code should be 5 digits long") +S.string->S.pattern(%re(`/^\d+$/`), ~message="Must be numeric") ``` +For standalone schemas or more control, use `S.meta` with the `errorMessage` field: + +```rescript +// Override a specific constraint message +S.email->S.meta({errorMessage: {format: "Must be a valid email"}}) + +// Use catchAll as a fallback for any constraint +S.email->S.meta({errorMessage: {catchAll: "Invalid input"}}) + +// Reset error messages (removes all overrides) +schema->S.meta({errorMessage: {}}) +``` + +Available fields: `format`, `type_`, `minimum`, `maximum`, `minLength`, `maxLength`, `minItems`, `maxItems`, `pattern`, `catchAll` (serialized as `_`). + #### ISO datetimes -The `S.string->S.datetime` function has following UTC validation: no timezone offsets with arbitrary sub-second decimal precision. +`S.isoDateTime` is a **standalone** string schema (`S.t`) that validates ISO 8601 UTC datetime strings: no timezone offsets allowed, with arbitrary sub-second decimal precision. Because the regex used to validate the input lives inside `S.enableIsoDateTime`, it is tree-shaken from your bundle unless you opt in — call `S.enableIsoDateTime()` once at your project root before using the schema. ```rescript -let datetimeSchema = S.string->S.datetime -// The datetimeSchema has the type S.t -// String is transformed to the Date.t instance +S.enableIsoDateTime() // ❕ Call at the project root. + +let schema = S.isoDateTime +// schema has the type S.t + +"2020-01-01T00:00:00Z"->S.parseOrThrow(~to=schema) // pass +"2020-01-01T00:00:00.123Z"->S.parseOrThrow(~to=schema) // pass +"2020-01-01T00:00:00.123456Z"->S.parseOrThrow(~to=schema) // pass (arbitrary precision) +"2020-01-01T00:00:00+02:00"->S.parseOrThrow(~to=schema) // fail (no offsets allowed) +``` -"2020-01-01T00:00:00Z"->S.parseOrThrow(datetimeSchema) // pass -"2020-01-01T00:00:00.123Z"->S.parseOrThrow(datetimeSchema) // pass -"2020-01-01T00:00:00.123456Z"->S.parseOrThrow(datetimeSchema) // pass (arbitrary precision) -"2020-01-01T00:00:00+02:00"->S.parseOrThrow(datetimeSchema) // fail (no offsets allowed) +To decode an ISO datetime string into a `Date.t`, combine it with `S.to(S.date)`: + +```rescript +let schema = S.string->S.to(S.date) +// schema has the type S.t ``` ### **`bool`** @@ -326,7 +374,7 @@ The `S.int` schema represents a data that is an integer. ```rescript S.int->S.max(5) // Number must be lower than or equal to 5 S.int->S.min(5) // Number must be greater than or equal to 5 -S.int->S.port // Invalid port +S.port // Standalone port schema (requires S.enablePort()) ``` ### **`float`** @@ -361,9 +409,9 @@ The `S.symbol` schema represents a data that is a symbol. ```rescript let schema = S.option(S.string) -"Hello World!"->S.parseOrThrow(schema) +"Hello World!"->S.parseOrThrow(~to=schema) // Some("Hello World!") -%raw(`undefined`)->S.parseOrThrow(schema) +%raw(`undefined`)->S.parseOrThrow(~to=schema) // None ``` @@ -376,9 +424,9 @@ The `S.option` schema represents a data of a specific type that might be undefin ```rescript let schema = S.option(S.string)->S.Option.getOr("Hello World!") -%raw(`undefined`)->S.parseOrThrow(schema) +%raw(`undefined`)->S.parseOrThrow(~to=schema) // "Hello World!" -"Goodbye World!"->S.parseOrThrow(schema) +"Goodbye World!"->S.parseOrThrow(~to=schema) // "Goodbye World!" ``` @@ -393,9 +441,9 @@ The `Option.getOr` augments a schema to add transformation logic for default val ```rescript let schema = S.option(S.array(S.string))->S.Option.getOrWith(() => ["Hello World!"]) -%raw(`undefined`)->S.parseOrThrow(schema) +%raw(`undefined`)->S.parseOrThrow(~to=schema) // ["Hello World!"] -["Goodbye World!"]->S.parseOrThrow(schema) +["Goodbye World!"]->S.parseOrThrow(~to=schema) // ["Goodbye World!"] ``` @@ -403,20 +451,35 @@ Also you can use `Option.getOrWith` for lazy evaluation of the default value. ### **`null`** -`S.t<'value> => S.t>` +`S.t<'value> => S.t>` ```rescript let schema = S.null(S.string) -"Hello World!"->S.parseOrThrow(schema) +"Hello World!"->S.parseOrThrow(~to=schema) +// Value("Hello World!") +%raw(`null`)->S.parseOrThrow(~to=schema) +// Null +``` + +The `S.null` schema represents a data of a specific type that might be null. + +### **`nullAsOption`** + +`S.t<'value> => S.t>` + +```rescript +let schema = S.nullAsOption(S.string) + +"Hello World!"->S.parseOrThrow(~to=schema) // Some("Hello World!") -%raw(`null`)->S.parseOrThrow(schema) +%raw(`null`)->S.parseOrThrow(~to=schema) // None ``` -The `S.null` schema represents a data of a specific type that might be null. +The `S.nullAsOption` schema represents a data of a specific type that might be null. -> 🧠 Since `S.null` transforms value into `option` type, you can use `Option.getOr`/`Option.getOrWith` for it as well. +> 🧠 Since `S.nullAsOption` transforms value into `option` type, you can use `Option.getOr`/`Option.getOrWith` for it as well. ### **`nullable`** @@ -425,11 +488,11 @@ The `S.null` schema represents a data of a specific type that might be null. ```rescript let schema = S.nullable(S.string) -"Hello World!"->S.parseOrThrow(schema) +"Hello World!"->S.parseOrThrow(~to=schema) // Some("Hello World!") -%raw(`null`)->S.parseOrThrow(schema) +%raw(`null`)->S.parseOrThrow(~to=schema) // Null -%raw(`undefined`)->S.parseOrThrow(schema) +%raw(`undefined`)->S.parseOrThrow(~to=schema) // Undefined ``` @@ -505,8 +568,8 @@ let pointSchema = S.object(s => { }) // It can be used both for parsing and serializing -{"x": 1, "y": -4}->S.parseOrThrow(pointSchema) -{x: 1, y: -4}->S.reverseConvertOrThrow(pointSchema) +{"x": 1, "y": -4}->S.parseOrThrow(~to=pointSchema) +{x: 1, y: -4}->S.decodeOrThrow(~from=pointSchema, ~to=S.unknown) ``` The `object` schema represents an object value, that can be transformed into any ReScript value. Here are some examples: @@ -527,9 +590,9 @@ let schema = S.object(s => { { "USER_ID": 1, "USER_NAME": "John", -}->S.parseOrThrow(schema) +}->S.parseOrThrow(~to=schema) // {id: 1, name: "John"} -{id: 1, name: "John"}->S.reverseConvertOrThrow(schema) +{id: 1, name: "John"}->S.decodeOrThrow(~from=schema, ~to=S.unknown) // {"USER_ID": 1, "USER_NAME": "John"} ``` @@ -549,14 +612,14 @@ let schema = S.object(s => { // It will have the S.t<(int, string)> type let schema = S.object(s => (s.field("USER_ID", S.int), s.field("USER_NAME", S.string))) -{"USER_ID":1,"USER_NAME":"John"}->S.parseOrThrow(schema) +{"USER_ID":1,"USER_NAME":"John"}->S.parseOrThrow(~to=schema) // (1, "John") ``` The same schema also works for serializing: ```rescript -(1, "John")->S.reverseConvertOrThrow(schema) +(1, "John")->S.decodeOrThrow(~from=schema, ~to=S.unknown) // {"USER_ID":1,"USER_NAME":"John"} ``` @@ -576,7 +639,7 @@ let schema = S.object(s => { { "kind": "circle", "radius": 1, -}->S.parseOrThrow(schema) +}->S.parseOrThrow(~to=schema) // Circle({radius: 1}) ``` @@ -597,7 +660,7 @@ let schema = S.schema(s => Circle({ You can use the schema for parsing as well as serializing: ```rescript -Circle({radius: 1})->S.reverseConvertOrThrow(schema) +Circle({radius: 1})->S.decodeOrThrow(~from=schema, ~to=S.unknown) // { // "kind": "circle", // "radius": 1, @@ -675,8 +738,8 @@ let schema = S.object(_ => ())->S.strict { "someField": "value", -}->S.parseOrThrow(schema) -// throws S.error with the message: `Failed parsing: Unrecognized key "unknownKey"` +}->S.parseOrThrow(~to=schema) +// throws S.error with the message: `Unrecognized key "unknownKey"` ``` By default **Sury** silently strips unrecognized keys when parsing objects. You can change the behaviour to disallow unrecognized keys with the `S.strict` function. @@ -699,7 +762,7 @@ let schema = S.object(_ => ())->S.strip { "someField": "value", -}->S.parseOrThrow(schema) +}->S.parseOrThrow(~to=schema) // () ``` @@ -775,14 +838,14 @@ type shape = Circle({radius: float}) | Square({x: float}) | Triangle({x: float, // It will have the S.t type let schema = S.float->S.shape(radius => Circle({radius: radius})) -1->S.parseOrThrow(schema) +1->S.parseOrThrow(~to=schema) // Circle({radius: 1.}) ``` The same schema also works for serializing: ```rescript -Circle({radius: 1})->S.reverseConvertOrThrow(schema) +Circle({radius: 1})->S.decodeOrThrow(~from=schema, ~to=S.unknown) // 1 ``` @@ -831,12 +894,12 @@ let shapeSchema = S.union([ { "kind": "circle", "radius": 1, -}->S.parseOrThrow(shapeSchema) +}->S.parseOrThrow(~to=shapeSchema) // Circle({radius: 1.}) ``` ```rescript -Square({x: 2.})->S.reverseConvertOrThrow(shapeSchema) +Square({x: 2.})->S.decodeOrThrow(~from=shapeSchema, ~to=S.unknown) // { // "kind": "square", // "x": 2, @@ -856,7 +919,7 @@ let schema = S.union([ S.literal(Loss), ]) -"draw"->S.parseOrThrow(schema) +"draw"->S.parseOrThrow(~to=schema) // Draw ``` @@ -873,7 +936,7 @@ let schema = S.enum([Win, Draw, Loss]) ```rescript let schema = S.array(S.string) -["Hello", "World"]->S.parseOrThrow(schema) +["Hello", "World"]->S.parseOrThrow(~to=schema) // ["Hello", "World"] ``` @@ -894,30 +957,30 @@ S.array(itemSchema)->S.length(5) // Array must be exactly 5 items long ```rescript let schema = S.list(S.string) -["Hello", "World"]->S.parseOrThrow(schema) +["Hello", "World"]->S.parseOrThrow(~to=schema) // list{"Hello", "World"} ``` The `S.list` schema represents an array of data of a specific type which is transformed to ReScript's list data-structure. -### **`unnest`** +### **`compactColumns`** -`S.t<'value> => S.t>` +`S.t<'value> => S.t>>` ```rescript -let schema = S.unnest(S.schema(s => { +let schema = S.compactColumns(S.schema(s => { id: s.matches(S.string), - name: s.matches(S.null(S.string)), + name: s.matches(S.nullAsOption(S.string)), deleted: s.matches(S.bool), })) -[{id: "0", name: Some("Hello"), deleted: false}, {id: "1", name: None, deleted: true}]->S.reverseConvertOrThrow(schema) +[{id: "0", name: Some("Hello"), deleted: false}, {id: "1", name: None, deleted: true}]->S.decodeOrThrow(~from=schema, ~to=S.unknown) // [["0", "1"], ["Hello", null], [false, true]] ``` The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field. -The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`. +The main concern of the approach described in the article is usability. And **Sury** completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.
@@ -971,8 +1034,8 @@ let pointSchema = S.tuple(s => { }) // It can be used both for parsing and serializing -["point", 1, -4]->S.parseOrThrow(pointSchema) -{ x: 1, y: -4 }->S.reverseConvertOrThrow(pointSchema) +["point", 1, -4]->S.parseOrThrow(~to=pointSchema) +{ x: 1, y: -4 }->S.decodeOrThrow(~from=pointSchema, ~to=S.unknown) ``` The `S.tuple` schema represents that a data is an array of a specific length with values each of a specific type. @@ -986,7 +1049,7 @@ For short tuples without the need for transformation, there are wrappers over `S ```rescript let schema = S.tuple3(S.string, S.int, S.bool) -%raw(`["a", 1, true]`)->S.parseOrThrow(schema) +%raw(`["a", 1, true]`)->S.parseOrThrow(~to=schema) // ("a", 1, true) ``` @@ -1000,12 +1063,54 @@ let schema = S.dict(S.string) { "foo": "bar", "baz": "qux", -}->S.parseOrThrow(schema) +}->S.parseOrThrow(~to=schema) // dict{foo: "bar", baz: "qux"} ``` The `dict` schema represents a dictionary of data of a specific type. +### **`date`** + +`S.t` + +```rescript +let schema = S.date + +Date.fromString("2024-01-01T00:00:00Z")->S.parseOrThrow(~to=schema) // passes +%raw(`new Date("invalid")`)->S.parseOrThrow(~to=schema) // throws - Invalid Date +%raw(`"2024-01-01"`)->S.parseOrThrow(~to=schema) // throws - not a Date instance +``` + +The `S.date` schema validates that the input is a `Date` instance and rejects Invalid Date. + +> Unlike `S.isoDateTime` (which validates ISO datetime strings) and `S.string->S.to(S.date)` (which decodes ISO strings into Date objects), `S.date` validates existing Date instances directly. + +You can use `S.to` to decode between strings and dates: + +```rescript +// Decode ISO string to Date +let schema = S.string->S.to(S.date) +"2024-01-01T00:00:00.000Z"->S.parseOrThrow(~to=schema) // Date + +// Encode Date to ISO string +Date.fromString("2024-01-01T00:00:00.000Z")->S.decodeOrThrow(~from=schema, ~to=S.unknown) // "2024-01-01T00:00:00.000Z" +``` + +### **`isoDateTime`** + +`S.t` + +```rescript +S.enableIsoDateTime() // ❕ Call at the project root. + +let schema = S.isoDateTime + +"2020-01-01T00:00:00Z"->S.parseOrThrow(~to=schema) // "2020-01-01T00:00:00Z" +"not-a-date"->S.parseOrThrow(~to=schema) // throws +``` + +Standalone string schema that validates ISO 8601 UTC datetime strings. The regex is tree-shaken from the bundle unless you call `S.enableIsoDateTime()`. See also [ISO datetimes](#iso-datetimes) under Strings for more details and examples. + ### **`instance`** `S.t` @@ -1023,7 +1128,7 @@ The `S.instance` schema represents an instance of a class. Requires some type ca ```rescript let schema = S.unknown -"Hello World!"->S.parseOrThrow(schema) +"Hello World!"->S.parseOrThrow(~to=schema) // "Hello World!" ``` @@ -1036,8 +1141,8 @@ The `S.unknown` schema represents any data. ```rescript let schema = S.never -%raw(`undefined`)->S.parseOrThrow(schema) -// throws S.error with the message: `Failed parsing: Expected never, received undefined` +%raw(`undefined`)->S.parseOrThrow(~to=schema) +// throws S.error with the message: `Expected never, received undefined` ``` The `never` schema will fail parsing for every value. @@ -1051,7 +1156,7 @@ S.enableJson() // ❕ Call at the project root. let schema = S.json -`"abc"`->S.parseOrThrow(schema) +`"abc"`->S.parseOrThrow(~to=schema) // "abc" of type JSON.t ``` @@ -1066,7 +1171,7 @@ S.enableJsonString() // ❕ Call at the project root. let schema = S.jsonString->S.to(S.int) -"123"->S.parseOrThrow(schema) +"123"->S.parseOrThrow(~to=schema) // 123 ``` @@ -1124,7 +1229,7 @@ let nodeSchema = S.recursive("Node", nodeSchema => { {"Id": "2", "Children": []}, {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], -}->S.parseOrThrow(nodeSchema) +}->S.parseOrThrow(~to=nodeSchema) // { // id: "1", // children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], @@ -1137,7 +1242,7 @@ The same schema works for serializing: { id: "1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], -}->S.reverseConvertOrThrow(nodeSchema) +}->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown) // { // "Id": "1", // "Children": [ @@ -1180,7 +1285,7 @@ let mySet = itemSchema => { ->Obj.magic ->Set.forEach( item => { - output->Set.add(S.parseOrThrow(item, itemSchema)) + output->Set.add(S.parseOrThrow(item, ~to=itemSchema)) }, ) output @@ -1191,28 +1296,62 @@ let mySet = itemSchema => { let intSetSchema = mySet(S.int) -S.parseOrThrow(%raw(`new Set([1, 2, 3])`), intSetSchema) // passes -S.parseOrThrow(%raw(`new Set([1, 2, "3"])`), intSetSchema) // throws S.Error: Failed parsing: Expected int32, received "3" -S.parseOrThrow(%raw(`[1, 2, 3]`), intSetSchema) // throws S.Error: Failed parsing: Expected Set.t, received [1, 2, 3] +S.parseOrThrow(%raw(`new Set([1, 2, 3])`), ~to=intSetSchema) // passes +S.parseOrThrow(%raw(`new Set([1, 2, "3"])`), ~to=intSetSchema) // throws S.Error: Expected int32, received "3" +S.parseOrThrow(%raw(`[1, 2, 3]`), ~to=intSetSchema) // throws S.Error: Expected Set.t, received [1, 2, 3] ``` ## Refinements -**Sury** lets you provide custom validation logic via refinements. It's useful to add checks that's not possible to cover with type system. For instance: checking that a number is an integer or that a string is a valid email address. +**Sury** lets you provide custom validation logic via refinements. Refinements let you define checks that are not expressible in the type system alone — for example, checking that a number is positive or that a string is a valid email address. ### **`refine`** -`(S.t<'value>, S.s<'value> => 'value => unit) => S.t<'value>` +`(S.t<'value>, 'value => bool, ~error: string=?, ~path: array=?) => S.t<'value>` ```rescript -let shortStringSchema = S.string->S.refine(s => value => - if value->String.length > 255 { - s.fail("String can't be more than 255 characters") - } +let positiveNumberSchema = S.int->S.refine(value => value > 0) +``` + +Refinement functions should return `true` to indicate success or `false` to signal failure. By default, a failed refinement throws with the message `"Refinement failed"`. + +#### Custom error message + +Provide a custom error message via the `~error` labeled argument: + +```rescript +let shortStringSchema = S.string->S.refine( + value => value->String.length <= 255, + ~error="String can't be more than 255 characters", +) +``` + +#### Custom error path + +When refining an object schema, you can use the `~path` labeled argument to attach the error to a specific field: + +```rescript +let passwordFormSchema = S.object(s => { + "password": s.field("password", S.string), + "confirm": s.field("confirm", S.string), +})->S.refine( + data => data["password"] === data["confirm"], + ~error="Passwords don't match", + ~path=["confirm"], ) ``` -The refine function is applied for both parser and serializer. +#### Chaining refinements + +Refinements can be chained. Each refinement is applied in order: + +```rescript +let evenPositiveSchema = S.int + ->S.refine(value => value > 0, ~error="Must be positive") + ->S.refine(value => mod(value, 2) === 0, ~error="Must be even") +``` + +The refine function is applied for both parsing and serializing. ## Transforms @@ -1243,14 +1382,13 @@ type user = { } let userSchema = - S.string - ->S.uuid + S.uuid ->S.transform(s => { asyncParser: userId => loadUser(~userId), serializer: user => user.id, }) -await "1"->S.parseAsyncOrThrow(userSchema) +await "1"->S.parseAsyncOrThrow(~to=userSchema) // { // id: "1", // name: "John", @@ -1259,7 +1397,7 @@ await "1"->S.parseAsyncOrThrow(userSchema) { id: "1", name: "John", -}->S.reverseConvertOrThrow(userSchema) +}->S.decodeOrThrow(~from=userSchema, ~to=S.unknown) // "1" ``` @@ -1267,122 +1405,144 @@ await "1"->S.parseAsyncOrThrow(userSchema) ### Built-in operations -The library provides a bunch of built-in operations that can be used to parse, convert, and assert values. +The library provides a bunch of built-in operations that can be used to parse, decode, and assert values. -Parsing means that the input value is validated against the schema and transformed to the expected output type. You can use the following operations to parse values: +**Parsing** validates the input value against the schema and transforms it to the expected output type: -| Operation | Interface | Description | -| ------------------------ | ---------------------------------------- | ------------------------------------------------------------- | -| S.parseOrThrow | `('any, S.t<'value>) => 'value` | Parses any value with the schema | -| S.parseJsonOrThrow | `(Js.Json.t, S.t<'value>) => 'value` | Parses JSON value with the schema | -| S.parseJsonStringOrThrow | `(string, S.t<'value>) => 'value` | Parses JSON string with the schema | -| S.parseAsyncOrThrow | `('any, S.t<'value>) => promise<'value>` | Parses any value with the schema having async transformations | +| Operation | Interface | Description | +| ------------------- | ---------------------------------------------------- | ------------------------------------------------------------- | +| S.parseOrThrow | `('any, ~to: S.t<'value>) => 'value` | Parses any value with the schema | +| S.parseAsyncOrThrow | `('any, ~to: S.t<'value>) => promise<'value>` | Parses any value with the schema having async transformations | -For advanced users you can only transform to the output type without type validations. But be careful, since the input type is not checked: +**Decoding** transforms between schemas without input validation. Be careful, since the input type is not checked: -| Operation | Interface | Description | -| ---------------------------- | ---------------------------------------- | ------------------------------------------------------------------ | -| S.convertOrThrow | `('any, S.t<'value>) => 'value` | Converts any value to the output type | -| S.convertToJsonOrThrow | `('any, S.t<'value>) => Js.Json.t` | Converts any value to JSON | -| S.convertToJsonStringOrThrow | `('any, S.t<'value>) => string` | Converts any value to JSON string | -| S.convertAsyncOrThrow | `('any, S.t<'value>) => promise<'value>` | Converts any value to the output type having async transformations | +| Operation | Interface | Description | +| ---------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------- | +| S.decodeOrThrow | `('from, ~from: S.t<'from>, ~to: S.t<'to>) => 'to` | Decodes a value from one schema to another | +| S.decodeAsyncOrThrow | `('from, ~from: S.t<'from>, ~to: S.t<'to>) => promise<'to>` | Async version of decodeOrThrow | -Note, that in this case only type validations are skipped. If your schema has refinements or transforms, they will be applied. +Common decode patterns: -Also, you can use `S.noValidation` helper to turn off type validations for the schema even when it's used with a parse operation. +```rescript +// Parse JSON value (replaces S.parseJsonOrThrow) +data->S.decodeOrThrow(~from=S.json, ~to=schema) + +// Parse JSON string (replaces S.parseJsonStringOrThrow) +data->S.decodeOrThrow(~from=S.jsonString, ~to=schema) -More often than converting input to output, you'll need to perform the reversed operation. It's usually called "serializing" or "decoding". The ReScript Schema has a unique mental model and provides an ability to reverse any schema with `S.reverse` which you can later use with all possible kinds of operations. But for convinence, there's a few helper functions that can be used to convert output values to the initial format: +// Serialize to unknown (replaces S.reverseConvertOrThrow) +data->S.decodeOrThrow(~from=schema, ~to=S.unknown) -| Operation | Interface | Description | -| ----------------------------------- | ---------------------------------------- | --------------------------------------------------------------------- | -| S.reverseConvertOrThrow | `('value, S.t<'value>) => 'any` | Converts schema value to the output type | -| S.reverseConvertToJsonOrThrow | `('value, S.t<'value>) => Js.Json.t` | Converts schema value to JSON | -| S.reverseConvertToJsonStringOrThrow | `('value, S.t<'value>) => string` | Converts schema value to JSON string | -| S.reverseConvertAsyncOrThrow | `('value, S.t<'value>) => promise<'any>` | Converts schema value to the output type having async transformations | +// Serialize to JSON (replaces S.reverseConvertToJsonOrThrow) +data->S.decodeOrThrow(~from=schema, ~to=S.json) + +// Serialize to JSON string (replaces S.reverseConvertToJsonStringOrThrow) +data->S.decodeOrThrow(~from=schema, ~to=S.jsonString) + +// Serialize to JSON string with space +data->S.decodeOrThrow(~from=schema, ~to=S.jsonStringWithSpace(2)) +``` -This is literally the same as convert operations applied to the reversed schema. +Also, you can use `S.noValidation` helper to turn off type validations for the schema even when it's used with a parse operation. -For some cases you might want to simply assert the input value is valid. For this there's `S.assertOrThrow` operation: +**Asserting** validates the input value without returning a transformed result: -| Operation | Interface | Description | -| --------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| S.assertOrThrow | `('any, S.t<'value>) => ()` | Asserts that the input value is valid. Since the operation doesn't return a value, it's 2-3 times faster than `parseOrThrow` depending on the schema | +| Operation | Interface | Description | +| -------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| S.assertOrThrow | `('any, ~to: S.t<'value>) => ()` | Asserts that the input value is valid. Since the operation doesn't return a value, it's 2-3 times faster than `parseOrThrow` depending on the schema | +| S.assertAsyncOrThrow | `('any, ~to: S.t<'value>) => promise<()>` | Async version of assertOrThrow | All operations either return the output value or throw an exception which you can catch with `try/catch` block: ```rescript -try true->S.parseOrThrow(schema) catch { +try true->S.parseOrThrow(~to=schema) catch { | S.Error(error) => Console.log(error.message) } ``` -### **`compile`** +### **`parser`** / **`asyncParser`** -`(S.t<'value>, ~input: input<'value, 'input>, ~output: output<'value, 'transformedOutput>, ~mode: mode<'transformedOutput, 'output>, ~typeValidation: bool=?) => 'input => 'output` +``` +S.parser: (~through: array>=?, ~to: S.t<'value>) => 'any => 'value +S.asyncParser: (~through: array>=?, ~to: S.t<'value>) => 'any => promise<'value> +``` -If you want to have the most possible performance, or the built-in operations doesn't cover your specific use case, you can use `compile` to create fine-tuned operation functions. +Returns a compiled parse function that validates input and transforms it to the schema's output type. This is the most performant way to parse values repeatedly. Use `~through` to chain intermediate schemas. ```rescript -let operation = S.compile( - S.string, - ~input=Any, - ~output=Assert, - ~mode=Async, -) -await operation("Hello world!") -// () +let parse = S.parser(~to=S.string) + +parse("Hello world!") +// "Hello world!" + +// Async version for schemas with async transformations +let parseAsync = S.asyncParser(~to=schemaWithAsyncTransform) +``` + +### **`decoder`** / **`asyncDecoder`** + +``` +S.decoder: (~from: S.t<'from>, ~through: array>=?, ~to: S.t<'to>) => 'from => 'to +S.asyncDecoder: (~from: S.t<'from>, ~through: array>=?, ~to: S.t<'to>) => 'from => promise<'to> ``` -For example, in the example above we've created an async assert operation, which is not available by default. +Returns a compiled decode function that transforms values from one schema to another. Use `~through` to chain intermediate schemas. + +```rescript +// Compile a serializer +let serialize = S.decoder(~from=schema, ~to=S.unknown) -You can configure compiled function `input` with the following options: +// Compile a JSON decoder +let decodeJson = S.decoder(~from=S.json, ~to=schema) -- `Value` - accepts `'value` of `S.t<'value>` and reverses the operation -- `Unknown` - accepts `unknown` -- `Any` - accepts `'any` -- `Json` - accepts `Js.Json.t` -- `JsonString` - accepts `string` and applies `JSON.parse` before parsing +// Compile a JSON string serializer +let toJsonString = S.decoder(~from=schema, ~to=S.jsonString) -You can configure compiled function `output` with the following options: +// Compile an async decoder +let decodeAsync = S.asyncDecoder(~from=S.json, ~to=schema) +``` -- `Value` - returns `'value` of `S.t<'value>` -- `Unknown` - returns `unknown` -- `Assert` - returns `unit` -- `Json` - validates that the schema is JSON compatible and returns `Js.Json.t` -- `JsonString` - validates that the schema is JSON compatible and converts output to JSON string +### **`decoder1`** / **`asyncDecoder1`** -You can configure compiled function `mode` with the following options: +``` +S.decoder1: S.t<'value> => unknown => 'value +S.asyncDecoder1: S.t<'value> => unknown => promise<'value> +``` -- `Sync` - for sync operations -- `Async` - for async operations - will wrap result in a promise +Returns a compiled decode function for a single schema, transforming from the schema's input type to its output type. This is useful for schemas with internal transformations. -And you can configure compiled function `typeValidation` with the following options: +```rescript +let schema = S.array(S.nullAsOption(S.string)) +let decode = S.decoder1(schema) -- `true (default)` - performs type validation -- `false` - doesn't perform type validation and only converts data to the output format. Note that refines are still applied. +// Input: array> (schema input) +// Output: array> (schema output) +decode(%raw(`["foo", null, "bar"]`)) +// [Some("foo"), None, Some("bar")] +``` ### **`reverse`** `(S.t<'value>) => S.t<'value>` ```rescript -S.null(S.string)->S.reverse +S.nullAsOption(S.string)->S.reverse // S.option(S.string) ``` ```rescript let schema = S.object(s => s.field("foo", S.string)) -{"foo": "bar"}->S.parseOrThrow(schema) +{"foo": "bar"}->S.parseOrThrow(~to=schema) // "bar" let reversed = schema->S.reverse -"bar"->S.parseOrThrow(reversed) +"bar"->S.parseOrThrow(~to=reversed) // {"foo": "bar"} -123->S.parseOrThrow(reversed) -// throws S.error with the message: `Failed parsing: Expected string, received 123` +123->S.parseOrThrow(~to=reversed) +// throws S.error with the message: `Expected string, received 123` ``` Reverses the schema. This gets especially magical for schemas with transformations 🪄 @@ -1396,11 +1556,11 @@ This very powerful API allows you to coerce another data type in a declarative w ```rescript let schema = S.string->S.to(S.float) -"123"->S.parseOrThrow(schema) //? 123. -"abc"->S.parseOrThrow(schema) //? throws: Failed parsing: Expected number, received "abc" +"123"->S.parseOrThrow(~to=schema) //? 123. +"abc"->S.parseOrThrow(~to=schema) //? throws: Expected number, received "abc" // Reverse works correctly as well 🔥 -123.->S.reverseConvertOrThrow(schema) //? "123" +123.->S.decodeOrThrow(~from=schema, ~to=S.unknown) //? "123" ``` ### **`isAsync`** @@ -1434,7 +1594,7 @@ Used internally for readable error messages. S.literal({"abc": 123})->S.toExpression // "{ "abc": 123 }" -S.string->S.name("Address")->S.toExpression +S.string->S.meta({name: "Address"})->S.toExpression // "Address" ``` @@ -1451,7 +1611,7 @@ let schema = S.object(s => s.field("abc", S.int))->S.noValidation(true) { "abc": 123, -}->S.parseOrThrow(schema) // This doesn't have `if (typeof i !== "object" || !i) {` check. But field types are still validated. +}->S.parseOrThrow(~to=schema) // This doesn't have `if (typeof i !== "object" || !i) {` check. But field types are still validated. // 123 ``` @@ -1466,14 +1626,14 @@ This can be useful to optimise `S.object` parsing when you construct the input d ```rescript let schema = S.literal(false) -true->S.parseOrThrow(schema) -// throws S.error with the message: `Failed parsing: Expected false, received true` +true->S.parseOrThrow(~to=schema) +// throws S.error with the message: `Expected false, received true` ``` If you want to handle the error, the best way to use `try/catch` block: ```rescript -try true->S.parseOrThrow(schema) catch { +try true->S.parseOrThrow(~to=schema) catch { | S.Error(error) => Console.log(error.message) } ``` diff --git a/packages/e2e/src/benchmark/Benchmark.res b/packages/e2e/src/benchmark/Benchmark.res index 1049474ae..a59ced3dc 100644 --- a/packages/e2e/src/benchmark/Benchmark.res +++ b/packages/e2e/src/benchmark/Benchmark.res @@ -169,19 +169,19 @@ module CrazyUnion = { let test = () => { Console.time("testData1 serialize") - let json = S.reverseConvertOrThrow(testData1, schema) + let json = S.decodeOrThrow(testData1, ~from=schema, ~to=S.unknown) Console.timeEnd("testData1 serialize") Console.time("testData1 parse") - let _ = S.parseOrThrow(json, schema) + let _ = S.parseOrThrow(json, ~to=schema) Console.timeEnd("testData1 parse") Console.time("testData2 serialize") - let json = S.reverseConvertOrThrow(testData2, schema) + let json = S.decodeOrThrow(testData2, ~from=schema, ~to=S.unknown) Console.timeEnd("testData2 serialize") Console.time("testData2 parse") - let _ = S.parseOrThrow(json, schema) + let _ = S.parseOrThrow(json, ~to=schema) Console.timeEnd("testData2 parse") } } @@ -194,30 +194,31 @@ let schema = makeObjectSchema() Console.timeEnd("makeObjectSchema") Console.time("parseOrThrow: 1") -data->S.parseOrThrow(schema)->ignore +data->S.parseOrThrow(~to=schema)->ignore Console.timeEnd("parseOrThrow: 1") Console.time("parseOrThrow: 2") -data->S.parseOrThrow(schema)->ignore +data->S.parseOrThrow(~to=schema)->ignore Console.timeEnd("parseOrThrow: 2") Console.time("parseOrThrow: 3") -data->S.parseOrThrow(schema)->ignore +data->S.parseOrThrow(~to=schema)->ignore Console.timeEnd("parseOrThrow: 3") Console.time("serializeWith: 1") -data->S.reverseConvertOrThrow(schema)->ignore +data->S.decodeOrThrow(~from=schema, ~to=S.unknown)->ignore Console.timeEnd("serializeWith: 1") Console.time("serializeWith: 2") -data->S.reverseConvertOrThrow(schema)->ignore +data->S.decodeOrThrow(~from=schema, ~to=S.unknown)->ignore Console.timeEnd("serializeWith: 2") Console.time("serializeWith: 3") -data->S.reverseConvertOrThrow(schema)->ignore +data->S.decodeOrThrow(~from=schema, ~to=S.unknown)->ignore Console.timeEnd("serializeWith: 3") Console.time("S.Error.make") -let _ = S.ErrorClass.constructor( - ~code=OperationFailed("Should be positive"), - ~flag=S.Flag.typeValidation, - ~path=S.Path.empty, +let _ = S.Error.make( + InvalidOperation({ + path: S.Path.empty, + reason: "Should be positive", + }), ) Console.timeEnd("S.Error.make") @@ -227,14 +228,14 @@ Suite.make() let data = makeTestObject() () => { let schema = makeObjectSchema() - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.addWithPrepare("S.schema - parse", () => { let schema = makeObjectSchema() let data = makeTestObject() () => { - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.addWithPrepare("S.schema - parse strict", () => { @@ -248,7 +249,7 @@ Suite.make() }) let data = makeTestObject() () => { - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.add("S.schema - make + reverse", () => makeObjectSchema()->S.reverse) @@ -256,20 +257,20 @@ Suite.make() let data = makeTestObject() () => { let schema = makeObjectSchema() - data->S.reverseConvertOrThrow(schema) + data->S.decodeOrThrow(~from=schema, ~to=S.unknown) } }) ->Suite.addWithPrepare("S.schema - reverse convert", () => { let schema = makeObjectSchema() let data = makeTestObject() () => { - data->S.reverseConvertOrThrow(schema) + data->S.decodeOrThrow(~from=schema, ~to=S.unknown) } }) ->Suite.addWithPrepare("S.schema - reverse convert (compiled)", () => { let schema = makeObjectSchema() let data = makeTestObject() - let fn = schema->S.compile(~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=false) + let fn = S.decoder(~from=schema, ~to=S.unknown) () => { fn(data) } @@ -278,13 +279,13 @@ Suite.make() let schema = makeObjectSchema() let data = makeTestObject() () => { - data->S.assertOrThrow(schema) + data->S.assertOrThrow(~to=schema) } }) ->Suite.addWithPrepare("S.schema - assert (compiled)", () => { let schema = makeObjectSchema() let data = makeTestObject() - let assertFn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=true) + let assertFn = S.decoder(~from=S.unknown, ~to=schema->S.to(S.literal()->S.noValidation(true))) () => { assertFn(data) } @@ -300,7 +301,7 @@ Suite.make() }) let data = makeTestObject() () => { - data->S.assertOrThrow(schema) + data->S.assertOrThrow(~to=schema) } }) ->Suite.add("S.object - make", () => makeAdvancedObjectSchema()) @@ -308,14 +309,14 @@ Suite.make() let data = makeTestObject() () => { let schema = makeAdvancedObjectSchema() - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.addWithPrepare("S.object - parse", () => { let schema = makeAdvancedObjectSchema() let data = makeTestObject() () => { - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.add("S.object - make + reverse", () => makeAdvancedObjectSchema()->S.reverse) @@ -323,28 +324,28 @@ Suite.make() let data = makeTestObject() () => { let schema = makeAdvancedObjectSchema() - data->S.reverseConvertOrThrow(schema) + data->S.decodeOrThrow(~from=schema, ~to=S.unknown) } }) ->Suite.addWithPrepare("S.object - reverse convert", () => { let schema = makeAdvancedObjectSchema() let data = makeTestObject() () => { - data->S.reverseConvertOrThrow(schema) + data->S.decodeOrThrow(~from=schema, ~to=S.unknown) } }) ->Suite.addWithPrepare("S.string - parse", () => { let schema = S.string let data = "Hello world!" () => { - data->S.parseOrThrow(schema) + data->S.parseOrThrow(~to=schema) } }) ->Suite.addWithPrepare("S.string - reverse convert", () => { let schema = S.string let data = "Hello world!" () => { - data->S.reverseConvertOrThrow(schema) + data->S.decodeOrThrow(~from=schema, ~to=S.unknown) } }) ->Suite.run diff --git a/packages/e2e/src/benchmark/Benchmark.res.mjs b/packages/e2e/src/benchmark/Benchmark.res.mjs index b732a530a..7792e09ca 100644 --- a/packages/e2e/src/benchmark/Benchmark.res.mjs +++ b/packages/e2e/src/benchmark/Benchmark.res.mjs @@ -132,13 +132,13 @@ let testData2 = { function test() { console.time("testData1 serialize"); - let json = S.reverseConvertOrThrow(testData1, schema); + let json = S.decodeOrThrow(testData1, schema, S.unknown); console.timeEnd("testData1 serialize"); console.time("testData1 parse"); S.parseOrThrow(json, schema); console.timeEnd("testData1 parse"); console.time("testData2 serialize"); - let json$1 = S.reverseConvertOrThrow(testData2, schema); + let json$1 = S.decodeOrThrow(testData2, schema, S.unknown); console.timeEnd("testData2 serialize"); console.time("testData2 parse"); S.parseOrThrow(json$1, schema); @@ -175,28 +175,29 @@ console.timeEnd("parseOrThrow: 3"); console.time("serializeWith: 1"); -S.reverseConvertOrThrow(data, schema$1); +S.decodeOrThrow(data, schema$1, S.unknown); console.timeEnd("serializeWith: 1"); console.time("serializeWith: 2"); -S.reverseConvertOrThrow(data, schema$1); +S.decodeOrThrow(data, schema$1, S.unknown); console.timeEnd("serializeWith: 2"); console.time("serializeWith: 3"); -S.reverseConvertOrThrow(data, schema$1); +S.decodeOrThrow(data, schema$1, S.unknown); console.timeEnd("serializeWith: 3"); console.time("S.Error.make"); -S.ErrorClass.constructor({ - TAG: "OperationFailed", - _0: "Should be positive" -}, S.Flag.typeValidation, S.Path.empty); +S.$$Error.make({ + code: "invalid_operation", + path: S.Path.empty, + reason: "Should be positive" +}); console.timeEnd("S.Error.make"); @@ -225,16 +226,16 @@ run(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(a let data = makeTestObject(); return () => { let schema = makeObjectSchema(); - return S.reverseConvertOrThrow(data, schema); + return S.decodeOrThrow(data, schema, S.unknown); }; }), "S.schema - reverse convert", () => { let schema = makeObjectSchema(); let data = makeTestObject(); - return () => S.reverseConvertOrThrow(data, schema); + return () => S.decodeOrThrow(data, schema, S.unknown); }), "S.schema - reverse convert (compiled)", () => { let schema = makeObjectSchema(); let data = makeTestObject(); - let fn = S.compile(schema, "Output", "Input", "Sync", false); + let fn = S.decoder(schema, S.unknown); return () => fn(data); }), "S.schema - assert", () => { let schema = makeObjectSchema(); @@ -243,7 +244,7 @@ run(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(a }), "S.schema - assert (compiled)", () => { let schema = makeObjectSchema(); let data = makeTestObject(); - let assertFn = S.compile(schema, "Any", "Assert", "Sync", true); + let assertFn = S.decoder(S.unknown, S.to(schema, S.noValidation(S.literal(), true))); return () => assertFn(data); }), "S.schema - assert strict", () => { S.global({ @@ -270,12 +271,12 @@ run(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(a let data = makeTestObject(); return () => { let schema = makeAdvancedObjectSchema(); - return S.reverseConvertOrThrow(data, schema); + return S.decodeOrThrow(data, schema, S.unknown); }; }), "S.object - reverse convert", () => { let schema = makeAdvancedObjectSchema(); let data = makeTestObject(); - return () => S.reverseConvertOrThrow(data, schema); -}), "S.string - parse", () => (() => S.parseOrThrow("Hello world!", S.string))), "S.string - reverse convert", () => (() => S.reverseConvertOrThrow("Hello world!", S.string)))); + return () => S.decodeOrThrow(data, schema, S.unknown); +}), "S.string - parse", () => (() => S.parseOrThrow("Hello world!", S.string))), "S.string - reverse convert", () => (() => S.decodeOrThrow("Hello world!", S.string, S.unknown)))); /* Not a pure module */ diff --git a/packages/e2e/src/genType/GenType.gen.ts b/packages/e2e/src/genType/GenType.gen.ts index c9057f1c0..2156de25c 100644 --- a/packages/e2e/src/genType/GenType.gen.ts +++ b/packages/e2e/src/genType/GenType.gen.ts @@ -5,9 +5,9 @@ import * as GenTypeJS from './GenType.res.mjs'; -import type {error as S_error} from 'sury/src/S.gen'; +import type {error as S_error} from './S.gen'; -import type {t as S_t} from 'sury/src/S.gen'; +import type {t as S_t} from './S.gen'; export const stringSchema: S_t = GenTypeJS.stringSchema as any; diff --git a/packages/e2e/src/genType/GenType.res b/packages/e2e/src/genType/GenType.res index 257179d98..52aeeb9c3 100644 --- a/packages/e2e/src/genType/GenType.res +++ b/packages/e2e/src/genType/GenType.res @@ -2,8 +2,9 @@ let stringSchema = S.string @genType -let error: S.error = U.error({ - operation: Parse, - code: OperationFailed("Something went wrong"), - path: S.Path.empty, -}) +let error: S.error = S.Error.make( + InvalidOperation({ + path: S.Path.empty, + reason: "Something went wrong", + }), +) diff --git a/packages/e2e/src/genType/GenType.res.mjs b/packages/e2e/src/genType/GenType.res.mjs index 96ff94bd7..874ae24ac 100644 --- a/packages/e2e/src/genType/GenType.res.mjs +++ b/packages/e2e/src/genType/GenType.res.mjs @@ -1,15 +1,11 @@ // Generated by ReScript, PLEASE EDIT WITH CARE import * as S from "sury/src/S.res.mjs"; -import * as U from "../utils/U.res.mjs"; -let error = U.error({ - operation: "Parse", - code: { - TAG: "OperationFailed", - _0: "Something went wrong" - }, - path: S.Path.empty +let error = S.$$Error.make({ + code: "invalid_operation", + path: S.Path.empty, + reason: "Something went wrong" }); let stringSchema = S.string; diff --git a/packages/e2e/src/genType/S.gen.ts b/packages/e2e/src/genType/S.gen.ts new file mode 100644 index 000000000..af3d29921 --- /dev/null +++ b/packages/e2e/src/genType/S.gen.ts @@ -0,0 +1,8 @@ +// FIXME: Shim so the relative `./S.gen` import that genType emits in +// GenType.gen.ts resolves to sury's hand-written type declarations. +// +// ReScript 12 genType computes a local path for modules from workspace +// (file:) dependencies instead of using the package-qualified path. +// Remove once genType emits `sury/src/S.gen` directly: +// https://github.com/rescript-lang/rescript/issues/8375 +export type { error, t, schema, Path_t } from "sury/src/S.gen"; diff --git a/packages/e2e/src/ppx/Ppx_Example_test.res b/packages/e2e/src/ppx/Ppx_Example_test.res index 560d1c9d5..0b13b098e 100644 --- a/packages/e2e/src/ppx/Ppx_Example_test.res +++ b/packages/e2e/src/ppx/Ppx_Example_test.res @@ -1,6 +1,8 @@ open Ava open U +S.enableUrl() + @schema type rating = | @as("G") GeneralAudiences @@ -45,9 +47,9 @@ test("Main example", t => { }) @schema -type matches = @s.matches(S.string->S.url) string +type matches = @s.matches(S.url) string test("@s.matches", t => { - t->assertEqualSchemas(matchesSchema, S.string->S.url) + t->assertEqualSchemas(matchesSchema, S.url) }) @schema @@ -68,13 +70,13 @@ test("@s.defaultWith", t => { @schema type null = @s.null option test("@s.null", t => { - t->assertEqualSchemas(nullSchema, S.null(S.string)) + t->assertEqualSchemas(nullSchema, S.nullAsOption(S.string)) }) @schema type nullWithDefault = @s.null @s.default("Unknown") string test("@s.null with @s.default", t => { - t->assertEqualSchemas(nullWithDefaultSchema, S.null(S.string)->S.Option.getOr("Unknown")) + t->assertEqualSchemas(nullWithDefaultSchema, S.nullAsOption(S.string)->S.Option.getOr("Unknown")) }) @schema diff --git a/packages/e2e/src/ppx/Ppx_Example_test.res.mjs b/packages/e2e/src/ppx/Ppx_Example_test.res.mjs index 71e50bdc0..a5ec5e709 100644 --- a/packages/e2e/src/ppx/Ppx_Example_test.res.mjs +++ b/packages/e2e/src/ppx/Ppx_Example_test.res.mjs @@ -4,6 +4,8 @@ import * as S from "sury/src/S.res.mjs"; import * as U from "../utils/U.res.mjs"; import Ava from "ava"; +S.enableUrl(); + let ratingSchema = S.union([ S.literal("G"), S.literal("PG"), @@ -38,9 +40,7 @@ Ava("Main example", t => U.assertEqualSchemas(t, filmSchema, S.schema(s => ({ })) })), undefined)); -let matchesSchema = S.url(S.string, undefined); - -Ava("@s.matches", t => U.assertEqualSchemas(t, matchesSchema, S.url(S.string, undefined), undefined)); +Ava("@s.matches", t => U.assertEqualSchemas(t, S.url, S.url, undefined)); let defaultSchema = S.Option.getOr(S.option(S.string), "Unknown"); @@ -50,13 +50,13 @@ let defaultWithSchema = S.Option.getOrWith(S.option(S.array(S.string)), () => [] Ava("@s.defaultWith", t => U.assertEqualSchemas(t, defaultWithSchema, S.Option.getOrWith(S.option(S.array(S.string)), () => []), undefined)); -let nullSchema = S.$$null(S.string); +let nullSchema = S.nullAsOption(S.string); -Ava("@s.null", t => U.assertEqualSchemas(t, nullSchema, S.$$null(S.string), undefined)); +Ava("@s.null", t => U.assertEqualSchemas(t, nullSchema, S.nullAsOption(S.string), undefined)); -let nullWithDefaultSchema = S.Option.getOr(S.$$null(S.string), "Unknown"); +let nullWithDefaultSchema = S.Option.getOr(S.nullAsOption(S.string), "Unknown"); -Ava("@s.null with @s.default", t => U.assertEqualSchemas(t, nullWithDefaultSchema, S.Option.getOr(S.$$null(S.string), "Unknown"), undefined)); +Ava("@s.null with @s.default", t => U.assertEqualSchemas(t, nullWithDefaultSchema, S.Option.getOr(S.nullAsOption(S.string), "Unknown"), undefined)); let nullableSchema = S.nullableAsOption(S.string); @@ -84,6 +84,8 @@ Ava("@s.description", t => U.assertEqualSchemas(t, describeSchema, S.meta(S.stri description: "A useful bit of text, if you know what to do with it." }), undefined)); +let matchesSchema = S.url; + export { ratingSchema, filmSchema, @@ -97,4 +99,4 @@ export { deprecatedSchema, describeSchema, } -/* ratingSchema Not a pure module */ +/* Not a pure module */ diff --git a/packages/e2e/src/ppx/Ppx_General_test.res b/packages/e2e/src/ppx/Ppx_General_test.res index e18b73702..7adac76c0 100644 --- a/packages/e2e/src/ppx/Ppx_General_test.res +++ b/packages/e2e/src/ppx/Ppx_General_test.res @@ -1,6 +1,8 @@ open Ava open U +S.enableUrl() + @schema type t = string test("Creates schema with the name schema from t type", t => { @@ -33,20 +35,20 @@ test("Creates schema with default", t => { }) @schema -type stringWithDefaultAndMatches = @s.default("Foo") @s.matches(S.string->S.url) string +type stringWithDefaultAndMatches = @s.default("Foo") @s.matches(S.url) string test("Creates schema with default using @s.matches", t => { t->assertEqualSchemas( stringWithDefaultAndMatchesSchema, - S.option(S.string->S.url)->S.Option.getOr("Foo"), + S.option(S.url)->S.Option.getOr("Foo"), ) }) @schema -type stringWithDefaultNullAndMatches = @s.default("Foo") @s.null @s.matches(S.string->S.url) string +type stringWithDefaultNullAndMatches = @s.default("Foo") @s.null @s.matches(S.url) string test("Creates schema with default null using @s.matches", t => { t->assertEqualSchemas( stringWithDefaultNullAndMatchesSchema, - S.null(S.string->S.url)->S.Option.getOr("Foo"), + S.nullAsOption(S.url)->S.Option.getOr("Foo"), ) }) diff --git a/packages/e2e/src/ppx/Ppx_General_test.res.mjs b/packages/e2e/src/ppx/Ppx_General_test.res.mjs index f5a3b4642..1afac4b20 100644 --- a/packages/e2e/src/ppx/Ppx_General_test.res.mjs +++ b/packages/e2e/src/ppx/Ppx_General_test.res.mjs @@ -4,6 +4,8 @@ import * as S from "sury/src/S.res.mjs"; import * as U from "../utils/U.res.mjs"; import Ava from "ava"; +S.enableUrl(); + Ava("Creates schema with the name schema from t type", t => U.assertEqualSchemas(t, S.string, S.string, undefined)); Ava("Creates schema with the type name and schema at the for non t types", t => U.assertEqualSchemas(t, S.int, S.int, undefined)); @@ -26,13 +28,13 @@ let stringWithDefaultSchema = S.Option.getOr(S.option(S.string), "Foo"); Ava("Creates schema with default", t => U.assertEqualSchemas(t, stringWithDefaultSchema, S.Option.getOr(S.option(S.string), "Foo"), undefined)); -let stringWithDefaultAndMatchesSchema = S.Option.getOr(S.option(S.url(S.string, undefined)), "Foo"); +let stringWithDefaultAndMatchesSchema = S.Option.getOr(S.option(S.url), "Foo"); -Ava("Creates schema with default using @s.matches", t => U.assertEqualSchemas(t, stringWithDefaultAndMatchesSchema, S.Option.getOr(S.option(S.url(S.string, undefined)), "Foo"), undefined)); +Ava("Creates schema with default using @s.matches", t => U.assertEqualSchemas(t, stringWithDefaultAndMatchesSchema, S.Option.getOr(S.option(S.url), "Foo"), undefined)); -let stringWithDefaultNullAndMatchesSchema = S.Option.getOr(S.$$null(S.url(S.string, undefined)), "Foo"); +let stringWithDefaultNullAndMatchesSchema = S.Option.getOr(S.nullAsOption(S.url), "Foo"); -Ava("Creates schema with default null using @s.matches", t => U.assertEqualSchemas(t, stringWithDefaultNullAndMatchesSchema, S.Option.getOr(S.$$null(S.url(S.string, undefined)), "Foo"), undefined)); +Ava("Creates schema with default null using @s.matches", t => U.assertEqualSchemas(t, stringWithDefaultNullAndMatchesSchema, S.Option.getOr(S.nullAsOption(S.url), "Foo"), undefined)); let ignoredNullWithMatchesSchema = S.option(S.string); diff --git a/packages/e2e/src/ppx/Ppx_Object_test.res b/packages/e2e/src/ppx/Ppx_Object_test.res index 4d16875bd..e7ccbdf4f 100644 --- a/packages/e2e/src/ppx/Ppx_Object_test.res +++ b/packages/e2e/src/ppx/Ppx_Object_test.res @@ -14,7 +14,7 @@ test("Simple object schema", t => { ), ) t->Assert.deepEqual( - %raw(`{label:"foo",value:1}`)->S.parseOrThrow(simpleObjectSchema), + %raw(`{label:"foo",value:1}`)->S.parseOrThrow(~to=simpleObjectSchema), {"label": "foo", "value": 1}, ) }) @@ -32,7 +32,7 @@ test("Simple object schema", t => { // ), // ) // t->Assert.deepEqual( -// %raw(`{"label":"foo",value:1}`)->S.parseOrThrow(objectWithAliasSchema), +// %raw(`{"label":"foo",value:1}`)->S.parseOrThrow(~to=objectWithAliasSchema), // {"label": "foo", "value": 1}, // ) // }) diff --git a/packages/e2e/src/ppx/Ppx_Primitive_test.res b/packages/e2e/src/ppx/Ppx_Primitive_test.res index fd32c02e0..9c16f2e1c 100644 --- a/packages/e2e/src/ppx/Ppx_Primitive_test.res +++ b/packages/e2e/src/ppx/Ppx_Primitive_test.res @@ -4,6 +4,7 @@ open U // TODO: Automatically enable it in PPX S.enableJson() +S.enableEmail() @schema type myString = string @@ -130,21 +131,21 @@ test("Big tuple schema", t => { }) @schema -type myCustomString = @s.matches(S.string->S.email) string +type myCustomString = @s.matches(S.email) string test("Custom string schema", t => { - t->assertEqualSchemas(myCustomStringSchema, S.string->S.email) + t->assertEqualSchemas(myCustomStringSchema, S.email) }) @schema -type myCustomLiteralString = @s.matches(S.literal("123")->S.email) string +type myCustomLiteralString = @s.matches(S.literal("123")) string test("Custom litaral string schema", t => { - t->assertEqualSchemas(myCustomLiteralStringSchema, S.literal("123")->S.email) + t->assertEqualSchemas(myCustomLiteralStringSchema, S.literal("123")) }) @schema -type myCustomOptionalString = option<@s.matches(S.string->S.email) string> +type myCustomOptionalString = option<@s.matches(S.email) string> test("Custom optional string schema", t => { - t->assertEqualSchemas(myCustomOptionalStringSchema, S.string->S.email->S.option) + t->assertEqualSchemas(myCustomOptionalStringSchema, S.email->S.option) }) // @schema @@ -153,7 +154,7 @@ test("Custom optional string schema", t => { // The incompatible parts: option vs myNullOfString (defined as null) // So use the code below instead @schema -type myNullOfString = @s.matches(S.null(S.string)) option +type myNullOfString = @s.matches(S.nullAsOption(S.string)) option test("Null of string schema", t => { - t->assertEqualSchemas(myNullOfStringSchema, S.null(S.string)) + t->assertEqualSchemas(myNullOfStringSchema, S.nullAsOption(S.string)) }) diff --git a/packages/e2e/src/ppx/Ppx_Primitive_test.res.mjs b/packages/e2e/src/ppx/Ppx_Primitive_test.res.mjs index f9ab6ba50..98f9bfe5a 100644 --- a/packages/e2e/src/ppx/Ppx_Primitive_test.res.mjs +++ b/packages/e2e/src/ppx/Ppx_Primitive_test.res.mjs @@ -6,6 +6,8 @@ import Ava from "ava"; S.enableJson(); +S.enableEmail(); + Ava("String schema", t => U.assertEqualSchemas(t, S.string, S.string, undefined)); Ava("Int schema", t => U.assertEqualSchemas(t, S.int, S.int, undefined)); @@ -89,21 +91,19 @@ Ava("Big tuple schema", t => U.assertEqualSchemas(t, myBigTupleSchema, S.schema( s.m(S.bool) ]), undefined)); -let myCustomStringSchema = S.email(S.string, undefined); - -Ava("Custom string schema", t => U.assertEqualSchemas(t, myCustomStringSchema, S.email(S.string, undefined), undefined)); +Ava("Custom string schema", t => U.assertEqualSchemas(t, S.email, S.email, undefined)); -let myCustomLiteralStringSchema = S.email(S.literal("123"), undefined); +let myCustomLiteralStringSchema = S.literal("123"); -Ava("Custom litaral string schema", t => U.assertEqualSchemas(t, myCustomLiteralStringSchema, S.email(S.literal("123"), undefined), undefined)); +Ava("Custom litaral string schema", t => U.assertEqualSchemas(t, myCustomLiteralStringSchema, S.literal("123"), undefined)); -let myCustomOptionalStringSchema = S.option(S.email(S.string, undefined)); +let myCustomOptionalStringSchema = S.option(S.email); -Ava("Custom optional string schema", t => U.assertEqualSchemas(t, myCustomOptionalStringSchema, S.option(S.email(S.string, undefined)), undefined)); +Ava("Custom optional string schema", t => U.assertEqualSchemas(t, myCustomOptionalStringSchema, S.option(S.email), undefined)); -let myNullOfStringSchema = S.$$null(S.string); +let myNullOfStringSchema = S.nullAsOption(S.string); -Ava("Null of string schema", t => U.assertEqualSchemas(t, myNullOfStringSchema, S.$$null(S.string), undefined)); +Ava("Null of string schema", t => U.assertEqualSchemas(t, myNullOfStringSchema, S.nullAsOption(S.string), undefined)); let myStringSchema = S.string; @@ -123,6 +123,8 @@ let myJsonSchema = S.json; let myJsonFromCoreSchema = S.json; +let myCustomStringSchema = S.email; + export { myStringSchema, myIntSchema, diff --git a/packages/e2e/src/ppx/Ppx_Record_test.res b/packages/e2e/src/ppx/Ppx_Record_test.res index 14d9f3a41..0cdea0f1f 100644 --- a/packages/e2e/src/ppx/Ppx_Record_test.res +++ b/packages/e2e/src/ppx/Ppx_Record_test.res @@ -15,7 +15,7 @@ test("Simple record schema", t => { }), ) t->Assert.deepEqual( - %raw(`{label:"foo",value:1}`)->S.parseOrThrow(simpleRecordSchema), + %raw(`{label:"foo",value:1}`)->S.parseOrThrow(~to=simpleRecordSchema), {label: "foo", value: 1}, ) }) @@ -34,7 +34,7 @@ test("Record schema with alias for field name", t => { }), ) t->Assert.deepEqual( - %raw(`{"aliased-label":"foo",value:1}`)->S.parseOrThrow(recordWithAliasSchema), + %raw(`{"aliased-label":"foo",value:1}`)->S.parseOrThrow(~to=recordWithAliasSchema), {label: "foo", value: 1}, ) }) @@ -53,32 +53,32 @@ test("Record schema with optional fields", t => { }), ) t->Assert.deepEqual( - %raw(`{"label":"foo",value:1}`)->S.parseOrThrow(recordWithOptionalSchema), + %raw(`{"label":"foo",value:1}`)->S.parseOrThrow(~to=recordWithOptionalSchema), {label: Some("foo"), value: 1}, ) t->Assert.deepEqual( - %raw(`{}`)->S.parseOrThrow(recordWithOptionalSchema), + %raw(`{}`)->S.parseOrThrow(~to=recordWithOptionalSchema), {label: %raw(`undefined`), value: %raw(`undefined`)}, ) }) @schema type recordWithNullableField = { - subscription: @s.matches(S.option(S.null(S.string))) option>, + subscription: @s.matches(S.option(S.nullAsOption(S.string))) option>, } test("Record schema with nullable field", t => { t->assertEqualSchemas( recordWithNullableFieldSchema, S.schema(s => { - subscription: s.matches(S.option(S.null(S.string))), + subscription: s.matches(S.option(S.nullAsOption(S.string))), }), ) t->Assert.deepEqual( - %raw(`{}`)->S.parseOrThrow(recordWithNullableFieldSchema), + %raw(`{}`)->S.parseOrThrow(~to=recordWithNullableFieldSchema), {subscription: None}, ) t->Assert.deepEqual( - %raw(`{"subscription":null}`)->S.parseOrThrow(recordWithNullableFieldSchema), + %raw(`{"subscription":null}`)->S.parseOrThrow(~to=recordWithNullableFieldSchema), {subscription: Some(None)}, ) }) diff --git a/packages/e2e/src/ppx/Ppx_Record_test.res.mjs b/packages/e2e/src/ppx/Ppx_Record_test.res.mjs index 205abe21b..6985a3108 100644 --- a/packages/e2e/src/ppx/Ppx_Record_test.res.mjs +++ b/packages/e2e/src/ppx/Ppx_Record_test.res.mjs @@ -58,12 +58,12 @@ Ava("Record schema with optional fields", t => { }); let recordWithNullableFieldSchema = S.schema(s => ({ - subscription: s.m(S.option(S.$$null(S.string))) + subscription: s.m(S.option(S.nullAsOption(S.string))) })); Ava("Record schema with nullable field", t => { U.assertEqualSchemas(t, recordWithNullableFieldSchema, S.schema(s => ({ - subscription: s.m(S.option(S.$$null(S.string))) + subscription: s.m(S.option(S.nullAsOption(S.string))) })), undefined); t.deepEqual(S.parseOrThrow({}, recordWithNullableFieldSchema), { subscription: undefined diff --git a/packages/e2e/src/ppx/Ppx_regression_test.res b/packages/e2e/src/ppx/Ppx_regression_test.res index 603ffbc9a..7a2c57af1 100644 --- a/packages/e2e/src/ppx/Ppx_regression_test.res +++ b/packages/e2e/src/ppx/Ppx_regression_test.res @@ -33,13 +33,13 @@ module CknittelBugReport = { b: 42, }, } - t->Assert.deepEqual(B(x)->S.reverseConvertOrThrow(schema), %raw(`{"payload":{"b":42}}`)) + t->Assert.deepEqual(B(x)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"payload":{"b":42}}`)) let x = { A.payload: { a: "foo", }, } - t->Assert.deepEqual(A(x)->S.reverseConvertOrThrow(schema), %raw(`{"payload":{"a":"foo"}}`)) + t->Assert.deepEqual(A(x)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"payload":{"a":"foo"}}`)) }) } @@ -73,7 +73,7 @@ module CknittelBugReport2 = { `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["test"];if(typeof v0==="object"&&v0){if(v0["type"]==="a"){let v1=v0["x"];if(typeof v1!=="number"||v1>2147483647||v1<-2147483648||v1%1!==0){e[1](v1)}v0={"TAG":"A","_0":{"x":v1,},}}else if(v0["type"]==="b"){let v2=v0["y"];if(typeof v2!=="string"){e[2](v2)}v0={"TAG":"B","_0":{"y":v2,},}}else{e[3](v0)}}else if(!(v0===void 0)){e[4](v0)}return {"test":v0,}}`, ) - t->Assert.deepEqual(S.parseJsonStringOrThrow("{}", schema), {test: None}) + t->Assert.deepEqual(S.decodeOrThrow("{}", ~from=S.jsonString, ~to=schema), {test: None}) }) type responseError = {serviceCode: string, text: string} @@ -99,6 +99,6 @@ module CknittelBugReport2 = { `i=>{if(typeof i==="object"&&i){if(typeof i["statusCode"]==="object"&&i["statusCode"]&&i["statusCode"]["kind"]==="ok"){i={"TAG":"Ok","_0":void 0,}}else if(typeof i["statusCode"]==="object"&&i["statusCode"]&&i["statusCode"]["kind"]==="serviceError"){let v0=i["statusCode"],v1=v0["serviceCode"],v2=v0["text"];if(typeof v1!=="string"){e[0](v1)}if(typeof v2!=="string"){e[1](v2)}i={"TAG":"Error","_0":{"serviceCode":v1,"text":v2,},}}else{e[2](i)}}else{e[3](i)}return i}`, ) - t->Assert.deepEqual(S.parseJsonStringOrThrow(`{"statusCode": {"kind": "ok"}}`, schema), Ok()) + t->Assert.deepEqual(S.decodeOrThrow(`{"statusCode": {"kind": "ok"}}`, ~from=S.jsonString, ~to=schema), Ok()) }) } diff --git a/packages/e2e/src/ppx/Ppx_regression_test.res.mjs b/packages/e2e/src/ppx/Ppx_regression_test.res.mjs index c3124d0e7..5b755e803 100644 --- a/packages/e2e/src/ppx/Ppx_regression_test.res.mjs +++ b/packages/e2e/src/ppx/Ppx_regression_test.res.mjs @@ -42,22 +42,22 @@ Ava("Union serializing of objects with optional fields", t => { })) ]); U.assertCompiledCode(t, schema$2, "ReverseConvert", "i=>{if(typeof i===\"object\"&&i){if(i[\"TAG\"]===\"A\"&&typeof i[\"_0\"]===\"object\"&&i[\"_0\"]&&typeof i[\"_0\"][\"payload\"]===\"object\"&&i[\"_0\"][\"payload\"]){let v0=i[\"_0\"];let v1=v0[\"payload\"];i=v0}else if(i[\"TAG\"]===\"B\"&&typeof i[\"_0\"]===\"object\"&&i[\"_0\"]&&typeof i[\"_0\"][\"payload\"]===\"object\"&&i[\"_0\"][\"payload\"]){let v2=i[\"_0\"];let v3=v2[\"payload\"];i=v2}}return i}", undefined); - t.deepEqual(S.reverseConvertOrThrow({ + t.deepEqual(S.decodeOrThrow({ TAG: "B", _0: { payload: { b: 42 } } - }, schema$2), {"payload":{"b":42}}); - t.deepEqual(S.reverseConvertOrThrow({ + }, schema$2, S.unknown), {"payload":{"b":42}}); + t.deepEqual(S.decodeOrThrow({ TAG: "A", _0: { payload: { a: "foo" } } - }, schema$2), {"payload":{"a":"foo"}}); + }, schema$2, S.unknown), {"payload":{"a":"foo"}}); }); let CknittelBugReport = { @@ -96,7 +96,7 @@ let schema$2 = S.schema(s => ({ Ava("Successfully parses nested optional union", t => { U.assertCompiledCode(t, schema$2, "Parse", "i=>{if(typeof i!==\"object\"||!i){e[0](i)}let v0=i[\"test\"];if(typeof v0===\"object\"&&v0){if(v0[\"type\"]===\"a\"){let v1=v0[\"x\"];if(typeof v1!==\"number\"||v1>2147483647||v1<-2147483648||v1%1!==0){e[1](v1)}v0={\"TAG\":\"A\",\"_0\":{\"x\":v1,},}}else if(v0[\"type\"]===\"b\"){let v2=v0[\"y\"];if(typeof v2!==\"string\"){e[2](v2)}v0={\"TAG\":\"B\",\"_0\":{\"y\":v2,},}}else{e[3](v0)}}else if(!(v0===void 0)){e[4](v0)}return {\"test\":v0,}}", undefined); - t.deepEqual(S.parseJsonStringOrThrow("{}", schema$2), { + t.deepEqual(S.decodeOrThrow("{}", S.jsonString, schema$2), { test: undefined }); }); @@ -122,7 +122,7 @@ Ava("Nested literal field with catch", t => { }) ]); U.assertCompiledCode(t, schema, "Parse", "i=>{if(typeof i===\"object\"&&i){if(typeof i[\"statusCode\"]===\"object\"&&i[\"statusCode\"]&&i[\"statusCode\"][\"kind\"]===\"ok\"){i={\"TAG\":\"Ok\",\"_0\":void 0,}}else if(typeof i[\"statusCode\"]===\"object\"&&i[\"statusCode\"]&&i[\"statusCode\"][\"kind\"]===\"serviceError\"){let v0=i[\"statusCode\"],v1=v0[\"serviceCode\"],v2=v0[\"text\"];if(typeof v1!==\"string\"){e[0](v1)}if(typeof v2!==\"string\"){e[1](v2)}i={\"TAG\":\"Error\",\"_0\":{\"serviceCode\":v1,\"text\":v2,},}}else{e[2](i)}}else{e[3](i)}return i}", undefined); - t.deepEqual(S.parseJsonStringOrThrow("{\"statusCode\": {\"kind\": \"ok\"}}", schema), { + t.deepEqual(S.decodeOrThrow("{\"statusCode\": {\"kind\": \"ok\"}}", S.jsonString, schema), { TAG: "Ok", _0: undefined }); diff --git a/packages/e2e/src/utils/U.res b/packages/e2e/src/utils/U.res index 2e1f6196f..f41bcbb6b 100644 --- a/packages/e2e/src/utils/U.res +++ b/packages/e2e/src/utils/U.res @@ -2,13 +2,11 @@ open Ava // The hack to bypass wallaby adding tags // and turning the function into: -// function noopOperation(i) {␊  -// var $_$c = $_$wf(3);␊  -// return $_$w(3, 444, $_$c), i;␊  +// function noopOperation(i) {␊ +// var $_$c = $_$wf(3);␊ +// return $_$w(3, 444, $_$c), i;␊ // } -let noopOpCode = ( - S.unknown->S.compile(~input=Any, ~output=Unknown, ~mode=Sync, ~typeValidation=false)->Obj.magic -)["toString"]() +let noopOpCode: string = (S.decoder(~from=S.unknown, ~to=S.unknown)->Obj.magic)["toString"]() external magic: 'a => 'b = "%identity" external castAnyToUnknown: 'any => unknown = "%identity" @@ -26,32 +24,6 @@ let unsafeGetVariantPayload = variant => (variant->Obj.magic)["_0"] exception Test let throwTestException = () => throw(Test) -type taggedFlag = - | Parse - | ParseAsync - | ReverseConvertToJson - | ReverseParse - | ReverseConvert - | Assert - -type errorPayload = {operation: taggedFlag, code: S.errorCode, path: S.Path.t} - -// TODO: Get rid of the helper -let error = ({operation, code, path}: errorPayload): S.error => { - S.ErrorClass.constructor( - ~code, - ~flag=switch operation { - | Parse => S.Flag.typeValidation - | ReverseParse => S.Flag.reverse->S.Flag.with(S.Flag.typeValidation) - | ReverseConvertToJson => S.Flag.reverse->S.Flag.with(S.Flag.jsonableOutput) - | ReverseConvert => S.Flag.reverse - | ParseAsync => S.Flag.typeValidation->S.Flag.with(S.Flag.async) - | Assert => S.Flag.typeValidation->S.Flag.with(S.Flag.assertOutput) - }, - ~path, - ) -} - let assertThrowsTestException = { (t, fn, ~message=?) => { try { @@ -67,30 +39,32 @@ let assertThrowsTestException = { let assertThrows = (t, cb, errorPayload) => { switch cb() { | any => t->Assert.fail("Asserted result is not Error. Recieved: " ++ any->unsafeStringify) - | exception S.Error({message}) => t->Assert.is(message, error(errorPayload).message) + | exception S.Exn({message}) => t->Assert.is(message, S.Error.make(errorPayload).message) } } -let assertThrowsMessage = (t, cb, errorMessage) => { +let assertThrowsMessage = (t, cb, errorMessage, ~message=?) => { switch cb() { | any => t->Assert.fail( - `Asserted result is not S.Error "${errorMessage}". Instead got: ${any->unsafeStringify}`, + `Asserted result is not S.Exn "${errorMessage}". Instead got: ${any->unsafeStringify}`, ) - | exception S.Error({message}) => t->Assert.is(message, errorMessage) + | exception S.Exn({message: actualErrorMessage}) => + t->Assert.is(actualErrorMessage, errorMessage, ~message?) } } let assertThrowsAsync = async (t, cb, errorPayload) => { switch await cb() { | any => t->Assert.fail("Asserted result is not Error. Recieved: " ++ any->unsafeStringify) - | exception S.Error({message}) => t->Assert.is(message, error(errorPayload).message) + | exception S.Exn({message}) => t->Assert.is(message, S.Error.make(errorPayload).message) } } let getCompiledCodeString = ( schema, ~op: [ + | #Parse | #Parse | #ParseAsync | #Convert @@ -102,61 +76,55 @@ let getCompiledCodeString = ( | #ReverseConvertToJson ], ) => { - let toCode = schema => - ( - switch op { - | #Parse - | #ParseAsync => - if op === #ParseAsync || schema->S.isAsync { - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Async, ~typeValidation=true) - fn->magic - } else { - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=true) - fn->magic - } - | #Convert => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=false) + let toFn = schema => + switch op { + | #Parse => + let fn = S.decoder(~from=S.unknown, ~to=schema) + fn->magic + | #ParseAsync => + let fn = S.asyncDecoder(~from=S.unknown, ~to=schema) + fn->magic + | #Convert => + let fn = S.decoder(~from=schema->S.reverse, ~to=S.unknown) + fn->magic + | #ConvertAsync => + let fn = S.asyncDecoder(~from=schema->S.reverse, ~to=S.unknown) + fn->magic + | #Assert => + let fn = S.decoder(~from=S.unknown, ~to=schema->S.to(S.literal()->S.noValidation(true))) + fn->magic + | #ReverseParse => { + let fn = S.decoder(~from=S.unknown, ~to=schema->S.reverse) fn->magic - | #ConvertAsync => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Async, ~typeValidation=false) + } + | #ReverseConvert => { + let fn = S.decoder(~from=schema, ~to=S.unknown) fn->magic - | #Assert => - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=true) + } + | #ReverseConvertAsync => { + let fn = S.asyncDecoder(~from=schema, ~to=S.unknown) + fn->magic + } + | #ReverseConvertToJson => { + let fn = S.decoder(~from=schema, ~to=S.json) fn->magic - | #ReverseParse => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=true) - fn->magic - } - | #ReverseConvert => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=false) - fn->magic - } - | #ReverseConvertAsync => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Async, ~typeValidation=false) - fn->magic - } - | #ReverseConvertToJson => { - let fn = schema->S.compile(~input=Value, ~output=Json, ~mode=Sync, ~typeValidation=false) - fn->magic - } } - )["toString"]() + } - let code = ref(schema->toCode) + let fn = schema->toFn + let code = ref(fn["toString"]()) switch (schema->S.untag).defs { - | Some(defs) => + | Some(defs) if code.contents !== noopOpCode => defs->Dict.forEachWithKey((schema, key) => try { - code := code.contents ++ "\n" ++ `${key}: ${schema->toCode}` + let defFn = schema->toFn + code := code.contents ++ "\n" ++ `${key}: ${defFn["toString"]()}` } catch { - | exn => Js.Console.error(exn) + | _exn => () } ) - | None => () + | _ => () } code.contents @@ -170,7 +138,9 @@ let rec cleanUpSchema = schema => { ->Array.forEach(((key, value)) => { switch key { | "output" - | "isAsync" => () + | "isAsync" + | "hasTransform" + | "seq" => () // ditemToItem leftovers FIXME: | "k" | "p" | "of" | "r" => () | _ => @@ -211,8 +181,8 @@ let assertEqualSchemas: ( let assertReverseParsesBack = (t, schema: S.t<'value>, value: 'value) => { t->Assert.unsafeDeepEqual( value - ->S.reverseConvertOrThrow(schema) - ->S.parseOrThrow(schema), + ->S.decodeOrThrow(~from=schema, ~to=S.unknown) + ->S.parseOrThrow(~to=schema), value, ) } diff --git a/packages/e2e/src/utils/U.res.mjs b/packages/e2e/src/utils/U.res.mjs index 36f524bbf..a3a8904ef 100644 --- a/packages/e2e/src/utils/U.res.mjs +++ b/packages/e2e/src/utils/U.res.mjs @@ -5,7 +5,7 @@ import * as Stdlib_Dict from "rescript/lib/es6/Stdlib_Dict.js"; import * as Primitive_option from "rescript/lib/es6/Primitive_option.js"; import * as Primitive_exceptions from "rescript/lib/es6/Primitive_exceptions.js"; -let noopOpCode = S.compile(S.unknown, "Any", "Input", "Sync", false).toString(); +let noopOpCode = S.decoder(S.unknown, S.unknown).toString(); function throwError(error) { throw error; @@ -24,31 +24,6 @@ function throwTestException() { }; } -function error(param) { - let tmp; - switch (param.operation) { - case "Parse" : - tmp = S.Flag.typeValidation; - break; - case "ParseAsync" : - tmp = S.Flag.typeValidation | S.Flag.async; - break; - case "ReverseConvertToJson" : - tmp = S.Flag.reverse | S.Flag.jsonableOutput; - break; - case "ReverseParse" : - tmp = S.Flag.reverse | S.Flag.typeValidation; - break; - case "ReverseConvert" : - tmp = S.Flag.reverse; - break; - case "Assert" : - tmp = S.Flag.typeValidation | S.Flag.assertOutput; - break; - } - return S.ErrorClass.constructor(param.code, tmp, param.path); -} - function assertThrowsTestException(t, fn, message) { try { fn(); @@ -70,8 +45,8 @@ function assertThrows(t, cb, errorPayload) { any = cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { - t.is(exn._1.message, error(errorPayload).message); + if (exn.RE_EXN_ID === S.Exn) { + t.is(exn._1.message, S.$$Error.make(errorPayload).message); return; } throw exn; @@ -79,19 +54,19 @@ function assertThrows(t, cb, errorPayload) { t.fail("Asserted result is not Error. Recieved: " + JSON.stringify(any)); } -function assertThrowsMessage(t, cb, errorMessage) { +function assertThrowsMessage(t, cb, errorMessage, message) { let any; try { any = cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { - t.is(exn._1.message, errorMessage); + if (exn.RE_EXN_ID === S.Exn) { + t.is(exn._1.message, errorMessage, message !== undefined ? Primitive_option.valFromOption(message) : undefined); return; } throw exn; } - t.fail("Asserted result is not S.Error \"" + errorMessage + "\". Instead got: " + JSON.stringify(any)); + t.fail("Asserted result is not S.Exn \"" + errorMessage + "\". Instead got: " + JSON.stringify(any)); } async function assertThrowsAsync(t, cb, errorPayload) { @@ -100,8 +75,8 @@ async function assertThrowsAsync(t, cb, errorPayload) { any = await cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { - t.is(exn._1.message, error(errorPayload).message); + if (exn.RE_EXN_ID === S.Exn) { + t.is(exn._1.message, S.$$Error.make(errorPayload).message); return; } throw exn; @@ -110,35 +85,39 @@ async function assertThrowsAsync(t, cb, errorPayload) { } function getCompiledCodeString(schema, op) { - let toCode = schema => ( - op === "Parse" || op === "ParseAsync" ? ( - op === "ParseAsync" || S.isAsync(schema) ? S.compile(schema, "Any", "Output", "Async", true) : S.compile(schema, "Any", "Output", "Sync", true) - ) : ( - op === "ReverseConvertToJson" ? S.compile(schema, "Output", "Json", "Sync", false) : ( - op === "ReverseConvert" ? S.compile(schema, "Output", "Input", "Sync", false) : ( - op === "Convert" ? S.compile(schema, "Any", "Output", "Sync", false) : ( - op === "Assert" ? S.compile(schema, "Any", "Assert", "Sync", true) : ( - op === "ReverseParse" ? S.compile(schema, "Output", "Input", "Sync", true) : ( - op === "ConvertAsync" ? S.compile(schema, "Any", "Output", "Async", false) : S.compile(schema, "Output", "Input", "Async", false) - ) - ) - ) - ) - ) - ) - ).toString(); + let toFn = schema => { + if (op === "ParseAsync") { + return S.asyncDecoder(S.unknown, schema); + } else if (op === "Parse") { + return S.decoder(S.unknown, schema); + } else if (op === "ReverseConvertToJson") { + return S.decoder(schema, S.json); + } else if (op === "ReverseConvert") { + return S.decoder(schema, S.unknown); + } else if (op === "Convert") { + return S.decoder(S.reverse(schema), S.unknown); + } else if (op === "Assert") { + return S.decoder(S.unknown, S.to(schema, S.noValidation(S.literal(), true))); + } else if (op === "ReverseParse") { + return S.decoder(S.unknown, S.reverse(schema)); + } else if (op === "ConvertAsync") { + return S.asyncDecoder(S.reverse(schema), S.unknown); + } else { + return S.asyncDecoder(schema, S.unknown); + } + }; + let fn = toFn(schema); let code = { - contents: toCode(schema) + contents: fn.toString() }; let defs = schema.$defs; - if (defs !== undefined) { + if (defs !== undefined && code.contents !== noopOpCode) { Stdlib_Dict.forEachWithKey(defs, (schema, key) => { try { - code.contents = code.contents + "\n" + (key + ": " + toCode(schema)); + let defFn = toFn(schema); + code.contents = code.contents + "\n" + (key + ": " + defFn.toString()); return; - } catch (raw_exn) { - let exn = Primitive_exceptions.internalToException(raw_exn); - console.error(exn); + } catch (_exn) { return; } }); @@ -152,12 +131,14 @@ function cleanUpSchema(schema) { let value = param[1]; let key = param[0]; switch (key) { + case "hasTransform" : case "isAsync" : case "k" : case "of" : case "output" : case "p" : case "r" : + case "seq" : return; default: if (typeof value === "function") { @@ -188,7 +169,7 @@ function assertCompiledCodeIsNoop(t, schema, op, message) { } function assertReverseParsesBack(t, schema, value) { - t.deepEqual(S.parseOrThrow(S.reverseConvertOrThrow(value, schema), schema), value); + t.deepEqual(S.parseOrThrow(S.decodeOrThrow(value, schema, S.unknown), schema), value); } function assertReverseReversesBack(t, schema) { @@ -203,7 +184,6 @@ export { unsafeGetVariantPayload, Test, throwTestException, - error, assertThrowsTestException, assertThrows, assertThrowsMessage, diff --git a/packages/sury-ppx/README.md b/packages/sury-ppx/README.md index 3259d3f67..a1b56f33c 100644 --- a/packages/sury-ppx/README.md +++ b/packages/sury-ppx/README.md @@ -50,7 +50,7 @@ type film = { @as("Rating") rating: rating, @as("Age") - deprecatedAgeRestriction: @s.deprecated("Use rating instead") option, + deprecatedAgeRestriction: @s.meta({deprecated: true}) option, } // 2. Generated by PPX ⬇️ @@ -65,7 +65,7 @@ let filmSchema = S.object(s => { 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.deprecated("Use rating instead")), + deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.meta({deprecated: true})), }) // 3. Parse data using the schema @@ -91,7 +91,7 @@ let filmSchema = S.object(s => { title: "Sad & sed", rating: ParentalStronglyCautioned, deprecatedAgeRestriction: None, -}->S.reverseConvertOrThrow(filmSchema) +}->S.decodeOrThrow(~from=filmSchema, ~to=S.unknown) // Ok(%raw(`{ // "Id": 2, // "Title": "Sad & sed", @@ -133,14 +133,14 @@ let schema = S.string->S.url **Applies to**: option type expressions -Tells to use `S.null` for the option schema constructor. +Tells to use `S.nullAsOption` for the option schema constructor. ```rescript @schema type t = @s.null option // Generated by PPX ⬇️ -let schema = S.null(S.string) +let schema = S.nullAsOption(S.string) ``` ### `@s.nullable` @@ -178,7 +178,7 @@ It might also be used together with `@s.null`: type t = @s.null @s.default("Unknown") string // Generated by PPX ⬇️ -let schema = S.null(S.string)->S.Option.getOr("Unknown") +let schema = S.nullAsOption(S.string)->S.Option.getOr("Unknown") ``` ### `@s.defaultWith(unit => 'value)` diff --git a/packages/sury-ppx/src/ppx/Structure.ml b/packages/sury-ppx/src/ppx/Structure.ml index 86843ae0e..9e1fe5acd 100644 --- a/packages/sury-ppx/src/ppx/Structure.ml +++ b/packages/sury-ppx/src/ppx/Structure.ml @@ -148,7 +148,7 @@ and generateCoreTypeSchemaExpression core_type = getAttributeByName ptyp_attributes "s.nullable" ) with | Ok None, Ok None -> [%expr S.option] - | Ok (Some _), Ok None -> [%expr S.null] + | Ok (Some _), Ok None -> [%expr S.nullAsOption] | Ok None, Ok (Some _) -> [%expr S.nullableAsOption] | Ok (Some _), Ok (Some _) -> fail ptyp_loc diff --git a/packages/sury/README.md b/packages/sury/README.md index 97ddb2fb1..5b3ffc8ca 100644 --- a/packages/sury/README.md +++ b/packages/sury/README.md @@ -18,7 +18,6 @@ The fastest schema with next-gen DX. - Declarative transformations with automatic serialization - Immutable API with 100+ different operations - Flexible global config -- Opt-in ReScript codegen from type definitions (ppx) Also, you can use **Sury** as a building block for your own tools or use existing ones: diff --git a/packages/sury/failing-tests.snapshot.txt b/packages/sury/failing-tests.snapshot.txt new file mode 100644 index 000000000..87b3fa148 --- /dev/null +++ b/packages/sury/failing-tests.snapshot.txt @@ -0,0 +1,24 @@ +# Failing Tests Baseline Snapshot +# +# To reproduce this list after making changes, run: +# cd packages/sury && npx ava 2>&1 | grep "✘ \[fail\]" | sed 's/lib › bs › //' | sed 's/tests › //' | sed 's/ Error thrown in test//' | sort -u +# +# Total: 17 failing tests + + ✘ [fail]: S_noValidation_test.res › Union dispatch still works when a case has noValidation + ✘ [fail]: S_object_discriminant_test.res › Fails to serialize object with discriminant that we don't know how to serialize "true | null" + ✘ [fail]: S_object_test.res › Compiled code snapshot for refined nested object + ✘ [fail]: S_refine_test.res › Compiled parse code snapshot for simple object with refine + ✘ [fail]: S_refine_test.res › Successfully parses + ✘ [fail]: S_toExpression_test.res › Expression of renamed schema + ✘ [fail]: S_to_test.res › Coerce from union to bigint with refinement on union + ✘ [fail]: S_to_test.res › Coerce from union to bigint with refinement on union (with an item transformed to) Should apply refinement after the item transformation + ✘ [fail]: S_to_test.res › Coerce from union to wider union should keep the original value type + ✘ [fail]: S_to_test.res › Coerce string to custom JSON schema I don't know what we expect here, but currently it works this way + ✘ [fail]: S_to_test.res › Transform from union to wider union with different items order (applies decoder to both one at a time) + ✘ [fail]: S_union_test.res › Compiled serialize code snapshot of crazy union + ✘ [fail]: S_union_test.res › Ensures parsing order with unknown schema + ✘ [fail]: S_union_test.res › NaN should be checked before number even if it's later item in the union + ✘ [fail]: S_union_test.res › Serializes when second struct misses serializer + ✘ [fail]: S_union_test.res › Union of strings with different refinements + ✘ [fail]: S_union_test.res › json-rpc response diff --git a/packages/sury/output-test.js b/packages/sury/output-test.js index 6e977a758..7b32e1b18 100644 --- a/packages/sury/output-test.js +++ b/packages/sury/output-test.js @@ -1,21 +1 @@ -(i) => { - if (typeof i === "string") { - let v0; - try { - v0 = BigInt(i); - } catch (_) { - e[0](i); - } - i = v0; - } else if (typeof i === "number" && !Number.isNaN(i)) { - i = BigInt(i); - } else if (typeof i === "boolean") { - throw e[1]; - } else { - e[2](i); - } - if (typeof i !== "bigint") { - e[3](i); - } - return i; -}; +i => { if (typeof i !== "string") { e[3](i) } try { let v0; (v0 = i === "true") || i === "false" || e[0](i); i = v0 } catch (e0) { if () { let v1 = "undefined"; if (i !== "undefined") { e[1](v1) } i = void 0 } else { e[2](i, e0) } } return i } \ No newline at end of file diff --git a/packages/sury/package.json b/packages/sury/package.json index 8abd96c1b..4b3a72dac 100644 --- a/packages/sury/package.json +++ b/packages/sury/package.json @@ -44,7 +44,7 @@ "build": "pnpm rescript --dev && node ./scripts/pack/Pack.res.mjs", "res": "rescript watch --dev", "test:res": "cd ./packages/tests && rescript clean && rescript watch --dev", - "test": "pnpm tsc && ava", + "test": "rescript build --dev && pnpm tsc && ava", "lint:stdlib": "rescript-stdlib-vendorer lint --ignore-path=src" }, "ava": { diff --git a/packages/sury/scripts/pack/Pack.res b/packages/sury/scripts/pack/Pack.res index 304986c09..c8e478926 100644 --- a/packages/sury/scripts/pack/Pack.res +++ b/packages/sury/scripts/pack/Pack.res @@ -112,14 +112,13 @@ NodeJs.Fs.mkdirSync(NodeJs.Path.join2(artifactsPath, "tests")) NodeJs.Fs.mkdirSync(NodeJs.Path.join2(artifactsPath, "scripts")) let filesMapping = [ - ("Error", "S.ErrorClass.value"), + ("Error", "S.$$Error.$$class"), ("string", "S.string"), ("boolean", "S.bool"), ("int32", "S.int"), ("number", "S.float"), ("bigint", "S.bigint"), ("symbol", "S.symbol"), - ("json", "S.json"), ("never", "S.never"), ("unknown", "S.unknown"), ("any", "S.unknown"), @@ -127,43 +126,47 @@ let filesMapping = [ ("nullable", "S.js_nullable"), ("nullish", "S.nullable"), ("array", "S.array"), + ("compactColumns", "S.compactColumns"), ("instance", "S.instance"), ("unnest", "S.unnest"), ("record", "S.dict"), + ("json", "S.json"), + ("enableJson", "S.enableJson"), ("jsonString", "S.jsonString"), + ("enableJsonString", "S.enableJsonString"), ("jsonStringWithSpace", "S.jsonStringWithSpace"), + ("uint8Array", "S.uint8Array"), + ("enableUint8Array", "S.enableUint8Array"), + ("date", "S.date"), + ("isoDateTime", "S.isoDateTime"), + ("enableIsoDateTime", "S.enableIsoDateTime"), ("union", "S.js_union"), ("object", "S.object"), ("schema", "S.js_schema"), ("safe", "S.js_safe"), ("safeAsync", "S.js_safeAsync"), ("reverse", "S.reverse"), - ("convertOrThrow", "S.convertOrThrow"), - ("convertToJsonOrThrow", "S.convertToJsonOrThrow"), - ("convertToJsonStringOrThrow", "S.convertToJsonStringOrThrow"), - ("reverseConvertOrThrow", "S.reverseConvertOrThrow"), - ("reverseConvertToJsonOrThrow", "S.reverseConvertToJsonOrThrow"), - ("reverseConvertToJsonStringOrThrow", " S.reverseConvertToJsonStringOrThrow"), - ("parseOrThrow", "S.parseOrThrow"), - ("parseJsonOrThrow", "S.parseJsonOrThrow"), - ("parseJsonStringOrThrow", "S.parseJsonStringOrThrow"), - ("parseAsyncOrThrow", "S.parseAsyncOrThrow"), - ("assertOrThrow", "S.assertOrThrow"), + ("parser", "S.parser"), + ("asyncParser", "S.asyncParser"), + ("decoder", "S.getDecoder"), + ("asyncDecoder", "S.asyncDecoder"), + ("encoder", "S.encoder"), + ("asyncEncoder", "S.asyncEncoder"), + ("assert", "S.js_assert"), ("recursive", "S.recursive"), ("merge", "S.js_merge"), ("strict", "S.strict"), ("deepStrict", "S.deepStrict"), ("strip", "S.strip"), ("deepStrip", "S.deepStrip"), - ("to", "S.to"), + ("to", "S.js_to"), ("toJSONSchema", "S.toJSONSchema"), ("fromJSONSchema", "S.fromJSONSchema"), ("extendJSONSchema", "S.extendJSONSchema"), ("shape", "S.shape"), ("tuple", "S.tuple"), - ("asyncParserRefine", "S.js_asyncParserRefine"), + ("asyncDecoderAssert", "S.js_asyncDecoderAssert"), ("refine", "S.js_refine"), - ("transform", "S.js_transform"), ("meta", "S.meta"), ("toExpression", "S.toExpression"), ("noValidation", "S.noValidation"), @@ -177,10 +180,7 @@ let filesMapping = [ ("cuid", "S.cuid"), ("url", "S.url"), ("pattern", "S.pattern"), - ("datetime", "S.datetime"), ("trim", "S.trim"), - ("enableJson", "S.enableJson"), - ("enableJsonString", "S.enableJsonString"), ("global", "S.global"), ("brand", "S.brand"), ] diff --git a/packages/sury/scripts/pack/Pack.res.mjs b/packages/sury/scripts/pack/Pack.res.mjs index ea4a4a629..ea3c02b27 100644 --- a/packages/sury/scripts/pack/Pack.res.mjs +++ b/packages/sury/scripts/pack/Pack.res.mjs @@ -55,7 +55,7 @@ Nodefs.mkdirSync(Nodepath.join(artifactsPath, "scripts")); let filesMapping = [ [ "Error", - "S.ErrorClass.value" + "S.$$Error.$$class" ], [ "string", @@ -81,10 +81,6 @@ let filesMapping = [ "symbol", "S.symbol" ], - [ - "json", - "S.json" - ], [ "never", "S.never" @@ -113,6 +109,10 @@ let filesMapping = [ "array", "S.array" ], + [ + "compactColumns", + "S.compactColumns" + ], [ "instance", "S.instance" @@ -125,14 +125,46 @@ let filesMapping = [ "record", "S.dict" ], + [ + "json", + "S.json" + ], + [ + "enableJson", + "S.enableJson" + ], [ "jsonString", "S.jsonString" ], + [ + "enableJsonString", + "S.enableJsonString" + ], [ "jsonStringWithSpace", "S.jsonStringWithSpace" ], + [ + "uint8Array", + "S.uint8Array" + ], + [ + "enableUint8Array", + "S.enableUint8Array" + ], + [ + "date", + "S.date" + ], + [ + "isoDateTime", + "S.isoDateTime" + ], + [ + "enableIsoDateTime", + "S.enableIsoDateTime" + ], [ "union", "S.js_union" @@ -158,48 +190,32 @@ let filesMapping = [ "S.reverse" ], [ - "convertOrThrow", - "S.convertOrThrow" - ], - [ - "convertToJsonOrThrow", - "S.convertToJsonOrThrow" - ], - [ - "convertToJsonStringOrThrow", - "S.convertToJsonStringOrThrow" - ], - [ - "reverseConvertOrThrow", - "S.reverseConvertOrThrow" - ], - [ - "reverseConvertToJsonOrThrow", - "S.reverseConvertToJsonOrThrow" + "parser", + "S.parser" ], [ - "reverseConvertToJsonStringOrThrow", - " S.reverseConvertToJsonStringOrThrow" + "asyncParser", + "S.asyncParser" ], [ - "parseOrThrow", - "S.parseOrThrow" + "decoder", + "S.getDecoder" ], [ - "parseJsonOrThrow", - "S.parseJsonOrThrow" + "asyncDecoder", + "S.asyncDecoder" ], [ - "parseJsonStringOrThrow", - "S.parseJsonStringOrThrow" + "encoder", + "S.encoder" ], [ - "parseAsyncOrThrow", - "S.parseAsyncOrThrow" + "asyncEncoder", + "S.asyncEncoder" ], [ - "assertOrThrow", - "S.assertOrThrow" + "assert", + "S.js_assert" ], [ "recursive", @@ -227,7 +243,7 @@ let filesMapping = [ ], [ "to", - "S.to" + "S.js_to" ], [ "toJSONSchema", @@ -250,17 +266,13 @@ let filesMapping = [ "S.tuple" ], [ - "asyncParserRefine", - "S.js_asyncParserRefine" + "asyncDecoderAssert", + "S.js_asyncDecoderAssert" ], [ "refine", "S.js_refine" ], - [ - "transform", - "S.js_transform" - ], [ "meta", "S.meta" @@ -313,22 +325,10 @@ let filesMapping = [ "pattern", "S.pattern" ], - [ - "datetime", - "S.datetime" - ], [ "trim", "S.trim" ], - [ - "enableJson", - "S.enableJson" - ], - [ - "enableJsonString", - "S.enableJsonString" - ], [ "global", "S.global" diff --git a/packages/sury/scripts/pack/Prepack.res.mjs b/packages/sury/scripts/pack/Prepack.res.mjs index 5e688efde..411c1d786 100644 --- a/packages/sury/scripts/pack/Prepack.res.mjs +++ b/packages/sury/scripts/pack/Prepack.res.mjs @@ -98,9 +98,9 @@ var filesMapping = [ ["extendJSONSchema", "S.extendJSONSchema"], ["shape", "S.shape"], ["tuple", "S.tuple"], - ["asyncParserRefine", "S.js_asyncParserRefine"], + ["asyncDecoderAssert", "S.js_asyncDecoderAssert"], ["refine", "S.js_refine"], - ["transform", "S.js_transform"], + ["transform", "S.js_to"], ["meta", "S.meta"], ["toExpression", "S.toExpression"], ["noValidation", "S.noValidation"], diff --git a/packages/sury/src/S.d.ts b/packages/sury/src/S.d.ts index 89ac6fb65..ca335176f 100644 --- a/packages/sury/src/S.d.ts +++ b/packages/sury/src/S.d.ts @@ -69,10 +69,6 @@ export declare namespace StandardSchemaV1 { >["output"]; } -export type EffectCtx = { - readonly schema: Schema; - readonly fail: (message: string) => never; -}; export type SuccessResult = { readonly success: true; @@ -95,27 +91,40 @@ export type JSON = | { [key: string]: JSON } | JSON[]; +export type NumberFormat = "int32" | "port"; +export type StringFormat = "json" | "date-time" | "email" | "uuid" | "cuid" | "url"; +export type ArrayFormat = "compactColumns"; +export type Format = NumberFormat | StringFormat | ArrayFormat; + export type Schema = { - with( - transform: ( + with( + to: ( schema: Schema, - parser: - | ((value: unknown, s: EffectCtx) => unknown) - | undefined, - serializer?: (value: unknown, s: EffectCtx) => Input + target: Schema, + decode?: ((value: unknown) => unknown) | undefined, + encode?: (value: unknown) => Output ) => Schema, - parser: - | ((value: Output, s: EffectCtx) => Transformed) - | undefined, - serializer?: (value: Transformed, s: EffectCtx) => Input - ): Schema; + target: Schema, + decode?: ((value: Output) => TargetInput) | undefined, + encode?: (value: TargetInput) => Output + ): Schema; with( refine: ( schema: Schema, - refiner: (value: unknown, s: EffectCtx) => Promise + refineCheck: (value: unknown) => boolean, + refineOptions?: { error?: string; path?: string[] } ) => Schema, - refiner: (value: Output, s: EffectCtx) => Promise + refineCheck: (value: Output) => boolean, + refineOptions?: { error?: string; path?: string[] } ): Schema; + // I don't know how, but it makes both S.refine and S.shape work + with( + fn: ( + schema: Schema, + callback: ((value: unknown) => unknown) | undefined + ) => Schema, + callback: ((value: Output) => Shape) | undefined + ): Schema; // with(message: string): t; TODO: implement with(fn: (schema: Schema) => Schema): Schema; with( @@ -142,6 +151,7 @@ export type Schema = { readonly noValidation?: boolean; readonly default?: Input; readonly to?: Schema; + readonly errorMessage?: SchemaErrorMessage; readonly ["~standard"]: StandardSchemaV1.Props; } & ( @@ -153,13 +163,18 @@ export type Schema = { } | { readonly type: "string"; - readonly format?: "json"; + readonly format?: StringFormat; readonly const?: string; + readonly minLength?: number; + readonly maxLength?: number; + readonly pattern?: RegExp; } | { readonly type: "number"; - readonly format?: "int32" | "port"; + readonly format?: NumberFormat; readonly const?: number; + readonly minimum?: number; + readonly maximum?: number; } | { readonly type: "bigint"; @@ -196,17 +211,19 @@ export type Schema = { } | { readonly type: "array"; - readonly items: Item[]; + readonly items: Schema; readonly additionalItems: "strip" | "strict" | Schema; - readonly unnest?: true; + readonly format?: ArrayFormat; + readonly minItems?: number; + readonly maxItems?: number; } | { readonly type: "object"; - readonly items: Item[]; readonly properties: { [key: string]: Schema; }; readonly additionalItems: "strip" | "strict" | Schema; + readonly required?: string[]; } | { readonly type: "union"; @@ -235,26 +252,62 @@ export type Schema = { } ); -export type Item = { - readonly schema: Schema; - readonly location: string; -}; - export abstract class Path { protected opaque: any; } /* simulate opaque types */ -export class Error { - readonly flag: number; - readonly code: ErrorCode; - readonly path: Path; - readonly message: string; - readonly reason: string; -} - -export abstract class ErrorCode { - protected opaque: any; -} /* simulate opaque types */ +export type Error = + | { + readonly code: "invalid_input"; + readonly path: Path; + readonly message: string; + readonly reason: string; + readonly expected: Schema; + readonly received: Schema; + readonly input?: unknown; + readonly unionErrors?: readonly Error[]; + } + | { + readonly code: "invalid_operation"; + readonly path: Path; + readonly message: string; + readonly reason: string; + } + | { + readonly code: "unsupported_decode"; + readonly path: Path; + readonly message: string; + readonly reason: string; + readonly from: Schema; + readonly to: Schema; + } + | { + readonly code: "invalid_conversion"; + readonly path: Path; + readonly message: string; + readonly reason: string; + readonly from: Schema; + readonly to: Schema; + readonly cause?: Error; + } + | { + readonly code: "unrecognized_keys"; + readonly path: Path; + readonly message: string; + readonly reason: string; + readonly keys: readonly string[]; + } + | { + readonly code: "custom"; + readonly path: Path; + readonly message: string; + readonly reason: string; + }; + +export const Error: { + new (): Error; + prototype: Error; +}; export type Output = T extends Schema ? Output @@ -262,6 +315,32 @@ export type Output = T extends Schema export type Infer = Output; export type Input = T extends Schema ? Input : never; +// Utility types for decoder function with multiple schemas +type ExtractFirstInput[]> = + T extends readonly [Schema, ...any[]] + ? FirstInput + : never; + +// Utility types for encoder function with multiple schemas +type ExtractFirstOutput[]> = + T extends readonly [Schema, ...any[]] + ? FirstOutput + : never; + +type ExtractLastOutput[]> = + T extends readonly [...any[], Schema] + ? LastOutput + : T extends readonly [Schema] + ? SingleOutput + : never; + +type ExtractLastInput[]> = + T extends readonly [...any[], Schema] + ? LastInput + : T extends readonly [Schema] + ? SingleInput + : never; + export type UnknownToOutput = T extends Schema ? Output : T extends (...args: any[]) => any @@ -420,6 +499,29 @@ export const jsonString: Schema; export const jsonStringWithSpace: (space: number) => Schema; export function enableJsonString(): void; +export const uint8Array: Schema; +export function enableUint8Array(): void; + +export const isoDateTime: Schema; +export function enableIsoDateTime(): void; + +export const port: Schema; +export function enablePort(): void; + +export const email: Schema; +export function enableEmail(): void; + +export const uuid: Schema; +export function enableUuid(): void; + +export const cuid: Schema; +export function enableCuid(): void; + +export const url: Schema; +export function enableUrl(): void; + +export const date: Schema; + export function safe(scope: () => Value): Result; export function safeAsync( scope: () => Promise @@ -429,52 +531,83 @@ export function reverse( schema: Schema ): Schema; -export function parseOrThrow( - data: unknown, - schema: Schema -): Output; -export function parseJsonOrThrow( - json: JSON, - schema: Schema -): Output; -export function parseJsonStringOrThrow( - jsonString: string, - schema: Schema -): Output; -export function parseAsyncOrThrow( - data: unknown, +export function parser( + schema: Schema +): (data: unknown) => Output; +export function parser( + from: Schema, + target: Schema +): (data: unknown) => Output; +export function parser< + Schemas extends readonly [Schema, ...Schema[]] +>(...schemas: Schemas): (data: unknown) => ExtractLastOutput; + +export function asyncParser( + schema: Schema +): (data: unknown) => Promise; +export function asyncParser( + from: Schema, + target: Schema +): (data: unknown) => Promise; +export function asyncParser< + Schemas extends readonly [Schema, ...Schema[]] +>(...schemas: Schemas): (data: unknown) => Promise>; + +export function decoder( schema: Schema -): Promise; +): (data: Input) => Output; +export function decoder( + from: Schema, + target: Schema +): (data: Input) => Output; +export function decoder< + Schemas extends readonly [Schema, ...Schema[]] +>( + ...schemas: Schemas +): (data: ExtractFirstInput) => ExtractLastOutput; -export function convertOrThrow( - data: Input, - schema: Schema -): Output; -export function convertToJsonOrThrow( - data: Input, +export function asyncDecoder( schema: Schema -): JSON; -export function convertToJsonStringOrThrow( - data: Input, - schema: Schema -): string; +): (data: Input) => Promise; +export function asyncDecoder( + from: Schema, + target: Schema +): (data: Input) => Promise; +export function decoder< + Schemas extends readonly [Schema, ...Schema[]] +>( + ...schemas: Schemas +): (data: ExtractFirstInput) => Promise>; -export function reverseConvertOrThrow( - value: Output, +export function encoder( schema: Schema -): Input; -export function reverseConvertToJsonOrThrow( - value: Output, - schema: Schema -): JSON; -export function reverseConvertToJsonStringOrThrow( - value: Output, - schema: Schema -): string; +): (data: Output) => Input; +export function encoder( + from: Schema, + target: Schema +): (data: Output) => Input; +export function encoder< + Schemas extends readonly [Schema, ...Schema[]] +>( + ...schemas: Schemas +): (data: ExtractFirstOutput) => ExtractLastInput; -export function assertOrThrow( - data: unknown, +export function asyncEncoder( schema: Schema +): (data: Output) => Promise; +export function asyncEncoder( + from: Schema, + target: Schema +): (data: Output) => Promise; +export function asyncEncoder< + Schemas extends readonly [Schema, ...Schema[]] +>( + ...schemas: Schemas +): (data: ExtractFirstOutput) => Promise>; + +export function assert( + schema: Schema, + data: unknown ): asserts data is Input; export function tuple( @@ -523,14 +656,9 @@ export const array: ( schema: Schema ) => Schema; -export const unnest: >( +export const compactColumns: ( schema: Schema -) => Schema< - Output[], - { - [K in keyof Input]: Input[K][]; - }[keyof Input][] ->; +) => Schema; export const record: ( schema: Schema @@ -590,12 +718,26 @@ export function recursive( definer: (schema: Schema) => Schema ): Schema; +export type SchemaErrorMessage = { + _?: string; + format?: string; + type?: string; + minimum?: string; + maximum?: string; + minLength?: string; + maxLength?: string; + minItems?: string; + maxItems?: string; + pattern?: string; +}; + export type Meta = { name?: string; title?: string; description?: string; deprecated?: boolean; examples?: Output[]; + errorMessage?: SchemaErrorMessage; }; export function meta( @@ -609,24 +751,20 @@ export function noValidation( value: boolean ): Schema; -export function asyncParserRefine( +export function asyncDecoderAssert( schema: Schema, - refiner: (value: Output, s: EffectCtx) => Promise + assertFn: (value: Output) => Promise ): Schema; export function refine( schema: Schema, - refiner: (value: Output, s: EffectCtx) => void + refineCheck: (value: Output) => boolean, + refineOptions?: { + error?: string; + path?: string[]; + } ): Schema; -export function transform( - schema: Schema, - parser: - | ((value: Output, s: EffectCtx) => Transformed) - | undefined, - serializer?: (value: Transformed, s: EffectCtx) => Output -): Schema; - export const min: ( schema: Schema, length: number, @@ -643,36 +781,7 @@ export const length: ( message?: string ) => Schema; -export const port: ( - schema: Schema, - message?: string -) => Schema; - -export const email: ( - schema: Schema, - message?: string -) => Schema; -export const uuid: ( - schema: Schema, - message?: string -) => Schema; -export const cuid: ( - schema: Schema, - message?: string -) => Schema; -export const url: ( - schema: Schema, - message?: string -) => Schema; -export const pattern: ( - schema: Schema, - re: RegExp, - message?: string -) => Schema; -export const datetime: ( - schema: Schema, - message?: string -) => Schema; +export const pattern: (re: RegExp, message?: string) => Schema; export const trim: ( schema: Schema ) => Schema; @@ -686,60 +795,22 @@ export type GlobalConfigOverride = { export function global(globalConfigOverride: GlobalConfigOverride): void; -type CompileInputMappings = { - Input: Input; - Output: Output; - Any: unknown; - Json: JSON; - JsonString: string; -}; - -type CompileOutputMappings = { - Output: Output; - Input: Input; - Assert: void; - Json: JSON; - JsonString: string; -}; - -export type CompileInputOption = keyof CompileInputMappings; -export type CompileOutputOption = keyof CompileOutputMappings; -export type CompileModeOption = "Sync" | "Async"; - -export function compile< - Output, - Input, - InputOption extends CompileInputOption, - OutputOption extends CompileOutputOption, - ModeOption extends CompileModeOption ->( - schema: Schema, - input: InputOption, - output: OutputOption, - mode: ModeOption, - typeValidation?: boolean -): ( - input: CompileInputMappings[InputOption] -) => ModeOption extends "Sync" - ? CompileOutputMappings[OutputOption] - : ModeOption extends "Async" - ? Promise[OutputOption]> - : never; - -export function shape( +export function shape( schema: Schema, shaper: (value: Output) => Shape ): Schema; export function to< - FromInput, - ToOutput, - FromOutput = FromInput, - ToInput = ToOutput + Output = unknown, + Input = unknown, + TargetInput = unknown, + TargetOutput = unknown >( - from: Schema, - to: Schema -): Schema; + schema: Schema, + target: Schema, + decode?: ((value: Output) => TargetInput) | undefined, + encode?: (value: TargetOutput) => Output +): Schema; export function toJSONSchema( schema: Schema diff --git a/packages/sury/src/S.gen.d.ts b/packages/sury/src/S.gen.d.ts index 4e2bbb3f5..395ed390b 100644 --- a/packages/sury/src/S.gen.d.ts +++ b/packages/sury/src/S.gen.d.ts @@ -1,6 +1,6 @@ // The file is hand written -import { Error, Item, Path, Schema } from "./S"; +import { Error, Path, Schema } from "./S"; /* eslint-disable */ /* tslint:disable */ @@ -8,8 +8,6 @@ import { Error, Item, Path, Schema } from "./S"; export type t = Schema; export type schema = Schema; -export type item = Item; - export type Path_t = Path; export type error = Error; diff --git a/packages/sury/src/S.js b/packages/sury/src/S.js index a8d5f271d..b2a1d5d79 100644 --- a/packages/sury/src/S.js +++ b/packages/sury/src/S.js @@ -1,14 +1,13 @@ /* @ts-self-types="./S.d.ts" */ import * as S from "./Sury.res.mjs" export { unit as void } from "./Sury.res.mjs" -export var Error = S.ErrorClass.value +export var Error = S.$$Error.$$class export var string = S.string export var boolean = S.bool export var int32 = S.int export var number = S.float export var bigint = S.bigint export var symbol = S.symbol -export var json = S.json export var never = S.never export var unknown = S.unknown export var any = S.unknown @@ -16,59 +15,65 @@ export var optional = S.js_optional export var nullable = S.js_nullable export var nullish = S.nullable export var array = S.array +export var compactColumns = S.compactColumns export var instance = S.instance export var unnest = S.unnest export var record = S.dict +export var json = S.json +export var enableJson = S.enableJson export var jsonString = S.jsonString +export var enableJsonString = S.enableJsonString export var jsonStringWithSpace = S.jsonStringWithSpace +export var uint8Array = S.uint8Array +export var enableUint8Array = S.enableUint8Array +export var isoDateTime = S.isoDateTime +export var enableIsoDateTime = S.enableIsoDateTime +export var port = S.port +export var enablePort = S.enablePort +export var email = S.email +export var enableEmail = S.enableEmail +export var uuid = S.uuid +export var enableUuid = S.enableUuid +export var cuid = S.cuid +export var enableCuid = S.enableCuid +export var url = S.url +export var enableUrl = S.enableUrl +export var date = S.date export var union = S.js_union export var object = S.object export var schema = S.js_schema export var safe = S.js_safe export var safeAsync = S.js_safeAsync export var reverse = S.reverse -export var convertOrThrow = S.convertOrThrow -export var convertToJsonOrThrow = S.convertToJsonOrThrow -export var convertToJsonStringOrThrow = S.convertToJsonStringOrThrow -export var reverseConvertOrThrow = S.reverseConvertOrThrow -export var reverseConvertToJsonOrThrow = S.reverseConvertToJsonOrThrow -export var reverseConvertToJsonStringOrThrow = S.reverseConvertToJsonStringOrThrow -export var parseOrThrow = S.parseOrThrow -export var parseJsonOrThrow = S.parseJsonOrThrow -export var parseJsonStringOrThrow = S.parseJsonStringOrThrow -export var parseAsyncOrThrow = S.parseAsyncOrThrow -export var assertOrThrow = S.assertOrThrow +export var parser = S.js_parser +export var asyncParser = S.js_asyncParser +export var decoder = S.getDecoder +export var asyncDecoder = S.js_asyncDecoder +export var encoder = S.js_encoder +export var asyncEncoder = S.js_asyncEncoder +export var assert = S.js_assert export var recursive = S.recursive export var merge = S.js_merge export var strict = S.strict export var deepStrict = S.deepStrict export var strip = S.strip export var deepStrip = S.deepStrip -export var to = S.to +export var to = S.js_to export var toJSONSchema = S.toJSONSchema export var fromJSONSchema = S.fromJSONSchema export var extendJSONSchema = S.extendJSONSchema export var shape = S.shape export var tuple = S.tuple -export var asyncParserRefine = S.js_asyncParserRefine +export var asyncDecoderAssert = S.js_asyncDecoderAssert export var refine = S.js_refine -export var transform = S.js_transform export var meta = S.meta export var toExpression = S.toExpression export var noValidation = S.noValidation export var compile = S.compile -export var port = S.port export var min = S.min export var max = S.max export var length = S.length -export var email = S.email -export var uuid = S.uuid -export var cuid = S.cuid -export var url = S.url export var pattern = S.pattern -export var datetime = S.datetime export var trim = S.trim -export var enableJson = S.enableJson -export var enableJsonString = S.enableJsonString export var global = S.global export var brand = S.brand \ No newline at end of file diff --git a/packages/sury/src/S.res.mjs b/packages/sury/src/S.res.mjs index 612cbfeb6..61efcb266 100644 --- a/packages/sury/src/S.res.mjs +++ b/packages/sury/src/S.res.mjs @@ -4,10 +4,10 @@ import * as Sury from "./Sury.res.mjs"; let Path = Sury.Path; -let $$Error = Sury.$$Error; - let Flag = Sury.Flag; +let Exn = Sury.Exn; + let never = Sury.never; let unknown = Sury.unknown; @@ -38,11 +38,41 @@ let jsonStringWithSpace = Sury.jsonStringWithSpace; let enableJsonString = Sury.enableJsonString; +let uint8Array = Sury.uint8Array; + +let enableUint8Array = Sury.enableUint8Array; + +let isoDateTime = Sury.isoDateTime; + +let enableIsoDateTime = Sury.enableIsoDateTime; + +let port = Sury.port; + +let enablePort = Sury.enablePort; + +let email = Sury.email; + +let enableEmail = Sury.enableEmail; + +let uuid = Sury.uuid; + +let enableUuid = Sury.enableUuid; + +let cuid = Sury.cuid; + +let enableCuid = Sury.enableCuid; + +let url = Sury.url; + +let enableUrl = Sury.enableUrl; + +let date = Sury.date; + let literal = Sury.literal; let array = Sury.array; -let unnest = Sury.unnest; +let compactColumns = Sury.compactColumns; let list = Sury.list; @@ -54,6 +84,8 @@ let option = Sury.option; let $$null = Sury.$$null; +let nullAsOption = Sury.nullAsOption; + let nullable = Sury.nullable; let nullableAsOption = Sury.nullableAsOption; @@ -72,31 +104,29 @@ let shape = Sury.shape; let to = Sury.to; -let compile = Sury.compile; +let parser = Sury.parser; -let parseOrThrow = Sury.parseOrThrow; +let asyncParser = Sury.asyncParser; -let parseJsonOrThrow = Sury.parseJsonOrThrow; +let decoder = Sury.decoder; -let parseJsonStringOrThrow = Sury.parseJsonStringOrThrow; +let asyncDecoder = Sury.asyncDecoder; -let parseAsyncOrThrow = Sury.parseAsyncOrThrow; +let decoder1 = Sury.decoder1; -let convertOrThrow = Sury.convertOrThrow; +let asyncDecoder1 = Sury.asyncDecoder1; -let convertToJsonOrThrow = Sury.convertToJsonOrThrow; +let parseOrThrow = Sury.parseOrThrow; -let convertToJsonStringOrThrow = Sury.convertToJsonStringOrThrow; +let parseAsyncOrThrow = Sury.parseAsyncOrThrow; -let convertAsyncOrThrow = Sury.convertAsyncOrThrow; +let assertOrThrow = Sury.assertOrThrow; -let reverseConvertOrThrow = Sury.reverseConvertOrThrow; +let assertAsyncOrThrow = Sury.assertAsyncOrThrow; -let reverseConvertToJsonOrThrow = Sury.reverseConvertToJsonOrThrow; +let decodeOrThrow = Sury.decodeOrThrow; -let reverseConvertToJsonStringOrThrow = Sury.reverseConvertToJsonStringOrThrow; - -let assertOrThrow = Sury.assertOrThrow; +let decodeAsyncOrThrow = Sury.decodeAsyncOrThrow; let isAsync = Sury.isAsync; @@ -134,14 +164,6 @@ let tuple3 = Sury.tuple3; let Option = Sury.Option; -let $$String = Sury.$$String; - -let Int = Sury.Int; - -let Float = Sury.Float; - -let $$Array = Sury.$$Array; - let Metadata = Sury.Metadata; let reverse = Sury.reverse; @@ -156,20 +178,8 @@ let floatMax = Sury.floatMax; let length = Sury.length; -let port = Sury.port; - -let email = Sury.email; - -let uuid = Sury.uuid; - -let cuid = Sury.cuid; - -let url = Sury.url; - let pattern = Sury.pattern; -let datetime = Sury.datetime; - let trim = Sury.trim; let toJSONSchema = Sury.toJSONSchema; @@ -180,12 +190,12 @@ let extendJSONSchema = Sury.extendJSONSchema; let global = Sury.global; -let ErrorClass = Sury.ErrorClass; +let $$Error = Sury.$$Error; export { Path, - $$Error, Flag, + Exn, never, unknown, unit, @@ -201,14 +211,30 @@ export { jsonString, jsonStringWithSpace, enableJsonString, + uint8Array, + enableUint8Array, + isoDateTime, + enableIsoDateTime, + port, + enablePort, + email, + enableEmail, + uuid, + enableUuid, + cuid, + enableCuid, + url, + enableUrl, + date, literal, array, - unnest, + compactColumns, list, instance, dict, option, $$null, + nullAsOption, nullable, nullableAsOption, union, @@ -218,19 +244,18 @@ export { refine, shape, to, - compile, + parser, + asyncParser, + decoder, + asyncDecoder, + decoder1, + asyncDecoder1, parseOrThrow, - parseJsonOrThrow, - parseJsonStringOrThrow, parseAsyncOrThrow, - convertOrThrow, - convertToJsonOrThrow, - convertToJsonStringOrThrow, - convertAsyncOrThrow, - reverseConvertOrThrow, - reverseConvertToJsonOrThrow, - reverseConvertToJsonStringOrThrow, assertOrThrow, + assertAsyncOrThrow, + decodeOrThrow, + decodeAsyncOrThrow, isAsync, recursive, noValidation, @@ -249,10 +274,6 @@ export { tuple2, tuple3, Option, - $$String, - Int, - Float, - $$Array, Metadata, reverse, min, @@ -260,18 +281,12 @@ export { max, floatMax, length, - port, - email, - uuid, - cuid, - url, pattern, - datetime, trim, toJSONSchema, fromJSONSchema, extendJSONSchema, global, - ErrorClass, + $$Error, } /* Sury Not a pure module */ diff --git a/packages/sury/src/S.resi b/packages/sury/src/S.resi index c2d127fef..5bf0c242f 100644 --- a/packages/sury/src/S.resi +++ b/packages/sury/src/S.resi @@ -21,9 +21,16 @@ module Path: { } type numberFormat = | @as("int32") Int32 | @as("port") Port -type stringFormat = | @as("json") JSON +type stringFormat = + | @as("json") JSON + | @as("date-time") DateTime + | @as("email") Email + | @as("uuid") Uuid + | @as("cuid") Cuid + | @as("url") Url +type arrayFormat = | @as("compactColumns") CompactColumns -type format = | ...numberFormat | ...stringFormat +type format = | ...numberFormat | ...stringFormat | ...arrayFormat @unboxed type additionalItemsMode = | @as("strip") Strip | @as("strict") Strict @@ -49,7 +56,7 @@ type tag = @tag("type") type rec t<'value> = private - | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool}) + | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool, errorMessage?: schemaErrorMessage}) | @as("unknown") Unknown({ name?: string, @@ -58,6 +65,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) | @as("string") String({ @@ -69,6 +77,10 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: string, + minLength?: int, + maxLength?: int, + pattern?: Js.Re.t, + errorMessage?: schemaErrorMessage, }) | @as("number") Number({ @@ -80,6 +92,9 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: float, + minimum?: float, + maximum?: float, + errorMessage?: schemaErrorMessage, }) | @as("bigint") BigInt({ @@ -90,6 +105,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bigint, + errorMessage?: schemaErrorMessage, }) | @as("boolean") Boolean({ @@ -100,6 +116,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bool, + errorMessage?: schemaErrorMessage, }) | @as("symbol") Symbol({ @@ -110,6 +127,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.symbol, + errorMessage?: schemaErrorMessage, }) | @as("null") Null({ @@ -118,6 +136,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("undefined") Undefined({ @@ -126,6 +145,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("nan") NaN({ @@ -134,6 +154,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("function") Function({ @@ -144,6 +165,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.function_val, + errorMessage?: schemaErrorMessage, }) | @as("instance") Instance({ @@ -155,31 +177,36 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.obj_val, + errorMessage?: schemaErrorMessage, }) | @as("array") Array({ - items: array, + items: array>, additionalItems: additionalItems, - unnest?: bool, + format?: arrayFormat, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: array, + minItems?: int, + maxItems?: int, + errorMessage?: schemaErrorMessage, }) | @as("object") Object({ - items: array, properties: dict>, additionalItems: additionalItems, + required?: array, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: dict, - }) // TODO: Add const for Object and Tuple + errorMessage?: schemaErrorMessage, + }) | @as("union") Union({ anyOf: array>, @@ -190,19 +217,36 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) - | @as("ref") Ref({@as("$ref") ref: string}) + | @as("ref") Ref({@as("$ref") ref: string, errorMessage?: schemaErrorMessage}) and schema<'a> = t<'a> +and schemaErrorMessage = { + @as("_") + catchAll?: string, + format?: string, + @as("type") + type_?: string, + minimum?: string, + maximum?: string, + minLength?: string, + maxLength?: string, + minItems?: string, + maxItems?: string, + pattern?: string, +} and meta<'value> = { name?: string, title?: string, description?: string, deprecated?: bool, examples?: array<'value>, + errorMessage?: schemaErrorMessage, } and untagged = private { @as("type") tag: tag, + seq: float, @as("$ref") ref?: string, @as("$defs") @@ -216,9 +260,9 @@ and untagged = private { deprecated?: bool, examples?: array, default?: unknown, - unnest?: bool, noValidation?: bool, - items?: array, + items?: array>, + required?: array, properties?: dict>, additionalItems?: additionalItems, anyOf?: array>, @@ -226,10 +270,6 @@ and untagged = private { to?: t, } @unboxed and additionalItems = | ...additionalItemsMode | Schema(t) -and item = { - schema: t, - location: string, -} and has = { string?: bool, number?: bool, @@ -250,39 +290,53 @@ and error = private { message: string, reason: string, path: Path.t, - code: errorCode, - flag: flag, } -and errorCode = - | OperationFailed(string) - | InvalidOperation({description: string}) - | InvalidType({expected: schema, received: unknown, unionErrors?: array}) - | UnsupportedTransformation({from: schema, to: schema}) - | ExcessField(string) - | UnexpectedAsync - | InvalidJsonSchema(schema) +@tag("code") +and errorDetails = + // When received input doesn't match the expected schema + | @as("invalid_input") + InvalidInput({ + path: Path.t, + reason: string, + expected: schema, + received: schema, + input?: unknown, + unionErrors?: array, + }) + // When an operation fails, because it's impossible or called incorrectly + | @as("invalid_operation") InvalidOperation({path: Path.t, reason: string}) + // When the value decoding between two schemas is not supported + | @as("unsupported_decode") + UnsupportedDecode({ + path: Path.t, + reason: string, + from: schema, + to: schema, + }) + // When a decoder/encoder fails + | @as("invalid_conversion") + InvalidConversion({ + path: Path.t, + reason: string, + from: schema, + to: schema, + cause?: exn, + }) + | @as("unrecognized_keys") UnrecognizedKeys({path: Path.t, reason: string, keys: array}) + | @as("custom") Custom({path: Path.t, reason: string}) and flag -type exn += private Error(error) - -type s<'value> = { - schema: t<'value>, - fail: 'a. (string, ~path: Path.t=?) => 'a, -} - module Flag: { - @inline let none: flag - @inline let typeValidation: flag - @inline let async: flag - @inline let assertOutput: flag - @inline let jsonableOutput: flag - @inline let jsonStringOutput: flag - @inline let reverse: flag - + let none: flag + let async: flag external with: (flag, flag) => flag = "%orint" - let has: (flag, flag) => bool } +type exn += private Exn(error) + + +type s<'value> = {fail: 'a. (string, ~path: Path.t=?) => 'a} + let never: t let unknown: t let unit: t @@ -301,15 +355,39 @@ let jsonString: t let jsonStringWithSpace: int => t let enableJsonString: unit => unit +let uint8Array: t +let enableUint8Array: unit => unit + +let isoDateTime: t +let enableIsoDateTime: unit => unit + +let port: t +let enablePort: unit => unit + +let email: t +let enableEmail: unit => unit + +let uuid: t +let enableUuid: unit => unit + +let cuid: t +let enableCuid: unit => unit + +let url: t +let enableUrl: unit => unit + +let date: t + let literal: 'value => t<'value> let array: t<'value> => t> -let unnest: t<'value> => t> +let compactColumns: t<'value> => t>> let list: t<'value> => t> let instance: unknown => t let dict: t<'value> => t> let option: t<'value> => t> -let null: t<'value> => t> -let nullable: t<'value> => t> +let null: t<'value> => t> +let nullAsOption: t<'value> => t> +let nullable: t<'value> => t> let nullableAsOption: t<'value> => t> let union: array> => t<'value> let enum: array<'value> => t<'value> @@ -326,51 +404,25 @@ type transformDefinition<'input, 'output> = { } let transform: (t<'input>, s<'output> => transformDefinition<'input, 'output>) => t<'output> -let refine: (t<'value>, s<'value> => 'value => unit) => t<'value> +let refine: (t<'value>, 'value => bool, ~error: string=?, ~path: array=?) => t<'value> let shape: (t<'value>, 'value => 'shape) => t<'shape> let to: (t<'from>, t<'to>) => t<'to> -type rec input<'value, 'computed> = - | @as("Output") Value: input<'value, 'value> - | @as("Input") Unknown: input<'value, unknown> - | Any: input<'value, 'any> - | Json: input<'value, Js.Json.t> - | JsonString: input<'value, string> -type rec output<'value, 'computed> = - | @as("Output") Value: output<'value, 'value> - | @as("Input") Unknown: output<'value, unknown> - | Assert: output<'value, unit> - | Json: output<'value, Js.Json.t> - | JsonString: output<'value, string> -type rec mode<'output, 'computed> = - | Sync: mode<'output, 'output> - | Async: mode<'output, promise<'output>> - -let compile: ( - t<'value>, - ~input: input<'value, 'input>, - ~output: output<'value, 'transformedOutput>, - ~mode: mode<'transformedOutput, 'output>, - ~typeValidation: bool=?, -) => 'input => 'output - -let parseOrThrow: ('any, t<'value>) => 'value -let parseJsonOrThrow: (Js.Json.t, t<'value>) => 'value -let parseJsonStringOrThrow: (string, t<'value>) => 'value -let parseAsyncOrThrow: ('any, t<'value>) => promise<'value> - -let convertOrThrow: ('any, t<'value>) => 'value -let convertToJsonOrThrow: ('any, t<'value>) => Js.Json.t -let convertToJsonStringOrThrow: ('any, t<'value>) => string -let convertAsyncOrThrow: ('any, t<'value>) => promise<'value> - -let reverseConvertOrThrow: ('value, t<'value>) => unknown -let reverseConvertToJsonOrThrow: ('value, t<'value>) => Js.Json.t -let reverseConvertToJsonStringOrThrow: ('value, t<'value>, ~space: int=?) => string - -let assertOrThrow: ('any, t<'value>) => unit +let parser: (~to: t<'value>) => 'any => 'value +let asyncParser: (~to: t<'value>) => 'any => promise<'value> +let decoder: (~from: t<'from>, ~to: t<'to>) => 'from => 'to +let asyncDecoder: (~from: t<'from>, ~to: t<'to>) => 'from => promise<'to> +let decoder1: t<'value> => unknown => 'value +let asyncDecoder1: t<'value> => unknown => promise<'value> + +let parseOrThrow: ('any, ~to: t<'value>) => 'value +let parseAsyncOrThrow: ('any, ~to: t<'value>) => promise<'value> +let assertOrThrow: ('any, ~to: t<'value>) => unit +let assertAsyncOrThrow: ('any, ~to: t<'value>) => promise +let decodeOrThrow: ('from, ~from: t<'from>, ~to: t<'to>) => 'to +let decodeAsyncOrThrow: ('from, ~from: t<'from>, ~to: t<'to>) => promise<'to> let isAsync: t<'value> => bool @@ -423,71 +475,6 @@ module Option: { let getOrWith: (t>, unit => 'value) => t<'value> } -module String: { - module Refinement: { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - | Email - | Uuid - | Cuid - | Url - | Pattern({re: Js.Re.t}) - | Datetime - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Int: { - module Refinement: { - type kind = - | Min({value: int}) - | Max({value: int}) - - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Float: { - module Refinement: { - type kind = - | Min({value: float}) - | Max({value: float}) - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Array: { - module Refinement: { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - module Metadata: { module Id: { type t<'metadata> @@ -509,13 +496,7 @@ let floatMax: (t, float, ~message: string=?) => t let length: (t<'value>, int, ~message: string=?) => t<'value> -let port: (t, ~message: string=?) => t -let email: (t, ~message: string=?) => t -let uuid: (t, ~message: string=?) => t -let cuid: (t, ~message: string=?) => t -let url: (t, ~message: string=?) => t let pattern: (t, Js.Re.t, ~message: string=?) => t -let datetime: (t, ~message: string=?) => t let trim: t => t let toJSONSchema: t<'value> => JSONSchema.t @@ -529,10 +510,12 @@ type globalConfigOverride = { let global: globalConfigOverride => unit -module ErrorClass: { - type t +module Error: { + type class + + let class: class - let value: t + let make: errorDetails => error - let constructor: (~code: errorCode, ~flag: flag, ~path: Path.t) => error + external classify: error => errorDetails = "%identity" } diff --git a/packages/sury/src/Sury.res b/packages/sury/src/Sury.res index 7622eeb5e..2a6e3991d 100644 --- a/packages/sury/src/Sury.res +++ b/packages/sury/src/Sury.res @@ -20,7 +20,7 @@ module X = { module Option = { external getUnsafe: option<'a> => 'a = "%identity" - // external unsafeToBool: option<'a> => bool = "%identity" + external unsafeToBool: option<'a> => bool = "%identity" } module Promise = { @@ -215,7 +215,7 @@ module Path = { switch array { | [] => "" | [location] => fromLocation(location) - | _ => "[" ++ array->Js.Array2.map(X.Inlined.Value.fromString)->Js.Array2.joinWith("][") ++ "]" + | _ => array->Js.Array2.map(fromLocation)->Js.Array2.joinWith("") } } @@ -223,11 +223,17 @@ module Path = { } let vendor = "sury" -// Internal symbol to easily identify the error +// Internal symbol to easily identify SuryError let s = X.Symbol.make(vendor) // Internal symbol to identify item proxy let itemSymbol = X.Symbol.make(vendor ++ ":item") +// A hacky way to prevent prepending path when error is caught. +// Can be removed after we remove effectCtx +// and there's not way to throw outside of the operation context. +@inline +let shouldPrependPathKey = "p" + type tag = | @as("string") String | @as("number") Number @@ -246,6 +252,25 @@ type tag = | @as("unknown") Unknown | @as("ref") Ref +// Use variables to reduce bundle size with min+gzip +// Also as a good practice (ignore that we have tag variant 😅) +let stringTag: tag = %raw(`"string"`) +let numberTag: tag = %raw(`"number"`) +let bigintTag: tag = %raw(`"bigint"`) +let booleanTag: tag = %raw(`"boolean"`) +let symbolTag: tag = %raw(`"symbol"`) +let nullTag: tag = %raw(`"null"`) +let undefinedTag: tag = %raw(`"undefined"`) +let nanTag: tag = %raw(`"nan"`) +// let functionTag: tag = %raw(`"function"`) +let instanceTag: tag = %raw(`"instance"`) +let arrayTag: tag = %raw(`"array"`) +let objectTag: tag = %raw(`"object"`) +let unionTag: tag = %raw(`"union"`) +let neverTag: tag = %raw(`"never"`) +let unknownTag: tag = %raw(`"unknown"`) +let refTag: tag = %raw(`"ref"`) + type standard = { version: int, vendor: string, @@ -255,9 +280,16 @@ type standard = { type internalDefault = {} type numberFormat = | @as("int32") Int32 | @as("port") Port -type stringFormat = | @as("json") JSON +type stringFormat = + | @as("json") JSON + | @as("date-time") DateTime + | @as("email") Email + | @as("uuid") Uuid + | @as("cuid") Cuid + | @as("url") Url +type arrayFormat = | @as("compactColumns") CompactColumns -type format = | ...numberFormat | ...stringFormat +type format = | ...numberFormat | ...stringFormat | ...arrayFormat @unboxed type additionalItemsMode = | @as("strip") Strip | @as("strict") Strict @@ -265,7 +297,7 @@ type additionalItemsMode = | @as("strip") Strip | @as("strict") Strict @tag("type") type rec t<'value> = private - | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool}) + | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool, errorMessage?: schemaErrorMessage}) | @as("unknown") Unknown({ name?: string, @@ -274,6 +306,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) | @as("string") String({ @@ -285,6 +318,10 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: string, + minLength?: int, + maxLength?: int, + pattern?: Js.Re.t, + errorMessage?: schemaErrorMessage, }) | @as("number") Number({ @@ -296,6 +333,9 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: float, + minimum?: float, + maximum?: float, + errorMessage?: schemaErrorMessage, }) | @as("bigint") BigInt({ @@ -306,6 +346,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bigint, + errorMessage?: schemaErrorMessage, }) | @as("boolean") Boolean({ @@ -316,6 +357,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bool, + errorMessage?: schemaErrorMessage, }) | @as("symbol") Symbol({ @@ -326,6 +368,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.symbol, + errorMessage?: schemaErrorMessage, }) | @as("null") Null({ @@ -334,6 +377,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("undefined") Undefined({ @@ -342,6 +386,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("nan") NaN({ @@ -350,6 +395,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("function") Function({ @@ -360,6 +406,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.function_val, + errorMessage?: schemaErrorMessage, }) | @as("instance") Instance({ @@ -371,31 +418,36 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.obj_val, + errorMessage?: schemaErrorMessage, }) | @as("array") Array({ - items: array, + items: array>, additionalItems: additionalItems, - unnest?: bool, + format?: arrayFormat, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: array, + minItems?: int, + maxItems?: int, + errorMessage?: schemaErrorMessage, }) | @as("object") Object({ - items: array, properties: dict>, additionalItems: additionalItems, + required?: array, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: dict, - }) // TODO: Add const for Object and Tuple + errorMessage?: schemaErrorMessage, + }) | @as("union") Union({ anyOf: array>, @@ -406,30 +458,46 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) | @as("ref") Ref({ @as("$ref") ref: string, + errorMessage?: schemaErrorMessage, }) @unboxed and additionalItems = | ...additionalItemsMode | Schema(t) and schema<'a> = t<'a> and internal = { @as("type") mutable tag: tag, + // A serial number for the schema + // to use for caching operations + mutable seq?: float, // Builder for transforming to the "to" schema // If missing, should apply coercion logic mutable parser?: builder, // A field on the "to" schema, // to turn it into "parser", when reversing mutable serializer?: builder, - // Builder refine that the value matches the schema - // Applies for both parsing and serializing - mutable refiner?: refinerBuilder, - // Compiler for custom schema logic - mutable compiler?: builder, + // Logic for built-in decoding to the schema type + mutable decoder: builder, + // Logic for built-in encoding from the schema type + mutable encoder?: encoder, + // Custom validations on input (before decoder) + mutable inputRefiner?: (~input: val) => array, + // Custom validations on output (after decoder) + mutable refiner?: (~input: val) => array, // A schema we transform to mutable to?: internal, + // When transforming with changing shape, + // store from which path it came from + // For S.object, S.tuple, and S.shape + mutable from?: array, + // The index of the flattened schema + // reshaping is happening from + mutable fromFlattened?: int, + mutable flattened?: array, mutable const?: char, // use char to avoid Caml_option.some mutable class?: char, // use char to avoid Caml_option.some mutable name?: string, @@ -443,29 +511,54 @@ and internal = { mutable has?: dict, mutable anyOf?: array, mutable additionalItems?: additionalItems, - mutable items?: array, + mutable items?: array, + mutable required?: array, mutable properties?: dict, mutable noValidation?: bool, - mutable unnest?: bool, + mutable minimum?: float, + mutable maximum?: float, + mutable minLength?: int, + mutable maxLength?: int, + mutable minItems?: int, + mutable maxItems?: int, + mutable pattern?: Js.Re.t, + mutable errorMessage?: schemaErrorMessage, mutable space?: int, @as("$ref") mutable ref?: string, @as("$defs") mutable defs?: dict, mutable isAsync?: bool, // Optional value means that it's not lazily computed yet. + mutable hasTransform?: bool, // Optional value means that it's not lazily computed yet. @as("~standard") mutable standard?: standard, // This is optional for convenience. The object added on make call } +and schemaErrorMessage = { + @as("_") + catchAll?: string, + format?: string, + @as("type") + type_?: string, + minimum?: string, + maximum?: string, + minLength?: string, + maxLength?: string, + minItems?: string, + maxItems?: string, + pattern?: string, +} and meta<'value> = { name?: string, title?: string, description?: string, deprecated?: bool, examples?: array<'value>, + errorMessage?: schemaErrorMessage, } and untagged = private { @as("type") tag: tag, + seq: float, @as("$ref") ref?: string, @as("$defs") @@ -479,19 +572,15 @@ and untagged = private { deprecated?: bool, examples?: array, default?: unknown, - unnest?: bool, noValidation?: bool, - items?: array, + items?: array>, + required?: array, properties?: dict>, additionalItems?: additionalItems, anyOf?: array>, has?: dict, to?: t, } -and item = { - schema: t, - location: string, -} and has = { string?: bool, number?: bool, @@ -508,79 +597,126 @@ and has = { array?: bool, object?: bool, } -and builder = (b, ~input: val, ~selfSchema: internal, ~path: Path.t) => val -and refinerBuilder = (b, ~inputVar: string, ~selfSchema: internal, ~path: Path.t) => string +and builder = (~input: val) => val +and encoder = (~input: val, ~target: internal) => val and val = { + // We might have the same value, but different instances of the val object + // Use the bond field, to connect the var call @as("b") - mutable b: b, + mutable bond?: val, + @as("p") + mutable parent?: val, @as("v") - mutable var: b => string, + mutable var: unit => string, @as("i") mutable inline: string, + // The schema of the value that is being parsed + @as("s") + mutable schema: internal, + // Whether the val is at input part of expected schema + // This means that when decoding we need to do it from input to output instead of unknown to output + @as("ii") + mutable isInput?: bool, + // Whether the val is at output part of expected schema + // Needed for schemas like S.array(S.nullAsOption) where child schemas might be transformed + @as("io") + mutable isOutput?: bool, + // The schema of the value that we expect to parse into + @as("e") + mutable expected: internal, + mutable prev?: val, @as("f") mutable flag: flag, - @as("type") - mutable tag: tag, - mutable const?: char, - @as("t") - mutable skipTo?: bool, - mutable properties?: dict, - mutable anyOf?: array, - mutable format?: format, - @as("$ref") - mutable ref?: string, - mutable additionalItems?: additionalItems, -} -and b = { - @as("c") - mutable code: string, + @as("d") + mutable vals?: dict, + @as("fv") + mutable flattenedVals?: array, + @as("cp") + mutable codeFromPrev: string, @as("l") mutable varsAllocation: string, @as("a") mutable allocate: string => unit, - @as("f") - mutable filterCode: string, + // Invariant: absent iff no checks. Never stored as `Some([])` so callers + // can test presence with `->unsafeToBool` instead of length. + @as("vc") + mutable checks?: array, + @as("u") + mutable isUnion?: bool, + // Whether the chain starting from the root prev has a transformation + @as("t") + mutable hasTransform?: bool, + mutable path: Path.t, @as("g") global: bGlobal, + // This is to mark an object field as optional + // Fields like this should be skipped when the value is undefined + @as("o") + mutable optional?: bool, } and bGlobal = { - @as("c") - mutable code: string, - @as("l") - mutable varsAllocation: string, - @as("a") - mutable allocate: string => unit, @as("v") mutable varCounter: int, @as("o") mutable flag: int, - @as("f") - mutable filterCode: string, @as("e") embeded: array, @as("d") mutable defs?: dict, } +// Adjacent checks sharing `fail` by reference equality are fused with `&&` +// in `emitChecks`, so pass the same helper (e.g. `B.failInvalidType`) to +// every check on a val if you want them to emit as one `||`-throw line. +and check = { + @as("c") + cond: (~inputVar: string) => string, + @as("f") + fail: (~input: val) => (unknown => errorDetails), +} and flag = int and error = private { message: string, reason: string, path: Path.t, - code: errorCode, - flag: flag, -} -and errorCode = - | OperationFailed(string) - | InvalidOperation({description: string}) - | InvalidType({expected: schema, received: unknown, unionErrors?: array}) - | UnsupportedTransformation({from: schema, to: schema}) - | ExcessField(string) - | UnexpectedAsync - | InvalidJsonSchema(schema) +} +@tag("code") +and errorDetails = + // When received input doesn't match the expected schema + | @as("invalid_input") + InvalidInput({ + path: Path.t, + reason: string, + expected: schema, + received: schema, + input?: unknown, + unionErrors?: array, + }) + // When an operation fails, because it's impossible or called incorrectly + | @as("invalid_operation") InvalidOperation({path: Path.t, reason: string}) + // When the value decoding between two schemas is not supported + | @as("unsupported_decode") + UnsupportedDecode({ + path: Path.t, + reason: string, + from: schema, + to: schema, + }) + // When a decoder/encoder fails + | @as("invalid_conversion") + InvalidConversion({ + path: Path.t, + reason: string, + from: schema, + to: schema, + cause?: exn, + }) + | @as("unrecognized_keys") UnrecognizedKeys({path: Path.t, reason: string, keys: array}) + | @as("custom") Custom({path: Path.t, reason: string}) + @tag("success") and jsResult<'value> = | @as(true) Success({value: 'value}) | @as(false) Failure({error: error}) -type exn += private Error(error) +type exn += private Exn(error) external castToUnknown: t<'any> => t = "%identity" external castToAny: t<'value> => t<'any> = "%identity" @@ -596,27 +732,21 @@ let constField = "const" let isLiteral = (schema: internal) => schema->Obj.magic->Dict.has(constField) let isOptional = schema => { - switch schema.tag { - | Undefined => true - | Union => schema.has->X.Option.getUnsafe->Dict.has((Undefined: tag :> string)) - | _ => false - } + schema.tag === undefinedTag || + (schema.tag === unionTag && + schema.has->X.Option.getUnsafe->Dict.has((undefinedTag: tag :> string))) } module ValFlag = { @inline let none = 0 - @inline let async = 2 + @inline let async = 1 } module Flag = { @inline let none = 0 - @inline let typeValidation = 1 - @inline let async = 2 - @inline let assertOutput = 4 - @inline let jsonableOutput = 8 - @inline let jsonStringOutput = 16 - @inline let reverse = 32 - @inline let flatten = 64 + @inline let async = 1 + @inline let disableNanNumberValidation = 2 + // @inline let flatten = 64 external with: (flag, flag) => flag = "%orint" @inline @@ -645,38 +775,36 @@ module TagFlag = { @inline let symbol = 32768 let flags = %raw(`{ - unknown: 1, - string: 2, - number: 4, - boolean: 8, - undefined: 16, - null: 32, - object: 64, - array: 128, - union: 256, - ref: 512, - bigint: 1024, - nan: 2048, - "function": 4096, - instance: 8192, - never: 16384, - symbol: 32768, + [unknownTag]: 1, + [stringTag]: 2, + [numberTag]: 4, + [booleanTag]: 8, + [undefinedTag]: 16, + [nullTag]: 32, + [objectTag]: 64, + [arrayTag]: 128, + [unionTag]: 256, + [refTag]: 512, + [bigintTag]: 1024, + [nanTag]: 2048, + ["function"]: 4096, + [instanceTag]: 8192, + [neverTag]: 16384, + [symbolTag]: 32768, }`) @inline let get = (tag: tag) => flags->Js.Dict.unsafeGet((tag :> string)) - @inline - let isArray = tag => tag->get->Flag.unsafeHas(array) } let rec stringify = unknown => { let tagFlag = unknown->Type.typeof->(Obj.magic: Type.t => tag)->TagFlag.get if tagFlag->Flag.unsafeHas(TagFlag.undefined) { - "undefined" + (undefinedTag :> string) } else if tagFlag->Flag.unsafeHas(TagFlag.object) { if unknown === %raw(`null`) { - "null" + (nullTag :> string) } else if unknown->X.Array.isArray { let array = unknown->(Obj.magic: unknown => array) let string = ref("[") @@ -703,9 +831,12 @@ let rec stringify = unknown => { unknown->Obj.magic->X.Object.internalClass } } else if tagFlag->Flag.unsafeHas(TagFlag.string) { - `"${unknown->Obj.magic}"` + let string: string = unknown->Obj.magic + `"${string}"` } else if tagFlag->Flag.unsafeHas(TagFlag.bigint) { `${unknown->Obj.magic}n` + } else if tagFlag->Flag.unsafeHas(TagFlag.function) { + `Function` } else { (unknown->Obj.magic)["toString"]() } @@ -722,12 +853,36 @@ let rec toExpression = schema => { ->(Obj.magic: array => array>) ->Js.Array2.map(toExpression) ->Js.Array2.joinWith(" | ") + | {format: ?Some(CompactColumns), ?to, ?additionalItems} => + // For compactColumns, show the column types if we have properties from .to + switch to { + | Some(toSchema) => + switch toSchema.properties { + | Some(props) => + let keys = props->Js.Dict.keys + `[${keys + ->Js.Array2.map(key => { + let propSchema = props->Js.Dict.unsafeGet(key)->castToPublic + `${propSchema->toExpression}[]` + }) + ->Js.Array2.joinWith(", ")}]` + | None => "unknown[][]" + } + | None => + // No S.to applied, reuse the array expression logic + switch additionalItems { + | Some(Schema(innerArraySchema)) => + let innerArraySchemaTyped: t<'a> = innerArraySchema->Obj.magic + `${innerArraySchemaTyped->toExpression}[]` + | _ => "unknown[][]" + } + } | {format} => (format :> string) | {tag: Object, ?properties, ?additionalItems} => let properties = properties->X.Option.getUnsafe let locations = properties->Js.Dict.keys if locations->Js.Array2.length === 0 { - if additionalItems->Js.typeof === "object" { + if additionalItems->Js.typeof === (objectTag :> string) { let additionalItems: internal = additionalItems->Obj.magic `{ [key: string]: ${additionalItems->castToPublic->toExpression}; }` } else { @@ -746,17 +901,17 @@ let rec toExpression = schema => { | {tag} if %raw(`schema.b`) => (tag :> string) | {tag: Array, ?items, ?additionalItems} => let items = items->X.Option.getUnsafe - if additionalItems->Js.typeof === "object" { + if additionalItems->Js.typeof === (objectTag :> string) { let additionalItems: internal = additionalItems->Obj.magic let itemName = additionalItems->castToPublic->toExpression - if additionalItems.tag === Union { + if (additionalItems.tag :> string) === (unionTag :> string) { `(${itemName})` } else { itemName } ++ "[]" } else { `[${items - ->Js.Array2.map(item => item.schema->castToInternal->castToPublic->toExpression) + ->Js.Array2.map(schema => schema->castToPublic->toExpression) ->Js.Array2.joinWith(", ")}]` } | {tag: Instance, ?class} => (class->Obj.magic)["name"] @@ -767,11 +922,11 @@ let rec toExpression = schema => { module InternalError = { %%raw(` class SuryError extends Error { - constructor(code, flag, path) { + constructor(params) { super(); - this.flag = flag; - this.code = code; - this.path = path; + for (let key in params) { + this[key] = params[key]; + } } } @@ -781,11 +936,6 @@ d(p, 'message', { return message(this); }, }) -d(p, 'reason', { - get() { - return reason(this); - } -}) d(p, 'name', {value: 'SuryError'}) d(p, 's', {value: s}) d(p, '_1', { @@ -794,10 +944,11 @@ d(p, '_1', { }, }); d(p, 'RE_EXN_ID', { - value: $$Error, + value: Exn, }); -var Schema = function(type) {this.type=type}, sp = Object.create(null); +var seq = 1; +var Schema = function() {}, sp = Object.create(null); d(sp, 'with', { get() { return (fn, ...args) => fn(this, ...args) @@ -808,7 +959,7 @@ Schema.prototype = sp; `) @new - external make: (~code: errorCode, ~flag: int, ~path: Path.t) => error = "SuryError" + external make: errorDetails => error = "SuryError" let getOrRethrow = (exn: exn) => { if %raw("exn&&exn.s===s") { @@ -822,71 +973,11 @@ Schema.prototype = sp; @inline let panic = message => X.Exn.throwError(X.Exn.makeError(`[Sury] ${message}`)) - let rec reason = (error: error, ~nestedLevel=0) => { - switch error.code { - | OperationFailed(reason) => reason - | InvalidOperation({description}) => description - | UnsupportedTransformation({from, to}) => - `Unsupported transformation from ${from->toExpression} to ${to->toExpression}` - | UnexpectedAsync => "Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead" - | ExcessField(fieldName) => `Unrecognized key "${fieldName}"` - | InvalidType({expected: schema, received, ?unionErrors}) => - let m = ref(`Expected ${schema->toExpression}, received ${received->stringify}`) - switch unionErrors { - | Some(errors) => { - let lineBreak = `\n${" "->Js.String2.repeat(nestedLevel * 2)}` - let reasonsDict = Js.Dict.empty() - for idx in 0 to errors->Js.Array2.length - 1 { - let error = errors->Js.Array2.unsafe_get(idx) - let reason = error->reason(~nestedLevel=nestedLevel->X.Int.plus(1)) - let location = switch error.path { - | "" => "" - | nonEmptyPath => `At ${nonEmptyPath}: ` - } - let line = `- ${location}${reason}` - if reasonsDict->Js.Dict.unsafeGet(line)->X.Int.unsafeToBool->not { - reasonsDict->Js.Dict.set(line, 1) - m := m.contents ++ lineBreak ++ line - } - } - } - | None => () - } - m.contents - | InvalidJsonSchema(schema) => `${schema->toExpression} is not valid JSON` - } - } - - let reason = error => reason(error) - let message = (error: error) => { - let op = error.flag - - let text = ref("Failed ") - if op->Flag.unsafeHas(Flag.async) { - text := text.contents ++ "async " - } - - text := - text.contents ++ if op->Flag.unsafeHas(Flag.typeValidation) { - if op->Flag.unsafeHas(Flag.assertOutput) { - "asserting" - } else { - "parsing" - } - } else { - "converting" - } - - if op->Flag.unsafeHas(Flag.jsonableOutput) { - text := - text.contents ++ " to JSON" ++ (op->Flag.unsafeHas(Flag.jsonStringOutput) ? " string" : "") - } - - `${text.contents}${switch error.path { + `${switch error.path { | "" => "" - | nonEmptyPath => ` at ${nonEmptyPath}` - }}: ${error->reason}` + | nonEmptyPath => `Failed at ${nonEmptyPath}: ` + }}${error.reason}` } } @@ -897,8 +988,8 @@ type globalConfig = { mutable defsAccumulator: option>, @as("a") mutable defaultAdditionalItems: additionalItems, - @as("n") - mutable disableNanNumberValidation: bool, + @as("f") + mutable defaultFlag: flag, } type globalConfigOverride = { @@ -907,16 +998,31 @@ type globalConfigOverride = { } let initialOnAdditionalItems: additionalItemsMode = Strip -let initialDisableNanNumberProtection = false +let initialDefaultFlag = Flag.none let globalConfig: globalConfig = { message: InternalError.message, defsAccumulator: None, defaultAdditionalItems: (initialOnAdditionalItems :> additionalItems), - disableNanNumberValidation: initialDisableNanNumberProtection, + defaultFlag: initialDefaultFlag, } +let valueOptions = Js.Dict.empty() +let configurableValueOptions = %raw(`{configurable: true}`) +let valKey = "value" +let reversedKey = "r" + @new -external base: tag => internal = "Schema" +external base: unit => internal = "Schema" +let base = (tag, ~selfReverse) => { + let s = base() + s.tag = tag + s.seq = %raw(`seq++`) + if selfReverse { + valueOptions->Js.Dict.set(valKey, s->Obj.magic) + let _ = X.Object.defineProperty(s, reversedKey, valueOptions->Obj.magic) + } + s +} let shakenRef = "as" @@ -933,45 +1039,44 @@ let shakenTraps: X.Proxy.traps = { } let shaken = (apiName: string) => { - let mut = base(Never) + let mut = base(neverTag, ~selfReverse=true) mut->Obj.magic->Js.Dict.set(shakenRef, apiName) mut->X.Proxy.make(shakenTraps) } -let unknown = base(Unknown) -let bool = base(Boolean) -let symbol = base(Symbol) -let string = base(String) -let int = base(Number) +let noopDecoder = (~input) => input + +let unknown = base(unknownTag, ~selfReverse=true) +unknown.decoder = noopDecoder +let bool = base(booleanTag, ~selfReverse=true) +let symbol = base(symbolTag, ~selfReverse=true) +let string = base(stringTag, ~selfReverse=true) +let int = base(numberTag, ~selfReverse=true) int.format = Some(Int32) -let float = base(Number) -let bigint = base(BigInt) -let unit = base(Undefined) +let float = base(numberTag, ~selfReverse=true) +let bigint = base(bigintTag, ~selfReverse=true) +let unit = base(undefinedTag, ~selfReverse=true) unit.const = %raw(`void 0`) +let nullLiteral = base(nullTag, ~selfReverse=true) +nullLiteral.const = %raw(`null`) +let nan = base(nanTag, ~selfReverse=true) +nan.const = %raw(`NaN`) -type s<'value> = { - schema: t<'value>, - fail: 'a. (string, ~path: Path.t=?) => 'a, -} +type s<'value> = {fail: 'a. (string, ~path: Path.t=?) => 'a} -// Need to copy without operations cache -// which use flag as a key. -// k > "a" is hacky way to skip all numbers -// Should actually benchmark whether it's faster -let copyWithoutCache: internal => internal = %raw(`(schema) => { - let c = new Schema(schema.type) +let copySchema: internal => internal = %raw(`(schema) => { + let c = new Schema() for (let k in schema) { - if (k > "a" || k === "$ref" || k === "$defs") { - c[k] = schema[k] - } + c[k] = schema[k] } + c.seq = seq++ return c }`) let updateOutput = (schema: internal, fn): t<'value> => { - let root = schema->copyWithoutCache + let root = schema->copySchema let mut = ref(root) while mut.contents.to->Obj.magic { - let next = mut.contents.to->X.Option.getUnsafe->copyWithoutCache + let next = mut.contents.to->X.Option.getUnsafe->copySchema mut.contents.to = Some(next) mut := next } @@ -979,29 +1084,25 @@ let updateOutput = (schema: internal, fn): t<'value> => { fn(mut.contents) root->castToPublic } -let resetCacheInPlace: internal => unit = %raw(`(schema) => { - for (let k in schema) { - if (Number(k[0])) { - delete schema[k]; - } - } -}`) -module ErrorClass = { - type t +module Error = { + type class - let value: t = %raw("SuryError") + let class: class = %raw("SuryError") - let constructor = InternalError.make + let make = InternalError.make + + external classify: error => errorDetails = "%identity" } module Builder = { type t = builder - let make = (Obj.magic: ((b, ~input: val, ~selfSchema: internal, ~path: Path.t) => val) => t) + let make = (Obj.magic: ((~input: val) => val) => t) + let encoder = (Obj.magic: ((~input: val, ~target: internal) => val) => encoder) module B = { - let embed = (b: b, value) => { + let embed = (b: val, value) => { let e = b.global.embeded let l = e->Js.Array2.length e->Js.Array2.unsafe_set(l, value->castAnyToUnknown) @@ -1030,13 +1131,13 @@ module Builder = { // Escape it once per compiled operation. // Use bGlobal as cache, so we don't allocate another object + it's garbage collected. - let inlineLocation = (b: b, location) => { + let inlineLocation = (global: bGlobal, location) => { let key = `"${location}"` - switch b.global->(Obj.magic: bGlobal => dict)->X.Dict.getUnsafeOption(key) { + switch global->(Obj.magic: bGlobal => dict)->X.Dict.getUnsafeOption(key) { | Some(i) => i | None => { let inlinedLocation = location->X.Inlined.Value.fromString - b.global->(Obj.magic: bGlobal => dict)->Js.Dict.set(key, inlinedLocation) + global->(Obj.magic: bGlobal => dict)->Js.Dict.set(key, inlinedLocation) inlinedLocation } } @@ -1053,46 +1154,18 @@ module Builder = { b.allocate = secondAllocate } - let rootScope = (~flag, ~defs) => { - let global = { - code: "", - allocate: initialAllocate, - varsAllocation: "", - // TODO: Add global varsAllocation here - // Set all the vars to the varsAllocation - // Measure performance - // TODO: Also try setting values to embed without allocation - // (Is it memory leak?) - varCounter: -1, - embeded: [], - flag, - filterCode: "", - ?defs, - } - (global->Obj.magic)["g"] = global - global->(Obj.magic: bGlobal => b) - } + let _var = () => (%raw(`this`)).inline - @inline - let scope = (b: b): b => { - { - allocate: initialAllocate, - global: b.global, - filterCode: "", - code: "", - varsAllocation: "", - } + let _bondVar = () => { + let val = %raw(`this`) + let bond = val.bond->X.Option.getUnsafe + bond.var() } - let allocateScope = (b: b): string => { - // Delete allocate, - // this is used to handle Val.var - // linked to allocated scopes - let _ = %raw(`delete b.a`) - let varsAllocation = b.varsAllocation - varsAllocation === "" - ? b.filterCode ++ b.code - : `${b.filterCode}let ${varsAllocation};${b.code}` + let _prevVar = () => { + let val = %raw(`this`) + let prev = val.prev->X.Option.getUnsafe + prev.var() } let varWithoutAllocation = (global: bGlobal) => { @@ -1101,1085 +1174,2046 @@ module Builder = { `v${newCounter->X.Int.unsafeToString}` } - let _var = _b => (%raw(`this`)).inline - let _notVar = b => { + let _notVarBeforeValidation = () => { + let val = %raw(`this`) + let v = val.global->varWithoutAllocation + val.codeFromPrev = `let ${v}=${val.inline};` + val.inline = v + val.var = _var + v + } + + let _notVarAtParent = () => { let val = %raw(`this`) - let v = b.global->varWithoutAllocation + let v = val.global->varWithoutAllocation + (val.parent->X.Option.getUnsafe).allocate(`${v}=${val.inline}`) + val.var = _var + val.inline = v + v + } + + let _notVar = () => { + let val: val = %raw(`this`) + let v = val.global->varWithoutAllocation + let target = switch val.prev { + | Some(from) => from + | None => val // FIXME: Validate that this never happens + } switch val.inline { - | "" => val.b.allocate(v) - | i if b.allocate !== %raw(`void 0`) => b.allocate(`${v}=${i}`) + | "" => target.allocate(v) | i => - b.code = b.code ++ `${v}=${i};` - b.global.allocate(v) + if val.codeFromPrev !== "" { + target.allocate(v) + val.codeFromPrev = `${val.codeFromPrev}${v}=${i};` + } else { + target.allocate(`${v}=${i}`) + } } val.var = _var val.inline = v v } - let allocateVal = (b: b, ~schema): val => { - let v = b.global->varWithoutAllocation - b.allocate(v) - {b, var: _var, inline: v, flag: ValFlag.none, tag: schema.tag} - } - - let val = (b: b, initial: string, ~schema): val => { - {b, var: _notVar, inline: initial, flag: ValFlag.none, tag: schema.tag} - } + @inline + let operationArgVar = "i" - let constVal = (b: b, ~schema): val => { + let operationArg = (~schema, ~expected, ~flag, ~defs): val => { { - b, - var: _notVar, - inline: b->inlineConst(schema), + codeFromPrev: "", + var: _var, + inline: operationArgVar, + allocate: initialAllocate, flag: ValFlag.none, - tag: schema.tag, - const: ?schema.const, + schema, + expected, + varsAllocation: "", + // TODO: Add global varsAllocation here + // Set all the vars to the varsAllocation + // Measure performance + // TODO: Also try setting values to embed without allocation + // (Is it memory leak?) + path: Path.empty, + global: { + ?defs, + flag, + embeded: [], + varCounter: -1, + }, } } - let asyncVal = (b: b, initial: string): val => { - { - b, - var: _notVar, - inline: initial, - flag: ValFlag.async, - tag: Unknown, // FIXME: Should pass schema here - } + let throw = (b: val, errorDetails) => { + X.Exn.throwAny(InternalError.make(errorDetails)) } - module Val = { - module Object = { - type t = { - ...val, - @as("j") - mutable join: (string, string) => string, - @as("c") - mutable asyncCount: int, - @as("r") - mutable promiseAllContent: string, - } + let failWithArg = (b: val, fn: 'arg => errorDetails, arg) => { + `${b->embed(arg => { + b->throw(fn(arg)) + })}(${arg})` + } - let objectJoin = (inlinedLocation, value) => { - `${inlinedLocation}:${value},` - } + let makeInvalidConversionDetails = (~input, ~to, ~cause) => { + if %raw("cause&&cause.s===s") { + let error: error = cause->Obj.magic - let arrayJoin = (_inlinedLocation, value) => { - value ++ "," + // Read about this in shouldPrependPathKey comment. + if !(cause->Obj.magic->Js.Dict.unsafeGet(shouldPrependPathKey)) { + (cause->Obj.magic)["path"] = input.path->Path.concat(error.path) } + error->Error.classify + } else { + InvalidConversion({ + from: input.schema->castToPublic, + to: to->castToPublic, + cause, + path: input.path, + reason: { + if %raw(`cause instanceof Error`) { + let text = %raw(`"" + cause`) + if text->String.startsWith("Error: ") { + text->String.slice(~start=7) + } else { + text + } + } else { + cause->Obj.magic->stringify + } + }, + }) + } + } - let make = (b: b, ~isArray): t => { - { - b, - var: _notVar, - inline: "", - flag: ValFlag.none, - join: isArray ? arrayJoin : objectJoin, - asyncCount: 0, - promiseAllContent: "", - tag: isArray ? Array : Object, - properties: Js.Dict.empty(), - additionalItems: Strict, + let makeInvalidInputDetails = ( + ~expected, + ~received, + ~path, + ~input, + ~includeInput, + ~unionErrors=?, + ) => { + let reasonRef = ref( + `Expected ${expected + ->castToPublic + ->toExpression}, received ${if includeInput { + input->stringify + } else { + received->toExpression + }}`, + ) + switch unionErrors { + | Some(caseErrors) => { + let reasonsDict = Js.Dict.empty() + for idx in 0 to caseErrors->Js.Array2.length - 1 { + let caseError = caseErrors->Js.Array2.unsafe_get(idx) + let caseReason = caseError.reason->Stdlib.String.split("\n")->Js.Array2.joinWith("\n ") + let location = switch caseError.path { + | "" => "" + | nonEmptyPath => `At ${nonEmptyPath}: ` + } + let line = `\n- ${location}${caseReason}` + if reasonsDict->Js.Dict.unsafeGet(line)->X.Int.unsafeToBool->not { + reasonsDict->Js.Dict.set(line, 1) + reasonRef := reasonRef.contents ++ line + } } } + | None => () + } - let add = (objectVal, ~location, val: val) => { - let inlinedLocation = objectVal.b->inlineLocation(location) - objectVal.properties->X.Option.getUnsafe->Js.Dict.set(location, val) - if val.flag->Flag.unsafeHas(ValFlag.async) { - objectVal.promiseAllContent = objectVal.promiseAllContent ++ val.inline ++ "," - objectVal.inline = - objectVal.inline ++ objectVal.join(inlinedLocation, `a[${%raw(`objectVal.c++`)}]`) - } else { - objectVal.inline = objectVal.inline ++ objectVal.join(inlinedLocation, val.inline) - } + let details = InvalidInput({ + expected: expected->castToPublic, + received, + path, + reason: reasonRef.contents, + ?unionErrors, + }) + if includeInput { + (details->Obj.magic)["input"] = input + } + details + } + + // Pass this as `fail` on every check that wants "expected X, received Y" + // error semantics. Stable reference → adjacent checks fuse. + let failInvalidType = (~input: val) => { + // Snapshot the three fields up front so the returned closure doesn't + // retain `input` — otherwise the compiled decoder's embed array would + // pin the entire val chain (prev, global, schemas) for its lifetime. + // + // Use prev.schema when available: checks run against prev.var(), so the + // value's actual runtime type at check time is prev.schema, not the + // post-narrowing schema stored on the current val. + let received = switch input.prev { + | Some(p) => p.schema->castToPublic + | None => input.schema->castToPublic + } + let path = input.path + let expected = input.expected + let override = switch expected.errorMessage { + | Some(em) => + let d: dict = em->Obj.magic + switch d->X.Dict.getUnsafeOption("type") { + | Some(m) => Some(m) + | None => d->X.Dict.getUnsafeOption("_") } + | None => None + } + switch override { + | Some(m) => _value => Custom({reason: m, path}) + | None => + value => + makeInvalidInputDetails( + ~expected, + ~received, + ~path, + ~input=value, + ~includeInput=true, + ) + } + } - let merge = (target, subObjectVal: val) => { - let locations = subObjectVal.properties->X.Option.getUnsafe->Js.Dict.keys - for idx in 0 to locations->Js.Array2.length - 1 { - let location = locations->Js.Array2.unsafe_get(idx) - target->add( - ~location, - subObjectVal.properties->X.Option.getUnsafe->Js.Dict.unsafeGet(location), - ) + + let failWithErrorMessage = (key, ~defaultMessage=?) => { + (~input: val) => { + let override = switch input.expected.errorMessage { + | Some(em) => + let d: dict = em->Obj.magic + switch d->X.Dict.getUnsafeOption(key) { + | Some(m) => Some(m) + | None => d->X.Dict.getUnsafeOption("_") } + | None => None } - - let complete = (objectVal, ~isArray) => { - objectVal.inline = isArray - ? "[" ++ objectVal.inline ++ "]" - : "{" ++ objectVal.inline ++ "}" - if objectVal.asyncCount->Obj.magic { - objectVal.flag = objectVal.flag->Flag.with(ValFlag.async) - objectVal.inline = `Promise.all([${objectVal.promiseAllContent}]).then(a=>(${objectVal.inline}))` + switch (override, defaultMessage) { + | (Some(m), _) | (None, Some(m)) => { + let path = input.path + _value => Custom({reason: m, path}) } - objectVal.additionalItems = Some(Strict) - (objectVal :> val) + | (None, None) => failInvalidType(~input) } } + } - @inline - let var = (b: b, val: val) => { - val.var(b) - } - - let addKey = (b: b, input: val, key, val: val) => { - `${b->var(input)}[${key}]=${val.inline}` + // Inline variant: emits the throw expression directly. Used by decoders + // that splice errors into custom JS (e.g. `catch(_){${embedInvalidInput}}`), + // not via the `check` pipeline. + let embedInvalidInput = (~input: val, ~expected=input.expected) => { + let received = switch input.prev { + | Some(p) => p.schema->castToPublic + | None => input.schema->castToPublic } + let path = input.path + input->failWithArg( + value => + makeInvalidInputDetails( + ~expected, + ~received, + ~path, + ~input=value, + ~includeInput=true, + ), + input.var(), + ) + } - let set = (b: b, input: val, val) => { - if input === val { - "" - } else { - // FIXME: Remove original ValFlag - let inputVar = b->var(input) - switch ( - input.flag->Flag.unsafeHas(ValFlag.async), - val.flag->Flag.unsafeHas(ValFlag.async), + // Caller must verify `val.checks->unsafeToBool` and + // `val.expected.noValidation !== Some(true)` first — the unwrap below + // is unchecked. `inputVar` is usually `val.prev.var()`. + let emitChecks = (val: val, ~inputVar: string): string => { + let checks = val.checks->X.Option.getUnsafe + let len = checks->Js.Array2.length + if len === 1 { + let check = checks->Js.Array2.unsafe_get(0) + `${check.cond(~inputVar)}||${val->failWithArg(check.fail(~input=val), inputVar)};` + } else { + let out = ref("") + let i = ref(0) + while i.contents < len { + let head = checks->Js.Array2.unsafe_get(i.contents) + let fail = head.fail + let cond = ref(head.cond(~inputVar)) + i := i.contents + 1 + // Extend the fused cond while the next check shares this `fail`. + while ( + i.contents < len && (checks->Js.Array2.unsafe_get(i.contents)).fail === fail ) { - | (false, true) => { - input.flag = input.flag->Flag.with(ValFlag.async) - `${inputVar}=${val.inline}` - } - | (false, false) - | (true, true) => - `${inputVar}=${val.inline}` - | (true, false) => `${inputVar}=Promise.resolve(${val.inline})` + cond := + cond.contents ++ + "&&" ++ (checks->Js.Array2.unsafe_get(i.contents)).cond(~inputVar) + i := i.contents + 1 } + out := + out.contents ++ + `${cond.contents}||${val->failWithArg(fail(~input=val), inputVar)};` } + out.contents } + } - let get = (b, targetVal: val, location) => { - let properties = targetVal.properties->X.Option.getUnsafe - switch properties->X.Dict.getUnsafeOption(location) { - | Some(val) => val - | None => { - let schema = switch targetVal.additionalItems->X.Option.getUnsafe { - | Schema(schema) => schema->castToInternal - | _ => InternalError.panic("The schema doesn't have additional items") - } - - let val = { - b, - var: _notVar, - inline: `${b->var(targetVal)}${Path.fromLocation(location)}`, - flag: ValFlag.none, - tag: schema.tag, + // AND-joins every check's cond — caller guarantees `checks` is non-empty. + // Used by union codegen to hoist a val's checks into a dispatch discriminant. + let andJoinChecks = (checks: array, ~inputVar: string): string => { + let result = ref((checks->Js.Array2.unsafe_get(0)).cond(~inputVar)) + for i in 1 to checks->Js.Array2.length - 1 { + result := + result.contents ++ "&&" ++ (checks->Js.Array2.unsafe_get(i)).cond(~inputVar) + } + result.contents + } + + // Walks the val.prev chain and assembles generated code. When + // `~hoistCond` is provided (union codegen), checks eligible for lift + // are AND-joined into that ref as a dispatch discriminant instead of + // being emitted. All other callers pass no `~hoistCond` and get the + // plain merge: every non-`noValidation` check is emitted inline. + let merge = (val: val, ~hoistCond: option>=?): string => { + let current = ref(Some(val)) + let code = ref("") + + while current.contents !== None { + let val = current.contents->X.Option.getUnsafe + current := val.prev + + let currentCode = ref("") + + if val.checks->X.Option.unsafeToBool { + let isHoistable = + hoistCond !== None && + (val.hasTransform === Some(true) + ? (val.prev->X.Option.getUnsafe).hasTransform !== Some(true) && + val.codeFromPrev === "" + : true) + if isHoistable { + // `noValidation` is intentionally bypassed here — the cond + // routes between union cases, it doesn't reject, so + // suppressing would break dispatch. + let cond = hoistCond->X.Option.getUnsafe + let prev = current.contents->X.Option.getUnsafe + let condCode = + val.checks->X.Option.getUnsafe->andJoinChecks(~inputVar=prev.var()) + if cond.contents->X.String.unsafeToBool { + cond := `${condCode}&&${cond.contents}` + } else { + cond := condCode } - // FIXME: Create additionalItems and properties for obj schemas - properties->Js.Dict.set(location, val) - val + } else if val.expected.noValidation !== Some(true) { + let prev = current.contents->X.Option.getUnsafe + currentCode := val->emitChecks(~inputVar=prev.var()) } } - } - - let setInlined = (b: b, input: val, inlined) => { - `${b->var(input)}=${inlined}` - } - let map = (inlinedFn, input: val) => { - { - b: input.b, - var: _notVar, - inline: `${inlinedFn}(${input.inline})`, - flag: ValFlag.none, - tag: Unknown, + if val.varsAllocation !== "" { + currentCode := currentCode.contents ++ `let ${val.varsAllocation};` } - } - } - @inline - let isInternalError = (_b: b, var) => { - `${var}&&${var}.s===s` - } - - let transform = (b: b, ~input: val, operation) => { - if input.flag->Flag.unsafeHas(ValFlag.async) { - let bb = b->scope - let operationInput: val = { - b, - var: _var, - inline: bb.global->varWithoutAllocation, - flag: ValFlag.none, - tag: Unknown, - } - let operationOutputVal = operation(bb, ~input=operationInput) - let operationCode = bb->allocateScope + // Delete allocate, + // this is used to handle Val.var + // linked to allocated scopes + let _ = %raw(`delete val$1.a`) - input.b->asyncVal( - `${input.inline}.then(${b->Val.var( - operationInput, - )}=>{${operationCode}return ${operationOutputVal.inline}})`, - ) - } else { - operation(b, ~input) + currentCode := val.codeFromPrev ++ currentCode.contents + + code := currentCode.contents ++ code.contents } + + code.contents } - let throw = (b: b, ~code, ~path) => { - X.Exn.throwAny(InternalError.make(~code, ~flag=b.global.flag, ~path)) + let next = (prev: val, initial: string, ~schema, ~expected=prev.expected): val => { + { + // FIXME: vals and other object.val fields should be copied + prev, + var: _notVar, + inline: initial, + flag: ValFlag.none, + schema, + expected, + codeFromPrev: "", + varsAllocation: "", + allocate: initialAllocate, + path: prev.path, + global: prev.global, + hasTransform: true, + vals: ?prev.vals, + } } - let embedSyncOperation = (b: b, ~input: val, ~fn: 'input => 'output) => { - if input.flag->Flag.unsafeHas(ValFlag.async) { - input.b->asyncVal(`${input.inline}.then(${b->embed(fn)})`) - } else { - Val.map(b->embed(fn), input) + // Pass a non-empty `~checks` or omit it. Never pass `~checks=[]` — + // that would break the val.checks "absent iff no checks" invariant. + let refine = (val: val, ~schema=val.schema, ~checks=?, ~expected=val.expected) => { + let shouldLink = val.var !== _var + let nextVal = { + prev: val, + inline: val.inline, + var: shouldLink ? _prevVar : _var, + flag: val.flag, + schema, + expected, + codeFromPrev: "", + varsAllocation: "", + allocate: initialAllocate, + checks: ?checks, + path: val.path, + global: val.global, + hasTransform: ?val.hasTransform, + vals: ?val.vals, + } + if shouldLink { + let valVar: unit => string = %raw(`val.v.bind(val)`) + val.var = () => { + let v = valVar() + nextVal.inline = v + nextVal.var = _var + v + } } + nextVal } - let embedAsyncOperation = (b: b, ~input, ~fn: 'input => promise<'output>) => { - if !(b.global.flag->Flag.unsafeHas(Flag.async)) { - b->throw(~code=UnexpectedAsync, ~path=Path.empty) + // Lazy-allocate helper for mutating an existing val (as opposed to + // building a local array and passing it through `refine`). + let pushCheck = (val: val, check: check) => { + switch val.checks { + | Some(arr) => arr->Js.Array2.push(check)->ignore + | None => val.checks = Some([check]) } - let val = b->embedSyncOperation(~input, ~fn) - val.flag = val.flag->Flag.with(ValFlag.async) - val } - let failWithArg = (b: b, ~path, fn: 'arg => errorCode, arg) => { - `${b->embed(arg => { - b->throw(~path, ~code=fn(arg)) - })}(${arg})` + // Used in union codegen: splice a literal child's checks into the parent + // as dispatch discriminants. Each cond's `inputVar` is rewritten to + // `parent[key]`; `fail` stays shared so lifted checks fuse with the + // parent's own type guard. No-op if the child has no checks. + let hoistChildChecks = (parent: val, ~child: val, ~key: string) => { + if child.checks->X.Option.unsafeToBool { + let pathAppend = parent.global->inlineLocation(key)->Path.fromInlinedLocation + child.checks + ->X.Option.getUnsafe + ->Js.Array2.forEach(check => { + parent->pushCheck({ + cond: (~inputVar) => check.cond(~inputVar=inputVar ++ pathAppend), + fail: check.fail, + }) + }) + child.checks = None + } } - let fail = (b: b, ~message, ~path) => { - `${b->embed(() => { - b->throw(~path, ~code=OperationFailed(message)) - })}()` + let dynamicScope = (from: val, ~locationVar): val => { + { + var: _notVarBeforeValidation, + inline: `${from.var()}[${locationVar}]`, + flag: from.flag, + schema: from.schema.additionalItems->(Obj.magic: option => internal), + expected: from.expected.additionalItems->(Obj.magic: option => internal), + codeFromPrev: "", + varsAllocation: "", + parent: from, + allocate: initialAllocate, + path: Path.empty, + global: from.global, + } } - let effectCtx = (b, ~selfSchema, ~path) => { - schema: selfSchema->castToPublic, - fail: (message, ~path as customPath=Path.empty) => { - b->throw(~path=path->Path.concat(customPath), ~code=OperationFailed(message)) - }, + let nextConst = (from: val, ~schema, ~expected=?): val => { + from->next(from->inlineConst(schema), ~schema, ~expected?) } - let invalidOperation = (b: b, ~path, ~description) => { - b->throw(~path, ~code=InvalidOperation({description: description})) + let asyncVal = (from: val, initial: string): val => { + let v = from->next(initial, ~schema=from.schema) + v.flag = ValFlag.async + v } - // TODO: Refactor - let withCatch = (b: b, ~input, ~catch, ~appendSafe=?, fn) => { - let prevCode = b.code + module Val = { + module Object = { + type t = { + ...val, + } - b.code = "" - let errorVar = b.global->varWithoutAllocation - let maybeResolveVal = catch(b, ~errorVar) - let catchCode = `if(${b->isInternalError(errorVar)}){${b.code}` - b.code = "" + let add = (objectVal: t, ~location, val: val) => { + if objectVal.schema.tag === arrayTag { + objectVal.schema.items->X.Option.getUnsafe->Stdlib.Array.push(val.schema) + } else { + if !(val.optional->X.Option.getUnsafe) { + objectVal.schema.required->X.Option.getUnsafe->Stdlib.Array.push(location)->ignore + } + objectVal.schema.properties->X.Option.getUnsafe->Stdlib.Dict.set(location, val.schema) + } - let bb = b->scope - let fnOutput = fn(bb) - b.code = b.code ++ bb->allocateScope + // Async field values must be reachable as a plain identifier so + // the accumulator in completeObjectVal can use val.inline as a + // destructuring/reference target. For e.g. array-of-async, the + // asyncVal's inline is a Promise.all(...) expression, not a var. + // This has to happen before val->merge, which deletes .allocate + // from the prev chain and locks the emitted code. + if val.flag->Flag.unsafeHas(ValFlag.async) { + let _ = val.var() + } + objectVal.codeFromPrev = objectVal.codeFromPrev ++ val->merge + objectVal.vals->X.Option.getUnsafe->Js.Dict.set(location, val) + } - let isNoop = fnOutput.inline === input.inline && b.code === "" - - switch appendSafe { - | Some(append) => append(b, ~output=fnOutput) - | None => () - } - - if isNoop { - fnOutput - } else { - let isAsync = fnOutput.flag->Flag.unsafeHas(ValFlag.async) - let output = - input === fnOutput - ? input - : switch appendSafe { - | Some(_) => fnOutput - | None => { - b, - var: _notVar, - inline: "", - flag: isAsync ? ValFlag.async : ValFlag.none, - tag: Unknown, - } - } - - let catchCode = switch maybeResolveVal { - | None => _ => `${catchCode}}throw ${errorVar}` - | Some(resolveVal) => - catchLocation => - catchCode ++ - switch catchLocation { - | #0 => b->Val.set(output, resolveVal) - | #1 => `return ${resolveVal.inline}` - } ++ - `}else{throw ${errorVar}}` + let merge = (target: t, vals: dict) => { + let locations = vals->Js.Dict.keys + for idx in 0 to locations->Js.Array2.length - 1 { + let location = locations->Js.Array2.unsafe_get(idx) + target->add(~location, vals->Js.Dict.unsafeGet(location)) + } } + } - b.code = - prevCode ++ - `try{${b.code}${switch isAsync { - | true => - b->Val.setInlined(output, `${fnOutput.inline}.catch(${errorVar}=>{${catchCode(#1)}})`) - | false => b->Val.set(output, fnOutput) - }}}catch(${errorVar}){${catchCode(#0)}}` + @inline + let var = (val: val) => { + val.var() + } - output + let addKey = (objVal: val, key, value: val) => { + `${objVal.var()}[${key}]=${value.inline}` } - } - let withPathPrepend = ( - b: b, - ~input, - ~path, - ~dynamicLocationVar as maybeDynamicLocationVar=?, - ~appendSafe=?, - fn, - ) => { - if path === Path.empty && maybeDynamicLocationVar === None { - fn(b, ~input, ~path) - } else { - try b->withCatch( - ~input, - ~catch=(b, ~errorVar) => { - b.code = `${errorVar}.path=${path->X.Inlined.Value.fromString}+${switch maybeDynamicLocationVar { - | Some(var) => `'["'+${var}+'"]'+` - | _ => "" - }}${errorVar}.path` - None - }, - ~appendSafe?, - b => fn(b, ~input, ~path=Path.empty), - ) catch { - | _ => - let error = %raw(`exn`)->InternalError.getOrRethrow - X.Exn.throwAny( - InternalError.make( - ~path=path->Path.concat(Path.dynamic)->Path.concat(error.path), - ~code=error.code, - ~flag=error.flag, - ), - ) + let scope = (val: val) => { + let shouldLink = val.var !== _var + + // TODO: Simplify bond + let nextVal = { + inline: val.inline, + schema: val.schema, + expected: val.expected, + flag: Flag.none, + path: val.path, + global: val.global, + var: shouldLink ? _bondVar : _var, + bond: val, + codeFromPrev: "", + isUnion: false, + varsAllocation: "", + hasTransform: false, + allocate: initialAllocate, + isInput: ?val.isInput, + isOutput: ?val.isOutput, + vals: ?val.vals, // TODO: Is this correct? + } + if shouldLink { + let valVar: unit => string = %raw(`val.v.bind(val)`) + val.var = () => { + let v = valVar() + nextVal.inline = v + nextVal.var = _var + v + } } + nextVal } - } - let rec validation = (b, ~inputVar, ~schema, ~negative) => { - let eq = negative ? "!==" : "===" - let and_ = negative ? "||" : "&&" - let exp = negative ? "!" : "" - - let tag = schema.tag - let tagFlag = tag->TagFlag.get - - if tagFlag->Flag.unsafeHas(TagFlag.nan) { - exp ++ `Number.isNaN(${inputVar})` - } else if schema->isLiteral { - `${inputVar}${eq}${b->inlineConst(schema)}` - } else if tagFlag->Flag.unsafeHas(TagFlag.number) { - `typeof ${inputVar}${eq}"${(tag :> string)}"` - } else if tagFlag->Flag.unsafeHas(TagFlag.object) { - `typeof ${inputVar}${eq}"${(tag :> string)}"${and_}${exp}${inputVar}` - } else if tagFlag->Flag.unsafeHas(TagFlag.array) { - `${exp}Array.isArray(${inputVar})` - } else if tagFlag->Flag.unsafeHas(TagFlag.instance) { - let c = `${inputVar} instanceof ${b->embed(schema.class)}` - negative ? `!(${c})` : c - } else { - `typeof ${inputVar}${eq}"${(tag :> string)}"` - } - } - and refinement = (b, ~inputVar, ~schema, ~negative) => { - let eq = negative ? "!==" : "===" - let and_ = negative ? "||" : "&&" - let not_ = negative ? "" : "!" - let lt = negative ? ">" : "<" - let gt = negative ? "<" : ">" - - switch schema { - | {const: _} => "" - | {format: Int32} => - `${and_}${inputVar}${lt}2147483647${and_}${inputVar}${gt}-2147483648${and_}${inputVar}%1${eq}0` - | {tag: Number} => - if globalConfig.disableNanNumberValidation { - "" - } else { - `${and_}${not_}Number.isNaN(${inputVar})` + let get = (parent: val, location) => { + let vals = switch parent.vals { + | Some(d) => d + | None => { + let d = Js.Dict.empty() + parent.vals = Some(d) + d + } } - | {tag: Object as tag | Array as tag, ?additionalItems, ?items} => { - let additionalItems = additionalItems->X.Option.getUnsafe - let items = items->X.Option.getUnsafe - - let length = items->Js.Array2.length - - let code = ref( - if tag === Array { - switch additionalItems { - | Strict => `${and_}${inputVar}.length${eq}${length->X.Int.unsafeToString}` - | Strip => `${and_}${inputVar}.length${gt}${length->X.Int.unsafeToString}` - | Schema(_) => "" - } - } else if additionalItems === Strip { - "" - } else { - `${and_}${not_}Array.isArray(${inputVar})` - }, - ) - for idx in 0 to items->Js.Array2.length - 1 { - let {schema: item, location} = items->Js.Array2.unsafe_get(idx) - let item = item->castToInternal - let itemCode = if item->isLiteral || schema.unnest->X.Option.getUnsafe { - b->validation( - ~inputVar=Path.concat( - inputVar, - Path.fromInlinedLocation(b->inlineLocation(location)), - ), - ~schema=item, - ~negative, - ) - } else if item.items->Obj.magic { - let inputVar = Path.concat( - inputVar, - Path.fromInlinedLocation(b->inlineLocation(location)), - ) - // TODO: Support noValidation - b->validation(~inputVar, ~schema=item, ~negative) ++ - b->refinement(~inputVar, ~schema=item, ~negative) + + switch vals->X.Dict.getUnsafeOption(location) { + | Some(v) => v->scope + | None => { + let locationSchema = if parent.schema.tag === objectTag { + parent.schema.properties->X.Option.getUnsafe->X.Dict.getUnsafeOption(location) } else { - "" + parent.schema.items + ->X.Option.getUnsafe + ->X.Array.getUnsafeOptionByString(location) } - if itemCode !== "" { - code.contents = code.contents ++ and_ ++ itemCode + let schema = switch locationSchema { + | Some(s) => s + | None => + switch parent.schema.additionalItems->X.Option.getUnsafe { + | Schema(s) => s->castToInternal + | _ => InternalError.panic("The schema doesn't have additional items") + } + } + + let pathAppend = Path.fromInlinedLocation(parent.global->inlineLocation(location)) + + let item = { + // FIXME: vals and other object.val fields should be copied + var: _notVarAtParent, + inline: if schema->isLiteral { + parent->inlineConst(schema) + } else { + `${parent->var}${pathAppend}` + }, + flag: ValFlag.none, + schema, + expected: schema, + codeFromPrev: "", + varsAllocation: "", + allocate: initialAllocate, + path: parent.path->Path.concat(pathAppend), + global: parent.global, + parent, } + vals->Js.Dict.set(location, item) + item } - code.contents } - | _ => "" } } - // FIXME: Combine with refinement and all the staff - let makeRefinedOf = (b: b, ~input: val, ~schema) => { - let mut = { - b, - var: input.var, - inline: input.inline, - flag: input.flag, - tag: schema.tag, - } - let rec loop = (~mut: val, ~schema) => { - if schema->isLiteral { - mut.const = schema.const - } - // if schema.format !== Some(Int32) { - // input.format = schema.format - // } - switch schema.items { - | Some(items) => { - let properties = Js.Dict.empty() - items->Js.Array2.forEach((item: item) => { - let schema = item.schema->castToInternal - let isConst = schema->isLiteral - if isConst || schema.items->Obj.magic { - let mut = { - b: mut.b, - var: _notVar, - inline: isConst - ? b->inlineConst(schema) - : `${mut.var(b)}${Path.fromInlinedLocation(b->inlineLocation(item.location))}`, - flag: ValFlag.none, - tag: schema.tag, - } - loop(~mut, ~schema) - properties->Js.Dict.set(item.location, mut) - } - }) - mut.properties = Some(properties) - mut.additionalItems = Some(Schema(unknown->castToPublic)) - } - | None => () + let embedTransformation = (~input: val, ~fn: 'input => 'output, ~isAsync) => { + let outputVar = input.global->varWithoutAllocation + input.allocate(outputVar) + let output = + input->next(outputVar, ~schema=unknown, ~expected=input.expected.to->Option.getUnsafe) + output.var = _var + if isAsync { + if !(input.global.flag->Flag.unsafeHas(Flag.async)) { + input->throw( + InvalidOperation({ + path: Path.empty, + reason: "Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead", + }), + ) } + output.flag = output.flag->Flag.with(ValFlag.async) } - loop(~mut, ~schema) - mut + let embededFn = input->embed(fn) + let failure = `${output->failWithArg( + e => makeInvalidConversionDetails(~input, ~to=unknown, ~cause=e), + `x`, + )}` + output.codeFromPrev = `try{${outputVar}=${embededFn}(${input.inline})${isAsync + ? `.catch(x=>${failure})` + : ""}}catch(x){${failure}}` + output + } + + let fail = (b: val, ~message) => { + `${b->embed(() => { + b->throw(Custom({reason: message, path: b.path})) + })}()` + } + + let effectCtx = (input: val) => { + fail: (message, ~path=Path.empty) => { + let error = InternalError.make( + Custom({ + reason: message, + path: input.path->Path.concat(path), + }), + ) + // Read about this in shouldPrependPathKey comment. + error->Obj.magic->Js.Dict.set(shouldPrependPathKey, 1) + Stdlib.JsExn.throw(error) + }, } - let typeFilterCode = (b: b, ~schema, ~input, ~path) => { + let invalidOperation = (val: val, ~description) => { + val->throw(InvalidOperation({reason: description, path: val.path})) + } + + let mergeWithCatch = (val: val, ~catch, ~appendSafe=?) => { + let valCode = val->merge if ( - schema.noValidation->X.Option.getUnsafe || - schema.tag - ->TagFlag.get - ->Flag.unsafeHas( - TagFlag.unknown - ->Flag.with(TagFlag.union) - ->Flag.with(TagFlag.ref) - ->Flag.with(TagFlag.never), - ) + valCode === "" && + // FIXME: Instead of this wrap all S.transform in a try/catch + !(val.flag->Flag.unsafeHas(ValFlag.async)) ) { - "" + valCode ++ + switch appendSafe { + | Some(append) => append() + | None => "" + } } else { - let inputVar = b->Val.var(input) - `if(${b->validation(~inputVar, ~schema, ~negative=true)}${b->refinement( - ~schema, - ~inputVar, - ~negative=true, - )}){${b->failWithArg( - ~path, - input => InvalidType({ - expected: schema->castToPublic, - received: input, - }), - inputVar, - )}}` + let errorVar = val.global->varWithoutAllocation + + let catchCode = `${catch(~errorVar)};throw ${errorVar}` + + if val.flag->Flag.unsafeHas(ValFlag.async) { + val.inline = `${val.inline}.catch(${errorVar}=>{${catchCode}})` + } + `try{${valCode}${switch appendSafe { + | Some(append) => append() + | None => "" + }}}catch(${errorVar}){${catchCode}}` + } + } + + let mergeWithPathPrepend = (val: val, ~parent, ~locationVar=?, ~appendSafe=?) => { + if val.path === Path.empty && locationVar === None { + val->merge + } else { + val->mergeWithCatch(~appendSafe?, ~catch=(~errorVar) => { + `${errorVar}.path=${switch parent { + | {path: ""} => "" + | {path} => `${path->X.Inlined.Value.fromString}+` + }}${switch locationVar { + | Some(var) => `'["'+${var}+'"]'+` + | _ => "" + }}${errorVar}.path` + }) + } + } + + let withPathPrepend = ( + ~input, + ~dynamicLocationVar as maybeDynamicLocationVar=?, + ~appendSafe=?, + fn, + ) => { + if input.path === Path.empty && maybeDynamicLocationVar === None { + fn(~input) + } else { + fn(~input) + + // try b->mergeWithCatch( + // ~path=Path.empty, + // ~input, + // ~catch=(b, ~errorVar) => { + // b.codeAfterValidation = `${errorVar}.path=${b.path->X.Inlined.Value.fromString}+${switch maybeDynamicLocationVar { + // | Some(var) => `'["'+${var}+'"]'+` + // | _ => "" + // }}${errorVar}.path` + // None + // }, + // ~appendSafe?, + // b => { + // fn(b, ~input) + // }, + // ) catch { + // | _ => + // let error = %raw(`exn`)->InternalError.getOrRethrow + // X.Exn.throwAny( + // InternalError.make( + // ~path=b.path->Path.concat(Path.dynamic)->Path.concat(error.path), + // ~code=error.codeAfterValidation, + // ~flag=error.flag, + // ), + // ) + // } } } - let unsupportedTransform = (b, ~from, ~target, ~path) => { + let unsupportedDecode = (b, ~from: internal, ~target: internal) => { b->throw( - ~code=UnsupportedTransformation({ - from: from->(Obj.magic: val => schema), + UnsupportedDecode({ + from: from->castToPublic, to: target->castToPublic, + reason: `Can't decode ${from->castToPublic->toExpression} to ${target + ->castToPublic + ->toExpression}. Use S.to to define a custom decoder`, + path: b.path, }), - ~path, ) } } let noopOperation = i => i->Obj.magic - - @inline - let intitialInputVar = "i" + (noopOperation->Obj.magic)["embedded"] = X.Array.immutableEmpty } // TODO: Split validation code and transformation code module B = Builder.B -let setHas = (has, tag: tag) => { - has->Js.Dict.set( - tag === Union || tag === Ref ? (Unknown: tag :> string) : (tag: tag :> string), - true, - ) +let inputToString = (input: val) => { + input->B.next(`""+${input.inline}`, ~schema=string) } -@inline -let failTransform = (b, ~inputVar, ~path, ~target) => { - b->B.failWithArg( - ~path, - input => InvalidType({ - expected: target->castToPublic, - received: input, - }), - inputVar, - ) +let int32FormatValidation = (~inputVar) => { + `${inputVar}<=2147483647&&${inputVar}>=-2147483648&&${inputVar}%1===0` } -let jsonName = `JSON` - -let jsonString = shaken("jsonString") - -let inputToString = (b, input: val) => { - b->B.val(`""+${input.inline}`, ~schema=string) -} +let numberDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + let checks = [ + { + cond: (~inputVar) => `typeof ${inputVar}==="${(numberTag :> string)}"`, + fail: B.failInvalidType, + }, + ] + switch input.expected.format { + | Some(Int32) => + checks + ->Js.Array2.push({ + cond: (~inputVar) => int32FormatValidation(~inputVar), + fail: B.failInvalidType, + }) + ->ignore + | _ => + if !(input.global.flag->Flag.unsafeHas(Flag.disableNanNumberValidation)) { + checks + ->Js.Array2.push({ + cond: (~inputVar) => `!Number.isNaN(${inputVar})`, + fail: B.failInvalidType, + }) + ->ignore + } + } + input->B.refine(~schema=input.expected, ~checks) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + let outputVar = input.global->B.varWithoutAllocation + input.allocate(`${outputVar}=+${input.var()}`) -let rec parse = (prevB: b, ~schema, ~input as inputArg: val, ~path) => { - let b = B.scope(prevB) + let output = input->B.next(outputVar, ~schema=input.expected) + output.var = B._var - if schema.defs->Obj.magic { - b.global.defs = schema.defs + output.checks = Some([ + { + cond: (~inputVar as _) => + switch input.expected.format { + | Some(Int32) => int32FormatValidation(~inputVar=outputVar) + | _ => `!Number.isNaN(${outputVar})` + }, + fail: B.failInvalidType, + }, + ]) + output + } else if !(inputTagFlag->Flag.unsafeHas(TagFlag.number)) { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) + } else if input.schema.format !== input.expected.format && input.expected.format === Some(Int32) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => int32FormatValidation(~inputVar), + fail: B.failInvalidType, + }, + ], + ) + } else { + input } +}) - let input = ref(inputArg) - - // Js.log({ - // "input": input.contents, - // "schema": schema, - // }) - - let isFromLiteral = input.contents->Obj.magic->isLiteral - let isSchemaLiteral = schema->isLiteral - let isSameTag = input.contents.tag === schema.tag - let schemaTagFlag = TagFlag.get(schema.tag) - let inputTagFlag = TagFlag.get(input.contents.tag) - let isUnsupported = ref(false) - if ( - schemaTagFlag->Flag.unsafeHas(TagFlag.union->Flag.with(TagFlag.unknown)) || - schema.format === Some(JSON) - ) { - () - } else if schema.name === Some(jsonName) && !(inputTagFlag->Flag.unsafeHas(TagFlag.unknown)) { - if ( - inputTagFlag->Flag.unsafeHas( - TagFlag.string->Flag.with(TagFlag.number)->Flag.with(TagFlag.boolean), - ) - ) { - () - } else if inputTagFlag->Flag.unsafeHas(TagFlag.bigint) { - input := b->inputToString(input.contents) - } else { - isUnsupported := true - } - } else if isSchemaLiteral { - if isFromLiteral { - if input.contents.const !== schema.const { - input := b->B.constVal(~schema) - } - } else if ( - inputTagFlag->Flag.unsafeHas(TagFlag.string) && - schemaTagFlag->Flag.unsafeHas( - TagFlag.boolean->Flag.with( - TagFlag.number->Flag.with( - TagFlag.bigint->Flag.with( - TagFlag.undefined->Flag.with(TagFlag.null->Flag.with(TagFlag.nan)), - ), - ), - ), - ) - ) { - let inputVar = input.contents.var(b) - b.filterCode = schema.noValidation->X.Option.getUnsafe - ? "" - : `${input.contents.inline}==="${schema.const->Obj.magic}"||${b->failTransform( - ~inputVar, - ~path, - ~target=schema, - )};` - input := b->B.constVal(~schema) - } else if schema.noValidation->X.Option.getUnsafe { - input := b->B.constVal(~schema) - } else { - // FIXME: More cases +float.decoder = numberDecoder +int.decoder = numberDecoder - b.filterCode = prevB->B.typeFilterCode(~schema, ~input=input.contents, ~path) - input.contents.tag = schema.tag - input.contents.const = schema.const - } - } else if isFromLiteral && !isSchemaLiteral { - if isSameTag { - () - } else if ( - schemaTagFlag->Flag.unsafeHas(TagFlag.string) && - inputTagFlag->Flag.unsafeHas( - TagFlag.boolean->Flag.with( - TagFlag.number->Flag.with( - TagFlag.bigint->Flag.with( - TagFlag.undefined->Flag.with(TagFlag.null->Flag.with(TagFlag.nan)), - ), - ), +let stringDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => `typeof ${inputVar}==="${(stringTag :> string)}"`, + fail: B.failInvalidType, + }, + ], + ) + } else if ( + inputTagFlag->Flag.unsafeHas( + TagFlag.boolean->Flag.with( + TagFlag.number->Flag.with( + TagFlag.bigint->Flag.with( + TagFlag.undefined->Flag.with(TagFlag.null->Flag.with(TagFlag.nan)), ), - ) - ) { - let const = %raw(`""+input.const`) - input := { - b, - flag: ValFlag.none, - tag: String, - const: const->Obj.magic, - var: B._notVar, - inline: `"${const}"`, - } - } else { - isUnsupported := true - } - } else if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { - switch schema.ref { - | Some(ref) => - let defs = b.global.defs->X.Option.getUnsafe - // Ignore #/$defs/ - let identifier = ref->Js.String2.sliceToEnd(~from=8) - let def = defs->Js.Dict.unsafeGet(identifier) - let flag = if schema.noValidation->X.Option.getUnsafe { - b.global.flag->Flag.without(Flag.typeValidation) - } else { - b.global.flag - } - let recOperation = switch def->Obj.magic->X.Dict.getUnsafeOptionByInt(flag) { - | Some(fn) => - // A hacky way to prevent infinite recursion - if fn === %raw(`0`) { - b->B.embed(def) ++ `[${flag->X.Int.unsafeToString}]` - } else { - b->B.embed(fn) - } - | None => { - def - ->Obj.magic - ->X.Dict.setByInt(flag, 0) - let fn = internalCompile(~schema=def, ~flag, ~defs=b.global.defs) - def - ->Obj.magic - ->X.Dict.setByInt(flag, fn) - b->B.embed(fn) - } - } - input := - b->B.withPathPrepend(~input=input.contents, ~path, (_, ~input, ~path as _) => { - let output = B.Val.map(recOperation, input) - if def.isAsync === None { - let defsMut = defs->X.Dict.copy - defsMut->Js.Dict.set(identifier, unknown) - let _ = def->isAsyncInternal(~defs=Some(defsMut)) - } - if def.isAsync->X.Option.getUnsafe { - output.flag = output.flag->Flag.with(ValFlag.async) - } - output - }) - // Force rec function execution - // for the case when the value is not used - let _ = input.contents.var(b) - | None => { - if b.global.flag->Flag.unsafeHas(Flag.typeValidation) { - b.filterCode = prevB->B.typeFilterCode(~schema, ~input=input.contents, ~path) - } - // FIXME: Make it simpler - let refined = b->B.makeRefinedOf(~input=input.contents, ~schema) - input.contents.tag = refined.tag - input.contents.inline = refined.inline - input.contents.var = refined.var - input.contents.additionalItems = refined.additionalItems - input.contents.properties = refined.properties - if refined->Obj.magic->isLiteral { - input.contents.const = refined.const - } - } - } + ), + ), + ) && input.schema->isLiteral + ) { + let const = %raw(`""+input.s.const`) + let schema = base(stringTag, ~selfReverse=false) + schema.const = const->Obj.magic + input->B.next(`"${const}"`, ~schema) } else if ( - schemaTagFlag->Flag.unsafeHas(TagFlag.string) && - inputTagFlag->Flag.unsafeHas( - TagFlag.boolean->Flag.with(TagFlag.number->Flag.with(TagFlag.bigint)), - ) + inputTagFlag->Flag.unsafeHas( + TagFlag.boolean->Flag.with(TagFlag.number->Flag.with(TagFlag.bigint)), + ) ) { - input := b->inputToString(input.contents) - } else if !isSameTag { - if inputTagFlag->Flag.unsafeHas(TagFlag.string) { - let inputVar = input.contents.var(b) - if schemaTagFlag->Flag.unsafeHas(TagFlag.boolean) { - let output = b->B.allocateVal(~schema) // FIXME: schema should be only simple bool - b.code = - b.code ++ - `(${output.inline}=${inputVar}==="true")||${inputVar}==="false"||${b->failTransform( - ~inputVar, - ~path, - ~target=schema, - )};` - input := output - } else if schemaTagFlag->Flag.unsafeHas(TagFlag.number) { - let output = b->B.val(`+${inputVar}`, ~schema) // FIXME: schema - let outputVar = output.var(b) - b.code = - b.code ++ - switch schema.format { - | None => `Number.isNaN(${outputVar})` - | Some(_) => - `(${b - ->B.refinement(~inputVar=outputVar, ~schema, ~negative=true) - ->Js.String2.sliceToEnd(~from=2)})` - } ++ - `&&${b->failTransform(~inputVar, ~path, ~target=schema)};` - input := output - } else if schemaTagFlag->Flag.unsafeHas(TagFlag.bigint) { - let output = b->B.allocateVal(~schema) // FIXME: - b.code = - b.code ++ - `try{${output.inline}=BigInt(${inputVar})}catch(_){${b->failTransform( - ~inputVar, - ~path, - ~target=schema, - )}}` - input := output - } else { - isUnsupported := true - } - } else if inputTagFlag->Flag.unsafeHas(TagFlag.number) { - if schemaTagFlag->Flag.unsafeHas(TagFlag.bigint) { - input := b->B.val(`BigInt(${input.contents.inline})`, ~schema) // FIXME: - } else { - isUnsupported := true - } - } else { - isUnsupported := true - } + input->inputToString + } else if !(inputTagFlag->Flag.unsafeHas(TagFlag.string)) { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) + } else { + input } +}) + +string.decoder = stringDecoder - if isUnsupported.contents { - b->B.unsupportedTransform(~from=input.contents->Obj.magic, ~target=schema, ~path) +let booleanDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => `typeof ${inputVar}==="${(booleanTag :> string)}"`, + fail: B.failInvalidType, + }, + ], + ) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + let outputVar = input.global->B.varWithoutAllocation + input.allocate(outputVar) + + let output = input->B.next(outputVar, ~schema=input.expected) + output.var = B._var + + let inputVar = input.var() + output.codeFromPrev = `(${output.inline}=${inputVar}==="true")||${inputVar}==="false"||${B.embedInvalidInput( + ~input, + )};` + output + } else if !(inputTagFlag->Flag.unsafeHas(TagFlag.boolean)) { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) + } else { + input } +}) - switch schema.compiler { - | Some(compiler) => input := compiler(b, ~input=input.contents, ~selfSchema=schema, ~path) - | None => () +bool.decoder = booleanDecoder + +let bigintDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => `typeof ${inputVar}==="${(bigintTag :> string)}"`, + fail: B.failInvalidType, + }, + ], + ) + } // TODO: Skip formats which 100% don't match + else if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + let outputVar = input.global->B.varWithoutAllocation + input.allocate(outputVar) + let output = input->B.next(outputVar, ~schema=input.expected) + output.var = B._var + output.codeFromPrev = `try{${outputVar}=BigInt(${input.var()})}catch(_){${B.embedInvalidInput( + ~input, + )}}` + output + } else if inputTagFlag->Flag.unsafeHas(TagFlag.number) { + input->B.next(`BigInt(${input.inline})`, ~schema=input.expected) + } else if !(inputTagFlag->Flag.unsafeHas(TagFlag.bigint)) { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) + } else { + input } +}) - if input.contents.skipTo !== Some(true) { - switch schema.refiner { - | Some(refiner) => - b.code = b.code ++ refiner(b, ~inputVar=input.contents.var(b), ~selfSchema=schema, ~path) - | None => () - } +bigint.decoder = bigintDecoder + +let symbolDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => `typeof ${inputVar}==="${(symbolTag :> string)}"`, + fail: B.failInvalidType, + }, + ], + ) + } else if !(inputTagFlag->Flag.unsafeHas(TagFlag.symbol)) { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) + } else { + input } +}) - switch schema.to { - | Some(to) => - switch schema.parser { - | Some(parser) => input := parser(b, ~input=input.contents, ~selfSchema=schema, ~path) - | None => () - } +symbol.decoder = symbolDecoder - if input.contents.skipTo !== Some(true) { - input := b->parse(~schema=to, ~input=input.contents, ~path) - } +let setHas = (has, tag: tag) => { + has->Js.Dict.set( + tag->TagFlag.get->Flag.unsafeHas(TagFlag.union->Flag.with(TagFlag.ref)) + ? (unknownTag: tag :> string) + : (tag: tag :> string), + true, + ) +} - | None => () - } +let jsonName = `JSON` + +let jsonString = shaken("jsonString") - prevB.code = prevB.code ++ b->B.allocateScope - input.contents +let jsonStringWithSpace = (space: int) => { + let mut = jsonString->copySchema + mut.space = Some(space) + mut->castToPublic } -and isAsyncInternal = (schema, ~defs) => { - try { - let b = B.rootScope(~flag=Flag.async, ~defs) - let input = { - b, - var: B._var, - flag: ValFlag.none, - inline: Builder.intitialInputVar, - tag: Unknown, - } - let output = parse(b, ~schema, ~input, ~path=Path.empty) - let isAsync = output.flag->Flag.has(ValFlag.async) - schema.isAsync = Some(isAsync) - isAsync - } catch { + +let json = shaken("json") + +module Literal = { + let literalDecoder = Builder.make((~input) => { + let expectedSchema = input.expected + if expectedSchema.noValidation->X.Option.getUnsafe { + input->B.nextConst(~schema=expectedSchema) + } else if input.schema->isLiteral { + // FIXME: test NaN case + if input.schema.const === expectedSchema.const { + input + } else { + input->B.nextConst(~schema=expectedSchema) + } + } else { + let schemaTagFlag = expectedSchema.tag->TagFlag.get + + if ( + input.schema.tag->TagFlag.get->Flag.unsafeHas(TagFlag.string) && + schemaTagFlag->Flag.unsafeHas( + TagFlag.boolean->Flag.with( + TagFlag.number->Flag.with( + TagFlag.bigint->Flag.with( + TagFlag.undefined->Flag.with(TagFlag.null->Flag.with(TagFlag.nan)), + ), + ), + ), + ) + ) { + // This is to have a nicer error message + let stringConstSchema = base(stringTag, ~selfReverse=false) + stringConstSchema.const = %raw(`"" + expectedSchema.const`) + + let stringConstVal = + input->B.nextConst(~schema=stringConstSchema, ~expected=stringConstSchema) + + // FIXME: Test, that when from item has a refinement + // and we need to keep existing validation + // S.string->S.check->S.to(S.literal(false)) + stringConstVal.checks = Some([ + { + cond: (~inputVar) => `${inputVar}==="${stringConstSchema.const->Obj.magic}"`, + fail: B.failInvalidType, + }, + ]) + + stringConstVal->B.nextConst(~schema=expectedSchema, ~expected=expectedSchema) + } else if schemaTagFlag->Flag.unsafeHas(TagFlag.nan) { + input->B.refine( + ~schema=expectedSchema, + ~checks=[ + { + cond: (~inputVar) => `Number.isNaN(${inputVar})`, + fail: B.failInvalidType, + }, + ], + ) + } else { + // TODO: Determine impossible cases during compilation + input->B.refine( + ~schema=expectedSchema, + ~checks=[ + { + cond: (~inputVar) => `${inputVar}===${input->B.inlineConst(expectedSchema)}`, + fail: B.failInvalidType, + }, + ], + ) + } + } + }) + + nullLiteral.decoder = literalDecoder + unit.decoder = literalDecoder + nan.decoder = literalDecoder + + let parse = (value): internal => { + let value = value->castAnyToUnknown + if value === %raw(`null`) { + nullLiteral + } else { + switch value->Type.typeof { + | #undefined => unit + | #number if value->(Obj.magic: unknown => float)->Js.Float.isNaN => nan + | #object => { + let s = base(instanceTag, ~selfReverse=true) + s.class = (value->Obj.magic)["constructor"] + s.const = value->Obj.magic + s.decoder = literalDecoder + s + } + | typeof => { + let s = base(typeof->(Obj.magic: Type.t => tag), ~selfReverse=true) + s.const = value->Obj.magic + s.decoder = literalDecoder + s + } + } + } + } +} + +let rec parse = (input: val) => { + let valRef = ref(input) + let appliedEncoderRef = ref(None) + let loopCount = ref(0) + while ( + { + !(valRef.contents.isOutput->Option.getUnsafe) || valRef.contents.expected.to->Obj.magic + } + ) { + let appliedEncoder = appliedEncoderRef.contents + appliedEncoderRef := None + let loopInput = valRef.contents + + loopCount := loopCount.contents + 1 + + // Js.log(loopInput) + if loopCount.contents > 50 { + let error = %raw(`new Error("Loop count exceeded 100")`) + X.Exn.throwAny(error) + } + + if loopInput.expected.defs->Obj.magic { + if loopInput.global.defs->Obj.magic { + let _ = + loopInput.global.defs + ->Stdlib.Option.getUnsafe + ->Stdlib.Dict.assign(loopInput.expected.defs->Stdlib.Option.getUnsafe) + } else { + loopInput.global.defs = loopInput.expected.defs + } + } + + if ( + loopInput.flag->Flag.unsafeHas( + ValFlag.async, + ) /* FIXME: why was it needed? && step.contents !== #convert */ + ) { + let operationInputVar = loopInput.var() + + let operationInput = loopInput->B.Val.scope + let operationOutput = operationInput->parse + let operationCode = operationOutput->B.merge + if operationInput.inline !== operationOutput.inline || operationCode !== "" { + valRef := + loopInput->B.next( + `${operationInputVar}.then(${operationInputVar}=>{${operationCode}return ${operationOutput.inline}})`, + ~schema=operationOutput.schema, + ~expected=operationOutput.expected, + ) + } else { + valRef := + loopInput->B.refine(~schema=operationOutput.schema, ~expected=operationOutput.expected) + } + valRef.contents.flag = valRef.contents.flag->Flag.with(ValFlag.async) + valRef.contents.isOutput = Some(true) + } else if loopInput.isOutput->Option.getUnsafe { + // It's guaranteed that to is not None, because it's checked in the while condition + let to = loopInput.expected.to->Option.getUnsafe + switch loopInput.expected { + | {parser} => valRef := parser(~input=loopInput) + | _ => valRef := valRef.contents->B.refine(~expected=to) + } + } else { + let maybeEncoder = loopInput.schema.encoder + if ( + !(loopInput.isInput->Option.getUnsafe) && + maybeEncoder->Obj.magic && + maybeEncoder->Obj.magic !== appliedEncoder && + loopInput.schema !== loopInput.expected && + loopInput.expected.tag !== unknownTag + ) { + valRef := (maybeEncoder->Option.getUnsafe)(~input=loopInput, ~target=loopInput.expected) + } + + // If encoder didn't change the value, we can decode it, + // otherwise let's start the loop from the beginning + if loopInput !== valRef.contents { + appliedEncoderRef := Some(maybeEncoder->Option.getUnsafe) + } else { + valRef := loopInput.expected.decoder(~input=loopInput) + + // If the decoder's return value is not marked as isOutput, + // we treat it as a primitive decoder with no internal transformations. + // Otherwise, we assume internal transformations are present, + // and expect the decoder itself to handle refinements and manage isInput/isOutput flags. + if !(valRef.contents.isOutput->Option.getUnsafe) { + let hasInputRefiner = valRef.contents.expected.inputRefiner->Obj.magic + let hasRefiner = valRef.contents.expected.refiner->Obj.magic + if hasInputRefiner || hasRefiner { + let checks = [] + if hasInputRefiner { + let arr = (valRef.contents.expected.inputRefiner->X.Option.getUnsafe)( + ~input=valRef.contents, + ) + for i in 0 to arr->Js.Array2.length - 1 { + checks->Js.Array2.push(arr->Js.Array2.unsafe_get(i))->ignore + } + } + if hasRefiner { + let arr = (valRef.contents.expected.refiner->X.Option.getUnsafe)( + ~input=valRef.contents, + ) + for i in 0 to arr->Js.Array2.length - 1 { + checks->Js.Array2.push(arr->Js.Array2.unsafe_get(i))->ignore + } + } + if checks->Js.Array2.length > 0 { + valRef.contents = valRef.contents->B.refine(~checks) + } + } + valRef.contents.isInput = Some(true) + valRef.contents.isOutput = Some(true) + } + } + } + } + + valRef.contents +} +and parseDynamic = input => { + try input->parse catch { + | _ => + let error = %raw(`exn`)->InternalError.getOrRethrow + (error->Obj.magic)["path"] = { + // For the case parent must always be present + switch input.parent { + | Some(p) => p.path + | None => Path.empty + }->Path.concat(input.path->Path.concat(Path.dynamic)->Path.concat(error.path)) + } + + X.Exn.throwAny(error) + } +} + +and isAsyncInternal = (schema, ~defs) => { + try { + let input = B.operationArg(~flag=Flag.async, ~defs, ~schema=unknown, ~expected=schema) + let output = input->parse + let isAsync = output.flag->Flag.has(ValFlag.async) + schema.isAsync = Some(isAsync) + isAsync + } catch { | _ => { let _ = %raw(`exn`)->InternalError.getOrRethrow false } } } -and internalCompile = (~schema, ~flag, ~defs) => { - let b = B.rootScope(~flag, ~defs) +and compileDecoder = (~schema, ~expected, ~flag, ~defs) => { + let input = B.operationArg( + ~flag, + ~defs, + ~schema=if schema->isLiteral { + unknown + } else { + schema + }, + ~expected, + ) + + let output = input->parse + let code = output->B.merge + + let isAsync = output.flag->Flag.has(ValFlag.async) + expected.isAsync = Some(isAsync) + let hasTransform = output.hasTransform === Some(true) + expected.hasTransform = Some(hasTransform) + + if ( + code === "" && + (output === input || output.inline === input.inline) && + !(flag->Flag.unsafeHas(Flag.async)) + ) { + Builder.noopOperation + } else { + let inlinedOutput = ref(output.inline) + if flag->Flag.unsafeHas(Flag.async) && !isAsync && !(defs->Obj.magic) { + inlinedOutput := `Promise.resolve(${inlinedOutput.contents})` + } + + let inlinedFunction = `${B.operationArgVar}=>{${code}return ${inlinedOutput.contents}}` + + // Js.log(inlinedFunction) + + let fn = X.Function.make2( + ~ctxVarName1="e", + ~ctxVarValue1=input.global.embeded, + ~ctxVarName2="s", + ~ctxVarValue2=s, + ~inlinedFunction, + ) + (fn->Obj.magic)["embedded"] = input.global.embeded + fn + } +} +and getOutputSchema = (schema: internal) => { + switch schema.to { + | Some(to) => getOutputSchema(to) + | None => schema + } +} +// FIXME: Define it as a schema property +and reverse = (schema: internal) => { + if schema->Obj.magic->Stdlib.Dict.has(reversedKey)->Obj.magic { + schema->Obj.magic->Stdlib.Dict.getUnsafe(reversedKey)->Obj.magic + } else { + let reversedHead = ref(None) + let current = ref(Some(schema)) + + while current.contents->Obj.magic { + let mut = current.contents->X.Option.getUnsafe->copySchema + let next = mut.to + switch reversedHead.contents { + | None => %raw(`delete mut.to`) + | Some(to) => mut.to = Some(to) + } + let parser = mut.parser + switch mut.serializer { + | Some(serializer) => mut.parser = Some(serializer) + | None => %raw(`delete mut.parser`) + } + switch parser { + | Some(parser) => mut.serializer = Some(parser) + | None => %raw(`delete mut.serializer`) + } + // Swap inputRefiner and refiner + let refiner = mut.refiner + switch mut.inputRefiner { + | Some(inputRefiner) => mut.refiner = Some(inputRefiner) + | None => %raw(`delete mut.refiner`) + } + switch refiner { + | Some(refiner) => mut.inputRefiner = Some(refiner) + | None => %raw(`delete mut.inputRefiner`) + } + let fromDefault = mut.fromDefault + switch mut.default { + | Some(default) => mut.fromDefault = Some(default) + | None => %raw(`delete mut.fromDefault`) + } + switch fromDefault { + | Some(fromDefault) => mut.default = Some(fromDefault) + | None => %raw(`delete mut.default`) + } + switch mut.items { + | Some(items) => + let newItems = Belt.Array.makeUninitializedUnsafe(items->Js.Array2.length) + for idx in 0 to items->Js.Array2.length - 1 { + newItems->Js.Array2.unsafe_set(idx, items->Js.Array2.unsafe_get(idx)->reverse) + } + mut.items = Some(newItems) + + | None => () + } + switch mut.properties { + | Some(properties) => { + let newProperties = Js.Dict.empty() + let keys = properties->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + newProperties->Js.Dict.set(key, properties->Js.Dict.unsafeGet(key)->reverse) + } + mut.properties = Some(newProperties) + } + // Skip tuple + | None => () + } + if mut.additionalItems->Type.typeof === #object { + mut.additionalItems = Some( + Schema( + mut.additionalItems + ->(Obj.magic: option => internal) + ->reverse + ->castToPublic, + ), + ) + } + switch mut.anyOf { + | Some(anyOf) => + let has = Js.Dict.empty() + let newAnyOf = [] + for idx in 0 to anyOf->Js.Array2.length - 1 { + let s = anyOf->Js.Array2.unsafe_get(idx) + let reversed = s->reverse + newAnyOf->Js.Array2.push(reversed)->ignore + has->setHas(reversed.tag) + } + mut.has = Some(has) + mut.anyOf = Some(newAnyOf) + | None => () + } + switch mut.defs { + | Some(defs) => { + let reversedDefs = Js.Dict.empty() + for idx in 0 to defs->Js.Dict.keys->Js.Array2.length - 1 { + let key = defs->Js.Dict.keys->Js.Array2.unsafe_get(idx) + reversedDefs->Js.Dict.set(key, defs->Js.Dict.unsafeGet(key)->reverse) + } + mut.defs = Some(reversedDefs) + } + | None => () + } + reversedHead := Some(mut) + current := next + } + + // Use defineProperty even though it's slower + // but it improves logging experience a lot + // for some reason Wallaby still shows the property + let r = reversedHead.contents->X.Option.getUnsafe + valueOptions->Js.Dict.set(valKey, r->Obj.magic) + let _ = X.Object.defineProperty(schema, reversedKey, valueOptions->Obj.magic) + valueOptions->Js.Dict.set(valKey, schema->Obj.magic) + let _ = X.Object.defineProperty(r, reversedKey, valueOptions->Obj.magic) + r + } +} + +let getDecoder = (~s1 as _, ~flag as _=?) => { + let args = %raw(`arguments`) + let idx = ref(0) + let flag = ref(None) + let keyRef = ref("") + let maxSeq = ref(0.) + let cacheTarget = ref(None) + + while flag.contents === None { + let arg = args->Js.Array2.unsafe_get(idx.contents) + if !(arg->Obj.magic) { + let f = globalConfig.defaultFlag + flag := Some(f) + keyRef := keyRef.contents ++ "-" ++ f->X.Int.unsafeToString + } else if Js.typeof(arg->Obj.magic) === "number" { + let f = arg->Obj.magic->Flag.with(globalConfig.defaultFlag) + flag := Some(f) + keyRef := keyRef.contents ++ "-" ++ f->X.Int.unsafeToString + } else { + let schema: internal = arg->Obj.magic + let seq: float = schema.seq->Obj.magic + if seq > maxSeq.contents { + maxSeq := seq + cacheTarget := Some(schema) + } + keyRef := keyRef.contents ++ seq->Obj.magic ++ "-" + idx := idx.contents + 1 + } + } + + switch cacheTarget.contents { + | None => InternalError.panic("No schema provided for decoder.") + | Some(cacheTarget) => { + let key = keyRef.contents + if cacheTarget->Obj.magic->Stdlib.Dict.has(key) { + cacheTarget->Obj.magic->Stdlib.Dict.getUnsafe(key)->Obj.magic + } else { + let schema = ref(args->Js.Array2.unsafe_get(idx.contents - 1)) + for i in idx.contents - 2 downto 0 { + let to = schema.contents + schema := + args + ->Js.Array2.unsafe_get(i) + ->updateOutput(mut => { + mut.to = Some(to) + }) + ->castToInternal + } + let f = compileDecoder( + ~schema=schema.contents, + ~expected=schema.contents, + ~flag=flag.contents->X.Option.getUnsafe, + ~defs=%raw(`0`), + ) + // Reusing the same object makes it a little bit faster + valueOptions->Js.Dict.set(valKey, f) + // Use defineProperty, so the cache keys are not enumerable + let _ = X.Object.defineProperty(cacheTarget, key, valueOptions->Obj.magic) + f->(Obj.magic: (unknown => unknown) => 'from => 'to) + } + } + } +} + +@val +external getDecoder2: (~s1: internal, ~s2: internal, ~flag: flag=?) => 'a => 'b = "getDecoder" + +@val +external getDecoder3: (~s1: internal, ~s2: internal, ~s3: internal, ~flag: flag=?) => 'a => 'b = + "getDecoder" + +let rec makeObjectVal = (prev: val, ~schema): B.Val.Object.t => { + { + prev, + var: B._notVar, + inline: "", + flag: ValFlag.none, + schema: schema.tag === arrayTag + ? { + tag: arrayTag, + items: [], + additionalItems: Strict, + decoder: arrayDecoder, + } + : { + { + tag: objectTag, + required: [], + properties: Js.Dict.empty(), + additionalItems: Strict, + decoder: objectDecoder, + } + }, + expected: prev.expected, + vals: Js.Dict.empty(), + hasTransform: true, + codeFromPrev: "", + varsAllocation: "", + allocate: B.initialAllocate, + path: prev.path, + global: prev.global, + } +} +and completeObjectVal = (objectVal: B.Val.Object.t) => { + let isArray = objectVal.schema.tag === arrayTag + let inline = ref("") + let promiseAllContent = ref("") + let optionalSettingCode = ref(None) + + let keys = objectVal.vals->X.Option.getUnsafe->Js.Dict.keys + + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + let val = objectVal.vals->X.Option.getUnsafe->Js.Dict.unsafeGet(key) + if val.flag->Flag.unsafeHas(ValFlag.async) { + promiseAllContent := promiseAllContent.contents ++ val.inline ++ "," + } + if val.optional->X.Option.getUnsafe { + let existingFn = optionalSettingCode.contents + optionalSettingCode := + Some( + (~objectVar) => { + switch existingFn { + | None => "" + | Some(fn) => fn(~objectVar) + } ++ + `if(${val.var()}!==void 0){${objectVar}[${objectVal.global->B.inlineLocation( + key, + )}]=${val.inline}}` + }, + ) + } else { + inline := + inline.contents ++ + (isArray + ? `${val.inline}` + : `${objectVal.global->B.inlineLocation(key)}:${val.inline}`) ++ "," + } + } + + objectVal.inline = isArray ? "[" ++ inline.contents ++ "]" : "{" ++ inline.contents ++ "}" + + // FIXME: Test whether it's needed + // objectVal.additionalItems = Some(Strict) + let valWithRequired = (objectVal :> val) + + if promiseAllContent.contents->X.String.unsafeToBool { + // FIXME: Test how it works with optional and fix it + let operationInput = valWithRequired->B.Val.scope + operationInput.isOutput = Some(true) + let operationOutput = operationInput->parse + let operationCode = operationOutput->B.merge + + if operationCode === "" && promiseAllContent.contents === `${operationOutput.inline},` { + valWithRequired.inline = operationOutput.inline + } else { + valWithRequired.inline = `Promise.all([${promiseAllContent.contents}]).then(([${promiseAllContent.contents}])=>{${operationCode}return ${operationOutput.inline}})` + } + valWithRequired.flag = valWithRequired.flag->Flag.with(ValFlag.async) + valWithRequired.schema = operationOutput.schema + valWithRequired.expected = operationOutput.expected + valWithRequired.isOutput = Some(true) + valWithRequired + } else { + switch optionalSettingCode.contents { + | None => valWithRequired + | Some(fn) => { + let code = fn(~objectVar=valWithRequired.var()) + let output = valWithRequired->B.refine + output.codeFromPrev = output.codeFromPrev ++ code + output + } + } + } +} +and array = item => { + let itemInternal = item->castToInternal + let mut = base( + arrayTag, + ~selfReverse=itemInternal->Obj.magic->Stdlib.Dict.getUnsafe(reversedKey) === + itemInternal->Obj.magic, + ) + mut.additionalItems = Some(Schema(itemInternal->castToPublic)) + mut.items = Some(X.Array.immutableEmpty) + mut.decoder = arrayDecoder + mut->castToPublic +} +and arrayDecoder: builder = (~input as unknownInput) => { + let isUnion = unknownInput.isUnion->X.Option.getUnsafe + let expectedSchema = unknownInput.expected + let unknownInputTagFlag = unknownInput.schema.tag->TagFlag.get + let expectedItems = expectedSchema.items->X.Option.getUnsafe + let expectedLength = expectedItems->Js.Array2.length + + let input = if unknownInputTagFlag->Flag.unsafeHas(TagFlag.unknown->Flag.with(TagFlag.array)) { + let isArrayInput = unknownInputTagFlag->Flag.unsafeHas(TagFlag.array) + let schema = if !isArrayInput { + array(unknown->castToPublic)->castToInternal + } else { + unknownInput.schema + } + let checks: array = [] + if !isArrayInput { + checks + ->Js.Array2.push({ + cond: (~inputVar) => `Array.isArray(${inputVar})`, + fail: B.failInvalidType, + }) + ->ignore + } + + let isExactSize = switch schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => false + | _ => schema.items->X.Option.getUnsafe->Js.Array2.length === expectedLength + } + + if !isExactSize { + switch expectedSchema.additionalItems->X.Option.getUnsafe { + | Strict => + checks + ->Js.Array2.push({ + cond: (~inputVar) => + `${inputVar}.length===${expectedLength->X.Int.unsafeToString}`, + fail: B.failInvalidType, + }) + ->ignore + | Strip => + checks + ->Js.Array2.push({ + cond: (~inputVar) => + `${inputVar}.length>=${expectedLength->X.Int.unsafeToString}`, + fail: B.failInvalidType, + }) + ->ignore + + | _ => () + } + } + // Apply refine also when there are no checks, + // so literals for union cases don't mutate input + // FIXME: This should be removed and validation be attached to output + if checks->Js.Array2.length > 0 { + unknownInput->B.refine(~schema, ~checks) + } else { + unknownInput->B.refine(~schema) + } + } else { + unknownInput->B.unsupportedDecode(~from=unknownInput.schema, ~target=expectedSchema) + } + + switch expectedSchema.additionalItems->X.Option.getUnsafe { + | Schema(itemSchema) => { + let itemSchema = itemSchema->castToInternal + if itemSchema === unknown { + input + } else { + let inputVar = input->B.Val.var + let iteratorVar = input.global->B.varWithoutAllocation + + let itemInput = input->B.dynamicScope(~locationVar=iteratorVar) + let itemOutput = itemInput->parseDynamic + let hasTransform = itemOutput.hasTransform->X.Option.getUnsafe + let output = hasTransform + ? input->B.next(`new Array(${inputVar}.length)`, ~schema=expectedSchema) // FIXME: schema here should be input.expected output + : input->B.refine(~schema=expectedSchema) + + let itemCode = + itemOutput->B.mergeWithPathPrepend( + ~parent=input, + ~locationVar=iteratorVar, + ~appendSafe=?hasTransform + ? Some(() => output->B.Val.addKey(iteratorVar, itemOutput)) + : None, + ) + + if hasTransform || itemCode !== "" { + output.codeFromPrev = + output.codeFromPrev ++ + `for(let ${iteratorVar}=${expectedLength->X.Int.unsafeToString};${iteratorVar}<${inputVar}.length;++${iteratorVar}){${itemCode}}` + } + + if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { + output->B.asyncVal(`Promise.all(${output.inline})`) + } else { + output + } + } + } + | _ => + let objectVal = input->makeObjectVal(~schema=expectedSchema) + let shouldRecreateInput = ref( + switch expectedSchema.additionalItems->X.Option.getUnsafe { + // Since we have a check validating the exact properties existence + | Strict => false + | Strip => + switch input.schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => true + | _ => input.schema.items->X.Option.getUnsafe->Js.Array2.length !== expectedLength + } + | _ => true + }, + ) + + for idx in 0 to expectedLength - 1 { + let schema = expectedItems->Js.Array2.unsafe_get(idx) + let key = idx->Js.Int.toString + let itemInput = input->B.Val.get(key) + itemInput.expected = schema + itemInput.isInput = Some(false) + itemInput.isOutput = Some(false) + itemInput.isUnion = Some(isUnion) // We want to controll validation on the decoder side + let itemOutput = itemInput->parse + + if isUnion && schema->isLiteral { + input->B.hoistChildChecks(~child=itemOutput, ~key) + } + + objectVal->B.Val.Object.add(~location=key, itemOutput) + if !shouldRecreateInput.contents { + shouldRecreateInput := itemOutput.hasTransform->X.Option.getUnsafe + } + } + + // After input.schema was used, set it to selfSchema + // so it has a more accurate name in error messages + if shouldRecreateInput.contents { + objectVal->completeObjectVal + } else { + let o = input->B.refine + o.codeFromPrev = objectVal.codeFromPrev + o.vals = objectVal.vals + o + } + } +} +and objectDecoder: Builder.t = (~input as unknownInput) => { + let isUnion = unknownInput.isUnion->X.Option.getUnsafe + let expectedSchema = unknownInput.expected + + let unknownInputTagFlag = unknownInput.schema.tag->TagFlag.get + + let input = if unknownInputTagFlag->Flag.unsafeHas(TagFlag.unknown->Flag.with(TagFlag.object)) { + let isObjectInput = unknownInputTagFlag->Flag.unsafeHas(TagFlag.object) + let schema = if !isObjectInput { + // TODO: Use dictFactory here + let mut = base(objectTag, ~selfReverse=false) + mut.properties = Some(X.Object.immutableEmpty) + mut.additionalItems = Some(Schema(unknown->castToPublic)) + mut + } else { + unknownInput.schema + } + let checks: array = [] + if !isObjectInput { + checks + ->Js.Array2.push({ + cond: (~inputVar) => + `typeof ${inputVar}==="${(objectTag :> string)}"&&${inputVar}`, + fail: B.failInvalidType, + }) + ->ignore + if expectedSchema.additionalItems->X.Option.getUnsafe !== Strip { + // For strip case we recreate the value + // For other cases we might optimize it, + // this is why the check is a must have + checks + ->Js.Array2.push({ + cond: (~inputVar) => `!Array.isArray(${inputVar})`, + fail: B.failInvalidType, + }) + ->ignore + } + } + + // Apply refine also when there are no checks, + // so literals for union cases don't mutate input + if checks->Js.Array2.length > 0 { + unknownInput->B.refine(~schema, ~checks) + } else { + unknownInput->B.refine(~schema) + } + } else { + unknownInput->B.unsupportedDecode(~from=unknownInput.schema, ~target=expectedSchema) + } + + switch expectedSchema.additionalItems->X.Option.getUnsafe { + | Schema(itemSchema) => { + let itemSchema = itemSchema->castToInternal + if itemSchema === unknown { + input + } else { + let inputVar = input.var() + let keyVar = input.global->B.varWithoutAllocation + let itemInput = input->B.dynamicScope(~locationVar=keyVar) + let itemOutput = itemInput->parseDynamic + + let hasTransform = itemOutput.hasTransform->X.Option.getUnsafe + let output = hasTransform + // FIXME: schema should be expectedSchema output + ? input->B.next("{}", ~schema=expectedSchema) + : input->B.refine(~schema=expectedSchema) + + let itemCode = + itemOutput->B.mergeWithPathPrepend( + ~parent=input, + ~locationVar=keyVar, + ~appendSafe=?hasTransform ? Some(() => output->B.Val.addKey(keyVar, itemOutput)) : None, + ) + + if hasTransform || itemCode !== "" { + output.codeFromPrev = + output.codeFromPrev ++ `for(let ${keyVar} in ${inputVar}){${itemCode}}` + } + + if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { + let resolveVar = output.global->B.varWithoutAllocation + let rejectVar = output.global->B.varWithoutAllocation + let asyncParseResultVar = output.global->B.varWithoutAllocation + let counterVar = output.global->B.varWithoutAllocation + let outputVar = B.Val.var(output) + output->B.asyncVal( + `new Promise((${resolveVar},${rejectVar})=>{let ${counterVar}=Object.keys(${outputVar}).length;for(let ${keyVar} in ${outputVar}){${outputVar}[${keyVar}].then(${asyncParseResultVar}=>{${outputVar}[${keyVar}]=${asyncParseResultVar};if(${counterVar}--===1){${resolveVar}(${outputVar})}},${rejectVar})}})`, + ) + } else { + output + } + } + } + | _ => { + let properties = expectedSchema.properties->X.Option.getUnsafe + let keys = Js.Dict.keys(properties) + let keysCount = keys->Js.Array2.length + + let objectVal = input->makeObjectVal(~schema=expectedSchema) + let shouldRecreateInput = ref( + switch expectedSchema.additionalItems->X.Option.getUnsafe { + // Since we have a check validating the exact properties existence + | Strict => false + | Strip => + switch input.schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => true + | _ => + input.schema.properties->X.Option.getUnsafe->Js.Dict.keys->Js.Array2.length !== + keysCount + } + | _ => true + }, + ) + + for idx in 0 to keysCount - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + let schema = properties->Js.Dict.unsafeGet(key) + + let itemInput = input->B.Val.get(key) + itemInput.expected = schema + itemInput.isInput = Some(false) + itemInput.isOutput = Some(false) + itemInput.isUnion = Some(isUnion) // We want to controll validation on the decoder side + let itemOutput = itemInput->parse + + if isUnion && schema->isLiteral { + input->B.hoistChildChecks(~child=itemOutput, ~key) + } + + objectVal->B.Val.Object.add(~location=key, itemOutput) + if !shouldRecreateInput.contents { + shouldRecreateInput := itemOutput.hasTransform->X.Option.getUnsafe + } + } + + if ( + expectedSchema.additionalItems === Some(Strict) && + switch input.schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => true + | _ => false + } + ) { + let keyVar = objectVal.global->B.varWithoutAllocation + input.allocate(keyVar) + objectVal.codeFromPrev = objectVal.codeFromPrev ++ `for(${keyVar} in ${input.var()}){if(` + switch keys { + | [] => objectVal.codeFromPrev = objectVal.codeFromPrev ++ "true" + | _ => + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + if idx !== 0 { + objectVal.codeFromPrev = objectVal.codeFromPrev ++ "&&" + } + objectVal.codeFromPrev = + objectVal.codeFromPrev ++ `${keyVar}!==${input.global->B.inlineLocation(key)}` + } + } + objectVal.codeFromPrev = + objectVal.codeFromPrev ++ `){${input->B.failWithArg(exccessFieldName => UnrecognizedKeys({ + path: objectVal.path, + reason: `Unrecognized key "${exccessFieldName}"`, + keys: [exccessFieldName], + }), keyVar)}}}` + } + + // After input.schema was used, set it to selfSchema + // so it has a more accurate name in error messages + if shouldRecreateInput.contents { + objectVal->completeObjectVal + } else { + let o = input->B.refine + o.codeFromPrev = objectVal.codeFromPrev + o.vals = objectVal.vals + o + } + } + } +} - if flag->Flag.unsafeHas(Flag.jsonableOutput) { - let output = schema->reverse - jsonableValidation(~output, ~parent=output, ~path=Path.empty, ~flag) - } +let recursiveDecoder = Builder.make((~input) => { + let expectedSchema = input.expected - let input = { - b, - var: B._var, - inline: Builder.intitialInputVar, - flag: ValFlag.none, - tag: Unknown, - } + let schemaRef = expectedSchema.ref->X.Option.getUnsafe + let defs = input.global.defs->X.Option.getUnsafe + // Ignore #/$defs/ + let identifier = schemaRef->Js.String2.sliceToEnd(~from=8) + let def = defs->Js.Dict.unsafeGet(identifier) + let flag = input.global.flag - let schema = if flag->Flag.unsafeHas(Flag.assertOutput) { - schema - ->updateOutput(mut => { - let t = base(unit.tag) - t.const = unit.const - t.noValidation = Some(true) - mut.to = Some(t) - }) - ->castToInternal - } else if flag->Flag.unsafeHas(Flag.jsonStringOutput) { - schema - ->updateOutput(mut => { - mut.to = Some(jsonString) - }) - ->castToInternal + let inputSchema = if input.schema.seq === expectedSchema.seq { + def } else { - schema + input.schema } - let output = parse(b, ~schema, ~input, ~path=Path.empty) + let key = `${inputSchema.seq->Obj.magic}-${def.seq->Obj.magic}--${flag->Obj.magic}` + let recOperation = ref("") - let code = b->B.allocateScope + switch def->Obj.magic->X.Dict.getUnsafeOption(key) { + | Some(fn) => + // Circular reference (fn === 0) or already compiled + recOperation := if fn === %raw(`0`) { + input->B.embed(def) ++ `["${key}"]` + } else { + input->B.embed(fn) + } + | None => { + // Optimistic compilation with recompile if assumptions were wrong + let assumedHasTransform = ref(def.hasTransform->Option.getOr(false)) + let assumedIsAsync = ref(def.isAsync->Option.getOr(false)) + let compileNeeded = ref(true) + let finalFn = ref(Obj.magic(0)) + + while compileNeeded.contents { + compileNeeded := false + + // Set optimistic values on def before compiling (if not already set) + // Inner circular references will read these values + if def.hasTransform === None { + def.hasTransform = Some(assumedHasTransform.contents) + } + if def.isAsync === None { + def.isAsync = Some(assumedIsAsync.contents) + } - let isAsync = output.flag->Flag.has(ValFlag.async) - schema.isAsync = Some(isAsync) + // Mark as in-progress + configurableValueOptions->Js.Dict.set(valKey, 0->Obj.magic) + let _ = X.Object.defineProperty(def, key, configurableValueOptions->Obj.magic) + + // Compile + let fn = compileDecoder(~schema=inputSchema, ~expected=def, ~flag, ~defs=Some(defs)) + + // Cache result + valueOptions->Js.Dict.set(valKey, fn) + let _ = X.Object.defineProperty(def, key, valueOptions->Obj.magic) + + finalFn := fn + + // Check if actual values differ from assumed + let actualHasTransform = def.hasTransform->X.Option.getUnsafe + let actualIsAsync = def.isAsync->X.Option.getUnsafe + + if ( + actualHasTransform !== assumedHasTransform.contents || + actualIsAsync !== assumedIsAsync.contents + ) { + // Wrong assumption - update and recompile + assumedHasTransform := actualHasTransform + assumedIsAsync := actualIsAsync + // Delete cached function to force recompilation + let _ = %raw(`delete def[key]`) + compileNeeded := true + } + } - if code === "" && output === input && !(flag->Flag.unsafeHas(Flag.async)) { - Builder.noopOperation - } else { - let inlinedOutput = ref(output.inline) - if flag->Flag.unsafeHas(Flag.async) && !isAsync && !(defs->Obj.magic) { - inlinedOutput := `Promise.resolve(${inlinedOutput.contents})` + // Embed only the final compiled function to avoid wasting embed slots on recompiles + recOperation := input->B.embed(finalFn.contents) } + } - let inlinedFunction = `${Builder.intitialInputVar}=>{${code}return ${inlinedOutput.contents}}` + let hasTransform = def.hasTransform === Some(true) + let isAsync = def.isAsync->X.Option.getUnsafe - // Js.log(inlinedFunction) + let output = if hasTransform || isAsync { + let outputVar = input.global->B.varWithoutAllocation + input.allocate(outputVar) - X.Function.make2( - ~ctxVarName1="e", - ~ctxVarValue1=b.global.embeded, - ~ctxVarName2="s", - ~ctxVarValue2=s, - ~inlinedFunction, - ) - } -} -and operationFn = (s, o) => { - let s = s->castToInternal - if %raw(`o in s`) { - %raw(`s[o]`) - } else { - let f = internalCompile( - ~schema=o->Flag.unsafeHas(Flag.reverse) ? s->reverse : s, - ~flag=o, - ~defs=%raw(`0`), - ) - let _ = %raw(`s[o] = f`) - f - } -} -and getOutputSchema = (schema: internal) => { - switch schema.to { - | Some(to) => getOutputSchema(to) - | None => schema - } -} -// FIXME: Cache reverse results -and reverse = (schema: internal) => { - let reversedHead = ref(None) - let current = ref(Some(schema)) - - while current.contents->Obj.magic { - let mut = current.contents->X.Option.getUnsafe->copyWithoutCache - let next = mut.to - switch reversedHead.contents { - | None => %raw(`delete mut.to`) - | Some(to) => mut.to = Some(to) - } - let parser = mut.parser - switch mut.serializer { - | Some(serializer) => mut.parser = Some(serializer) - | None => %raw(`delete mut.parser`) - } - switch parser { - | Some(parser) => mut.serializer = Some(parser) - | None => %raw(`delete mut.serializer`) - } - let fromDefault = mut.fromDefault - switch mut.default { - | Some(default) => mut.fromDefault = Some(default) - | None => %raw(`delete mut.fromDefault`) - } - switch fromDefault { - | Some(fromDefault) => mut.default = Some(fromDefault) - | None => %raw(`delete mut.default`) - } - switch mut.items { - | Some(items) => - let properties = Js.Dict.empty() - let newItems = Belt.Array.makeUninitializedUnsafe(items->Js.Array2.length) - for idx in 0 to items->Js.Array2.length - 1 { - let item = items->Js.Array2.unsafe_get(idx) - let reversed = { - ...item, - schema: item.schema->castToInternal->reverse->castToPublic, - } + let output = input->B.next(outputVar, ~schema=expectedSchema, ~expected=expectedSchema) + output.var = B._var - // Keep ritem if it's present. Super unsafe and might break - // TODO: Test double reverse - if (item->Obj.magic)["r"] { - (reversed->Obj.magic)["r"] = (item->Obj.magic)["r"] - } - properties->Js.Dict.set(item.location, reversed.schema->castToInternal) - newItems->Js.Array2.unsafe_set(idx, reversed) - } - mut.items = Some(newItems) - switch mut.properties { - | Some(_) => mut.properties = Some(properties) - // Skip tuple - | None => () - } - | None => () - } - if mut.additionalItems->Type.typeof === #object { - mut.additionalItems = Some( - Schema( - mut.additionalItems - ->(Obj.magic: option => internal) - ->reverse - ->castToPublic, - ), - ) - } - switch mut.anyOf { - | Some(anyOf) => - let has = Js.Dict.empty() - let newAnyOf = [] - for idx in 0 to anyOf->Js.Array2.length - 1 { - let s = anyOf->Js.Array2.unsafe_get(idx) - let reversed = s->reverse - newAnyOf->Js.Array2.push(reversed)->ignore - has->setHas(reversed.tag) - } - mut.has = Some(has) - mut.anyOf = Some(newAnyOf) - | None => () - } - switch mut.defs { - | Some(defs) => { - let reversedDefs = Js.Dict.empty() - for idx in 0 to defs->Js.Dict.keys->Js.Array2.length - 1 { - let key = defs->Js.Dict.keys->Js.Array2.unsafe_get(idx) - reversedDefs->Js.Dict.set(key, defs->Js.Dict.unsafeGet(key)->reverse) - } - mut.defs = Some(reversedDefs) - } - | None => () + output.codeFromPrev = `${outputVar}=${recOperation.contents}(${input.inline});` + + if isAsync { + output.flag = output.flag->Flag.with(ValFlag.async) } - reversedHead := Some(mut) - current := next + output + } else { + // No transform: call for validation but don't capture result + let output = input->B.refine(~schema=expectedSchema, ~expected=expectedSchema) + output.codeFromPrev = `${recOperation.contents}(${input.inline});` + output } - reversedHead.contents->X.Option.getUnsafe -} -and jsonableValidation = (~output, ~parent, ~path, ~flag) => { - let tagFlag = output.tag->TagFlag.get + output.prev = None + output.codeFromPrev = output->B.mergeWithPathPrepend(~parent=input) + // Restore allocate after merge deleted it, since this val may be reused as + // input to a subsequent parser (e.g. S.transform on a recursive schema). + output.allocate = B.initialAllocate + output.prev = Some(input) - if ( - tagFlag->Flag.unsafeHas( - TagFlag.unknown - ->Flag.with(TagFlag.nan) - ->Flag.with(TagFlag.bigint) - ->Flag.with(TagFlag.function) - ->Flag.with(TagFlag.instance) - ->Flag.with(TagFlag.symbol), - ) || (tagFlag->Flag.unsafeHas(TagFlag.undefined) && parent.tag !== Object) + output +}) + +let instanceDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => + `${inputVar} instanceof ${input->B.embed(input.expected.class)}`, + fail: B.failInvalidType, + }, + ], + ) + } else if ( + inputTagFlag->Flag.unsafeHas(TagFlag.instance) && input.schema.class === input.expected.class ) { - X.Exn.throwAny(InternalError.make(~code=InvalidJsonSchema(parent->castToPublic), ~flag, ~path)) - } - if tagFlag->Flag.unsafeHas(TagFlag.union) { - output.anyOf - ->X.Option.getUnsafe - ->Js.Array2.forEach(s => jsonableValidation(~output=s, ~parent, ~path, ~flag)) - } else if tagFlag->Flag.unsafeHas(TagFlag.object->Flag.with(TagFlag.array)) { - switch output.additionalItems->X.Option.getUnsafe { - | Schema(additionalItems) => - jsonableValidation(~output=additionalItems->castToInternal, ~parent, ~path, ~flag) - | _ => () - } - switch output.properties { - // Case for objects - | Some(p) => { - let keys = Js.Dict.keys(p) - for idx in 0 to keys->Js.Array2.length - 1 { - let key = keys->Js.Array2.unsafe_get(idx) - jsonableValidation(~output=p->Js.Dict.unsafeGet(key), ~parent, ~path, ~flag) - } - } - // Case for arrays - | None => - output.items - ->X.Option.getUnsafe - ->Js.Array2.forEach(item => { - jsonableValidation( - ~output=item.schema->castToInternal, - ~parent=output, - ~path=path->Path.concat(Path.fromLocation(item.location)), - ~flag, - ) - }) - } + input + } else { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) } +}) + +let instance = class_ => { + let mut = base(instanceTag, ~selfReverse=true) + mut.class = class_->Obj.magic + mut.decoder = instanceDecoder + mut->castToPublic } X.Object.defineProperty( @@ -2195,9 +3229,7 @@ X.Object.defineProperty( validate: input => { try { { - "value": (schema->castToPublic->operationFn(Flag.typeValidation))( - input->Obj.magic, - )->Obj.magic, + "value": getDecoder2(~s1=unknown, ~s2=schema)(input->Obj.magic)->Obj.magic, } } catch { | _ => { @@ -2205,7 +3237,7 @@ X.Object.defineProperty( { "issues": [ { - "message": error->InternalError.reason, + "message": error.reason, "path": error.path === Path.empty ? None : Some(error.path->Path.toArray), }, ], @@ -2219,180 +3251,64 @@ X.Object.defineProperty( }, ) -type rec input<'value, 'computed> = - | @as("Output") Value: input<'value, 'value> - | @as("Input") Unknown: input<'value, unknown> - | Any: input<'value, 'any> - | Json: input<'value, Js.Json.t> - | JsonString: input<'value, string> -type rec output<'value, 'computed> = - | @as("Output") Value: output<'value, 'value> - | @as("Input") Unknown: output<'value, unknown> - | Assert: output<'value, unit> - | Json: output<'value, Js.Json.t> - | JsonString: output<'value, string> -type rec mode<'output, 'computed> = - | Sync: mode<'output, 'output> - | Async: mode<'output, promise<'output>> - -@@warning("-37") -type internalInput = - | Output - | Input - | Any - | Json - | JsonString -type internalOutput = - | Output - | Input - | Assert - | Json - | JsonString -type internalMode = - | Sync - | Async -@@warning("+37") - -let compile = ( - schema: t<'value>, - ~input: input<'value, 'input>, - ~output: output<'value, 'transformedOutput>, - ~mode: mode<'transformedOutput, 'output>, - ~typeValidation=true, -): ('input => 'output) => { - let output = output->(Obj.magic: output<'value, 'transformedOutput> => internalOutput) - let input = input->(Obj.magic: input<'schemaInput, 'input> => internalInput) - let mode = mode->(Obj.magic: mode<'transformedOutput, 'output> => internalMode) - - let schema = schema->castToUnknown - - let flag = ref(Flag.none) - switch output { - | Output - | Input => { - if output === input->Obj.magic { - InternalError.panic(`Can't compile operation to converting value to self`) - } - () - } - | Assert => flag := flag.contents->Flag.with(Flag.assertOutput) - | Json => flag := flag.contents->Flag.with(Flag.jsonableOutput) - | JsonString => - flag := flag.contents->Flag.with(Flag.jsonableOutput->Flag.with(Flag.jsonStringOutput)) - } - switch mode { - | Sync => () - | Async => flag := flag.contents->Flag.with(Flag.async) - } - if typeValidation { - flag := flag.contents->Flag.with(Flag.typeValidation) - } - if input === Output { - flag := flag.contents->Flag.with(Flag.reverse) - } - let fn = schema->operationFn(flag.contents)->Obj.magic - - switch input { - | JsonString => - let flag = flag.contents - jsonString => { - try jsonString->Obj.magic->Js.Json.parseExn->fn catch { - | _ => - X.Exn.throwAny( - InternalError.make(~code=OperationFailed(%raw(`exn.message`)), ~flag, ~path=Path.empty), - ) - } - } - | _ => fn - } -} - // ============= -// Operations +// Builder functions // ============= -@inline -let parseOrThrow = (any, schema) => { - (schema->operationFn(Flag.typeValidation))(any) +let parser = (~to as schema) => { + getDecoder2(~s1=unknown, ~s2=schema->castToInternal) } -let parseJsonOrThrow = parseOrThrow->Obj.magic - -let parseJsonStringOrThrow = (jsonString: string, schema: t<'value>): 'value => { - try { - jsonString->Js.Json.parseExn - } catch { - | _ => - X.Exn.throwAny( - InternalError.make( - ~code=OperationFailed(%raw(`exn.message`)), - ~flag=Flag.typeValidation, - ~path=Path.empty, - ), - ) - }->parseOrThrow(schema) +let asyncParser = (~to as schema) => { + getDecoder2(~s1=unknown, ~s2=schema->castToInternal, ~flag=Flag.async) } -let parseAsyncOrThrow = (any, schema) => { - (schema->operationFn(Flag.async->Flag.with(Flag.typeValidation)))(any) +let decoder = (type from to, ~from: t, ~to: t): (from => to) => { + getDecoder2(~s1=from->castToInternal->reverse, ~s2=to->castToInternal) } -let convertOrThrow = (input, schema) => { - (schema->operationFn(Flag.none))(input) +let asyncDecoder = (type from to, ~from: t, ~to: t): (from => promise) => { + getDecoder2(~s1=from->castToInternal->reverse, ~s2=to->castToInternal, ~flag=Flag.async) } -let convertToJsonOrThrow = (any, schema) => { - (schema->operationFn(Flag.jsonableOutput))(any) +let decoder1 = (type value, schema: t): (unknown => value) => { + getDecoder(~s1=schema->castToInternal) } -let convertToJsonStringOrThrow = (input, schema) => { - (schema->operationFn(Flag.jsonableOutput->Flag.with(Flag.jsonStringOutput)))(input) +let asyncDecoder1 = (type value, schema: t): (unknown => promise) => { + getDecoder(~s1=schema->castToInternal, ~flag=Flag.async) } -let convertAsyncOrThrow = (any, schema) => { - (schema->operationFn(Flag.async))(any) -} +// ============= +// Operations +// ============= -let reverseConvertOrThrow = (value, schema) => { - (schema->operationFn(Flag.reverse))(value) -} +let assertResult = unit->copySchema +assertResult.noValidation = Some(true) @inline -let reverseConvertToJsonOrThrow = (value, schema) => { - (schema->operationFn(Flag.jsonableOutput->Flag.with(Flag.reverse)))(value) +let parseOrThrow = (any, ~to as schema) => { + getDecoder2(~s1=unknown, ~s2=schema->castToInternal)(any) } -let reverseConvertToJsonStringOrThrow = (value: 'value, schema: t<'value>, ~space=0): string => { - value->reverseConvertToJsonOrThrow(schema)->Js.Json.stringifyWithSpace(space) +let parseAsyncOrThrow = (any, ~to as schema) => { + getDecoder2(~s1=unknown, ~s2=schema->castToInternal, ~flag=Flag.async)(any) } -let assertOrThrow = (any, schema) => { - (schema->operationFn(Flag.typeValidation->Flag.with(Flag.assertOutput)))(any) +let assertOrThrow = (any, ~to as schema) => { + getDecoder3(~s1=unknown, ~s2=schema->castToInternal, ~s3=assertResult)(any) } -module Literal = { - let null = base(Null) - null.const = %raw(`null`) +let assertAsyncOrThrow = (any, ~to as schema) => { + getDecoder3(~s1=unknown, ~s2=schema->castToInternal, ~s3=assertResult, ~flag=Flag.async)(any) +} - let parse = (value): internal => { - let value = value->castAnyToUnknown - if value === %raw(`null`) { - null - } else { - let schema = switch value->Type.typeof { - | #undefined => unit - | #number if value->(Obj.magic: unknown => float)->Js.Float.isNaN => base(NaN) - | #object => { - let i = base(Instance) - i.class = (value->Obj.magic)["constructor"] - i - } - | typeof => base(typeof->(Obj.magic: Type.t => tag)) - } - schema.const = Some(value->Obj.magic) - schema - } - } +let decodeOrThrow = (any, ~from, ~to) => { + getDecoder2(~s1=from->castToInternal->reverse, ~s2=to->castToInternal)(any) +} + +let decodeAsyncOrThrow = (any, ~from, ~to) => { + getDecoder2(~s1=from->castToInternal->reverse, ~s2=to->castToInternal, ~flag=Flag.async)(any) } let isAsync = schema => { @@ -2460,7 +3376,7 @@ module Metadata = { let set = (schema, ~id: Id.t<'metadata>, metadata: 'metadata) => { let schema = schema->castToInternal - let mut = schema->copyWithoutCache + let mut = schema->copySchema mut->setInPlace(~id, metadata) mut->castToPublic } @@ -2469,9 +3385,10 @@ module Metadata = { let defsPath = `#/$defs/` let recursive = (name, fn) => { let ref = `${defsPath}${name}` - let refSchema = base(Ref) + let refSchema = base(refTag, ~selfReverse=false) refSchema.ref = Some(ref) refSchema.name = Some(name) + refSchema.decoder = recursiveDecoder // This is for mutual recursion let isNestedRec = globalConfig.defsAccumulator->Obj.magic @@ -2491,10 +3408,11 @@ let recursive = (name, fn) => { if isNestedRec { refSchema->castToPublic } else { - let schema = base(Ref) + let schema = base(refTag, ~selfReverse=false) schema.name = def.name schema.ref = Some(ref) schema.defs = globalConfig.defsAccumulator + schema.decoder = recursiveDecoder globalConfig.defsAccumulator = None @@ -2504,7 +3422,7 @@ let recursive = (name, fn) => { let noValidation = (schema, value) => { let schema = schema->castToInternal - let mut = schema->copyWithoutCache + let mut = schema->copySchema // TODO: Test for discriminant literal // TODO: Better test reverse @@ -2512,39 +3430,67 @@ let noValidation = (schema, value) => { mut->castToPublic } -let appendRefiner = (maybeExistingRefiner, refiner) => { - switch maybeExistingRefiner { - | Some(existingRefiner) => - (b, ~inputVar, ~selfSchema, ~path) => { - existingRefiner(b, ~inputVar, ~selfSchema, ~path) ++ refiner(b, ~inputVar, ~selfSchema, ~path) - } - | None => refiner - } -} - -let internalRefine = (schema, refiner) => { +let internalRefine = (schema, makeRefiner) => { let schema = schema->castToInternal updateOutput(schema, mut => { - mut.refiner = Some(appendRefiner(mut.refiner, refiner)) + let refiner = makeRefiner(mut) + switch mut.refiner { + | Some(existingRefiner) => + mut.refiner = Some( + (~input) => { + let arr = existingRefiner(~input) + let next = refiner(~input) + for i in 0 to next->Js.Array2.length - 1 { + arr->Js.Array2.push(next->Js.Array2.unsafe_get(i))->ignore + } + arr + }, + ) + | None => mut.refiner = Some(refiner) + } }) } -let refine: (t<'value>, s<'value> => 'value => unit) => t<'value> = (schema, refiner) => { - schema->internalRefine((b, ~inputVar, ~selfSchema, ~path) => { - `${b->B.embed(refiner(b->B.effectCtx(~selfSchema, ~path)))}(${inputVar});` - }) +let refine: (t<'value>, 'value => bool, ~error: string=?, ~path: array=?) => t<'value> = ( + schema, + refineCheck, + ~error=?, + ~path=?, +) => { + let message = switch error { + | Some(e) => e + | None => "Refinement failed" + } + let extraPath = switch path { + | Some(p) => Path.fromArray(p) + | None => Path.empty + } + schema->internalRefine(_ => + (~input) => { + let embeddedCheck = input->B.embed(refineCheck) + [ + { + cond: (~inputVar) => `${embeddedCheck}(${inputVar})`, + fail: (~input) => { + let path = if extraPath === Path.empty { + input.path + } else { + input.path->Path.concat(extraPath) + } + _value => Custom({reason: message, path}) + }, + }, + ] + } + ) } -let addRefinement = (schema, ~metadataId, ~refinement, ~refiner) => { - schema - ->Metadata.set( - ~id=metadataId, - switch schema->Metadata.get(~id=metadataId) { - | Some(refinements) => refinements->X.Array.append(refinement) - | None => [refinement] - }, - ) - ->internalRefine(refiner) +let getMutErrorMessage = (~mut: internal): dict => { + let em: dict = mut.errorMessage->X.Option.unsafeToBool + ? (mut.errorMessage->X.Option.getUnsafe->(Obj.magic: schemaErrorMessage => dict))->X.Dict.copy + : Js.Dict.empty() + mut.errorMessage = Some(em->Obj.magic) + em } type transformDefinition<'input, 'output> = { @@ -2562,31 +3508,34 @@ let transform: (t<'input>, s<'output> => transformDefinition<'input, 'output>) = let schema = schema->castToInternal updateOutput(schema, mut => { mut.parser = Some( - Builder.make((b, ~input, ~selfSchema, ~path) => { - switch transformer(b->B.effectCtx(~selfSchema, ~path)) { - | {parser, asyncParser: ?None} => b->B.embedSyncOperation(~input, ~fn=parser) - | {parser: ?None, asyncParser} => b->B.embedAsyncOperation(~input, ~fn=asyncParser) - | {parser: ?None, asyncParser: ?None, serializer: ?None} => input + Builder.make((~input) => { + switch transformer(input->B.effectCtx) { + | {parser, asyncParser: ?None} => B.embedTransformation(~input, ~fn=parser, ~isAsync=false) + | {parser: ?None, asyncParser} => + B.embedTransformation(~input, ~fn=asyncParser, ~isAsync=true) + | {parser: ?None, asyncParser: ?None, serializer: ?None} => + input->B.refine(~expected=input.expected.to->Option.getUnsafe) | {parser: ?None, asyncParser: ?None, serializer: _} => - b->B.invalidOperation(~path, ~description=`The S.transform parser is missing`) + input->B.invalidOperation(~description=`The S.transform parser is missing`) | {parser: _, asyncParser: _} => - b->B.invalidOperation( - ~path, + input->B.invalidOperation( ~description=`The S.transform doesn't allow parser and asyncParser at the same time. Remove parser in favor of asyncParser`, ) } }), ) mut.to = Some({ - let to = base(Unknown) + let to = base(unknownTag, ~selfReverse=false) + to.decoder = noopDecoder to.serializer = Some( - (b, ~input, ~selfSchema, ~path) => { - switch transformer(b->B.effectCtx(~selfSchema, ~path)) { - | {serializer} => b->B.embedSyncOperation(~input, ~fn=serializer) - | {parser: ?None, asyncParser: ?None, serializer: ?None} => input + (~input) => { + switch transformer(input->B.effectCtx) { + | {serializer} => B.embedTransformation(~input, ~fn=serializer, ~isAsync=false) + | {parser: ?None, asyncParser: ?None, serializer: ?None} => + input->B.refine(~expected=input.expected.to->Option.getUnsafe) | {serializer: ?None, asyncParser: ?Some(_)} | {serializer: ?None, parser: ?Some(_)} => - b->B.invalidOperation(~path, ~description=`The S.transform serializer is missing`) + input->B.invalidOperation(~description=`The S.transform serializer is missing`) } }, ) @@ -2596,72 +3545,45 @@ let transform: (t<'input>, s<'output> => transformDefinition<'input, 'output>) = }) } -let nullAsUnit = base(Null) +let nullAsUnit = base(nullTag, ~selfReverse=false) nullAsUnit.const = %raw(`null`) nullAsUnit.to = Some(unit) +nullAsUnit.decoder = Literal.literalDecoder let nullAsUnit = nullAsUnit->castToPublic -let neverBuilder = Builder.make((b, ~input, ~selfSchema, ~path) => { - b.code = - b.code ++ - b->B.failWithArg( - ~path, - input => InvalidType({ - expected: selfSchema->castToPublic, - received: input, - }), - input.inline, - ) ++ ";" - input +let never = base(neverTag, ~selfReverse=true) +let neverBuilder = Builder.make((~input) => { + let output = input->B.refine(~expected=never) + output.codeFromPrev = B.embedInvalidInput(~input) ++ ";" + output }) - -let never = base(Never) -never.compiler = Some(neverBuilder) +never.decoder = neverBuilder let never: t = never->castToPublic let nestedLoc = "BS_PRIVATE_NESTED_SOME_NONE" +module Dict = { + let factory = item => { + let item = item->castToInternal + let mut = base( + objectTag, + ~selfReverse=item->Obj.magic->Stdlib.Dict.getUnsafe(reversedKey) === item->Obj.magic, + ) + mut.properties = Some(X.Object.immutableEmpty) + mut.additionalItems = Some(Schema(item->castToPublic)) + mut.decoder = objectDecoder + mut->castToPublic + } +} + module Union = { @unboxed type itemCode = Single(string) | Multiple(array) - let getItemCode = (b, ~schema, ~input: val, ~output: val, ~deopt, ~path) => { - try { - let globalFlag = b.global.flag - if deopt { - b.global.flag = globalFlag->Flag.with(Flag.typeValidation) - } - let bb = b->B.scope - let input = if deopt { - input->Obj.magic->X.Dict.copy->Obj.magic - } else { - bb->B.makeRefinedOf(~input, ~schema) - } - let itemOutput = bb->parse(~schema, ~input, ~path) - - if itemOutput !== input { - itemOutput.b = bb - - if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { - output.flag = output.flag->Flag.with(ValFlag.async) - } - bb.code = - bb.code ++ - // Need to allocate a var here, so we don't mutate the input object field - `${output.var(b)}=${itemOutput.inline}` - } - - b.global.flag = globalFlag - bb->B.allocateScope - } catch { - | _ => "throw " ++ b->B.embed(%raw(`exn`)->InternalError.getOrRethrow) - } - } - - let isPriority = (tagFlag, byKey: dict>) => { + let isPriority = (tagFlag, byKey: dict>) => { (tagFlag->Flag.unsafeHas(TagFlag.array->Flag.with(TagFlag.instance)) && - byKey->Dict.has((Object: tag :> string))) || - (tagFlag->Flag.unsafeHas(TagFlag.nan) && byKey->Dict.has((Number: tag :> string))) + byKey->Stdlib.Dict.has((objectTag: tag :> string))) || + (tagFlag->Flag.unsafeHas(TagFlag.nan) && byKey->Stdlib.Dict.has((numberTag: tag :> string))) } let isWiderUnionSchema = (~schemaAnyOf, ~inputAnyOf) => { @@ -2681,398 +3603,521 @@ module Union = { ) ) && inputSchema.tag === schema.tag && - inputSchema.const === schema.const + inputSchema.const === schema.const && + inputSchema.to === None | None => false } }) } - let compiler = Builder.make((b, ~input, ~selfSchema, ~path) => { + let rec unionDecoder: Builder.t = (~input) => { + let selfSchema = input.expected let schemas = selfSchema.anyOf->X.Option.getUnsafe + let initialInputTagFlag = input.schema.tag->TagFlag.get - switch input.anyOf { - | Some(inputAnyOf) => - if isWiderUnionSchema(~schemaAnyOf=schemas, ~inputAnyOf) { - input - } else { - b->B.unsupportedTransform(~from=input, ~target=selfSchema, ~path) + let toPerCase = switch selfSchema { + | {parser: ?None, to} => Some(to) + | _ => None + } + + if ( + (initialInputTagFlag->Flag.unsafeHas(TagFlag.union) && + isWiderUnionSchema( + ~schemaAnyOf=schemas, + ~inputAnyOf=input.schema.anyOf->X.Option.getUnsafe, + ) && + toPerCase === None) || (input.isOutput->Option.getUnsafe && input.expected === input.schema) + ) { + input + } else { + if ( + input.schema.encoder === None && + initialInputTagFlag->Flag.unsafeHas(TagFlag.union->Flag.with(TagFlag.ref)) + ) { + input.schema = unknown } - | None => { - let fail = caught => { - `${b->B.embed( - ( - _ => { - let args = %raw(`arguments`) - b->B.throw( - ~path, - ~code=InvalidType({ - expected: selfSchema->castToPublic, - received: args->Js.Array2.unsafe_get(0), - unionErrors: ?( - args->Js.Array2.length > 1 - ? Some(args->X.Array.fromArguments->Js.Array2.sliceFrom(1)) - : None - ), - }), - ) - } - )->X.Function.toExpression, - )}(${input.var(b)}${caught})` - } - let typeValidation = b.global.flag->Flag.unsafeHas(Flag.typeValidation) - - let output = input - let initialInline = input.inline - - let deoptIdx = ref(-1) - let lastIdx = schemas->Js.Array2.length - 1 - let byKey = ref(Js.Dict.empty()) - let keys = ref([]) - for idx in 0 to lastIdx { - let schema = switch selfSchema.to { - | Some(target) if !(selfSchema.parser->Obj.magic) && target.tag !== Union => - updateOutput(schemas->Js.Array2.unsafe_get(idx), mut => { - switch selfSchema.refiner { - | Some(refiner) => mut.refiner = Some(appendRefiner(mut.refiner, refiner)) - | None => () + let initialInline = input.inline + + let fail = caught => { + `${input->B.embed( + ( + _ => { + let args = %raw(`arguments`) + input->B.throw( + B.makeInvalidInputDetails( + ~path=input.path, + ~expected=selfSchema, + ~received=unknown->castToPublic, + ~input=args->Js.Array2.unsafe_get(0), + ~includeInput=true, + ~unionErrors=?args->Js.Array2.length > 1 + ? Some(args->X.Array.fromArguments->Js.Array2.sliceFrom(1)) + : None, + ), + ) } - mut.to = Some(target) - })->castToInternal - | _ => schemas->Js.Array2.unsafe_get(idx) - } - let tag = schema.tag - let tagFlag = TagFlag.get(tag) + )->X.Function.toExpression, + )}(${input.var()}${caught})` + } - if ( - tagFlag->Flag.unsafeHas(TagFlag.undefined) && - selfSchema->Obj.magic->Dict.has("fromDefault") - ) { - // skip it - () - } // The tags without a determined refinement, - // for which we can't apply optimizations. - // So we run them and everything before them in a deopt mode. - else if ( - tagFlag->Flag.unsafeHas( - TagFlag.union - ->Flag.with(TagFlag.ref) - ->Flag.with(TagFlag.unknown) - ->Flag.with(TagFlag.never), - ) || (!(input.tag->TagFlag.get->Flag.unsafeHas(TagFlag.unknown)) && input.tag !== tag) - ) { - deoptIdx := idx - byKey := Js.Dict.empty() - keys := [] - } else { - let key = - tagFlag->Flag.unsafeHas(TagFlag.instance) - ? (schema.class->Obj.magic)["name"] - : (tag :> string) - switch byKey.contents->X.Dict.getUnsafeOption(key) { - | Some(arr) => - if ( - tagFlag->Flag.unsafeHas(TagFlag.object) && - schema.properties->X.Option.getUnsafe->Dict.has(nestedLoc) - ) { - // This is a special case for https://github.com/DZakh/sury/issues/150 - // When nested option goes together with an empty object schema - // Since we put None case check second, we need to change priority here. - arr->Js.Array2.unshift(schema)->ignore - } else if ( - // There can only be one valid. Dedupe - !( - tagFlag->Flag.unsafeHas( - TagFlag.undefined->Flag.with(TagFlag.null)->Flag.with(TagFlag.nan), - ) + // Create a copy of the input val, so we can mutate it + // It's still the same value though, until mutated + let output = input->B.refine + let outputAnyOf = [] + + let getArrItemsCode = (arr: array, ~isDeopt) => { + let typeValidationInput = arr->Js.Array2.unsafe_get(0)->(Obj.magic: unknown => val) + let typeValidationOutput = arr->Js.Array2.unsafe_get(1)->(Obj.magic: unknown => val) + + let itemStart = ref("") + let itemEnd = ref("") + let itemNextElse = ref(false) + let itemNoop = ref("") + let caught = ref("") + + // Accumulate schemas code by refinement (discriminant) + // so if we have two schemas with the same discriminant + // We can generate a single switch statement + // with try/catch blocks for each item + // If we come across an item without a discriminant + // we need to dump all accumulated schemas in try block + // and have the item without discriminant as catch all + // If we come across an item without a discriminant + // and without any code, it means that this item is always valid + // and we should exit early + let byDiscriminant = ref(Js.Dict.empty()) + + let preItems = 2 + let itemIdx = ref(preItems) + let lastIdx = arr->Js.Array2.length - 1 + while itemIdx.contents <= lastIdx { + // Copy it one more time, since every case decoder + // might mutate the input + let input = typeValidationOutput->B.Val.scope + input.isUnion = Some(true) + input.hasTransform = typeValidationOutput.hasTransform + input.isInput = Some(false) + input.isOutput = Some(false) + input.expected = + arr->Stdlib.Array.getUnsafe(itemIdx.contents)->(Obj.magic: unknown => internal) + + let isLast = itemIdx.contents === lastIdx + let isFirst = itemIdx.contents === preItems + let withExhaustiveCheck = ref(!(isFirst && isLast)) + + let itemCode = ref("") + let itemCond = ref("") + try { + let itemOutput = input->parse + outputAnyOf->Js.Array2.push(itemOutput.schema->castToPublic)->ignore + + itemCode := itemOutput->B.merge(~hoistCond=itemCond) + + if itemOutput.hasTransform->X.Option.getUnsafe { + output.hasTransform = Some(true) + if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { + output.flag = output.flag->Flag.with(ValFlag.async) + } + itemCode := + itemCode.contents ++ + // Need to allocate a var here, so we don't mutate the input object field + `${typeValidationInput.var()}=${itemOutput.inline}` + } + } catch { + | _ => { + let errorVar = input->B.embed(%raw(`exn`)->InternalError.getOrRethrow) + if isLast { + // FIXME: + withExhaustiveCheck := false + } + itemCode := ( + isLast && !isDeopt + ? { + withExhaustiveCheck := false + fail(`,${errorVar}`) + } + : "throw " ++ errorVar ) - ) { - arr->Js.Array2.push(schema)->ignore + } + } + let itemCond = itemCond.contents + let itemCode = itemCode.contents + + // Accumulate item parser when it has a discriminant + if itemCond->X.String.unsafeToBool { + if itemCode->X.String.unsafeToBool { + switch byDiscriminant.contents->X.Dict.getUnsafeOption(itemCond) { + | Some(Multiple(arr)) => arr->Js.Array2.push(itemCode)->ignore + | Some(Single(code)) => + byDiscriminant.contents->Js.Dict.set(itemCond, Multiple([code, itemCode])) + | None => byDiscriminant.contents->Js.Dict.set(itemCond, Single(itemCode)) } - | None => { - if isPriority(tagFlag, byKey.contents) { - // Not the fastest way, but it's the simplest way - // to make sure NaN is checked before number - // And instance and array checked before object - keys.contents->Js.Array2.unshift(key)->ignore - } else { - keys.contents->Js.Array2.push(key)->ignore + } else { + // We have a condition but without additional parsing logic + // So we accumulate it in case it's needed for a refinement later + itemNoop := ( + itemNoop.contents->X.String.unsafeToBool + ? `${itemNoop.contents}||${itemCond}` + : itemCond + ) + } + } + + // Allocate all accumulated discriminants + // If we have an item without a discriminant + // and need to deopt. Or we are at the last item + if itemCond->X.String.unsafeToBool->not || isLast { + let accedDiscriminants = byDiscriminant.contents->Js.Dict.keys + for idx in 0 to accedDiscriminants->Js.Array2.length - 1 { + let discrim = accedDiscriminants->Js.Array2.unsafe_get(idx) + let if_ = itemNextElse.contents ? "else if" : "if" + itemStart := itemStart.contents ++ if_ ++ `(${discrim}){` + switch byDiscriminant.contents->Js.Dict.unsafeGet(discrim) { + | Single(code) => itemStart := itemStart.contents ++ code ++ "}" + | Multiple(arr) => + let caught = ref("") + for idx in 0 to arr->Js.Array2.length - 1 { + let code = arr->Js.Array2.unsafe_get(idx) + let errorVar = `e` ++ idx->X.Int.unsafeToString + itemStart := itemStart.contents ++ `try{${code}}catch(${errorVar}){` + caught := `${caught.contents},${errorVar}` } - byKey.contents->Js.Dict.set(key, [schema]) + itemStart := + itemStart.contents ++ + fail(caught.contents) ++ + Js.String2.repeat("}", arr->Js.Array2.length) ++ "}" } + itemNextElse := true } + byDiscriminant.contents = Js.Dict.empty() } - } - let deoptIdx = deoptIdx.contents - let byKey = byKey.contents - let keys = keys.contents - - let start = ref("") - let end = ref("") - let caught = ref("") - // If we got a case which always passes, - // we can exit early - let exit = ref(false) - - if deoptIdx !== -1 { - for idx in 0 to deoptIdx { - if !exit.contents { - let schema = schemas->Js.Array2.unsafe_get(idx) - // Recreate input val for every union item - // since it might be mutated. - let itemCode = b->getItemCode(~schema, ~input, ~output, ~deopt=true, ~path) - if itemCode->X.String.unsafeToBool { - let errorVar = `e` ++ idx->X.Int.unsafeToString - start := start.contents ++ `try{${itemCode}}catch(${errorVar}){` - end := "}" ++ end.contents + if itemCond->X.String.unsafeToBool->not { + if itemCode->X.String.unsafeToBool->not { + // If we don't have a condition (discriminant) + // and additional parsing logic, + // it means that this item is always passes + // so we can remove preceding accumulated refinements + // and exit early even if there are other items + itemNoop := "" + itemIdx := lastIdx + withExhaustiveCheck := false + } else { + // The item without refinement should switch to deopt mode + // Since there might be validation in the body + if itemNoop.contents->X.String.unsafeToBool { + let if_ = itemNextElse.contents ? "else if" : "if" + itemStart := itemStart.contents ++ if_ ++ `(!(${itemNoop.contents})){` + itemEnd := "}" ++ itemEnd.contents + itemNoop := "" + itemNextElse := false + } + if isLast && (isDeopt || !withExhaustiveCheck.contents || isFirst) { + // For the last item don't add try/catch + itemStart := + itemStart.contents ++ `${itemNextElse.contents ? "else{" : ""}${itemCode}` + itemEnd := (itemNextElse.contents ? "}" : "") ++ itemEnd.contents + } else { + let errorVar = `e` ++ (itemIdx.contents - preItems)->X.Int.unsafeToString + itemStart := + itemStart.contents ++ + `${itemNextElse.contents ? "else{" : ""}try{${itemCode}}catch(${errorVar}){` + itemEnd := (itemNextElse.contents ? "}" : "") ++ "}" ++ itemEnd.contents caught := `${caught.contents},${errorVar}` + itemNextElse := false + } + } + } + if isLast { + if itemNoop.contents->X.String.unsafeToBool { + if itemStart.contents->X.String.unsafeToBool { + let if_ = itemNextElse.contents ? "else if" : "if" + itemStart := + itemStart.contents ++ if_ ++ `(!(${itemNoop.contents})){${fail(caught.contents)}}` } else { - exit := true + typeValidationOutput->B.pushCheck({ + cond: (~inputVar as _) => `(${itemNoop.contents})`, + fail: B.failInvalidType, + }) } + } else if withExhaustiveCheck.contents { + let errorCode = fail(caught.contents) + itemStart := + itemStart.contents ++ (itemNextElse.contents ? `else{${errorCode}}` : errorCode) } } - } - - if !exit.contents { - let nextElse = ref(false) - let noop = ref("") - - for idx in 0 to keys->Js.Array2.length - 1 { - let schemas = byKey->Js.Dict.unsafeGet(keys->Js.Array2.unsafe_get(idx)) - - let isMultiple = schemas->Js.Array2.length > 1 - let firstSchema = schemas->Js.Array2.unsafe_get(0) - - // Make cond as a weird callback, to prevent input.var call until it's needed - let cond = ref(%raw(`0`)) - - let body = if isMultiple { - let inputVar = input.var(b) - - let itemStart = ref("") - let itemEnd = ref("") - let itemNextElse = ref(false) - let itemNoop = ref("") - let caught = ref("") - - // Accumulate schemas code by refinement (discriminant) - // so if we have two schemas with the same discriminant - // We can generate a single switch statement - // with try/catch blocks for each item - // If we come across an item without a discriminant - // we need to dump all accumulated schemas in try block - // and have the item without discriminant as catch all - // If we come across an item without a discriminant - // and without any code, it means that this item is always valid - // and we should exit early - let byDiscriminant = ref(Js.Dict.empty()) - - let itemIdx = ref(0) - let lastIdx = schemas->Js.Array2.length - 1 - while itemIdx.contents <= lastIdx { - let schema = schemas->Js.Array2.unsafe_get(itemIdx.contents) - - let itemCond = - (schema->isLiteral ? b->B.validation(~inputVar, ~schema, ~negative=false) : "") ++ - b - ->B.refinement(~inputVar, ~schema, ~negative=false) - ->Js.String2.sliceToEnd(~from=2) - let itemCode = b->getItemCode(~schema, ~input, ~output, ~deopt=false, ~path) - - // Accumulate item parser when it has a discriminant - if itemCond->X.String.unsafeToBool { - if itemCode->X.String.unsafeToBool { - switch byDiscriminant.contents->X.Dict.getUnsafeOption(itemCond) { - | Some(Multiple(arr)) => arr->Js.Array2.push(itemCode)->ignore - | Some(Single(code)) => - byDiscriminant.contents->Js.Dict.set(itemCond, Multiple([code, itemCode])) - | None => byDiscriminant.contents->Js.Dict.set(itemCond, Single(itemCode)) - } - } else { - // We have a condition but without additional parsing logic - // So we accumulate it in case it's needed for a refinement later - itemNoop := ( - itemNoop.contents->X.String.unsafeToBool - ? `${itemNoop.contents}||${itemCond}` - : itemCond - ) - } - } - - // Allocate all accumulated discriminants - // If we have an item without a discriminant - // and need to deopt. Or we are at the last item - if itemCond->X.String.unsafeToBool->not || itemIdx.contents === lastIdx { - let accedDiscriminants = byDiscriminant.contents->Js.Dict.keys - for idx in 0 to accedDiscriminants->Js.Array2.length - 1 { - let discrim = accedDiscriminants->Js.Array2.unsafe_get(idx) - let if_ = itemNextElse.contents ? "else if" : "if" - itemStart := itemStart.contents ++ if_ ++ `(${discrim}){` - switch byDiscriminant.contents->Js.Dict.unsafeGet(discrim) { - | Single(code) => itemStart := itemStart.contents ++ code ++ "}" - | Multiple(arr) => - let caught = ref("") - for idx in 0 to arr->Js.Array2.length - 1 { - let code = arr->Js.Array2.unsafe_get(idx) - let errorVar = `e` ++ idx->X.Int.unsafeToString - itemStart := itemStart.contents ++ `try{${code}}catch(${errorVar}){` - caught := `${caught.contents},${errorVar}` - } - itemStart := - itemStart.contents ++ - fail(caught.contents) ++ - Js.String2.repeat("}", arr->Js.Array2.length) ++ "}" - } - itemNextElse := true - } - byDiscriminant.contents = Js.Dict.empty() - } - if itemCond->X.String.unsafeToBool->not { - // If we don't have a condition (discriminant) - // and additional parsing logic, - // it means that this item is always passes - // so we can remove preceding accumulated refinements - // and exit early even if there are other items - if itemCode->X.String.unsafeToBool->not { - itemNoop := "" - itemIdx := lastIdx - } else { - // The item without refinement should switch to deopt mode - // Since there might be validation in the body - if itemNoop.contents->X.String.unsafeToBool { - let if_ = itemNextElse.contents ? "else if" : "if" - itemStart := itemStart.contents ++ if_ ++ `(!(${itemNoop.contents})){` - itemEnd := "}" ++ itemEnd.contents - itemNoop := "" - itemNextElse := false - } - let errorVar = `e` ++ itemIdx.contents->X.Int.unsafeToString - itemStart := - itemStart.contents ++ - `${itemNextElse.contents ? "else{" : ""}try{${itemCode}}catch(${errorVar}){` - itemEnd := (itemNextElse.contents ? "}" : "") ++ "}" ++ itemEnd.contents - caught := `${caught.contents},${errorVar}` - itemNextElse := false - } - } + itemIdx := itemIdx.contents->X.Int.plus(1) + } - itemIdx := itemIdx.contents->X.Int.plus(1) - } + itemStart.contents ++ itemEnd.contents + } - cond := - ( - (~inputVar) => - b->B.validation( - ~inputVar, - ~schema={tag: firstSchema.tag, parser: %raw(`0`)}, - ~negative=false, - ) + let start = ref("") + let end = ref("") + let caught = ref("") + // If we got a case which always passes, + // we can exit early + let exit = ref(false) + + let lastIdx = schemas->Js.Array2.length - 1 + let byKey: ref>> = ref(Js.Dict.empty()) + let keys = ref([]) + let updatedSchemas = [] + for idx in 0 to lastIdx { + let schema = switch toPerCase { + | Some(target) => + updateOutput(schemas->Js.Array2.unsafe_get(idx), mut => { + // switch selfSchema.refiner { + // | Some(refiner) => mut.refiner = Some(appendRefiner(mut.refiner, refiner)) + // | None => () + // } + mut.to = Some(target) + })->castToInternal + | _ => schemas->Js.Array2.unsafe_get(idx) + } + updatedSchemas->Js.Array2.push(schema)->ignore + let tag = schema.tag + let tagFlag = TagFlag.get(tag) + let key = + tagFlag->Flag.unsafeHas(TagFlag.instance) + ? (schema.class->Obj.magic)["name"] + : (tag :> string) + + if ( + tagFlag->Flag.unsafeHas(TagFlag.undefined) && + selfSchema->Obj.magic->Stdlib.Dict.has("fromDefault") + ) { + // skip it + () + } else { + let initialArr = byKey.contents->X.Dict.getUnsafeOption(key) + switch initialArr { + | Some(arr) => + if ( + tagFlag->Flag.unsafeHas(TagFlag.object) && + schema.properties->X.Option.getUnsafe->Stdlib.Dict.has(nestedLoc) + ) { + // This is a special case for https://github.com/DZakh/sury/issues/150 + // When nested option goes together with an empty object schema + // Since we put None case check second, we need to change priority here. + arr + ->Stdlib.Array.splice( + ~start=arr->Stdlib.Array.length - 1, + ~remove=0, + ~insert=[schema->(Obj.magic: internal => unknown)], + ) + ->ignore + } else if ( + // TODO: Is this check needed? + // There can only be one valid. Dedupe + !( + tagFlag->Flag.unsafeHas( + TagFlag.undefined->Flag.with(TagFlag.null)->Flag.with(TagFlag.nan), ) + ) + ) { + arr->Js.Array2.push(schema->(Obj.magic: internal => unknown))->ignore + } + | None => + // Recreate input val for every schema + // since we will mutate it + let typeValidationInput = input->B.Val.scope + typeValidationInput.expected = if tagFlag->Flag.unsafeHas(TagFlag.null) { + nullLiteral + } else if tagFlag->Flag.unsafeHas(TagFlag.undefined) { + unit + } else if tagFlag->Flag.unsafeHas(TagFlag.object) { + Dict.factory(unknown->castToPublic)->castToInternal + } else if tagFlag->Flag.unsafeHas(TagFlag.array) { + array(unknown->castToPublic)->castToInternal + } else if tagFlag->Flag.unsafeHas(TagFlag.instance) { + instance(schema.class)->castToInternal + } else if tagFlag->Flag.unsafeHas(TagFlag.nan) { + nan + } else if tagFlag->Flag.unsafeHas(TagFlag.string) { + string + } else if tagFlag->Flag.unsafeHas(TagFlag.number) { + float + } else if tagFlag->Flag.unsafeHas(TagFlag.boolean) { + bool + } else if tagFlag->Flag.unsafeHas(TagFlag.bigint) { + bigint + } else if tagFlag->Flag.unsafeHas(TagFlag.symbol) { + symbol + } else { + unknown + } - if itemNoop.contents->X.String.unsafeToBool { - if itemStart.contents->X.String.unsafeToBool { - if typeValidation { - let if_ = itemNextElse.contents ? "else if" : "if" - itemStart := - itemStart.contents ++ - if_ ++ - `(!(${itemNoop.contents})){${fail(caught.contents)}}` - } - } else { - let condBefore = cond.contents - cond := ((~inputVar) => condBefore(~inputVar) ++ `&&(${itemNoop.contents})`) - } - } else if typeValidation && itemStart.contents->X.String.unsafeToBool { - let errorCode = fail(caught.contents) - itemStart := - itemStart.contents ++ (itemNextElse.contents ? `else{${errorCode}}` : errorCode) + let typeValidationOutput = try { + typeValidationInput->parse + } catch { + | _ => { + // Discard any checks parse managed to push before throwing, + // so the deopt path doesn't see leftover partial state. + typeValidationInput.checks = None + typeValidationInput } + } - itemStart.contents ++ itemEnd.contents + if isPriority(tagFlag, byKey.contents) { + // Not the fastest way, but it's the simplest way + // to make sure NaN is checked before number + // And instance and array checked before object + keys.contents->Js.Array2.unshift(key)->ignore } else { - cond := - ( - (~inputVar) => { - b->B.validation(~inputVar, ~schema=firstSchema, ~negative=false) ++ - b->B.refinement(~inputVar, ~schema=firstSchema, ~negative=false) - } - ) - - b->getItemCode(~schema=firstSchema, ~input, ~output, ~deopt=false, ~path) + keys.contents->Js.Array2.push(key)->ignore } + byKey.contents->Js.Dict.set( + key, + [ + typeValidationInput->(Obj.magic: val => unknown), + typeValidationOutput->(Obj.magic: val => unknown), + schema->(Obj.magic: internal => unknown), + ], + ) - if body->X.String.unsafeToBool || isPriority(firstSchema.tag->TagFlag.get, byKey) { - let if_ = nextElse.contents ? "else if" : "if" - start := - start.contents ++ if_ ++ `(${cond.contents(~inputVar=input.var(b))}){${body}}` - nextElse := true - } else if typeValidation { - let cond = cond.contents(~inputVar=input.var(b)) - noop := (noop.contents->X.String.unsafeToBool ? `${noop.contents}||${cond}` : cond) + let shouldDeopt = ref(true) + let valRef = ref(Some(typeValidationOutput)) + while valRef.contents !== None && shouldDeopt.contents { + let v = valRef.contents->X.Option.getUnsafe + valRef := v.prev + shouldDeopt := + !( + v.checks->X.Option.unsafeToBool && ( + v.hasTransform === Some(true) + ? (v.prev->X.Option.getUnsafe).hasTransform !== Some(true) && + v.codeFromPrev === "" + : true + ) + ) } - } - if typeValidation || deoptIdx === lastIdx { - let errorCode = fail(caught.contents) - start := - start.contents ++ if noop.contents->X.String.unsafeToBool { - let if_ = nextElse.contents ? "else if" : "if" - if_ ++ `(!(${noop.contents})){${errorCode}}` - } else if nextElse.contents { - `else{${errorCode}}` - } else { - errorCode + if shouldDeopt.contents { + for keyIdx in 0 to keys.contents->Stdlib.Array.length - 1 { + let key = keys.contents->Stdlib.Array.getUnsafe(keyIdx) + if !exit.contents { + let arr = byKey.contents->Stdlib.Dict.getUnsafe(key) + let typeValidationOutput = + arr->Stdlib.Array.getUnsafe(1)->(Obj.magic: unknown => val) + let itemsCode = getArrItemsCode(arr, ~isDeopt=true) + let blockCode = typeValidationOutput->B.merge ++ itemsCode + + if blockCode->X.String.unsafeToBool { + let errorVar = `e` ++ (idx + keyIdx)->X.Int.unsafeToString + start := start.contents ++ `try{${blockCode}}catch(${errorVar}){` + end := "}" ++ end.contents + caught := `${caught.contents},${errorVar}` + } else { + exit := true + } + } } + + byKey := Js.Dict.empty() + keys := [] + } } } + } - b.code = b.code ++ start.contents ++ end.contents + let byKey = byKey.contents + let keys = keys.contents - let o = if output.flag->Flag.unsafeHas(ValFlag.async) { - b->B.asyncVal(`Promise.resolve(${output.inline})`) - } else if output.var === B._var { - // TODO: Think how to make it more robust - // Recreate to not break the logic to determine - // whether the output is changed + if !exit.contents { + let nextElse = ref(false) + let noop = ref("") - // Use output.b instead of b because of withCatch - // Should refactor withCatch to make it simpler - // All of this is a hack to make withCatch think that there are no changes. eg S.array(S.option(item)) - if ( - b.code === "" && - output.b.code === "" && - (output.b.varsAllocation === `${output.inline}=${initialInline}` || - initialInline === "i") - ) { - output.b.varsAllocation = "" - output.b.allocate = B.initialAllocate - output.var = B._notVar - output.inline = initialInline - output + for idx in 0 to keys->Js.Array2.length - 1 { + let arr = byKey->Js.Dict.unsafeGet(keys->Js.Array2.unsafe_get(idx)) + let typeValidationOutput = arr->Stdlib.Array.getUnsafe(1)->(Obj.magic: unknown => val) + let firstSchema = arr->Stdlib.Array.getUnsafe(2)->(Obj.magic: unknown => internal) + + let itemsCode = getArrItemsCode(arr, ~isDeopt=false) + + let blockCond = ref("") + let blockCode = typeValidationOutput->B.merge(~hoistCond=blockCond) ++ itemsCode + let blockCond = blockCond.contents + + if blockCode->X.String.unsafeToBool || isPriority(firstSchema.tag->TagFlag.get, byKey) { + let if_ = nextElse.contents ? "else if" : "if" + start := start.contents ++ if_ ++ `(${blockCond}){${blockCode}}` + nextElse := true } else { - output->Obj.magic->X.Dict.copy->Obj.magic + noop := ( + noop.contents->X.String.unsafeToBool ? `${noop.contents}||${blockCond}` : blockCond + ) } - } else { - output } - o.anyOf = selfSchema.anyOf - o.tag = switch selfSchema.to { - | Some(to) if to.tag !== Union => { - o.skipTo = Some(true) - (to->getOutputSchema).tag + let errorCode = fail(caught.contents) + start := + start.contents ++ if noop.contents->X.String.unsafeToBool { + let if_ = nextElse.contents ? "else if" : "if" + if_ ++ `(!(${noop.contents})){${errorCode}}` + } else if nextElse.contents { + `else{${errorCode}}` + } else { + errorCode } - | _ => Union + } + + output.codeFromPrev = output.codeFromPrev ++ start.contents ++ end.contents + + // In case if input.var was called, but output.var wasn't + if input.inline !== output.inline { + output.inline = input.inline + } + + let o = if output.flag->Flag.unsafeHas(ValFlag.async) { + output.inline = `Promise.resolve(${output.inline})` + output.var = B._notVar + output + } else if output.var === B._var { + // TODO: Think how to make it more robust + // Recreate to not break the logic to determine + // whether the output is changed + + // Use output.b instead of b because of mergeWithCatch + // Should refactor mergeWithCatch to make it simpler + // All of this is a hack to make mergeWithCatch think that there are no changes. eg S.array(S.option(item)) + if ( + input.codeFromPrev === "" && + output.codeFromPrev === "" && + (output.varsAllocation === `${output.inline}=${initialInline}` || initialInline === "i") + ) { + // FIXME: Might not be not needed + input.varsAllocation = "" + input.allocate = B.initialAllocate + input.var = B._notVar + input.inline = initialInline + input + } else { + output } + } else { + output + } - o + // Build the output schema from collected case output schemas + o.schema = if outputAnyOf->Stdlib.Array.length->X.Int.unsafeToBool { + factory(outputAnyOf)->castToInternal + } else { + never->castToInternal + } + o.expected = switch toPerCase { + | Some(to) => { + o.isOutput = Some(true) + to->getOutputSchema + } + | _ => selfSchema } - } - }) - let factory = schemas => { + o + } + } + and factory = schemas => { let schemas: array = schemas->Obj.magic // TODO: // 1. Fitler out items without parser @@ -3090,7 +4135,7 @@ module Union = { let schema = schemas->Js.Array2.unsafe_get(idx) // Check if the union is not transformed - if schema.tag === Union && schema.to === None { + if schema.tag === unionTag && schema.to === None { schema.anyOf ->X.Option.getUnsafe ->Js.Array2.forEach(item => { @@ -3102,9 +4147,9 @@ module Union = { has->setHas(schema.tag) } } - let mut = base(Union) + let mut = base(unionTag, ~selfReverse=false) mut.anyOf = Some(anyOf->X.Set.toArray) - mut.compiler = Some(compiler) + mut.decoder = unionDecoder mut.has = Some(has) mut->castToPublic } @@ -3117,35 +4162,34 @@ module Option = { let nestedOption = { let nestedNone = () => { let itemSchema = Literal.parse(0) - let item: item = { - schema: itemSchema->castToPublic, - location: nestedLoc, - } // FIXME: dict{} let properties = Js.Dict.empty() properties->Js.Dict.set(nestedLoc, itemSchema) { - tag: Object, + tag: objectTag, + required: [nestedLoc], properties, - items: [item], additionalItems: Strip, + decoder: objectDecoder, // TODO: Support this as a default coercion - serializer: Builder.make((b, ~input as _, ~selfSchema, ~path as _) => { - b->B.constVal(~schema=selfSchema.to->X.Option.getUnsafe) + serializer: Builder.make((~input) => { + let nextSchema = input.expected.to->X.Option.getUnsafe + input->B.nextConst(~schema=nextSchema, ~expected=nextSchema) + // FIXME: Need to set isOutput? }), } } - let parser = Builder.make((b, ~input as _, ~selfSchema, ~path as _) => { - b->B.val( + let parser = Builder.make((~input) => { + let nextSchema = input.expected.to->X.Option.getUnsafe + input->B.next( `{${nestedLoc}:${( - ( - (selfSchema->getOutputSchema).items - ->X.Option.getUnsafe - ->Js.Array2.unsafe_get(0) - ).schema->castToInternal + (input.expected->getOutputSchema).properties + ->X.Option.getUnsafe + ->Js.Dict.unsafeGet(nestedLoc) ).const->Obj.magic}}`, - ~schema=selfSchema.to->X.Option.getUnsafe, + ~schema=nextSchema, + ~expected=nextSchema, ) }) @@ -3170,7 +4214,7 @@ module Option = { let mutHas = has->X.Option.getUnsafe->X.Dict.copy let newAnyOf = [] - for idx in 0 to schemas->Array.length - 1 { + for idx in 0 to schemas->Stdlib.Array.length - 1 { let schema = schemas->Js.Array2.unsafe_get(idx) newAnyOf ->Js.Array2.push( @@ -3185,19 +4229,15 @@ module Option = { | Some(nestedSchema) => schema ->updateOutput(mut => { - let newItem = { - location: nestedLoc, - schema: { - tag: nestedSchema.tag, - parser: ?nestedSchema.parser, - const: nestedSchema.const->Obj.magic->X.Int.plus(1)->Obj.magic, - }->castToPublic, - } - // FIXME: dict{} let properties = Js.Dict.empty() - properties->Js.Dict.set(nestedLoc, newItem.schema->castToInternal) - mut.items = Some([newItem]) + properties->Js.Dict.set( + nestedLoc, + { + ...nestedSchema, + const: nestedSchema.const->Obj.magic->X.Int.plus(1)->Obj.magic, + }, + ) mut.properties = Some(properties) }) ->castToInternal @@ -3230,8 +4270,8 @@ module Option = { let item = ref(None) let itemOutputSchema = ref(None) - for idx in 0 to anyOf->Array.length - 1 { - let schema = anyOf->Array.getUnsafe(idx) + for idx in 0 to anyOf->Stdlib.Array.length - 1 { + let schema = anyOf->Stdlib.Array.getUnsafe(idx) let outputSchema = schema->getOutputSchema switch outputSchema.tag { | Undefined => () @@ -3252,41 +4292,42 @@ module Option = { | Some(s) => s } - // FIXME: Should delete schema.unnest on reverse? // FIXME: Ensure that default has the same type as the item // Or maybe not, but need to make it properly with JSON Schema mut.parser = Some( - Builder.make((b, ~input, ~selfSchema, ~path as _) => { - b->B.transform( - ~input, - (b, ~input) => { - let inputVar = input.var(b) - b->B.val( - `${inputVar}===void 0?${switch default { - | Value(v) => b->B.inlineConst(Literal.parse(v)) - | Callback(cb) => `${b->B.embed(cb)}()` - }}:${inputVar}`, - ~schema=selfSchema.to->X.Option.getUnsafe, - ) - }, + Builder.make((~input) => { + let nextSchema = input.expected.to->X.Option.getUnsafe + let inputVar = input.var() + input->B.next( + `${inputVar}===void 0?${switch default { + | Value(v) => input->B.inlineConst(Literal.parse(v)) + | Callback(cb) => `${input->B.embed(cb)}()` + }}:${inputVar}`, + ~schema=nextSchema, + ~expected=nextSchema, ) }), ) - let to = itemOutputSchema.contents->X.Option.getUnsafe->copyWithoutCache - switch to.compiler { - | Some(compiler) => { - to.serializer = Some(compiler) - %raw(`delete to.compiler`) - } - | None => to.serializer = Some((_b, ~input, ~selfSchema as _, ~path as _) => input) - } + let to = itemOutputSchema.contents->X.Option.getUnsafe->copySchema + + let originalDecoder = to.decoder + to.serializer = Some( + Builder.make((~input) => { + let nextSchema = item->reverse + originalDecoder(~input)->B.refine(~schema=nextSchema, ~expected=nextSchema) + }), + ) + + // FIXME: This looks wrong, but this is how it was with prev architecture + to.decoder = noopDecoder + mut.to = Some(to) switch default { | Value(v) => try mut.default = - operationFn(item->castToPublic, Flag.reverse)(v)->( + getDecoder(~s1=item->reverse)(v)->( Obj.magic: unknown => option ) catch { | _ => () @@ -3305,73 +4346,6 @@ module Option = { schema->getWithDefault(Callback(defalutCb->(Obj.magic: (unit => 'a) => unit => unknown))) } -module Array = { - module Refinement = { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - type t = { - kind: kind, - message: string, - } - - let metadataId: Metadata.Id.t> = Metadata.Id.internal("Array.refinements") - } - - let refinements = schema => { - switch schema->Metadata.get(~id=Refinement.metadataId) { - | Some(m) => m - | None => [] - } - } - - let arrayCompiler = Builder.make((b, ~input, ~selfSchema, ~path) => { - let item = selfSchema.additionalItems->(Obj.magic: option => internal) - - let inputVar = b->B.Val.var(input) - let iteratorVar = b.global->B.varWithoutAllocation - - let bb = b->B.scope - let itemInput = bb->B.val(`${inputVar}[${iteratorVar}]`, ~schema=unknown) // FIXME: should get from additionalItems - let itemOutput = - bb->B.withPathPrepend(~input=itemInput, ~path, ~dynamicLocationVar=iteratorVar, ( - b, - ~input, - ~path, - ) => b->parse(~schema=item, ~input, ~path)) - let itemCode = bb->B.allocateScope - let isTransformed = itemInput !== itemOutput - let output = isTransformed - ? b->B.val(`new Array(${inputVar}.length)`, ~schema=selfSchema) - : input // FIXME: schema - output.tag = selfSchema.tag - output.additionalItems = selfSchema.additionalItems - - if isTransformed || itemCode !== "" { - b.code = - b.code ++ - `for(let ${iteratorVar}=0;${iteratorVar}<${inputVar}.length;++${iteratorVar}){${itemCode}${isTransformed - ? b->B.Val.addKey(output, iteratorVar, itemOutput) - : ""}}` - } - - if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { - output.b->B.asyncVal(`Promise.all(${output.inline})`) - } else { - output - } - }) - - let factory = item => { - let mut = base(Array) - mut.additionalItems = Some(Schema(item->castToInternal->castToPublic)) - mut.items = Some(X.Array.immutableEmpty) - mut.compiler = Some(arrayCompiler) - mut->castToPublic - } -} - module Object = { type rec s = { @as("f") field: 'value. (string, t<'value>) => 'value, @@ -3384,32 +4358,50 @@ module Object = { let rec setAdditionalItems = (schema, additionalItems, ~deep) => { let schema = schema->castToInternal switch schema { - | {additionalItems: currentAdditionalItems, ?items} + | {additionalItems: currentAdditionalItems} if currentAdditionalItems !== additionalItems && - currentAdditionalItems->Js.typeof !== "object" => - let mut = schema->copyWithoutCache - mut.additionalItems = Some(additionalItems) - if deep { - let items = items->X.Option.getUnsafe - - let newItems = [] - let newProperties = Js.Dict.empty() - for idx in 0 to items->Js.Array2.length - 1 { - let item = items->Js.Array2.unsafe_get(idx) - let newSchema = - setAdditionalItems( - item.schema->(Obj.magic: t => t<'a>), - additionalItems, - ~deep, - )->castToUnknown - let newItem = newSchema === item.schema ? item : {...item, schema: newSchema} - newProperties->Js.Dict.set(item.location, newSchema->castToInternal) - newItems->Js.Array2.push(newItem)->ignore + currentAdditionalItems->Js.typeof !== (objectTag :> string) => { + let mut = schema->copySchema + mut.additionalItems = Some(additionalItems) + if deep { + switch schema.items { + | Some(items) => { + let newItems = [] + for idx in 0 to items->Js.Array2.length - 1 { + let s = items->Js.Array2.unsafe_get(idx) + newItems + ->Js.Array2.push( + s->castToPublic->setAdditionalItems(additionalItems, ~deep)->castToInternal, + ) + ->ignore + } + mut.items = Some(newItems) + } + | None => () + } + + switch schema.properties { + | Some(properties) => { + let newProperties = Js.Dict.empty() + let keys = properties->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + newProperties->Js.Dict.set( + key, + properties + ->Js.Dict.unsafeGet(key) + ->castToPublic + ->setAdditionalItems(additionalItems, ~deep) + ->castToInternal, + ) + } + mut.properties = Some(newProperties) + } + | None => () + } } - mut.items = Some(newItems) - mut.properties = Some(newProperties) + mut->castToPublic } - mut->castToPublic | _ => schema->castToPublic } } @@ -3431,62 +4423,6 @@ let deepStrict = schema => { schema->Object.setAdditionalItems(Strict, ~deep=true) } -module Dict = { - let dictCompiler = Builder.make((b, ~input, ~selfSchema, ~path) => { - let item = selfSchema.additionalItems->(Obj.magic: option => internal) - - let inputVar = b->B.Val.var(input) - let keyVar = b.global->B.varWithoutAllocation - - let bb = b->B.scope - let itemInput = bb->B.val(`${inputVar}[${keyVar}]`, ~schema=unknown) // FIXME: should get from additionalItems - let itemOutput = - bb->B.withPathPrepend(~path, ~input=itemInput, ~dynamicLocationVar=keyVar, ( - b, - ~input, - ~path, - ) => b->parse(~schema=item, ~input, ~path)) - let itemCode = bb->B.allocateScope - let isTransformed = itemInput !== itemOutput - let output = isTransformed ? b->B.val("{}", ~schema=selfSchema) : input // FIXME: schema - output.tag = selfSchema.tag - output.additionalItems = selfSchema.additionalItems - - // FIXME: What about async? - - if isTransformed || itemCode !== "" { - b.code = - b.code ++ - `for(let ${keyVar} in ${inputVar}){${itemCode}${isTransformed - ? b->B.Val.addKey(output, keyVar, itemOutput) - : ""}}` - } - - if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { - let resolveVar = b.global->B.varWithoutAllocation - let rejectVar = b.global->B.varWithoutAllocation - let asyncParseResultVar = b.global->B.varWithoutAllocation - let counterVar = b.global->B.varWithoutAllocation - let outputVar = b->B.Val.var(output) - b->B.asyncVal( - `new Promise((${resolveVar},${rejectVar})=>{let ${counterVar}=Object.keys(${outputVar}).length;for(let ${keyVar} in ${outputVar}){${outputVar}[${keyVar}].then(${asyncParseResultVar}=>{${outputVar}[${keyVar}]=${asyncParseResultVar};if(${counterVar}--===1){${resolveVar}(${outputVar})}},${rejectVar})}})`, - ) - } else { - output - } - }) - - let factory = item => { - let item = item->castToInternal - let mut = base(Object) - mut.properties = Some(X.Object.immutableEmpty) - mut.items = Some(X.Array.immutableEmpty) - mut.additionalItems = Some(Schema(item->castToPublic)) - mut.compiler = Some(dictCompiler) - mut->castToPublic - } -} - module Tuple = { type s = { item: 'value. (int, t<'value>) => 'value, @@ -3494,83 +4430,216 @@ module Tuple = { } } -module String = { - module Refinement = { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - | Email - | Uuid - | Cuid - | Url - | Pattern({re: Js.Re.t}) - | Datetime - type t = { - kind: kind, - message: string, - } - - let metadataId: Metadata.Id.t> = Metadata.Id.internal("String.refinements") - } +let jsonEncoder = Builder.encoder((~input, ~target) => { + let toTagFlag = target.tag->TagFlag.get - let refinements = schema => { - switch schema->Metadata.get(~id=Refinement.metadataId) { - | Some(m) => m - | None => [] + if ( + toTagFlag->Flag.unsafeHas( + TagFlag.string + ->Flag.with(TagFlag.boolean) + ->Flag.with(TagFlag.number) + ->Flag.with(TagFlag.null), + ) + ) { + input->B.refine(~schema=unknown, ~expected=target)->parse + } else if toTagFlag->Flag.unsafeHas(TagFlag.undefined->Flag.with(TagFlag.nan)) { + let jsonExpected = nullLiteral->copySchema + jsonExpected.to = Some(target) + input->B.refine(~schema=unknown, ~expected=jsonExpected)->parse + } else if toTagFlag->Flag.unsafeHas(TagFlag.array) { + // Validate that the input is an array + // and then update the schema to be an array of json instead of array of unknown + let jsonExpected = array(unknown->castToPublic)->castToInternal + let output = input->B.refine(~schema=unknown, ~expected=jsonExpected)->parse + output.schema.additionalItems = Some(Schema(json->castToPublic)) + output.expected = target + output.isInput = Some(false) + output.isOutput = Some(false) + output + } else if toTagFlag->Flag.unsafeHas(TagFlag.object) { + // Validate that the input is an object + // and then update the schema to be an object of json instead of object of unknown + let jsonExpected = Dict.factory(unknown->castToPublic)->castToInternal + let output = input->B.refine(~schema=unknown, ~expected=jsonExpected)->parse + output.schema.additionalItems = Some(Schema(json->castToPublic)) + output.expected = target + output.isInput = Some(false) + output.isOutput = Some(false) + output + } else if toTagFlag->Flag.unsafeHas(TagFlag.union->Flag.with(TagFlag.ref)) { + input + } else { + // For non-JSON types (bigint, instance, etc.), try decoding through string + try { + let jsonExpected = string->copySchema + jsonExpected.to = Some(target) + input->B.refine(~schema=unknown, ~expected=jsonExpected)->parse + } catch { + | _ => input } } +}) - let cuidRegex = /^c[^\s-]{8,}$/i - let uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i - // Adapted from https://stackoverflow.com/a/46181/1550155 - let emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i - // Adapted from https://stackoverflow.com/a/3143231 - let datetimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/ +let rec isJsonable = schema => { + let tagFlag = schema.tag->TagFlag.get + tagFlag->Flag.unsafeHas( + TagFlag.string + ->Flag.with(TagFlag.number) + ->Flag.with(TagFlag.boolean) + ->Flag.with(TagFlag.null), + ) || + schema.ref === json.ref || + tagFlag->Flag.unsafeHas(TagFlag.union) && + schema.anyOf->X.Option.getUnsafe->Js.Array2.every(isJsonable) || + tagFlag->Flag.unsafeHas(TagFlag.array) && + switch schema.additionalItems->X.Option.getUnsafe { + | Schema(s) => s->castToInternal->isJsonable + | _ => true + } && + schema.items->X.Option.getUnsafe->Js.Array2.every(isJsonable) || + (tagFlag->Flag.unsafeHas(TagFlag.object) && + switch schema.additionalItems->X.Option.getUnsafe { + | Schema(s) => s->castToInternal->isJsonable + | _ => true + } && + schema.properties->X.Option.getUnsafe->Stdlib.Dict.valuesToArray->Js.Array2.every(isJsonable)) } -let json = shaken("json") +let jsonDecoder = (~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + + if input.schema->isJsonable { + input + } else if inputTagFlag->Flag.unsafeHas(TagFlag.undefined->Flag.with(TagFlag.nan)) { + input->B.nextConst(~schema=nullLiteral) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.array) { + let expected = base(arrayTag, ~selfReverse=false) + expected.items = Some( + input.schema.items + ->X.Option.getUnsafe + ->Js.Array2.map(_ => json), + ) + expected.decoder = arrayDecoder + expected.additionalItems = Some( + switch input.schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => Schema(json->castToPublic) + | v => v + }, + ) + expected.to = input.expected.to + input->B.refine(~expected)->parse + } else if inputTagFlag->Flag.unsafeHas(TagFlag.object) { + switch input.schema.additionalItems->X.Option.getUnsafe { + | Schema(_) => { + let expected = Dict.factory(json->castToPublic)->castToInternal + expected.to = input.expected.to + input->B.refine(~expected)->parse + } + | _ => { + let jsonVal = makeObjectVal(input, ~schema=input.schema) + jsonVal.expected = json + if input.expected.to->Obj.magic { + jsonVal.expected = jsonVal.expected->copySchema + jsonVal.expected.to = input.expected.to + } + + let keys = input.schema.properties->X.Option.getUnsafe->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + let itemVal = input->B.Val.get(key) + itemVal.isInput = Some(false) + itemVal.isOutput = Some(false) + + if ( + itemVal.schema.tag === unionTag && + itemVal.schema.has->X.Option.getUnsafe->Js.Dict.unsafeGet((undefinedTag :> string)) + ) { + itemVal.expected = + Union.factory([unit->castToPublic, json->castToPublic])->castToInternal + let itemOutput = itemVal->parse + itemOutput.optional = Some(true) + jsonVal->B.Val.Object.add(~location=key, itemOutput) + } else { + itemVal.expected = json + jsonVal->B.Val.Object.add(~location=key, itemVal->parse) + } + } + + jsonVal->completeObjectVal + } + } + } else if inputTagFlag->Flag.unsafeHas(TagFlag.ref) { + // FIXME: Should be a unified solution for ref inputs + recursiveDecoder(~input) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + let to = input.expected.to->X.Option.getUnsafe + // Whether we can optimize encoding during decoding + let preEncode: bool = to->Obj.magic && !(input.expected.parser->Obj.magic) // && !(selfSchema.refiner->Obj.magic) FIXME: + if preEncode { + input.schema = json + jsonEncoder(~input, ~target=input.expected) + } else if input.expected.noValidation->X.Option.getUnsafe { + input.schema = json + input + } else { + recursiveDecoder(~input) + } + } else { + try { + let expected = string->copySchema + expected.to = Some(input.expected) + input.expected = expected + input->parse + } catch { + | _ => input->B.unsupportedDecode(~from=input.schema, ~target=json) + } + } +} let enableJson = () => { if json->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { let _ = %raw(`delete json.as`) - let jsonRef = base(Ref) + let jsonRef = base(refTag, ~selfReverse=true) jsonRef.ref = Some(`${defsPath}${jsonName}`) jsonRef.name = Some(jsonName) + + // FIXME: Validate whether dcoders needs to be here + jsonRef.decoder = jsonDecoder + jsonRef.encoder = Some(jsonEncoder) json.tag = jsonRef.tag json.ref = jsonRef.ref json.name = Some(jsonName) + json.decoder = jsonDecoder + json.encoder = Some(jsonEncoder) + + let anyOf = [ + string, + bool, + float, + nullLiteral, + Dict.factory(jsonRef->castToPublic)->castToInternal, + array(jsonRef->castToPublic)->castToInternal, + ] + let has = Js.Dict.empty() + anyOf->Js.Array2.forEach(schema => { + has->Js.Dict.set((schema.tag :> string), true) + }) + + let jsonDef = base(unionTag, ~selfReverse=true) + jsonDef.anyOf = Some(anyOf) + jsonDef.has = Some(has) + jsonDef.decoder = Union.unionDecoder + jsonDef.name = Some(jsonName) + jsonDef.tag = unionTag + let defs = Js.Dict.empty() - defs->Js.Dict.set( - jsonName, - { - name: jsonName, - tag: Union, - anyOf: [ - string, - bool, - float, - Literal.null, - Dict.factory(jsonRef->castToPublic)->castToInternal, - Array.factory(jsonRef->castToPublic)->castToInternal, - ], - has: dict{ - "string": true, - "boolean": true, - "number": true, - "null": true, - "object": true, - "array": true, - }, - compiler: Union.compiler, - }, - ) + defs->Js.Dict.set(jsonName, jsonDef) json.defs = Some(defs) } } let enableJsonString = { - let inlineJsonString = (b, ~schema, ~selfSchema, ~path) => { + let inlineJsonString = (input, ~schema) => { let tagFlag = schema.tag->TagFlag.get let const = schema.const if tagFlag->Flag.unsafeHas(TagFlag.undefined->Flag.with(TagFlag.null)) { @@ -3582,165 +4651,332 @@ let enableJsonString = { } else if tagFlag->Flag.unsafeHas(TagFlag.number->Flag.with(TagFlag.boolean)) { `"${const->Obj.magic}"` } else { - b->B.unsupportedTransform(~from=schema->Obj.magic, ~target=selfSchema, ~path) + input->B.unsupportedDecode(~from=schema, ~target=input.expected) + } + } + + let constSchemaToJsonStringConst = (input, ~target) => { + let tagFlag = target.tag->TagFlag.get + let const = target.const + if tagFlag->Flag.unsafeHas(TagFlag.undefined->Flag.with(TagFlag.null)) { + `null` + } else if tagFlag->Flag.unsafeHas(TagFlag.string) { + const->Obj.magic->X.Inlined.Value.fromString->Obj.magic + } else if tagFlag->Flag.unsafeHas(TagFlag.bigint) { + `"${const->Obj.magic}"` + } else if tagFlag->Flag.unsafeHas(TagFlag.number->Flag.with(TagFlag.boolean)) { + %raw(`""+$$const`) + } else { + input->B.unsupportedDecode(~from=input.schema, ~target) } } + let jsonStringEncoder = Builder.encoder((~input, ~target) => { + if target.format !== Some(JSON) { + if target->isLiteral { + let jsonStringConstSchema = base(stringTag, ~selfReverse=true) + jsonStringConstSchema.const = input->constSchemaToJsonStringConst(~target)->Obj.magic + jsonStringConstSchema.to = Some(target) + jsonStringConstSchema.decoder = Literal.literalDecoder + input->B.refine(~expected=jsonStringConstSchema) + } else { + let outputVar = input.global->B.varWithoutAllocation + input.allocate(outputVar) + + let nextSchema = json->copySchema + nextSchema.to = Some(target) + + let output = input->B.next(outputVar, ~schema=nextSchema, ~expected=nextSchema) + output.isInput = Some(true) + output.isOutput = Some(true) + output.var = B._var + + let inputVar = input.var() + output.codeFromPrev = `try{${outputVar}=JSON.parse(${inputVar})}catch(t){${B.embedInvalidInput( + ~input, + ~expected=input.schema, + )}}` + + output + } + } else { + input + } + }) + + let jsonStringDecoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + let expectedSchema = input.expected + + if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + let to = expectedSchema.to->X.Option.getUnsafe + // Whether we can optimize encoding during decoding + let preEncode: bool = + to->Obj.magic && + to.tag !== unknownTag && + !(expectedSchema.parser->Obj.magic) && + !(expectedSchema.refiner->Obj.magic) + + let stringVal = stringDecoder(~input) + stringVal.schema = expectedSchema + stringVal.expected = expectedSchema + + if preEncode { + jsonStringEncoder(~input=stringVal, ~target=to) + } else { + let stringVar = stringVal.var() + let output = stringVal->B.refine(~schema=expectedSchema) + output.codeFromPrev = `try{JSON.parse(${stringVar})}catch(t){${B.embedInvalidInput( + ~input=stringVal, + )}}` + output + } + } else if input.schema.format === Some(JSON) { + input + } else if input.schema->isLiteral { + input->B.next(input->inlineJsonString(~schema=input.schema), ~schema=expectedSchema) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + input->B.next(`JSON.stringify(${input.inline})`, ~schema=expectedSchema) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.number->Flag.with(TagFlag.boolean)) { + let output = input->inputToString + output.schema = expectedSchema + output + } else if inputTagFlag->Flag.unsafeHas(TagFlag.bigint) { + input->B.next(`"\\""+${input.inline}+"\\""`, ~schema=expectedSchema) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.object->Flag.with(TagFlag.array)) { + let jsonVal = input->B.refine(~expected=json)->parse + jsonVal->B.next( + `JSON.stringify(${jsonVal.inline}${switch expectedSchema.space { + | Some(0) + | None => "" + | Some(v) => `,null,${v->X.Int.unsafeToString}` + }})`, + ~schema=expectedSchema, + ~expected=expectedSchema, + ) + } else { + input->B.unsupportedDecode(~from=input.schema, ~target=expectedSchema) + } + }) + () => { if jsonString->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { let _ = %raw(`delete jsonString.as`) - jsonString.tag = String + enableJson() + + jsonString.tag = stringTag jsonString.format = Some(JSON) jsonString.name = Some(`${jsonName} string`) - jsonString.compiler = Some( - Builder.make((b, ~input as inputArg, ~selfSchema, ~path) => { - let inputTagFlag = inputArg.tag->TagFlag.get - let input = ref(inputArg) - - if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { - let to = selfSchema.to->X.Option.getUnsafe - let hasTo: bool = to->Obj.magic - - if hasTo && to->isLiteral { - let inputVar = b->B.Val.var(input.contents) - b.filterCode = `${inputVar}===${b->inlineJsonString( - ~selfSchema, - ~schema=to, - ~path, - )}||${b->B.failWithArg( - ~path, - input => InvalidType({ - expected: to->castToPublic, - received: input, - }), - inputVar, - )};` - input := b->B.constVal(~schema=to) - } else if hasTo && to.format === Some(JSON) { - () - } else { - let inputVar = b->B.Val.var(input.contents) - let withTypeValidation = b.global.flag->Flag.unsafeHas(Flag.typeValidation) + jsonString.encoder = Some(jsonStringEncoder) + jsonString.decoder = jsonStringDecoder + } + } +} - // FIXME: Should have the check in allocate scope? - if withTypeValidation { - b.filterCode = b->B.typeFilterCode(~schema=string, ~input=input.contents, ~path) - } +let uint8Array = shaken("uint8Array") + +let enableUint8Array = () => { + if uint8Array->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete uint8Array.as`) + uint8Array.tag = instanceTag + uint8Array.class = %raw(`Uint8Array`) + uint8Array.decoder = Builder.make((~input as inputArg) => { + let inputTagFlag = inputArg.schema.tag->TagFlag.get + let input = ref(inputArg) + + if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + input := + input.contents->B.next( + `${input.contents->B.embed( + %raw(`new TextEncoder()`), + )}.encode(${input.contents.inline})`, + ~schema=uint8Array, + ) + } else if inputTagFlag->Flag.unsafeHas(TagFlag.unknown->Flag.with(TagFlag.instance)) { + input := instanceDecoder(~input=input.contents) + } - // FIXME: S.refine should run here - - if hasTo || withTypeValidation { - b.code = - b.code ++ - `try{${if hasTo { - jsonableValidation(~output=to, ~parent=to, ~path, ~flag=b.global.flag) - - let targetVal = b->B.allocateVal(~schema=unknown) - input := targetVal - targetVal.inline ++ "=" - } else { - "" - }}JSON.parse(${inputVar})}catch(t){${b->B.failWithArg( - ~path, - input => InvalidType({ - expected: selfSchema->castToPublic, - received: input, - }), - inputVar, - )}}` - } - } - } else { - if input.contents->Obj.magic->isLiteral { - input := - b->B.val( - b->inlineJsonString(~selfSchema, ~schema=input.contents->Obj.magic, ~path), - ~schema=string, - ) - } else if inputTagFlag->Flag.unsafeHas(TagFlag.string) { - if input.contents.format !== Some(JSON) { - input := b->B.val(`JSON.stringify(${input.contents.inline})`, ~schema=string) - } - } else if inputTagFlag->Flag.unsafeHas(TagFlag.number->Flag.with(TagFlag.boolean)) { - input := b->inputToString(input.contents) - } else if inputTagFlag->Flag.unsafeHas(TagFlag.bigint) { - input := b->B.val(`"\\""+${input.contents.inline}+"\\""`, ~schema=string) - } else if inputTagFlag->Flag.unsafeHas(TagFlag.object->Flag.with(TagFlag.array)) { - jsonableValidation( - ~output=input.contents->Obj.magic, - ~parent=input.contents->Obj.magic, - ~path, - ~flag=b.global.flag, + switch inputArg.expected { + | {to, parser: ?None} => { + let toTagFlag = to.tag->TagFlag.get + if toTagFlag->Flag.unsafeHas(TagFlag.string) { + input := + input.contents->B.next( + `${input.contents->B.embed( + %raw(`new TextDecoder()`), + )}.decode(${input.contents.inline})`, + ~schema=string, ) - input := - b->B.val( - `JSON.stringify(${input.contents.inline}${switch selfSchema.space { - | Some(0) - | None => "" - | Some(v) => `,null,${v->X.Int.unsafeToString}` - }})`, - ~schema=string, - ) - } else { - b->B.unsupportedTransform(~from=input.contents, ~target=selfSchema, ~path) - } - input.contents.format = Some(JSON) } - input.contents - }), - ) - } + } + | _ => input.contents + } + }) } } -let jsonStringWithSpace = (space: int) => { - let mut = jsonString->copyWithoutCache - mut.space = Some(space) - mut->castToPublic +let isoDateTime = shaken("isoDateTime") + +let enableIsoDateTime = () => { + if isoDateTime->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete isoDateTime.as`) + // Adapted from https://stackoverflow.com/a/3143231 + // Kept inline so the regex is only pulled into the bundle when + // `enableIsoDateTime` is imported; unused otherwise it's tree-shaken away. + let datetimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/ + isoDateTime.tag = stringTag + isoDateTime.decoder = string.decoder + // Set `format` directly on the schema so `toJSONSchema` picks up + // `"format": "date-time"` from the existing `Some(DateTime)` arm in + // `internalToJSONSchemaBase`'s String case — no `extendJSONSchema` + // metadata overlay needed. + isoDateTime.format = Some(DateTime) + isoDateTime.refiner = Some( + (~input) => { + [{cond: (~inputVar) => `${input->B.embed(datetimeRe)}.test(${inputVar})`, fail: B.failWithErrorMessage("format", ~defaultMessage="Invalid datetime string! Expected UTC")}] + }, + ) + } } -module Int = { - module Refinement = { - type kind = - | Min({value: int}) - | Max({value: int}) +let port = shaken("port") + +let enablePort = () => { + if port->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete port.as`) + port.tag = numberTag + port.decoder = int.decoder + port.format = Some(Port) + port.refiner = Some( + (~input as _) => { + [{ + cond: (~inputVar) => `${inputVar}>0&&${inputVar}<65536&&${inputVar}%1===0`, + fail: B.failWithErrorMessage("format"), + }] + }, + ) + } +} - type t = { - kind: kind, - message: string, - } +let email = shaken("email") + +let enableEmail = () => { + if email->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete email.as`) + // Adapted from https://stackoverflow.com/a/46181/1550155 + let emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i + email.tag = stringTag + email.decoder = string.decoder + email.format = Some(Email) + email.refiner = Some( + (~input) => { + [{cond: (~inputVar) => `${input->B.embed(emailRegex)}.test(${inputVar})`, fail: B.failWithErrorMessage("format")}] + }, + ) + } +} - let metadataId: Metadata.Id.t> = Metadata.Id.internal("Int.refinements") +let uuid = shaken("uuid") + +let enableUuid = () => { + if uuid->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete uuid.as`) + let uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i + uuid.tag = stringTag + uuid.decoder = string.decoder + uuid.format = Some(Uuid) + uuid.refiner = Some( + (~input) => { + [{cond: (~inputVar) => `${input->B.embed(uuidRegex)}.test(${inputVar})`, fail: B.failWithErrorMessage("format")}] + }, + ) } +} - let refinements = schema => { - switch schema->Metadata.get(~id=Refinement.metadataId) { - | Some(m) => m - | None => [] - } +let cuid = shaken("cuid") + +let enableCuid = () => { + if cuid->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete cuid.as`) + let cuidRegex = /^c[^\s-]{8,}$/i + cuid.tag = stringTag + cuid.decoder = string.decoder + cuid.format = Some(Cuid) + cuid.refiner = Some( + (~input) => { + [{cond: (~inputVar) => `${input->B.embed(cuidRegex)}.test(${inputVar})`, fail: B.failWithErrorMessage("format")}] + }, + ) } } -module Float = { - module Refinement = { - type kind = - | Min({value: float}) - | Max({value: float}) - type t = { - kind: kind, - message: string, - } - - let metadataId: Metadata.Id.t> = Metadata.Id.internal("Float.refinements") +let url = shaken("url") + +let enableUrl = () => { + if url->Obj.magic->Js.Dict.unsafeGet(shakenRef)->Obj.magic { + let _ = %raw(`delete url.as`) + let urlValidator: unknown = %raw(`s=>{try{new URL(s);return true}catch(_){return false}}`) + url.tag = stringTag + url.decoder = string.decoder + url.format = Some(Url) + url.refiner = Some( + (~input) => { + [{cond: (~inputVar) => `${input->B.embed(urlValidator)}(${inputVar})`, fail: B.failWithErrorMessage("format")}] + }, + ) } +} + +let invalidDateRefine = (input: val) => + input->B.refine( + ~schema=input.expected, + ~checks=[ + { + cond: (~inputVar) => `!Number.isNaN(${inputVar}.getTime())`, + fail: B.failInvalidType, + }, + ], + ) - let refinements = schema => { - switch schema->Metadata.get(~id=Refinement.metadataId) { - | Some(m) => m - | None => [] +let date = { + let mut = base(instanceTag, ~selfReverse=true) + mut.class = %raw(`Date`) + mut.decoder = Builder.make((~input) => { + let inputTagFlag = input.schema.tag->TagFlag.get + if inputTagFlag->Flag.unsafeHas(TagFlag.string) { + input + ->B.next(`new Date(${input.inline})`, ~schema=mut->castToPublic->castToInternal) + ->invalidDateRefine + } else if inputTagFlag->Flag.unsafeHas(TagFlag.unknown) { + instanceDecoder(~input)->invalidDateRefine + } else if inputTagFlag->Flag.unsafeHas(TagFlag.instance) && input.schema.class === mut.class { + input + } else { + input->B.unsupportedDecode(~from=input.schema, ~target=input.expected) } - } + }) + + // Encoder: Date → string (via toISOString) when target is string + mut.encoder = Some( + Builder.encoder((~input, ~target) => { + let toTagFlag = target.tag->TagFlag.get + if toTagFlag->Flag.unsafeHas(TagFlag.string) { + let dateTimeString = string->copySchema + dateTimeString.format = Some(DateTime) + input + ->B.next( + `${input.inline}.toISOString()`, + ~schema=dateTimeString, + ~expected=target, + ) + ->parse + } else { + input + } + }), + ) + mut->castToPublic } let to = (from, target) => { @@ -3759,7 +4995,7 @@ let to = (from, target) => { // switch parser { // | Some(p) => // mut.parser = Some( - // Builder.make((b, ~input, ~selfSchema as _, ~path as _) => { + // Builder.make((b, ~input, , ~path as _) => { // // TODO: Support async, reverse, nested parsing // b->B.embedSyncOperation(~input, ~fn=p) // }), @@ -3772,23 +5008,17 @@ let to = (from, target) => { let list = schema => { schema - ->Array.factory + ->array ->transform(_ => { parser: array => array->Belt.List.fromArray, serializer: list => list->Belt.List.toArray, }) } -let instance = class_ => { - let mut = base(Instance) - mut.class = class_->Obj.magic - mut->castToPublic -} - // TODO: Better test reverse let meta = (schema: t<'value>, data: meta<'value>) => { let schema = schema->castToInternal - let mut = schema->copyWithoutCache + let mut = schema->copySchema switch data.name { | Some("") => mut.name = None | Some(name) => mut.name = Some(name) @@ -3809,9 +5039,18 @@ let meta = (schema: t<'value>, data: meta<'value>) => { | None => () } switch data.examples { - | Some([]) => mut.examples = None - | Some(examples) => - mut.examples = Some(examples->X.Array.map(schema->castToPublic->operationFn(Flag.reverse))) + | Some([]) => mut.examples = None // FIXME: Delete instead of None + | Some(examples) => mut.examples = Some(examples->X.Array.map(getDecoder(~s1=schema->reverse))) + | None => () + } + switch data.errorMessage { + | Some(em) => + let emDict: dict = em->Obj.magic + if emDict->Js.Dict.keys->Js.Array2.length === 0 { + mut.errorMessage = None + } else { + mut.errorMessage = Some(em) + } | None => () } mut->castToPublic @@ -3819,41 +5058,21 @@ let meta = (schema: t<'value>, data: meta<'value>) => { let brand = (schema: t<'value>, id: string) => { let schema = schema->castToInternal - let mut = schema->copyWithoutCache + let mut = schema->copySchema mut.name = Some(id) mut->castToPublic } module Schema = { + type rec shapedSerializerAcc = { + mutable val?: val, + mutable properties?: dict, + mutable flattened?: array, + } + type s = {@as("m") matches: 'value. t<'value> => 'value} - // Definition item - @tag("k") - type rec ditem = - | @as(0) Item({schema: internal, location: string}) // Needed only for ditemToItem - | @as(1) - ItemField({ - location: string, - schema: internal, - @as("of") - target: ditem, - @as("p") - path: string, - }) - | @as(2) - Root({ - schema: internal, - @as("p") - path: string, - @as("i") - idx: int, - }) - // Like ditem but for reversed schema - @tag("k") - type ritem = - | @as(0) Registred({@as("p") path: Path.t, @as("s") schema: internal}) - | @as(1) Discriminant({@as("p") path: Path.t, @as("s") schema: internal}) - | @as(2) Node({@as("p") path: Path.t, @as("s") schema: internal}) + let inputFrom = X.Array.immutableEmpty type advancedObjectCtx = { // Public API for JS/TS users. @@ -3867,443 +5086,70 @@ module Schema = { module Definition = { type t<'embeded> - type node<'embeded> = dict> @inline let isNode = (definition: 'any) => definition->Type.typeof === #object && definition !== %raw(`null`) - let toConstant = (Obj.magic: t<'embeded> => unknown) - let toNode = (Obj.magic: t<'embeded> => node<'embeded>) - @inline - let toEmbededItem = (definition: t<'embeded>): option => + let toEmbededItem = (definition: t<'embeded>): option<'embeded> => definition->Obj.magic->X.Dict.getUnsafeOptionBySymbol(itemSymbol) } - @inline - let getRitemSchema = (ritem: ritem): internal => (ritem->Obj.magic)["s"] - @inline - let getRitemPath = (ritem: ritem): string => (ritem->Obj.magic)["p"] - - external ditemToItem: ditem => item = "%identity" - external itemToDitem: item => ditem = "%identity" - - let rec getFullDitemPath = (ditem: ditem) => { - switch ditem { - | ItemField({target, path}) => Path.concat(getFullDitemPath(target), path) - | Item({location}) => location->Path.fromLocation - | Root({path}) => path - } - } - - // Can probably only keep the target path - @inline - let setItemRitem = (item: ditem, ritem: ritem) => (item->Obj.magic)["r"] = ritem - @inline - let getItemRitem = (item: ditem): option => (item->Obj.magic)["r"] - - @inline - let getDitemSchema = (item: ditem): internal => (item->Obj.magic)["schema"] - @inline - let getUnsafeDitemIndex = (item: ditem): string => (item->Obj.magic)["i"] - - let rec definitionToOutput = ( - b, - ~definition: Definition.t, - ~getItemOutput, - ~outputSchema, - ) => { - if outputSchema->isLiteral { - b->B.constVal(~schema=outputSchema) - } else { - switch definition->Definition.toEmbededItem { - | Some(item) => item->getItemOutput - | None => { - let node = definition->Definition.toNode - let isArray = TagFlag.isArray(outputSchema.tag) - let objectVal = b->B.Val.Object.make(~isArray) - - outputSchema.items - ->X.Option.getUnsafe - ->Js.Array2.forEach(item => { - objectVal->B.Val.Object.add( - ~location=item.location, - b->definitionToOutput( - ~definition=node->Js.Dict.unsafeGet(item.location), - ~getItemOutput, - ~outputSchema=item.schema->castToInternal, - ), - ) - }) - - objectVal->B.Val.Object.complete(~isArray) - } - } - } - } - - let objectStrictModeCheck = (b, ~input, ~items, ~selfSchema: internal, ~path) => { - if ( - selfSchema.tag === Object && - selfSchema.additionalItems === Some(Strict) && - b.global.flag->Flag.unsafeHas(Flag.typeValidation) - ) { - let key = b->B.allocateVal(~schema=unknown) - let keyVar = key.inline - b.code = b.code ++ `for(${keyVar} in ${b->B.Val.var(input)}){if(` - switch items { - | [] => b.code = b.code ++ "true" - | _ => - for idx in 0 to items->Js.Array2.length - 1 { - let {location} = items->Js.Array2.unsafe_get(idx) - if idx !== 0 { - b.code = b.code ++ "&&" - } - b.code = b.code ++ `${keyVar}!==${b->B.inlineLocation(location)}` - } - } - b.code = - b.code ++ - `){${b->B.failWithArg(~path, exccessFieldName => ExcessField(exccessFieldName), keyVar)}}}` + let rec proxifyShapedSchema = (schema: internal, ~from, ~fromFlattened=?): 'a => { + let mut = schema->getOutputSchema->copySchema + mut.from = Some(from) + switch fromFlattened { + | Some(index) => mut.fromFlattened = Some(index) + | None => () } - } - - let rec proxify = (item: ditem): 'a => - X.Object.immutableEmpty->X.Proxy.make({ - get: (~target as _, ~prop) => { + mut + ->X.Proxy.make({ + get: (~target, ~prop) => { if prop === itemSymbol->Obj.magic { - item->Obj.magic + target->Obj.magic } else { let location = prop->(Obj.magic: unknown => string) - let inlinedLocation = location->X.Inlined.Value.fromString - ItemField({ - schema: { - let targetReversed = item->getDitemSchema->getOutputSchema - let maybeField = switch targetReversed { - | {properties} => properties->X.Dict.getUnsafeOption(location) - // If there are no properties, then it must be Tuple - | {items} => - switch items->X.Array.getUnsafeOptionByString(location) { - | Some(i) => Some(i.schema->castToInternal) - | None => None - } - | _ => None - } - if maybeField === None { - InternalError.panic( - `Cannot read property ${inlinedLocation} of ${targetReversed - ->castToPublic - ->toExpression}`, - ) - } - maybeField->X.Option.getUnsafe - }, - location, - target: item, - path: Path.fromInlinedLocation(inlinedLocation), - }) - ->proxify - ->Obj.magic - } - }, - }) - - let rec schemaCompiler = (b, ~input: val, ~selfSchema, ~path) => { - let additionalItems = selfSchema.additionalItems - let items = selfSchema.items->X.Option.getUnsafe - let isArray = TagFlag.isArray(selfSchema.tag) - - if b.global.flag->Flag.unsafeHas(Flag.flatten) { - let objectVal = b->B.Val.Object.make(~isArray) - for idx in 0 to items->Js.Array2.length - 1 { - let {location} = items->Js.Array2.unsafe_get(idx) - objectVal->B.Val.Object.add( - ~location, - input.properties->X.Option.getUnsafe->Js.Dict.unsafeGet(location), - ) - } - objectVal->B.Val.Object.complete(~isArray) - } else { - let objectVal = b->B.Val.Object.make(~isArray) - - for idx in 0 to items->Js.Array2.length - 1 { - let {schema, location} = items->Js.Array2.unsafe_get(idx) - let schema = schema->castToInternal - - let itemInput = b->B.Val.get(input, location) - let path = path->Path.concat(b->B.inlineLocation(location)->Path.fromInlinedLocation) - objectVal->B.Val.Object.add(~location, b->parse(~schema, ~input=itemInput, ~path)) - } - - b->objectStrictModeCheck(~input, ~items, ~selfSchema, ~path) - - if ( - (additionalItems !== Some(Strip) || b.global.flag->Flag.unsafeHas(Flag.reverse)) && - // A hacky way to detect that the schema is not transformed - // If we don't Strip or perform a reverse operation, return the original - // instance of Val, so other code also think that the schema value is not transformed - items->Js.Array2.every(item => { - objectVal.properties - ->X.Option.getUnsafe - ->Js.Dict.unsafeGet(item.location) === - input.properties - ->X.Option.getUnsafe - ->Js.Dict.unsafeGet(item.location) - }) - ) { - input.additionalItems = Some(Strip) - input - } else { - objectVal->B.Val.Object.complete(~isArray) - } - } - } - - and advancedBuilder = (~definition, ~flattened: option>=?) => - (b, ~input: val, ~selfSchema, ~path) => { - let isFlatten = b.global.flag->Flag.unsafeHas(Flag.flatten) - let outputs = isFlatten ? input.properties->Obj.magic : Js.Dict.empty() - - if !isFlatten { - let items = selfSchema.items->X.Option.getUnsafe - - for idx in 0 to items->Js.Array2.length - 1 { - let {schema, location} = items->Js.Array2.unsafe_get(idx) - let schema = schema->castToInternal - - let itemInput = b->B.Val.get(input, location) - let path = path->Path.concat(b->B.inlineLocation(location)->Path.fromInlinedLocation) - outputs->Js.Dict.set(location, b->parse(~schema, ~input=itemInput, ~path)) - } - - b->objectStrictModeCheck(~input, ~items, ~selfSchema, ~path) - } - - switch flattened { - | None => () - | Some(rootItems) => - let prevFlag = b.global.flag - b.global.flag = prevFlag->Flag.with(Flag.flatten) - for idx in 0 to rootItems->Js.Array2.length - 1 { - let item = rootItems->Js.Array2.unsafe_get(idx) - outputs - ->Js.Dict.set( - item->getUnsafeDitemIndex, - b->parse(~schema=item->getDitemSchema, ~input, ~path), - ) - ->ignore - } - b.global.flag = prevFlag - } - - let rec getItemOutput = item => { - switch item { - | ItemField({target: item, location}) => b->B.Val.get(item->getItemOutput, location) - | Item({location}) => outputs->Js.Dict.unsafeGet(location) - | Root({idx}) => outputs->Js.Dict.unsafeGet(idx->X.Int.unsafeToString) - } - } - - let output = - b->definitionToOutput( - ~definition=definition->(Obj.magic: unknown => Definition.t), - ~getItemOutput, - ~outputSchema=selfSchema.to->X.Option.getUnsafe, - ) - - output - } - and definitionToTarget = (~definition, ~to=?, ~flattened=?) => { - let definition = definition->(Obj.magic: unknown => Definition.t) - - let ritemsByItemPath = Js.Dict.empty() - - let ritem = definition->definitionToRitem(~path=Path.empty, ~ritemsByItemPath) - let mut = ritem->getRitemSchema - - // This should be done in the parser/serializer - let _ = %raw(`delete mut.refiner`) - let _ = %raw(`delete mut.compiler`) - - mut.serializer = Some( - Builder.make((b, ~input, ~selfSchema, ~path) => { - let getRitemInput = ritem => { - let ritemPath = ritem->getRitemPath - ritemPath === Path.empty - ? input - : { - let rec loop = (~input, ~locations) => { - switch locations { - | [] => input - | _ => { - let location = locations->Js.Array2.unsafe_get(0) - loop( - ~input=b->B.Val.get(input, location), - ~locations=locations->Js.Array2.sliceFrom(1), - ) - } - } - } - - // FIXME: Use better path representation - loop(~input, ~locations=ritemPath->Path.toArray) - } - } - - let rec schemaToOutput = (schema, ~originalPath) => { - let outputSchema = schema->getOutputSchema - if outputSchema->isLiteral { - b->B.constVal(~schema=outputSchema) - } else if schema->isLiteral { - b->parse(~schema, ~input=b->B.constVal(~schema), ~path) - } else { - switch outputSchema { - | {items, tag, ?additionalItems} - // Ignore S.dict and S.array - if additionalItems->Obj.magic->Js.typeof === "string" => { - let isArray = TagFlag.isArray(tag) - let objectVal = b->B.Val.Object.make(~isArray) - for idx in 0 to items->Js.Array2.length - 1 { - let item = items->Js.Array2.unsafe_get(idx) - let itemPath = - originalPath->Path.concat( - Path.fromInlinedLocation(b->B.inlineLocation(item.location)), - ) - let itemInput = switch ritemsByItemPath->X.Dict.getUnsafeOption(itemPath) { - | Some(ritem) => - b->parse( - ~schema=item.schema->castToInternal, - ~input=ritem->getRitemInput, - ~path=ritem->getRitemPath, - ) - | None => item.schema->castToInternal->schemaToOutput(~originalPath=itemPath) - } - objectVal->B.Val.Object.add(~location=item.location, itemInput) - } - objectVal->B.Val.Object.complete(~isArray) - } - | _ => - b->B.invalidOperation( - ~path, - ~description={ - switch originalPath { - | "" => `Schema isn't registered` - | _ => `Schema for ${originalPath} isn't registered` - } - }, - ) - } - } - } - - let getItemOutput = (item, ~itemPath, ~shouldReverse) => { - switch item->getItemRitem { - | Some(ritem) => { - // If item is transformed to root, then we - // don't want to apply the whole parse chain, - // but only to the output schema. - // Because it'll be parsed later anyways. - let targetSchema = if shouldReverse { - item->getDitemSchema->reverse - } else if itemPath === Path.empty { - item->getDitemSchema->getOutputSchema - } else { - item->getDitemSchema - } - - let itemInput = ritem->getRitemInput - - let path = path->Path.concat(ritem->getRitemPath) - b->parse(~schema=targetSchema, ~input=itemInput, ~path) - } - | None => schemaToOutput(item->getDitemSchema, ~originalPath=itemPath) - } - } - - switch to { - | Some(ditem) => ditem->getItemOutput(~itemPath=Path.empty, ~shouldReverse=false) - | None => { - let originalSchema = selfSchema.to->X.Option.getUnsafe - - b->objectStrictModeCheck( - ~input, - ~items=selfSchema.items->X.Option.getUnsafe, - ~selfSchema, - ~path, - ) - let isArray = (originalSchema: internal).tag === Array - let items = originalSchema.items->X.Option.getUnsafe - let objectVal = b->B.Val.Object.make(~isArray) - switch flattened { - | None => () - | Some(rootItems) => - for idx in 0 to rootItems->Js.Array2.length - 1 { - objectVal->B.Val.Object.merge( - rootItems - ->Js.Array2.unsafe_get(idx) - ->getItemOutput(~itemPath=Path.empty, ~shouldReverse=true), - ) - } + { + let maybeField = switch target { + | {properties} => properties->X.Dict.getUnsafeOption(location) + // If there are no properties, then it must be Tuple + | {items} => items->X.Array.getUnsafeOptionByString(location) + | _ => None } - - for idx in 0 to items->Js.Array2.length - 1 { - let item: item = items->Js.Array2.unsafe_get(idx) - - // TODO: Improve the hack to ignore items belonging to a flattened schema - if !(objectVal.properties->X.Option.getUnsafe->Stdlib.Dict.has(item.location)) { - objectVal->B.Val.Object.add( - ~location=item.location, - item - ->itemToDitem - ->getItemOutput( - ~itemPath=b->B.inlineLocation(item.location)->Path.fromInlinedLocation, - ~shouldReverse=false, - ), - ) - } + if maybeField === None { + InternalError.panic( + `Cannot read property "${location}" of ${target + ->castToPublic + ->toExpression}`, + ) } - - objectVal->B.Val.Object.complete(~isArray) + maybeField->X.Option.getUnsafe } + ->proxifyShapedSchema( + ~from=target.from->X.Option.getUnsafe->X.Array.append(location), + ~fromFlattened=?target.fromFlattened, + ) + ->Obj.magic } - }), - ) - - mut + }, + }) + ->Obj.magic } - and shape = { + let rec shape = { (schema: t<'value>, definer: 'value => 'variant): t<'variant> => { let schema = schema->castToInternal schema->updateOutput(mut => { - let ditem: ditem = Root({ - schema, - path: Path.empty, - idx: 0, - }) - let definition: unknown = definer(ditem->proxify)->Obj.magic - - mut.parser = Some( - Builder.make((b, ~input, ~selfSchema, ~path as _) => { - let rec getItemOutput = item => { - switch item { - | ItemField({target: item, location}) => b->B.Val.get(item->getItemOutput, location) - | _ => input - } - } - let output = - b->definitionToOutput( - ~definition=definition->(Obj.magic: unknown => Definition.t), - ~getItemOutput, - ~outputSchema=selfSchema.to->X.Option.getUnsafe, - ) - - output - }), - ) - mut.to = Some(definitionToTarget(~definition, ~to=ditem)) + let fromProxy = mut->proxifyShapedSchema(~from=inputFrom) + let definition: unknown = definer(fromProxy)->Obj.magic + if definition === fromProxy { + () + } else { + mut.parser = Some(shapedParser) + mut.to = Some(definitionToShapedSchema(definition)) + } }) } } @@ -4314,24 +5160,22 @@ module Schema = { switch parentCtx->X.Dict.getUnsafeOption(cacheId) { | Some(ctx) => ctx | None => { - let schemas = [] - let properties = Js.Dict.empty() - let items = [] - + let required = [] let schema = { - let schema = base(Object) - schema.items = Some(items) + let schema = base(objectTag, ~selfReverse=false) + schema.required = Some(required) schema.properties = Some(properties) schema.additionalItems = Some(globalConfig.defaultAdditionalItems) - schema.compiler = Some(schemaCompiler) + schema.decoder = objectDecoder schema->castToPublic } - let target = + let parentSchema = ( parentCtx.field(fieldName, schema) ->Definition.toEmbededItem - ->X.Option.getUnsafe + ->X.Option.getUnsafe: internal + ) let field: type value. (string, schema) => value = @@ -4341,17 +5185,12 @@ module Schema = { if properties->Stdlib.Dict.has(fieldName) { InternalError.panic(`The field ${inlinedLocation} defined twice`) } - let ditem: ditem = ItemField({ - target, - schema, - location: fieldName, - path: Path.fromInlinedLocation(inlinedLocation), - }) - let item = ditem->ditemToItem + required->Js.Array2.push(fieldName)->ignore properties->Js.Dict.set(fieldName, schema) - items->Js.Array2.push(item)->ignore - schemas->Js.Array2.push(schema)->ignore - ditem->proxify + schema->proxifyShapedSchema( + ~from=parentSchema.from->X.Option.getUnsafe->X.Array.append(fieldName), + ~fromFlattened=?parentSchema.fromFlattened, + ) } let tag = (tag, asValue) => { @@ -4365,7 +5204,7 @@ module Schema = { let flatten = schema => { let schema = schema->castToInternal switch schema { - | {tag: Object, items: ?flattenedItems, ?to} => { + | {tag: Object, properties: ?flattenedProperties, ?to} => { if to->Obj.magic { InternalError.panic( `Unsupported nested flatten for transformed object schema ${schema @@ -4373,11 +5212,15 @@ module Schema = { ->toExpression}`, ) } - let flattenedItems = flattenedItems->X.Option.getUnsafe + let flattenedProperties = flattenedProperties->X.Option.getUnsafe + let flattenedKeys = flattenedProperties->Js.Dict.keys let result = Js.Dict.empty() - for idx in 0 to flattenedItems->Js.Array2.length - 1 { - let item = flattenedItems->Js.Array2.unsafe_get(idx) - result->Js.Dict.set(item.location, field(item.location, item.schema)) + for idx in 0 to flattenedKeys->Js.Array2.length - 1 { + let key = flattenedKeys->Js.Array2.unsafe_get(idx) + result->Js.Dict.set( + key, + field(key, flattenedProperties->Js.Dict.unsafeGet(key)->castToPublic), + ) } result->Obj.magic } @@ -4405,41 +5248,30 @@ module Schema = { and object: type value. (Object.s => value) => schema = definer => { - let flattened = %raw(`void 0`) - let items = [] + let flattened: option> = %raw(`void 0`) let properties = Js.Dict.empty() let flatten = schema => { let schema = schema->castToInternal switch schema { - | {tag: Object, items: ?flattenedItems} => { - let flattenedItems = flattenedItems->X.Option.getUnsafe - for idx in 0 to flattenedItems->Js.Array2.length - 1 { - let {location, schema: flattenedSchema} = flattenedItems->Js.Array2.unsafe_get(idx) - let flattenedSchema = flattenedSchema->castToInternal - switch properties->X.Dict.getUnsafeOption(location) { + | {tag: Object, properties: ?flattenedProperties} => { + let flattenedProperties = flattenedProperties->X.Option.getUnsafe + let flattenedKeys = flattenedProperties->Js.Dict.keys + for idx in 0 to flattenedKeys->Js.Array2.length - 1 { + let key = flattenedKeys->Js.Array2.unsafe_get(idx) + let flattenedSchema = flattenedProperties->Js.Dict.unsafeGet(key) + switch properties->X.Dict.getUnsafeOption(key) { | Some(schema) if schema === flattenedSchema => () | Some(_) => - InternalError.panic( - `The field "${location}" defined twice with incompatible schemas`, - ) - | None => - let item = Item({ - schema: flattenedSchema, - location, - })->ditemToItem - items->Js.Array2.push(item)->ignore - properties->Js.Dict.set(location, flattenedSchema) + InternalError.panic(`The field "${key}" defined twice with incompatible schemas`) + | None => properties->Js.Dict.set(key, flattenedSchema) } } let f = %raw(`flattened || (flattened = [])`) - let item = Root({ - schema, - path: Path.empty, - idx: f->Js.Array2.length, - }) - f->Js.Array2.push(item)->ignore - item->proxify + schema->proxifyShapedSchema( + ~from=inputFrom, + ~fromFlattened=f->Js.Array2.push(schema) - 1, + ) } | _ => InternalError.panic( @@ -4456,14 +5288,8 @@ module Schema = { if properties->Stdlib.Dict.has(fieldName) { InternalError.panic(`The field "${fieldName}" defined twice with incompatible schemas`) } - let ditem: ditem = Item({ - schema, - location: fieldName, - }) - let item = ditem->ditemToItem properties->Js.Dict.set(fieldName, schema) - items->Js.Array2.push(item)->ignore - ditem->proxify + schema->proxifyShapedSchema(~from=[fieldName]) } let tag = (tag, asValue) => { @@ -4487,12 +5313,16 @@ module Schema = { let definition = definer((ctx :> Object.s))->(Obj.magic: value => unknown) - let mut = base(Object) - mut.items = Some(items) + let mut = base(objectTag, ~selfReverse=false) + mut.required = Some(properties->Js.Dict.keys) mut.properties = Some(properties) mut.additionalItems = Some(globalConfig.defaultAdditionalItems) - mut.parser = Some(advancedBuilder(~definition, ~flattened)) - mut.to = Some(definitionToTarget(~definition, ~flattened)) + mut.decoder = objectDecoder + mut.parser = Some(shapedParser) + mut.to = Some(definitionToShapedSchema(definition)) + if flattened !== None { + mut.flattened = flattened + } mut->castToPublic } and tuple = definer => { @@ -4507,12 +5337,8 @@ module Schema = { if items->X.Array.has(idx) { InternalError.panic(`The item [${location}] is defined multiple times`) } else { - let ditem = Item({ - schema, - location, - }) - items->Js.Array2.unsafe_set(idx, ditem->ditemToItem) - ditem->proxify + items->Js.Array2.unsafe_set(idx, schema) + schema->proxifyShapedSchema(~from=[idx->Js.Int.toString]) } } @@ -4529,160 +5355,357 @@ module Schema = { for idx in 0 to items->Js.Array2.length - 1 { if items->Js.Array2.unsafe_get(idx)->Obj.magic->not { - let location = idx->Js.Int.toString - let ditem = { - location, - schema: unit->castToPublic, - } - items->Js.Array2.unsafe_set(idx, ditem) + items->Js.Array2.unsafe_set(idx, unit) } } - let mut = base(Array) + let mut = base(arrayTag, ~selfReverse=false) mut.items = Some(items) mut.additionalItems = Some(Strict) - mut.parser = Some(advancedBuilder(~definition)) - mut.to = Some(definitionToTarget(~definition)) + mut.decoder = arrayDecoder + mut.parser = Some(shapedParser) + mut.to = Some(definitionToShapedSchema(definition)) mut->castToPublic } - and definitionToRitem = (definition: Definition.t, ~path, ~ritemsByItemPath) => { - if definition->Definition.isNode { - switch definition->Definition.toEmbededItem { - | Some(item) => - let ritemSchema = item->getDitemSchema->getOutputSchema->copyWithoutCache - let _ = %raw(`delete ritemSchema.serializer`) - let ritem = Registred({ - path, - schema: ritemSchema, - }) - item->setItemRitem(ritem) - ritemsByItemPath->Js.Dict.set(item->getFullDitemPath, ritem) - ritem - | None => { - let node = definition->Definition.toNode - if node->X.Array.isArray { - let node = node->(Obj.magic: Definition.node => array>) - let items = [] - for idx in 0 to node->Js.Array2.length - 1 { - let location = idx->Js.Int.toString - let inlinedLocation = `"${location}"` - let ritem = definitionToRitem( - node->Js.Array2.unsafe_get(idx), - ~path=path->Path.concat(Path.fromInlinedLocation(inlinedLocation)), - ~ritemsByItemPath, + and getValByFrom = (~input, ~from, ~idx) => { + // FIXME: TODO: something with flattened + switch from->X.Array.getUnsafeOption(idx) { + | Some(key) => + getValByFrom( + ~input=input.vals->X.Option.getUnsafe->Js.Dict.unsafeGet(key), + ~from, + ~idx=idx + 1, + ) + | None => input + } + } + and getShapedParserOutput = (~input, ~targetSchema) => { + let v = switch targetSchema { + | {fromFlattened} => + getValByFrom( + ~input=input.flattenedVals->X.Option.getUnsafe->Js.Array2.unsafe_get(fromFlattened), + ~from=targetSchema.from->X.Option.getUnsafe, + ~idx=0, + )->B.Val.scope + | {from} => getValByFrom(~input, ~from, ~idx=0)->B.Val.scope + | _ => + if targetSchema->isLiteral { + input->B.nextConst(~schema=targetSchema) + } else { + let output = makeObjectVal(input, ~schema=targetSchema) + output.isOutput = Some(true) + switch targetSchema { + | {items} => + for idx in 0 to items->Js.Array2.length - 1 { + let location = idx->Js.Int.toString + output->B.Val.Object.add( + ~location, + getShapedParserOutput(~input, ~targetSchema=items->Js.Array2.unsafe_get(idx)), + ) + } + | {properties} => { + let keys = properties->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + let location = keys->Js.Array2.unsafe_get(idx) + output->B.Val.Object.add( + ~location, + getShapedParserOutput( + ~input, + ~targetSchema=properties->Js.Dict.unsafeGet(location), + ), ) - let item = { - location, - schema: ritem->getRitemSchema->castToPublic, + } + } + | _ => + // FIXME: Use a path + InternalError.panic( + `Don't know where the value is coming from: ${targetSchema + ->castToPublic + ->toExpression}`, + ) + } + output->completeObjectVal + } + } + v.prev = None + v.expected = targetSchema + v + } + and shapedParser = (~input) => { + switch input.expected.flattened { + | Some(flattened) => + let flattenedVals = [] + for idx in 0 to flattened->Js.Array2.length - 1 { + let flattenedSchema = flattened->Js.Array2.unsafe_get(idx) + let flattenedInput = input->B.Val.scope + flattenedInput.expected = flattenedSchema + flattenedInput.isOutput = Some(false) + flattenedInput.isInput = Some(false) + let flattenedVal = flattenedInput->parse + flattenedVals->Js.Array2.push(flattenedVal)->ignore + input.codeFromPrev = input.codeFromPrev ++ flattenedVal->B.merge + } + input.flattenedVals = Some(flattenedVals) + | None => () + } + + let targetSchema = input.expected.to->X.Option.getUnsafe + let output = getShapedParserOutput(~input, ~targetSchema) + output.hasTransform = Some(true) + output.prev = Some(input) + output + } + + and prepareShapedSerializerAcc = (~acc: shapedSerializerAcc, ~input: val) => { + switch input { + | {expected: {from, ?fromFlattened}} => + let accAtFrom = ref( + switch fromFlattened { + | Some(idx) => { + if acc.flattened === None { + acc.flattened = Some([]) + } + switch acc.flattened->X.Option.getUnsafe->X.Array.getUnsafeOption(idx) { + | None => { + let newAcc: shapedSerializerAcc = {} + acc.flattened->X.Option.getUnsafe->Js.Array2.unsafe_set(idx, newAcc) + newAcc } - items->Js.Array2.unsafe_set(idx, item) + | Some(acc) => acc + } + } + | None => acc + }, + ) + for idx in 0 to from->Js.Array2.length - 1 { + let key = from->Js.Array2.unsafe_get(idx) + let p = switch accAtFrom.contents.properties { + | Some(p) => p + | None => { + let p = Js.Dict.empty() + + accAtFrom.contents.properties = Some(p) + p + } + } + accAtFrom := + switch p->X.Dict.getUnsafeOption(key) { + | Some(acc) => acc + | None => { + let newAcc: shapedSerializerAcc = {} + p->Js.Dict.set(key, newAcc) + newAcc + } + } + } + accAtFrom.contents.val = Some(input) + | {vals} => { + let keys = vals->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + prepareShapedSerializerAcc( + ~acc, + ~input=vals->Js.Dict.unsafeGet(keys->Js.Array2.unsafe_get(idx)), + ) + } + } + | _ => () + } + } + and getShapedSerializerOutput = ( + ~input, + ~acc: option, + ~targetSchema: internal, + ~path, + ) => { + switch acc { + | Some({val}) => { + let v = val->B.Val.scope + v.hasTransform = Some(true) + v.schema = targetSchema + v.expected = targetSchema + v->parse + } + | _ => + if targetSchema->isLiteral { + let v = input->B.nextConst(~schema=targetSchema, ~expected=targetSchema) + v.prev = None + v.parent = Some(input) + v.var = B._notVarAtParent + v.isOutput = Some(true) + v->parse + } else { + // When acc is None (discriminant field with no input), follow the to chain + // to get the actual output schema properties (e.g., for reversed transformed objects) + let resolvedTargetSchema = if acc === None { + targetSchema->getOutputSchema + } else { + targetSchema + } + let v = makeObjectVal(input, ~schema=resolvedTargetSchema) + v.expected = resolvedTargetSchema + v.isOutput = Some(true) + v.prev = None + v.parent = Some(input) + v.var = B._notVarAtParent + + switch resolvedTargetSchema { + | {items} + if !( + acc === None && + resolvedTargetSchema.additionalItems->Js.typeof === (objectTag :> string) + ) => + for idx in 0 to items->Js.Array2.length - 1 { + let location = idx->Js.Int.toString + v->B.Val.Object.add( + ~location, + getShapedSerializerOutput( + ~input, + ~acc=switch acc { + | Some({properties}) => properties->X.Dict.getUnsafeOption(location) + | _ => None + }, + ~targetSchema=items->Js.Array2.unsafe_get(idx), + ~path=path->Path.concat( + Path.fromInlinedLocation(input.global->B.inlineLocation(location)), + ), + ), + ) + } + | {properties, ?flattened} + if !( + acc === None && + resolvedTargetSchema.additionalItems->Js.typeof === (objectTag :> string) + ) => { + switch (flattened, acc) { + | (Some(flattenedSchemas), Some({flattened: flattenedAcc})) => + flattenedAcc->Js.Array2.forEachi((acc, idx) => { + let flattenedOutput = getShapedSerializerOutput( + ~input, + ~acc=Some(acc), + ~targetSchema=flattenedSchemas->Js.Array2.unsafe_get(idx)->reverse, + ~path, + ) + v->B.Val.Object.merge(flattenedOutput.vals->X.Option.getUnsafe) + }) + | _ => () } - Node({ - path, - schema: { - let mut = base(Array) - mut.items = Some(items) - mut.additionalItems = Some(Strict) - mut.serializer = Some(neverBuilder) - mut - }, - }) - } else { - let fieldNames = node->Js.Dict.keys - let node = node->(Obj.magic: Definition.node => dict>) - let properties = Js.Dict.empty() - let items = [] - for idx in 0 to fieldNames->Js.Array2.length - 1 { - let location = fieldNames->Js.Array2.unsafe_get(idx) - let inlinedLocation = location->X.Inlined.Value.fromString - let ritem = definitionToRitem( - node->Js.Dict.unsafeGet(location), - ~path=path->Path.concat(Path.fromInlinedLocation(inlinedLocation)), - ~ritemsByItemPath, - ) - let item = { - location, - schema: ritem->getRitemSchema->castToPublic, + let keys = properties->Js.Dict.keys + for idx in 0 to keys->Js.Array2.length - 1 { + let location = keys->Js.Array2.unsafe_get(idx) + + // Skip fields added by flattened + if !(v.vals->X.Option.getUnsafe->Stdlib.Dict.has(location)) { + v->B.Val.Object.add( + ~location, + getShapedSerializerOutput( + ~input, + ~acc=switch acc { + | Some({properties}) => properties->X.Dict.getUnsafeOption(location) + | _ => None + }, + ~targetSchema=properties->Js.Dict.unsafeGet(location), + ~path=path->Path.concat( + Path.fromInlinedLocation(input.global->B.inlineLocation(location)), + ), + ), + ) } - items->Js.Array2.unsafe_set(idx, item) - properties->Js.Dict.set(location, item.schema->castToInternal) } - - Node({ - path, - schema: { - let mut = base(Object) - mut.items = Some(items) - mut.properties = Some(properties) - mut.additionalItems = Some(globalConfig.defaultAdditionalItems) - mut.serializer = Some(neverBuilder) - mut - }, - }) } + | _ => + let path = switch targetSchema.from { + | Some(from) => path ++ from->Js.Array2.map(item => `["${item}"]`)->Js.Array2.joinWith("") + | None => path + } + input->B.invalidOperation( + ~description={ + `Missing input for ${targetSchema->castToPublic->toExpression}` ++ + switch path { + | "" => "" + | _ => ` at ${path}` + } + }, + ) } + + v->completeObjectVal } - } else { - Discriminant({ - path, - schema: Literal.parse(definition->Definition.toConstant)->copyWithoutCache, - }) } } - and definitionToSchema = (definition: unknown): internal => { - if definition->Definition.isNode { - if definition->isSchemaObject { - definition->(Obj.magic: unknown => internal) - } else if definition->X.Array.isArray { - let node = definition->(Obj.magic: unknown => array) - for idx in 0 to node->Js.Array2.length - 1 { - let schema = node->Js.Array2.unsafe_get(idx)->definitionToSchema - let location = idx->Js.Int.toString - node->Js.Array2.unsafe_set( - idx, - { - location, - schema: schema->castToPublic, - }->(Obj.magic: item => unknown), - ) - } - let items = node->(Obj.magic: array => array) + and shapedSerializer = (~input) => { + let acc: shapedSerializerAcc = {} + prepareShapedSerializerAcc(~acc, ~input) + + let targetSchema = input.expected.to->X.Option.getUnsafe + let output = getShapedSerializerOutput(~input, ~acc=Some(acc), ~targetSchema, ~path=Path.empty) + output.hasTransform = Some(true) + output.prev = Some(input) + output + } - let mut = base(Array) - mut.items = Some(items) - mut.additionalItems = Some(Strict) - mut.compiler = Some(schemaCompiler) - mut + and definitionToShapedSchema = definition => { + let s = + definition + ->traverseDefinition( + ~onNode=Definition.toEmbededItem->( + Obj.magic: (Definition.t => option) => unknown => option + ), + ) + ->copySchema + s.serializer = Some(shapedSerializer) + s + } + and definitionToSchema = definition => + definition->traverseDefinition(~onNode=node => { + if node->isSchemaObject { + node->(Obj.magic: unknown => option) } else { - let cnstr = (definition->Obj.magic)["constructor"] - if cnstr->Obj.magic && cnstr !== %raw(`Object`) { - { - tag: Instance, - class: cnstr, - const: definition->Obj.magic, + None + } + }) + and traverseDefinition = (definition: unknown, ~onNode): internal => { + if definition->Definition.isNode { + switch onNode(definition) { + | Some(s) => s + | None => + if definition->X.Array.isArray { + let node = definition->(Obj.magic: unknown => array) + for idx in 0 to node->Js.Array2.length - 1 { + let schema = node->Js.Array2.unsafe_get(idx)->traverseDefinition(~onNode) + node->Js.Array2.unsafe_set(idx, schema->(Obj.magic: internal => unknown)) } + let items = node->(Obj.magic: array => array) + + let mut = base(arrayTag, ~selfReverse=false) + mut.items = Some(items) + mut.additionalItems = Some(Strict) + mut.decoder = arrayDecoder + mut } else { - let node = definition->(Obj.magic: unknown => dict) - let fieldNames = node->Js.Dict.keys - let length = fieldNames->Js.Array2.length - let items = [] - for idx in 0 to length - 1 { - let location = fieldNames->Js.Array2.unsafe_get(idx) - let schema = node->Js.Dict.unsafeGet(location)->definitionToSchema - let item = { - schema: schema->castToPublic, - location, + let cnstr = (definition->Obj.magic)["constructor"] + if cnstr->Obj.magic && cnstr !== %raw(`Object`) { + let mut = base(instanceTag, ~selfReverse=true) + mut.class = cnstr + mut.const = definition->Obj.magic + mut.decoder = Literal.literalDecoder + mut + } else { + let node = definition->(Obj.magic: unknown => dict) + let fieldNames = node->Js.Dict.keys + let length = fieldNames->Js.Array2.length + for idx in 0 to length - 1 { + let location = fieldNames->Js.Array2.unsafe_get(idx) + let schema = node->Js.Dict.unsafeGet(location)->traverseDefinition(~onNode) + node->Js.Dict.set(location, schema->(Obj.magic: internal => unknown)) } - node->Js.Dict.set(location, schema->(Obj.magic: internal => unknown)) - items->Js.Array2.unsafe_set(idx, item) + let mut = base(objectTag, ~selfReverse=false) + mut.required = Some(fieldNames) + mut.properties = Some(node->(Obj.magic: dict => dict)) + mut.additionalItems = Some(globalConfig.defaultAdditionalItems) + mut.decoder = objectDecoder + mut } - let mut = base(Object) - mut.items = Some(items) - mut.properties = Some(node->(Obj.magic: dict => dict)) - mut.additionalItems = Some(globalConfig.defaultAdditionalItems) - mut.compiler = Some(schemaCompiler) - mut } } } else { @@ -4704,12 +5727,6 @@ module Schema = { } } -module Null = { - let factory = item => { - Option.factory(item, ~unit=nullAsUnit) - } -} - let schema = Schema.factory let js_schema = definition => definition->Obj.magic->Schema.definitionToSchema->castToPublic @@ -4717,148 +5734,276 @@ let literal = js_schema let enum = values => Union.factory(values->Js.Array2.map(literal)) -let unnestSerializer = Builder.make((b, ~input, ~selfSchema, ~path) => { - let schema = selfSchema.additionalItems->(Obj.magic: option => internal) - let items = schema.items->X.Option.getUnsafe - - let inputVar = b->B.Val.var(input) - let iteratorVar = b.global->B.varWithoutAllocation - let outputVar = b.global->B.varWithoutAllocation +let compactColumnsDecoder = (~input) => { + let selfSchema = input.expected + let isUnknownInput = input.schema.tag->TagFlag.get->Flag.unsafeHas(TagFlag.unknown) - let bb = b->B.scope - let itemInput = { - b: bb, - var: B._var, - inline: `${inputVar}[${iteratorVar}]`, - flag: ValFlag.none, - tag: Unknown, + // Find the object schema whose properties define the columns. + // Forward (columnar → rows): props come from selfSchema.to.additionalItems. + // Reverse (rows → columnar): props come from input.schema.additionalItems (the + // object schema left over after the preceding parse pipeline step). + let forwardProps = switch selfSchema.to { + | Some({additionalItems: ?Some(Schema(item))}) => (item->castToInternal).properties + | _ => None } - let itemOutput = bb->B.withPathPrepend( - ~input=itemInput, - ~path, - ~dynamicLocationVar=iteratorVar, - ~appendSafe=(bb, ~output) => { - let initialArraysCode = ref("") - let settingCode = ref("") - for idx in 0 to items->Js.Array2.length - 1 { - let toItem = items->Js.Array2.unsafe_get(idx) - initialArraysCode := initialArraysCode.contents ++ `new Array(${inputVar}.length),` - settingCode := - settingCode.contents ++ - `${outputVar}[${idx->X.Int.unsafeToString}][${iteratorVar}]=${( - b->B.Val.get(output, toItem.location) - ).inline};` - } - b.allocate(`${outputVar}=[${initialArraysCode.contents}]`) - bb.code = bb.code ++ settingCode.contents - }, - (b, ~input, ~path) => b->parse(~schema, ~input, ~path), - ) - let itemCode = bb->B.allocateScope - - b.code = - b.code ++ - `for(let ${iteratorVar}=0;${iteratorVar}<${inputVar}.length;++${iteratorVar}){${itemCode}}` - - if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { - { - b, - var: B._notVar, - inline: `Promise.all(${outputVar})`, - flag: ValFlag.async, - tag: Array, - } + let isForwardDirection = forwardProps->Obj.magic + let maybeProperties = if isForwardDirection { + forwardProps } else { - { - b, - var: B._var, - inline: outputVar, - flag: ValFlag.none, - tag: Array, + switch input.schema.additionalItems { + | Some(Schema(item)) => (item->castToInternal).properties + | _ => None } } -}) -let unnest = schema => { - switch schema { - | Object({items}) => - if items->Js.Array2.length === 0 { - InternalError.panic("Invalid empty object for S.unnest schema.") - } - let schema = schema->castToInternal - let mut = base(Array) - mut.items = Some( - items->Js.Array2.mapi((item, idx) => { - let location = idx->Js.Int.toString - { - schema: Array.factory(item.schema), - location, - } - }), + switch maybeProperties { + | None => + InternalError.panic( + "S.compactColumns supports only object schemas. Use S.compactColumns(S.unknown)->S.to(S.array(objectSchema)).", ) - mut.additionalItems = Some(Strict) - mut.parser = Some( - Builder.make((b, ~input, ~selfSchema, ~path) => { - let inputVar = b->B.Val.var(input) - let iteratorVar = b.global->B.varWithoutAllocation + | Some(properties) => { + let keys = properties->Js.Dict.keys + let keysLen = keys->Js.Array2.length + + // Common output schema setup shared by all branches below. + // In reverse direction we propagate the original chain's .to so that + // subsequent steps (e.g. json validation) continue to run. + let outputSchema = if isForwardDirection { + base(arrayTag, ~selfReverse=false) + } else { + let s = array(array(unknown->castToPublic))->castToInternal + s.to = selfSchema.to + s + } - let bb = b->B.scope - let itemInput = bb->B.Val.Object.make(~isArray=false) - let lengthCode = ref("") - for idx in 0 to items->Js.Array2.length - 1 { - let item = items->Js.Array2.unsafe_get(idx) - itemInput->B.Val.Object.add( - ~location=item.location, - bb->B.val(`${inputVar}[${idx->X.Int.unsafeToString}][${iteratorVar}]`, ~schema=unknown), // FIXME: should get from somewhere + if keysLen === 0 { + let input = if isUnknownInput { + input->B.refine( + ~checks=[ + { + cond: (~inputVar) => + `Array.isArray(${inputVar})&&${inputVar}.length===0`, + fail: B.failInvalidType, + }, + ], ) - lengthCode := lengthCode.contents ++ `${inputVar}[${idx->X.Int.unsafeToString}].length,` + } else { + input } - - let output = - b->B.val( - `new Array(Math.max(${lengthCode.contents}))`, - ~schema=selfSchema.to->X.Option.getUnsafe, + let output = input->B.next("[]", ~schema=outputSchema, ~expected=outputSchema) + output.isOutput = Some(true) + output + } else if isForwardDirection { + // Forward direction: columnar → rows + let input = if isUnknownInput { + input->B.refine( + ~checks=[ + { + cond: (~inputVar) => { + let check = ref( + `Array.isArray(${inputVar})&&${inputVar}.length===${keysLen->X.Int.unsafeToString}`, + ) + for idx in 0 to keysLen - 1 { + check := + check.contents ++ + `&&Array.isArray(${inputVar}[${idx->X.Int.unsafeToString}])` + } + check.contents + }, + fail: B.failInvalidType, + }, + ], ) - let outputVar = b->B.Val.var(output) - - let itemOutput = bb->B.withPathPrepend( - ~input=itemInput->B.Val.Object.complete(~isArray=false), - ~path, - ~dynamicLocationVar=iteratorVar, - ~appendSafe=(bb, ~output as itemOutput) => { - bb.code = bb.code ++ bb->B.Val.addKey(output, iteratorVar, itemOutput) ++ ";" - }, - (b, ~input, ~path) => { - b->parse(~schema, ~input, ~path) - }, - ) - let itemCode = bb->B.allocateScope + } else { + input + } - b.code = - b.code ++ - `for(let ${iteratorVar}=0;${iteratorVar}<${outputVar}.length;++${iteratorVar}){${itemCode}}` + let inputVar = input.var() + let iteratorVar = input.global->B.varWithoutAllocation + let outputVar = input.global->B.varWithoutAllocation - if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { - output.b->B.asyncVal(`Promise.all(${output.inline})`) + // Declared source item type from selfSchema (the compactColumns schema). + let declaredItemSchema: internal = { + let innerArray: internal = selfSchema.additionalItems->Obj.magic + innerArray.additionalItems->Obj.magic + } + + // Actual runtime item type: unknown for top-level parser, or + // the typed source when the caller passed already-typed data. + let runtimeItemSchema: internal = if isUnknownInput { + unknown + } else { + let innerArray: internal = input.schema.additionalItems->Obj.magic + innerArray.additionalItems->Obj.magic + } + + let lengthCode = ref("") + let itemBuildCode = ref("") + let itemParseCode = ref("") + let asyncInlines = ref("") + let hasAsync = ref(false) + for idx in 0 to keysLen - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + let idxStr = idx->X.Int.unsafeToString + let rawValueCode = `${inputVar}[${idxStr}][${iteratorVar}]` + + let fieldSchema = properties->Js.Dict.unsafeGet(key) + + // When the declared source differs from the runtime type + // (e.g. runtime=unknown, declared=json), chain through the + // declared type first so parse validates the value matches + // the source schema before converting to the field type. + let itemExpected = if declaredItemSchema !== runtimeItemSchema { + let chained = declaredItemSchema->copySchema + chained.to = Some(fieldSchema) + chained + } else { + fieldSchema + } + + let itemInput = input->B.Val.scope + itemInput.inline = rawValueCode + itemInput.schema = runtimeItemSchema + itemInput.expected = itemExpected + itemInput.var = B._notVarBeforeValidation + itemInput.isInput = Some(false) + itemInput.isOutput = Some(false) + // Path like ["bar"] so validation errors carry the field location. + itemInput.path = Path.fromInlinedLocation(input.global->B.inlineLocation(key)) + + let itemOutput = itemInput->parse + if itemOutput.flag->Flag.unsafeHas(ValFlag.async) { + hasAsync := true + } + + itemParseCode := itemParseCode.contents ++ itemOutput->B.merge + lengthCode := lengthCode.contents ++ `${inputVar}[${idxStr}].length,` + asyncInlines := asyncInlines.contents ++ `${itemOutput.inline},` + itemBuildCode := + itemBuildCode.contents ++ + `${key->X.Inlined.Value.fromString}:${itemOutput.inline},` + } + + input.allocate(`${outputVar}=new Array(Math.max(${lengthCode.contents}))`) + + let output = input->B.next(outputVar, ~schema=outputSchema, ~expected=outputSchema) + output.var = B._var + output.isOutput = Some(true) + + // Wrap the row body in a single try/catch that prepends the row index to + // any thrown error — giving paths like ["0"]["bar"]. A single wrapper is + // used (rather than per-field) so that `let` variables declared while + // parsing one field remain in scope for the object construction. + let rowAssign = if hasAsync.contents { + // For async fields, each row becomes a promise that awaits all field values + // via Promise.all, and the final output is Promise.all of all row promises. + let rowResultVar = input.global->B.varWithoutAllocation + let asyncBuildCode = ref("") + for idx in 0 to keysLen - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + asyncBuildCode := + asyncBuildCode.contents ++ + `${key->X.Inlined.Value.fromString}:${rowResultVar}[${idx->X.Int.unsafeToString}],` + } + `${outputVar}[${iteratorVar}]=Promise.all([${asyncInlines.contents}]).then(${rowResultVar}=>({${asyncBuildCode.contents}}));` + } else { + `${outputVar}[${iteratorVar}]={${itemBuildCode.contents}};` + } + + let rowBody = itemParseCode.contents ++ rowAssign + let wrappedBody = if itemParseCode.contents === "" { + rowBody + } else { + let errorVar = input.global->B.varWithoutAllocation + `try{${rowBody}}catch(${errorVar}){${errorVar}.path='["'+${iteratorVar}+'"]'+${errorVar}.path;throw ${errorVar}}` + } + output.codeFromPrev = + output.codeFromPrev ++ + `for(let ${iteratorVar}=0;${iteratorVar}<${outputVar}.length;++${iteratorVar}){${wrappedBody}}` + + if hasAsync.contents { + output->B.asyncVal(`Promise.all(${outputVar})`) } else { output } - }), - ) + } else { + // Reverse direction: rows → columnar + // When the declared source type is unknown, field values have + // already been transformed by the object schema's reverse parse + // and can be copied directly. When it differs (e.g. json), we + // need per-field parse to convert values back to the source type + // (e.g. bigint→string for json compatibility). + let inputVar = input->B.Val.var + let iteratorVar = input.global->B.varWithoutAllocation + let outputVar = input.global->B.varWithoutAllocation + + let declaredItemSchema: internal = { + let innerArray: internal = selfSchema.additionalItems->Obj.magic + innerArray.additionalItems->Obj.magic + } + let needsPerFieldTransform = declaredItemSchema !== unknown - let to = base(Array) - to.items = Some(X.Array.immutableEmpty) - to.additionalItems = Some(Schema(schema->castToPublic)) - to.serializer = Some(unnestSerializer) + let initialArraysCode = ref("") + let settingCode = ref("") + let perFieldCode = ref("") + for idx in 0 to keysLen - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + initialArraysCode := initialArraysCode.contents ++ `new Array(${inputVar}.length),` + + if needsPerFieldTransform { + let fieldSchema = properties->Js.Dict.unsafeGet(key) + let rawValueCode = `${inputVar}[${iteratorVar}][${key->X.Inlined.Value.fromString}]` + + let itemInput = input->B.Val.scope + itemInput.inline = rawValueCode + itemInput.schema = fieldSchema + itemInput.expected = declaredItemSchema + itemInput.var = B._notVarBeforeValidation + itemInput.isInput = Some(false) + itemInput.isOutput = Some(false) + itemInput.path = Path.fromInlinedLocation(input.global->B.inlineLocation(key)) + + let itemOutput = itemInput->parse + perFieldCode := perFieldCode.contents ++ itemOutput->B.merge + settingCode := + settingCode.contents ++ + `${outputVar}[${idx->X.Int.unsafeToString}][${iteratorVar}]=${itemOutput.inline};` + } else { + settingCode := + settingCode.contents ++ + `${outputVar}[${idx->X.Int.unsafeToString}][${iteratorVar}]=${inputVar}[${iteratorVar}][${key->X.Inlined.Value.fromString}];` + } + } - mut.unnest = Some(true) - mut.to = Some(to) + input.allocate(`${outputVar}=[${initialArraysCode.contents}]`) - mut->castToPublic - | _ => InternalError.panic("S.unnest supports only object schemas.") + let output = input->B.next(outputVar, ~schema=outputSchema, ~expected=outputSchema) + output.var = B._var + output.isOutput = Some(true) + let loopBody = perFieldCode.contents ++ settingCode.contents + let wrappedBody = if needsPerFieldTransform && perFieldCode.contents !== "" { + let errorVar = input.global->B.varWithoutAllocation + `try{${loopBody}}catch(${errorVar}){${errorVar}.path='["'+${iteratorVar}+'"]'+${errorVar}.path;throw ${errorVar}}` + } else { + loopBody + } + output.codeFromPrev = + output.codeFromPrev ++ + `for(let ${iteratorVar}=0;${iteratorVar}<${inputVar}.length;++${iteratorVar}){${wrappedBody}}` + output + } + } } } +let compactColumns = inputSchema => { + let innerArray = array(inputSchema) + let mut = array(innerArray)->castToInternal + mut.format = Some(CompactColumns) + mut.decoder = compactColumnsDecoder + mut->castToPublic +} + // let inline = { // let rec internalInline = (schema, ~variant as maybeVariant=?, ()) => { // let mut = schema->castToInternal->copy @@ -4914,7 +6059,7 @@ let unnest = schema => { // | BigInt => `S.bigint` // | Bool => `S.bool` // | Option(schema) => `S.option(${schema->internalInline()})` -// | Null(schema) => `S.null(${schema->internalInline()})` +// | Null(schema) => `S.nullAsOption(${schema->internalInline()})` // | Never => `S.never` // | Unknown => `S.unknown` // | Array(schema) => `S.array(${schema->internalInline()})` @@ -4988,8 +6133,6 @@ let unnest = schema => { // `->S.pattern(%re(${re // ->X.Re.toString // ->X.Inlined.Value.fromString}), ~message=${message->X.Inlined.Value.fromString})` -// | {kind: Datetime, message} => -// `->S.datetime(~message=${message->X.Inlined.Value.fromString})` // } // }) // ->Js.Array2.joinWith("") @@ -5080,9 +6223,10 @@ let unnest = schema => { // } let object = Schema.object -let null = Null.factory +let nullAsOption = item => Option.factory(item, ~unit=nullAsUnit) +let null = item => Union.factory([item->castToUnknown, nullLiteral->castToPublic]) let option = item => item->Option.factory(~unit=unit->castToPublic) -let array = Array.factory +let array = array let dict = Dict.factory let shape = Schema.shape let tuple = Schema.tuple @@ -5099,295 +6243,190 @@ let union = Union.factory // Built-in refinements // ============= +let assertNumber: (string, 'a) => unit = (fnName, n) => + if Js.typeof(n->Obj.magic) !== "number" || %raw(`Number.isNaN(n)`) { + X.Exn.throwAny( + InternalError.make( + InvalidOperation({ + path: Path.empty, + reason: `[S.${fnName}] Expected number, received ${n->Obj.magic->stringify}`, + }), + ), + ) + } + let intMin = (schema, minValue, ~message as maybeMessage=?) => { + assertNumber("min", minValue) let message = switch maybeMessage { | Some(m) => m | None => `Number must be greater than or equal to ${minValue->X.Int.unsafeToString}` } - schema->addRefinement( - ~metadataId=Int.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}<${b->B.embed(minValue)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Min({value: minValue}), - message, - }, - ) + schema->internalRefine(mut => { + mut.minimum = Some(minValue->Js.Int.toFloat) + getMutErrorMessage(~mut)->Js.Dict.set("minimum", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}>${(minValue - 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("minimum", ~defaultMessage=message)}] + } + }) } let intMax = (schema, maxValue, ~message as maybeMessage=?) => { + assertNumber("max", maxValue) let message = switch maybeMessage { | Some(m) => m | None => `Number must be lower than or equal to ${maxValue->X.Int.unsafeToString}` } - schema->addRefinement( - ~metadataId=Int.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}>${b->B.embed(maxValue)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Max({value: maxValue}), - message, - }, - ) -} - -let port = (schema, ~message=?) => { - let mutStandard = - schema - ->internalRefine((b, ~inputVar, ~selfSchema, ~path) => { - `${inputVar}>0&&${inputVar}<65536&&${inputVar}%1===0||${switch message { - | Some(m) => b->B.fail(~message=m, ~path) - | None => - b->B.failWithArg( - ~path, - input => InvalidType({ - expected: selfSchema->castToPublic, - received: input, - }), - inputVar, - ) - }};` - }) - ->castToInternal - - mutStandard.format = Some(Port) - (mutStandard->reverse).format = Some(Port) - - mutStandard->castToPublic + schema->internalRefine(mut => { + mut.maximum = Some(maxValue->Js.Int.toFloat) + getMutErrorMessage(~mut)->Js.Dict.set("maximum", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}<${(maxValue + 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("maximum", ~defaultMessage=message)}] + } + }) } let floatMin = (schema, minValue, ~message as maybeMessage=?) => { + assertNumber("min", minValue) let message = switch maybeMessage { | Some(m) => m | None => `Number must be greater than or equal to ${minValue->X.Float.unsafeToString}` } - schema->addRefinement( - ~metadataId=Float.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}<${b->B.embed(minValue)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Min({value: minValue}), - message, - }, - ) + schema->internalRefine(mut => { + mut.minimum = Some(minValue) + getMutErrorMessage(~mut)->Js.Dict.set("minimum", message) + (~input) => { + [{cond: (~inputVar) => `${inputVar}>=${input->B.embed(minValue)}`, fail: B.failWithErrorMessage("minimum", ~defaultMessage=message)}] + } + }) } let floatMax = (schema, maxValue, ~message as maybeMessage=?) => { + assertNumber("max", maxValue) let message = switch maybeMessage { | Some(m) => m | None => `Number must be lower than or equal to ${maxValue->X.Float.unsafeToString}` } - schema->addRefinement( - ~metadataId=Float.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}>${b->B.embed(maxValue)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Max({value: maxValue}), - message, - }, - ) + schema->internalRefine(mut => { + mut.maximum = Some(maxValue) + getMutErrorMessage(~mut)->Js.Dict.set("maximum", message) + (~input) => { + [{cond: (~inputVar) => `${inputVar}<=${input->B.embed(maxValue)}`, fail: B.failWithErrorMessage("maximum", ~defaultMessage=message)}] + } + }) } let arrayMinLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("min", length) let message = switch maybeMessage { | Some(m) => m | None => `Array must be ${length->X.Int.unsafeToString} or more items long` } - schema->addRefinement( - ~metadataId=Array.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length<${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Min({length: length}), - message, - }, - ) + schema->internalRefine(mut => { + mut.minItems = Some(length) + getMutErrorMessage(~mut)->Js.Dict.set("minItems", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length>${(length - 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("minItems", ~defaultMessage=message)}] + } + }) } let arrayMaxLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("max", length) let message = switch maybeMessage { | Some(m) => m | None => `Array must be ${length->X.Int.unsafeToString} or fewer items long` } - schema->addRefinement( - ~metadataId=Array.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length>${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Max({length: length}), - message, - }, - ) + schema->internalRefine(mut => { + mut.maxItems = Some(length) + getMutErrorMessage(~mut)->Js.Dict.set("maxItems", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length<${(length + 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("maxItems", ~defaultMessage=message)}] + } + }) } let arrayLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("length", length) let message = switch maybeMessage { | Some(m) => m | None => `Array must be exactly ${length->X.Int.unsafeToString} items long` } - schema->addRefinement( - ~metadataId=Array.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length!==${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Length({length: length}), - message, - }, - ) + schema->internalRefine(mut => { + mut.minItems = Some(length) + mut.maxItems = Some(length) + let em = getMutErrorMessage(~mut) + em->Js.Dict.set("minItems", message) + em->Js.Dict.set("maxItems", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length===${length->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("minItems", ~defaultMessage=message)}] + } + }) } let stringMinLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("min", length) let message = switch maybeMessage { | Some(m) => m | None => `String must be ${length->X.Int.unsafeToString} or more characters long` } - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length<${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Min({length: length}), - message, - }, - ) + schema->internalRefine(mut => { + mut.minLength = Some(length) + getMutErrorMessage(~mut)->Js.Dict.set("minLength", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length>${(length - 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("minLength", ~defaultMessage=message)}] + } + }) } let stringMaxLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("max", length) let message = switch maybeMessage { | Some(m) => m | None => `String must be ${length->X.Int.unsafeToString} or fewer characters long` } - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length>${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Max({length: length}), - message, - }, - ) + schema->internalRefine(mut => { + mut.maxLength = Some(length) + getMutErrorMessage(~mut)->Js.Dict.set("maxLength", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length<${(length + 1)->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("maxLength", ~defaultMessage=message)}] + } + }) } let stringLength = (schema, length, ~message as maybeMessage=?) => { + assertNumber("length", length) let message = switch maybeMessage { - | Some(m) => m - | None => `String must be exactly ${length->X.Int.unsafeToString} characters long` - } - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(${inputVar}.length!==${b->B.embed(length)}){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Length({length: length}), - message, - }, - ) -} - -let email = (schema, ~message=`Invalid email address`) => { - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(!${b->B.embed(String.emailRegex)}.test(${inputVar})){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Email, - message, - }, - ) -} - -let uuid = (schema, ~message=`Invalid UUID`) => { - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(!${b->B.embed(String.uuidRegex)}.test(${inputVar})){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Uuid, - message, - }, - ) -} - -let cuid = (schema, ~message=`Invalid CUID`) => { - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `if(!${b->B.embed(String.cuidRegex)}.test(${inputVar})){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Cuid, - message, - }, - ) -} - -let url = (schema, ~message=`Invalid url`) => { - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - `try{new URL(${inputVar})}catch(_){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Url, - message, - }, - ) -} - -let pattern = (schema, re, ~message=`Invalid`) => { - schema->addRefinement( - ~metadataId=String.Refinement.metadataId, - ~refiner=(b, ~inputVar, ~selfSchema as _, ~path) => { - if re->Js.Re.global { - // TODO Write a regression test when it's needed - `${b->B.embed(re)}.lastIndex=0;` - } else { - "" - } ++ - `if(!${b->B.embed(re)}.test(${inputVar})){${b->B.fail(~message, ~path)}}` - }, - ~refinement={ - kind: Pattern({re: re}), - message, - }, - ) -} - -let datetime = (schema, ~message=`Invalid datetime string! Expected UTC`) => { - let refinement = { - String.Refinement.kind: Datetime, - message, - } - schema - ->Metadata.set( - ~id=String.Refinement.metadataId, - { - switch schema->Metadata.get(~id=String.Refinement.metadataId) { - | Some(refinements) => refinements->X.Array.append(refinement) - | None => [refinement] - } - }, - ) - ->transform(s => { - parser: string => { - if String.datetimeRe->Js.Re.test_(string)->not { - s.fail(message) - } - Js.Date.fromString(string) - }, - serializer: date => date->Js.Date.toISOString, + | Some(m) => m + | None => `String must be exactly ${length->X.Int.unsafeToString} characters long` + } + schema->internalRefine(mut => { + mut.minLength = Some(length) + mut.maxLength = Some(length) + let em = getMutErrorMessage(~mut) + em->Js.Dict.set("minLength", message) + em->Js.Dict.set("maxLength", message) + (~input as _) => { + [{cond: (~inputVar) => `${inputVar}.length===${length->X.Int.unsafeToString}`, fail: B.failWithErrorMessage("minLength", ~defaultMessage=message)}] + } + }) +} + +let pattern = (schema, re, ~message=`Invalid pattern`) => { + schema->internalRefine(mut => { + mut.pattern = Some(re) + getMutErrorMessage(~mut)->Js.Dict.set("pattern", message) + (~input) => { + let embededRe = input->B.embed(re) + [{ + cond: (~inputVar) => + if re->Js.Re.global { + `(${embededRe}.lastIndex=0,${embededRe}.test(${inputVar}))` + } else { + `${embededRe}.test(${inputVar})` + }, + fail: B.failWithErrorMessage("pattern", ~defaultMessage=message), + }] + } }) } @@ -5397,7 +6436,7 @@ let trim = schema => { } let nullable = schema => { - Union.factory([schema->castToUnknown, unit->castToPublic, Literal.null->castToPublic]) + Union.factory([schema->castToUnknown, unit->castToPublic, nullLiteral->castToPublic]) } let nullableAsOption = schema => { @@ -5408,37 +6447,110 @@ let nullableAsOption = schema => { // JS/TS API // ============= +let js_parser = %raw(`(...args) => getDecoder(unknown, ...args)`) + +let js_asyncParser = %raw(`(...args) => getDecoder(unknown, ...args, 1)`) + +let js_asyncDecoder = %raw(`(...args) => getDecoder(...args, 1)`) + +let js_encoder = %raw(`(...args) => getDecoder(...args.map(reverse))`) + +let js_asyncEncoder = %raw(`(...args) => getDecoder(...args.map(reverse), 1)`) + +let js_assert = (schema, data) => { + getDecoder3(~s1=unknown, ~s2=schema->castToInternal, ~s3=assertResult)(data) +} + let js_union = values => Union.factory( values->Js.Array2.map(Schema.definitionToSchema)->(Obj.magic: array => array<'a>), ) -let js_transform = (schema, ~parser as maybeParser=?, ~serializer as maybeSerializer=?) => { - schema->transform(s => { - { - parser: ?switch maybeParser { - | Some(parser) => Some(v => parser(v, s)) - | None => None - }, - serializer: ?switch maybeSerializer { - | Some(serializer) => Some(v => serializer(v, s)) - | None => None - }, - } - }) +let js_to = { + // FIXME: Test how it'll work if we have async var as input + // FIXME: Might not work well with object targets + let customBuilder = (~fn) => { + Builder.make((~input) => { + let target = input.expected.to->X.Option.getUnsafe + let outputVar = input.global->B.varWithoutAllocation + input.allocate(outputVar) + let output = input->B.next(outputVar, ~schema=target, ~expected=target) + output.var = B._var + output.codeFromPrev = `try{${output.inline}=${input->B.embed( + fn, + )}(${input.inline})}catch(x){${output->B.failWithArg( + e => B.makeInvalidConversionDetails(~input, ~to=target, ~cause=e), + `x`, + )}}` + output + }) + } + + ( + schema, + target, + ~decoder as maybeDecoder: option<'value => 'target>=?, + ~encoder as maybeEncoder: option<'target => 'value>=?, + ) => { + updateOutput(schema->castToInternal, mut => { + let target = target->castToInternal + switch maybeEncoder { + | Some(fn) => + let targetMut = target->copySchema + targetMut.serializer = Some(customBuilder(~fn)) + mut.to = Some(targetMut) + | None => mut.to = Some(target) + } + switch maybeDecoder { + | Some(fn) => mut.parser = Some(customBuilder(~fn)) + | None => () + } + }) + } } -let js_refine = (schema, refiner) => { - schema->refine(s => { - v => refiner(v, s) - }) +let js_refine = (schema, refineCheck, refineOptions) => { + let message = switch refineOptions { + | Some(options) => + switch (options->Obj.magic)["error"] { + | Some(e) => e + | None => "Refinement failed" + } + | None => "Refinement failed" + } + let extraPath = switch refineOptions { + | Some(options) => + switch (options->Obj.magic)["path"] { + | Some(p) => Path.fromArray(p) + | None => Path.empty + } + | None => Path.empty + } + schema->internalRefine(_ => + (~input) => { + let embeddedCheck = input->B.embed(refineCheck) + [ + { + cond: (~inputVar) => `${embeddedCheck}(${inputVar})`, + fail: (~input) => { + let path = if extraPath === Path.empty { + input.path + } else { + input.path->Path.concat(extraPath) + } + _value => Custom({reason: message, path}) + }, + }, + ] + } + ) } let noop = a => a -let js_asyncParserRefine = (schema, refine) => { - schema->transform(s => { +let js_asyncDecoderAssert = (schema, assertFn) => { + schema->transform(_ => { { - asyncParser: v => refine(v, s)->X.Promise.thenResolve(() => v), + asyncParser: v => assertFn(v)->X.Promise.thenResolve(() => v), serializer: noop, } }) @@ -5467,44 +6579,29 @@ let js_nullable = (schema, maybeOr) => { let js_merge = (s1, s2) => { switch switch (s1, s2) { | ( - Object({items: items1, additionalItems: additionalItems1}), - Object({items: items2, additionalItems: additionalItems2}), + Object({properties: properties1, additionalItems: additionalItems1}), + Object({properties: properties2, additionalItems: additionalItems2}), ) // Filter out S.record schemas if additionalItems1->Type.typeof === #string && additionalItems2->Type.typeof === #string && !((s1->castToInternal).to->Obj.magic) && !((s2->castToInternal).to->Obj.magic) => - let properties = Js.Dict.empty() - let locations = [] - let items = [] - for idx in 0 to items1->Js.Array2.length - 1 { - let item = items1->Js.Array2.unsafe_get(idx) - locations->Js.Array2.push(item.location)->ignore - properties->Js.Dict.set(item.location, item.schema->castToInternal) - } - for idx in 0 to items2->Js.Array2.length - 1 { - let item = items2->Js.Array2.unsafe_get(idx) - if !(properties->Stdlib.Dict.has(item.location)) { - locations->Js.Array2.push(item.location)->ignore - } - properties->Js.Dict.set(item.location, item.schema->castToInternal) - } - for idx in 0 to locations->Js.Array2.length - 1 { - let location = locations->Js.Array2.unsafe_get(idx) - items - ->Js.Array2.push({ - location, - schema: properties->Js.Dict.unsafeGet(location)->castToPublic, - }) - ->ignore + let properties = properties1->X.Dict.copy + let keys2 = properties2->Js.Dict.keys + + for idx in 0 to keys2->Js.Array2.length - 1 { + let key = keys2->Js.Array2.unsafe_get(idx) + properties->Js.Dict.set(key, properties2->Js.Dict.unsafeGet(key)) } - let mut = base(Object) - mut.items = Some(items) - mut.properties = Some(properties) + let mut = base(objectTag, ~selfReverse=false) + + // TODO: Merge to required fields + mut.required = Some(properties->(Obj.magic: dict> => dict)->Js.Dict.keys) + mut.properties = Some(properties->(Obj.magic: dict> => dict)) mut.additionalItems = Some(additionalItems1) - mut.compiler = Some(Schema.schemaCompiler) + mut.decoder = objectDecoder Some(mut->castToPublic) | _ => None } { @@ -5519,13 +6616,9 @@ let global = override => { | Some(defaultAdditionalItems) => defaultAdditionalItems | None => initialOnAdditionalItems } :> additionalItems) - let prevDisableNanNumberCheck = globalConfig.disableNanNumberValidation - globalConfig.disableNanNumberValidation = switch override.disableNanNumberValidation { - | Some(disableNanNumberValidation) => disableNanNumberValidation - | None => initialDisableNanNumberProtection - } - if prevDisableNanNumberCheck != globalConfig.disableNanNumberValidation { - resetCacheInPlace(float) // FIXME: Should it be done for int? + globalConfig.defaultFlag = switch override.disableNanNumberValidation { + | Some(true) => Flag.disableNanNumberValidation + | _ => initialDefaultFlag } } @@ -5539,64 +6632,157 @@ module RescriptJSONSchema = { @val external merge: (@as(json`{}`) _, t, t) => t = "Object.assign" - let rec internalToJSONSchema = (schema: schema, ~defs): JSONSchema.t => { + let applyMetadataOverlay = (jsonSchema: Mutable.t, schema: schema, ~defs): unit => { + switch schema->untag { + | {description: m} => jsonSchema.description = Some(m) + | _ => () + } + switch schema->untag { + | {title: m} => jsonSchema.title = Some(m) + | _ => () + } + switch schema->untag { + | {deprecated} => jsonSchema.deprecated = Some(deprecated) + | _ => () + } + switch schema->untag { + | {examples} => + jsonSchema.examples = Some( + examples->( + Obj.magic: // If a schema is Jsonable, + // then examples are Jsonable too. + array => array + ), + ) + | _ => () + } + switch schema->untag { + | {defs: schemaDefs} => + let _ = defs->X.Dict.mixin(schemaDefs) + | _ => () + } + switch schema->Metadata.get(~id=jsonSchemaMetadataId) { + | Some(metadataRawSchema) => jsonSchema->Mutable.mixin(metadataRawSchema) + | None => () + } + } + + let rec encodeToJsonSchema = (schema: schema, ~path, ~defs, ~parent): option< + JSONSchema.t, + > => { + let schemaInternal = schema->castToInternal + let reversed = schemaInternal->reverse + let input = B.operationArg( + ~flag=Flag.none, + ~defs=%raw(`0`), + ~schema=unknown, + ~expected=reversed, + ) + try { + let output = input->parse + // The parse produces a val whose .schema reflects the + // JSON-compatible transformed structure. + Some(internalToJSONSchema(output.schema->castToPublic, ~path, ~defs, ~parent)) + } catch { + | _ => { + let _ = %raw(`exn`)->InternalError.getOrRethrow + // Parse failed — caller falls through to normal tag-based logic. + None + } + } + } + and internalToJSONSchema = (schema: schema, ~path, ~defs, ~parent): JSONSchema.t => { + let schemaInternal = schema->castToInternal + // When a schema has `.to`, we can try to encode-reverse it to get a more + // precise JSON schema (e.g. `format: "date-time"` for `S.string->S.to(S.date)`). + // But for structural tags (object/array/union) encoding would lose items + // metadata, so we only attempt it for leaf tags where it's safe. + let hasUserTo = + schemaInternal.to->Obj.magic && + !( + schemaInternal.tag + ->TagFlag.get + ->Flag.unsafeHas( + TagFlag.object->Flag.with(TagFlag.array)->Flag.with(TagFlag.union), + ) + ) + let encoded = if hasUserTo { + encodeToJsonSchema(schema, ~path, ~defs, ~parent) + } else { + None + } + switch encoded { + | Some(encodedJsonSchema) => + let mutableJs = encodedJsonSchema->Mutable.fromReadOnly + mutableJs->applyMetadataOverlay(schema, ~defs) + mutableJs->Mutable.toReadOnly + | None => internalToJSONSchemaBase(schema, ~path, ~defs, ~parent) + } + } + and internalToJSONSchemaBase = ( + schema: schema, + ~path, + ~defs, + ~parent, + ): JSONSchema.t => { let jsonSchema: Mutable.t = {} switch schema { - | String({?const}) => - jsonSchema.type_ = Some(Arrayable.single(#string)) - schema - ->String.refinements - ->Js.Array2.forEach(refinement => { - switch refinement { - | {kind: Email} => jsonSchema.format = Some("email") - | {kind: Url} => jsonSchema.format = Some("uri") - | {kind: Uuid} => jsonSchema.format = Some("uuid") - | {kind: Datetime} => jsonSchema.format = Some("date-time") - | {kind: Cuid} => () - | {kind: Length({length})} => { - jsonSchema.minLength = Some(length) - jsonSchema.maxLength = Some(length) - } - | {kind: Max({length})} => jsonSchema.maxLength = Some(length) - | {kind: Min({length})} => jsonSchema.minLength = Some(length) - | {kind: Pattern({re})} => jsonSchema.pattern = Some(re->Js.String2.make) + | String({?const, ?format}) => { + jsonSchema.type_ = Some(Arrayable.single(#string)) + switch format { + | Some(DateTime) => jsonSchema.format = Some("date-time") + | Some(Email) => jsonSchema.format = Some("email") + | Some(Uuid) => jsonSchema.format = Some("uuid") + | Some(Url) => jsonSchema.format = Some("uri") + | Some(Cuid) | Some(JSON) | None => () + } + let internal = schema->castToInternal + switch internal.minLength { + | Some(v) => jsonSchema.minLength = Some(v) + | None => () + } + switch internal.maxLength { + | Some(v) => jsonSchema.maxLength = Some(v) + | None => () + } + switch internal.pattern { + | Some(re) => + jsonSchema.pattern = Some((re->(Obj.magic: Js.Re.t => {..}))["source"]) + | None => () + } + switch const { + | Some(value) => jsonSchema.const = Some(Js.Json.string(value)) + | None => () } - }) - switch const { - | Some(value) => jsonSchema.const = Some(Js.Json.string(value)) - | None => () } - | Number({?format, ?const}) => - switch format { - | Some(Int32) => - jsonSchema.type_ = Some(Arrayable.single(#integer)) - schema - ->Int.refinements - ->Js.Array2.forEach(refinement => { - switch refinement { - | {kind: Max({value})} => jsonSchema.maximum = Some(value->Js.Int.toFloat) - | {kind: Min({value})} => jsonSchema.minimum = Some(value->Js.Int.toFloat) + | Number({?format, ?const}) => { + let internal = schema->castToInternal + switch format { + | Some(Int32) => { + jsonSchema.type_ = Some(Arrayable.single(#integer)) + jsonSchema.minimum = Some(-2147483648.) + jsonSchema.maximum = Some(2147483647.) } - }) - | Some(Port) => { - jsonSchema.type_ = Some(Arrayable.single(#integer)) - jsonSchema.maximum = Some(65535.) - jsonSchema.minimum = Some(0.) - } - | None => - jsonSchema.type_ = Some(Arrayable.single(#number)) - schema - ->Float.refinements - ->Js.Array2.forEach(refinement => { - switch refinement { - | {kind: Max({value})} => jsonSchema.maximum = Some(value) - | {kind: Min({value})} => jsonSchema.minimum = Some(value) + | Some(Port) => { + jsonSchema.type_ = Some(Arrayable.single(#integer)) + jsonSchema.minimum = Some(0.) + jsonSchema.maximum = Some(65535.) } - }) - } - switch const { - | Some(value) => jsonSchema.const = Some(Js.Json.number(value)) - | None => () + | None => + jsonSchema.type_ = Some(Arrayable.single(#number)) + } + switch internal.minimum { + | Some(v) => jsonSchema.minimum = Some(v) + | None => () + } + switch internal.maximum { + | Some(v) => jsonSchema.maximum = Some(v) + | None => () + } + switch const { + | Some(value) => jsonSchema.const = Some(Js.Json.number(value)) + | None => () + } } | Boolean({?const}) => { jsonSchema.type_ = Some(Arrayable.single(#boolean)) @@ -5608,23 +6794,38 @@ module RescriptJSONSchema = { | Array({additionalItems, items}) => switch additionalItems { | Schema(childSchema) => - jsonSchema.items = Some(Arrayable.single(Schema(internalToJSONSchema(childSchema, ~defs)))) + jsonSchema.items = Some( + Arrayable.single( + Schema( + internalToJSONSchema( + childSchema, + ~parent=schema, + ~path=path->Path.concat(Path.dynamic), + ~defs, + ), + ), + ), + ) jsonSchema.type_ = Some(Arrayable.single(#array)) - schema - ->Array.refinements - ->Js.Array2.forEach(refinement => { - switch refinement { - | {kind: Max({length})} => jsonSchema.maxItems = Some(length) - | {kind: Min({length})} => jsonSchema.minItems = Some(length) - | {kind: Length({length})} => { - jsonSchema.maxItems = Some(length) - jsonSchema.minItems = Some(length) - } - } - }) + let internal = schema->castToInternal + switch internal.minItems { + | Some(v) => jsonSchema.minItems = Some(v) + | None => () + } + switch internal.maxItems { + | Some(v) => jsonSchema.maxItems = Some(v) + | None => () + } | _ => { - let items = items->Js.Array2.map(item => { - Schema(internalToJSONSchema(item.schema, ~defs)) + let items = items->Js.Array2.mapi((itemSchema, idx) => { + Schema( + internalToJSONSchema( + itemSchema, + ~parent=schema, + ~path=path->Path.concat(Path.fromLocation(idx->Js.Int.toString)), + ~defs, + ), + ) }) let itemsNumber = items->Js.Array2.length @@ -5642,10 +6843,12 @@ module RescriptJSONSchema = { anyOf->Js.Array2.forEach(childSchema => { switch childSchema { // Filter out undefined to support optional fields - | Undefined(_) => () + | Undefined(_) if (parent->castToInternal).tag === objectTag => () | _ => { items - ->Js.Array2.push(Schema(internalToJSONSchema(childSchema, ~defs))) + ->Js.Array2.push( + Schema(internalToJSONSchema(childSchema, ~parent=schema, ~path, ~defs)), + ) ->ignore switch childSchema->castToInternal->isLiteral { | true => @@ -5676,86 +6879,81 @@ module RescriptJSONSchema = { jsonSchema.anyOf = Some(items) } } - | Object({items, additionalItems}) => + | Object({properties, additionalItems}) => switch additionalItems { | Schema(childSchema) => { jsonSchema.type_ = Some(Arrayable.single(#object)) - jsonSchema.additionalProperties = Some(Schema(internalToJSONSchema(childSchema, ~defs))) + let childJsonSchema = internalToJSONSchema( + childSchema, + ~path=path->Path.concat(Path.dynamic), + ~defs, + ~parent=schema, + ) + jsonSchema.additionalProperties = Some( + if (childJsonSchema->Obj.magic: dict<'a>)->Js.Dict.keys->Js.Array2.length === 0 { + JSONSchema.Any + } else { + Schema(childJsonSchema) + }, + ) } | _ => { - let properties = Js.Dict.empty() let required = [] - items->Js.Array2.forEach(item => { - let fieldSchema = internalToJSONSchema(item.schema, ~defs) - if item.schema->castToInternal->isOptional->not { - required->Js.Array2.push(item.location)->ignore + let keys = properties->Js.Dict.keys + let jsonProperties = Js.Dict.empty() + + for idx in 0 to keys->Js.Array2.length - 1 { + let key = keys->Js.Array2.unsafe_get(idx) + let itemSchema = properties->Js.Dict.unsafeGet(key) + let fieldSchema = internalToJSONSchema( + itemSchema, + ~path=path->Path.concat(Path.fromLocation(key)), + ~defs, + ~parent=schema, + ) + if itemSchema->castToInternal->isOptional->not { + required->Js.Array2.push(key)->ignore } - properties->Js.Dict.set(item.location, Schema(fieldSchema)) - }) + jsonProperties->Js.Dict.set(key, Schema(fieldSchema)) + } jsonSchema.type_ = Some(Arrayable.single(#object)) - jsonSchema.properties = Some(properties) - jsonSchema.additionalProperties = Some( - switch additionalItems { - | Strict => JSONSchema.Never - | Strip - | Schema(_) => - JSONSchema.Any - }, - ) + jsonSchema.properties = Some(jsonProperties) + switch additionalItems { + | Strict => jsonSchema.additionalProperties = Some(JSONSchema.Never) + | Strip + | Schema(_) => () + } switch required { | [] => () | required => jsonSchema.required = Some(required) } } } - | Unknown(_) => () - | Ref({ref}) if ref === `${defsPath}${jsonName}` => () + | Ref({ref}) if ref === `${defsPath}${jsonName}` => () // S.json → empty {} | Ref({ref}) => jsonSchema.ref = Some(ref) | Null(_) => jsonSchema.type_ = Some(Arrayable.single(#null)) | Never(_) => jsonSchema.not = Some(Schema({})) - // This case should never happen, - // since we have jsonableValidate in the toJSONSchema function - | _ => InternalError.panic("Unexpected schema type") - } - - switch schema->untag { - | {description: m} => jsonSchema.description = Some(m) - | _ => () - } - switch schema->untag { - | {title: m} => jsonSchema.title = Some(m) - | _ => () - } - - switch schema->untag { - | {deprecated} => jsonSchema.deprecated = Some(deprecated) - | _ => () - } - - switch schema->untag { - | {examples} => - jsonSchema.examples = Some( - examples->( - Obj.magic: // If a schema is Jsonable, - // then examples are Jsonable too. - array => array + | _ => + X.Exn.throwAny( + InternalError.make( + B.makeInvalidInputDetails( + ~received=if (parent->castToInternal).tag->TagFlag.get->Flag.unsafeHas(TagFlag.union) { + parent + } else { + schema + }, + ~expected=json, + ~path, + ~input=%raw(`0`), + ~includeInput=false, + ), ), ) - | _ => () - } - - switch schema->untag { - | {defs: schemaDefs} => - let _ = defs->X.Dict.mixin(schemaDefs) - | _ => () } - switch schema->Metadata.get(~id=jsonSchemaMetadataId) { - | Some(metadataRawSchema) => jsonSchema->Mutable.mixin(metadataRawSchema) - | None => () - } + jsonSchema->applyMetadataOverlay(schema, ~defs) jsonSchema->Mutable.toReadOnly } @@ -5763,9 +6961,11 @@ module RescriptJSONSchema = { let toJSONSchema = schema => { let target = schema->castToInternal - jsonableValidation(~output=target, ~parent=target, ~path=Path.empty, ~flag=Flag.jsonableOutput) let defs = Js.Dict.empty() - let jsonSchema = target->castToPublic->RescriptJSONSchema.internalToJSONSchema(~defs) + let jsonSchema = + target + ->castToPublic + ->RescriptJSONSchema.internalToJSONSchema(~path=Path.empty, ~parent=target->castToPublic, ~defs) let _ = %raw(`delete defs.JSON`) let defsKeys = defs->Js.Dict.keys if defsKeys->Js.Array2.length->X.Int.unsafeToBool { @@ -5773,11 +6973,13 @@ let toJSONSchema = schema => { // Nothing critical, just because we can let jsonSchemDefs = defs->(Obj.magic: dict> => dict) defsKeys->Js.Array2.forEach(key => { + let schema = defs->Js.Dict.unsafeGet(key) jsonSchemDefs->Js.Dict.set( key, - defs - ->Js.Dict.unsafeGet(key) + schema ->RescriptJSONSchema.internalToJSONSchema( + ~parent=schema, + ~path=Path.empty, // It's not possible to have nested recursive schema. // It should be grouped to a single $defs of the most top-level schema. ~defs=%raw(`0`), @@ -5852,26 +7054,29 @@ let rec fromJSONSchema: RescriptJSONSchema.t => t = { | {type_} if type_ === JSONSchema.Arrayable.single(#object) => let schema = switch jsonSchema.properties { | Some(properties) => - let schema = object(s => { - let obj = Js.Dict.empty() - properties - ->Js.Dict.keys - ->Js.Array2.forEach(key => { - let property = properties->Js.Dict.unsafeGet(key) - let propertySchema = property->definitionToSchema - let propertySchema = switch jsonSchema.required { - | Some(r) if r->Js.Array2.includes(key) => propertySchema - | _ => - switch property->definitionToDefaultValue { - | Some(defaultValue) => - propertySchema->option->Option.getOr(defaultValue)->castAnySchemaToJsonableS - | None => propertySchema->option->castAnySchemaToJsonableS + let schema = + { + let obj = Js.Dict.empty() + properties + ->Js.Dict.keys + ->Js.Array2.forEach(key => { + let property = properties->Js.Dict.unsafeGet(key) + let propertySchema = property->definitionToSchema + let propertySchema = switch jsonSchema.required { + | Some(r) if r->Js.Array2.includes(key) => propertySchema + | _ => + switch property->definitionToDefaultValue { + | Some(defaultValue) => + propertySchema->option->Option.getOr(defaultValue)->castAnySchemaToJsonableS + | None => propertySchema->option->castAnySchemaToJsonableS + } } - } - Js.Dict.set(obj, key, s.field(key, propertySchema)) - }) - obj - }) + Js.Dict.set(obj, key, propertySchema) + }) + obj->(Obj.magic: dict> => unknown) + } + ->Schema.definitionToSchema + ->castToPublic let schema = switch jsonSchema { | {additionalProperties} if additionalProperties === Never => schema->strict | _ => schema @@ -5916,55 +7121,38 @@ let rec fromJSONSchema: RescriptJSONSchema.t => t = { | {anyOf: definitions} => union(definitions->Js.Array2.map(definitionToSchema)) | {allOf: []} => anySchema | {allOf: [d]} => d->definitionToSchema - | {allOf: definitions} => - anySchema->refine(s => - data => { - definitions->Js.Array2.forEach(d => { - try data->assertOrThrow(d->definitionToSchema) catch { - | _ => s.fail("Should pass for all schemas of the allOf property.") - } - }) - } - ) - | {oneOf: []} => anySchema - | {oneOf: [d]} => d->definitionToSchema - | {oneOf: definitions} => - anySchema->refine(s => - data => { - let hasOneValidRef = ref(false) - definitions->Js.Array2.forEach(d => { - let passed = try { - let _ = data->assertOrThrow(d->definitionToSchema) - true - } catch { - | _ => false - } - if passed { - if hasOneValidRef.contents { - s.fail("Should pass single schema according to the oneOf property.") - } - hasOneValidRef.contents = true - } - }) - if hasOneValidRef.contents->not { - s.fail("Should pass at least one schema according to the oneOf property.") - } - } - ) - | {not} => - anySchema->refine(s => - data => { - let passed = try { - let _ = data->assertOrThrow(not->definitionToSchema) + | {allOf: definitions} => anySchema->refine(data => { + definitions->Js.Array2.every(d => { + try { + data->assertOrThrow(~to=d->definitionToSchema) true } catch { | _ => false } - if passed { - s.fail("Should NOT be valid against schema in the not property.") + }) + }, ~error="Should pass for all schemas of the allOf property.") + | {oneOf: []} => anySchema + | {oneOf: [d]} => d->definitionToSchema + | {oneOf: definitions} => anySchema->refine(data => { + let validCount = ref(0) + definitions->Js.Array2.forEach(d => { + try { + let _ = data->assertOrThrow(~to=d->definitionToSchema) + validCount := validCount.contents + 1 + } catch { + | _ => () } + }) + validCount.contents === 1 + }, ~error="Should pass exactly one schema according to the oneOf property.") + | {not} => anySchema->refine(data => { + try { + let _ = data->assertOrThrow(~to=not->definitionToSchema) + false + } catch { + | _ => true } - ) + }, ~error="Should NOT be valid against schema in the not property.") // needs to come before primitives | {enum: []} => anySchema | {enum: [p]} => p->primitiveToSchema @@ -5981,12 +7169,25 @@ let rec fromJSONSchema: RescriptJSONSchema.t => t = { }), ) | {type_} if type_ === JSONSchema.Arrayable.single(#string) => - let schema = string->castToPublic + let schema = switch jsonSchema { + | {format: "email"} => + enableEmail() + email->castToPublic + | {format: "uri"} => + enableUrl() + url->castToPublic + | {format: "uuid"} => + enableUuid() + uuid->castToPublic + | {format: "date-time"} => + enableIsoDateTime() + isoDateTime->castToPublic + | _ => string->castToPublic + } let schema = switch jsonSchema { | {pattern: p} => schema->pattern(Js.Re.fromString(p)) | _ => schema } - let schema = switch jsonSchema { | {minLength} => schema->stringMinLength(minLength) | _ => schema @@ -5995,13 +7196,7 @@ let rec fromJSONSchema: RescriptJSONSchema.t => t = { | {maxLength} => schema->stringMaxLength(maxLength) | _ => schema } - switch jsonSchema { - | {format: "email"} => schema->email->castAnySchemaToJsonableS - | {format: "uri"} => schema->url->castAnySchemaToJsonableS - | {format: "uuid"} => schema->uuid->castAnySchemaToJsonableS - | {format: "date-time"} => schema->datetime->castAnySchemaToJsonableS - | _ => schema->castAnySchemaToJsonableS - } + schema->castAnySchemaToJsonableS | {type_} if type_ === JSONSchema.Arrayable.single(#integer) => jsonSchema->toIntSchema | {type_, format: "int64"} if type_ === JSONSchema.Arrayable.single(#number) => @@ -6030,22 +7225,27 @@ let rec fromJSONSchema: RescriptJSONSchema.t => t = { let ifSchema = if_->definitionToSchema let thenSchema = then->definitionToSchema let elseSchema = else_->definitionToSchema - anySchema->refine(_ => - data => { - let passed = try { - let _ = data->assertOrThrow(ifSchema) - true - } catch { - | _ => false - } + anySchema->refine(data => { + let passed = try { + let _ = data->assertOrThrow(~to=ifSchema) + true + } catch { + | _ => false + } + try { if passed { - data->assertOrThrow(thenSchema) + data->assertOrThrow(~to=thenSchema) } else { - data->assertOrThrow(elseSchema) + data->assertOrThrow(~to=elseSchema) } + true + } catch { + | _ => false } - ) + }, ~error="Should pass the if/then/else schema validation.") } + | _ if jsonSchema.type_ !== None => + InternalError.panic(`Unknown JSON Schema type: ${(jsonSchema.type_->Obj.magic: string)}`) | _ => anySchema } @@ -6104,6 +7304,13 @@ let length = (schema, length, ~message as maybeMessage=?) => { let unknown: t = unknown->castToPublic let json: t = json->castToPublic let jsonString: t = jsonString->castToPublic +let uint8Array: t = uint8Array->castToPublic +let isoDateTime: t = isoDateTime->castToPublic +let port: t = port->castToPublic +let email: t = email->castToPublic +let uuid: t = uuid->castToPublic +let cuid: t = cuid->castToPublic +let url: t = url->castToPublic let bool: t = bool->castToPublic let symbol: t = symbol->castToPublic let string: t = string->castToPublic diff --git a/packages/sury/src/Sury.res.mjs b/packages/sury/src/Sury.res.mjs index f3045d5b7..d2f656d9d 100644 --- a/packages/sury/src/Sury.res.mjs +++ b/packages/sury/src/Sury.res.mjs @@ -2,6 +2,7 @@ import * as Belt_List from "rescript/lib/es6/Belt_List.js"; import * as JSONSchema from "./JSONSchema.res.mjs"; +import * as Stdlib_Option from "rescript/lib/es6/Stdlib_Option.js"; import * as Primitive_option from "rescript/lib/es6/Primitive_option.js"; import * as Primitive_exceptions from "rescript/lib/es6/Primitive_exceptions.js"; @@ -50,7 +51,7 @@ function fromArray(array) { let len = array.length; if (len !== 1) { if (len !== 0) { - return "[" + array.map(fromString).join("][") + "]"; + return array.map(fromLocation).join(""); } else { return ""; } @@ -69,19 +70,47 @@ let s = Symbol(vendor); let itemSymbol = Symbol(vendor + ":item"); -let $$Error = /* @__PURE__ */Primitive_exceptions.create("Sury.Error"); +let stringTag = "string"; + +let numberTag = "number"; + +let bigintTag = "bigint"; + +let booleanTag = "boolean"; + +let symbolTag = "symbol"; + +let nullTag = "null"; + +let undefinedTag = "undefined"; + +let nanTag = "nan"; + +let instanceTag = "instance"; + +let arrayTag = "array"; + +let objectTag = "object"; + +let unionTag = "union"; + +let neverTag = "never"; + +let unknownTag = "unknown"; + +let refTag = "ref"; + +let Exn = /* @__PURE__ */Primitive_exceptions.create("Sury.Exn"); let constField = "const"; function isOptional(schema) { - let match = schema.type; - switch (match) { - case "undefined" : - return true; - case "union" : - return "undefined" in schema.has; - default: - return false; + if (schema.type === undefinedTag) { + return true; + } else if (schema.type === unionTag) { + return undefinedTag in schema.has; + } else { + return false; } } @@ -90,40 +119,42 @@ function has(acc, flag) { } let flags = { - unknown: 1, - string: 2, - number: 4, - boolean: 8, - undefined: 16, - null: 32, - object: 64, - array: 128, - union: 256, - ref: 512, - bigint: 1024, - nan: 2048, - "function": 4096, - instance: 8192, - never: 16384, - symbol: 32768, + [unknownTag]: 1, + [stringTag]: 2, + [numberTag]: 4, + [booleanTag]: 8, + [undefinedTag]: 16, + [nullTag]: 32, + [objectTag]: 64, + [arrayTag]: 128, + [unionTag]: 256, + [refTag]: 512, + [bigintTag]: 1024, + [nanTag]: 2048, + ["function"]: 4096, + [instanceTag]: 8192, + [neverTag]: 16384, + [symbolTag]: 32768, }; function stringify(unknown) { let tagFlag = flags[typeof unknown]; if (tagFlag & 16) { - return "undefined"; + return undefinedTag; } if (!(tagFlag & 64)) { if (tagFlag & 2) { return "\"" + unknown + "\""; } else if (tagFlag & 1024) { return unknown + "n"; + } else if (tagFlag & 4096) { + return "Function"; } else { return unknown.toString(); } } if (unknown === null) { - return "null"; + return nullTag; } if (Array.isArray(unknown)) { let string = "["; @@ -150,6 +181,7 @@ function stringify(unknown) { function toExpression(schema) { let tag = schema.type; + let to = schema.to; let $$const = schema.const; let name = schema.name; if (name !== undefined) { @@ -164,18 +196,37 @@ function toExpression(schema) { return anyOf.map(toExpression).join(" | "); } if (format !== undefined) { - return format; + if (format !== "compactColumns") { + return format; + } + let additionalItems = schema.additionalItems; + if (to === undefined) { + if (additionalItems !== undefined && additionalItems !== "strip" && additionalItems !== "strict") { + return toExpression(additionalItems) + "[]"; + } else { + return "unknown[][]"; + } + } + let props = to.properties; + if (props === undefined) { + return "unknown[][]"; + } + let keys = Object.keys(props); + return "[" + keys.map(key => { + let propSchema = props[key]; + return toExpression(propSchema) + "[]"; + }).join(", ") + "]"; } switch (tag) { case "nan" : return "NaN"; case "object" : - let additionalItems = schema.additionalItems; + let additionalItems$1 = schema.additionalItems; let properties = schema.properties; let locations = Object.keys(properties); if (locations.length === 0) { - if (typeof additionalItems === "object") { - return "{ [key: string]: " + toExpression(additionalItems) + "; }"; + if (typeof additionalItems$1 === objectTag) { + return "{ [key: string]: " + toExpression(additionalItems$1) + "; }"; } else { return "{}"; } @@ -190,14 +241,14 @@ function toExpression(schema) { case "instance" : return schema.class.name; case "array" : - let additionalItems$1 = schema.additionalItems; + let additionalItems$2 = schema.additionalItems; let items = schema.items; - if (typeof additionalItems$1 !== "object") { - return "[" + items.map(item => toExpression(item.schema)).join(", ") + "]"; + if (typeof additionalItems$2 !== objectTag) { + return "[" + items.map(toExpression).join(", ") + "]"; } - let itemName = toExpression(additionalItems$1); + let itemName = toExpression(additionalItems$2); return ( - additionalItems$1.type === "union" ? "(" + itemName + ")" : itemName + additionalItems$2.type === unionTag ? "(" + itemName + ")" : itemName ) + "[]"; default: return tag; @@ -206,11 +257,11 @@ function toExpression(schema) { } class SuryError extends Error { - constructor(code, flag, path) { + constructor(params) { super(); - this.flag = flag; - this.code = code; - this.path = path; + for (let key in params) { + this[key] = params[key]; + } } } @@ -220,11 +271,6 @@ d(p, 'message', { return message(this); }, }) -d(p, 'reason', { - get() { - return reason(this); - } -}) d(p, 'name', {value: 'SuryError'}) d(p, 's', {value: s}) d(p, '_1', { @@ -233,10 +279,11 @@ d(p, '_1', { }, }); d(p, 'RE_EXN_ID', { - value: $$Error, + value: Exn, }); -var Schema = function(type) {this.type=type}, sp = Object.create(null); +var seq = 1; +var Schema = function() {}, sp = Object.create(null); d(sp, 'with', { get() { return (fn, ...args) => fn(this, ...args) @@ -253,74 +300,38 @@ function getOrRethrow(exn) { throw exn; } -function reason(error, nestedLevelOpt) { - let nestedLevel = nestedLevelOpt !== undefined ? nestedLevelOpt : 0; - let reason$1 = error.code; - if (typeof reason$1 !== "object") { - return "Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead"; - } - switch (reason$1.TAG) { - case "OperationFailed" : - return reason$1._0; - case "InvalidOperation" : - return reason$1.description; - case "InvalidType" : - let unionErrors = reason$1.unionErrors; - let m = "Expected " + toExpression(reason$1.expected) + ", received " + stringify(reason$1.received); - if (unionErrors !== undefined) { - let lineBreak = "\n" + " ".repeat((nestedLevel << 1)); - let reasonsDict = {}; - for (let idx = 0, idx_finish = unionErrors.length; idx < idx_finish; ++idx) { - let error$1 = unionErrors[idx]; - let reason$2 = reason(error$1, nestedLevel + 1); - let nonEmptyPath = error$1.path; - let location = nonEmptyPath === "" ? "" : "At " + nonEmptyPath + ": "; - let line = "- " + location + reason$2; - if (!reasonsDict[line]) { - reasonsDict[line] = 1; - m = m + lineBreak + line; - } - - } - } - return m; - case "UnsupportedTransformation" : - return "Unsupported transformation from " + toExpression(reason$1.from) + " to " + toExpression(reason$1.to); - case "ExcessField" : - return "Unrecognized key \"" + reason$1._0 + "\""; - case "InvalidJsonSchema" : - return toExpression(reason$1._0) + " is not valid JSON"; - } -} - function message(error) { - let op = error.flag; - let text = "Failed "; - if (op & 2) { - text = text + "async "; - } - text = text + ( - op & 1 ? ( - op & 4 ? "asserting" : "parsing" - ) : "converting" - ); - if (op & 8) { - text = text + " to JSON" + ( - op & 16 ? " string" : "" - ); - } let nonEmptyPath = error.path; - let tmp = nonEmptyPath === "" ? "" : " at " + nonEmptyPath; - return text + tmp + ": " + reason(error, undefined); + let tmp = nonEmptyPath === "" ? "" : "Failed at " + nonEmptyPath + ": "; + return tmp + error.reason; } let globalConfig = { m: message, d: undefined, a: "strip", - n: false + f: 0 }; +let valueOptions = {}; + +let configurableValueOptions = {configurable: true}; + +let valKey = "value"; + +let reversedKey = "r"; + +function base(tag, selfReverse) { + let s = new Schema(); + s.type = tag; + s.seq = (seq++); + if (selfReverse) { + valueOptions[valKey] = s; + d(s, reversedKey, valueOptions); + } + return s; +} + let shakenRef = "as"; let shakenTraps = { @@ -339,46 +350,59 @@ let shakenTraps = { }; function shaken(apiName) { - let mut = new Schema("never"); + let mut = base(neverTag, true); mut[shakenRef] = apiName; return new Proxy(mut, shakenTraps); } -let unknown = new Schema("unknown"); +function noopDecoder(input) { + return input; +} + +let unknown = base(unknownTag, true); + +unknown.decoder = noopDecoder; -let bool = new Schema("boolean"); +let bool = base(booleanTag, true); -let symbol = new Schema("symbol"); +let symbol = base(symbolTag, true); -let string = new Schema("string"); +let string = base(stringTag, true); -let int = new Schema("number"); +let int = base(numberTag, true); int.format = "int32"; -let float = new Schema("number"); +let float = base(numberTag, true); -let bigint = new Schema("bigint"); +let bigint = base(bigintTag, true); -let unit = new Schema("undefined"); +let unit = base(undefinedTag, true); unit.const = (void 0); -let copyWithoutCache = ((schema) => { - let c = new Schema(schema.type) +let nullLiteral = base(nullTag, true); + +nullLiteral.const = null; + +let nan = base(nanTag, true); + +nan.const = NaN; + +let copySchema = ((schema) => { + let c = new Schema() for (let k in schema) { - if (k > "a" || k === "$ref" || k === "$defs") { - c[k] = schema[k] - } + c[k] = schema[k] } + c.seq = seq++ return c }); function updateOutput(schema, fn) { - let root = copyWithoutCache(schema); + let root = copySchema(schema); let mut = root; while (mut.to) { - let next = copyWithoutCache(mut.to); + let next = copySchema(mut.to); mut.to = next; mut = next; }; @@ -386,23 +410,15 @@ function updateOutput(schema, fn) { return root; } -let resetCacheInPlace = ((schema) => { - for (let k in schema) { - if (Number(k[0])) { - delete schema[k]; - } - } -}); - -let value = SuryError; +let $$class = SuryError; -function constructor(prim0, prim1, prim2) { - return new SuryError(prim0, prim1, prim2); +function make(prim) { + return new SuryError(prim); } -let ErrorClass = { - value: value, - constructor: constructor +let $$Error$1 = { + $$class: $$class, + make: make }; function embed(b, value) { @@ -428,14 +444,14 @@ function inlineConst(b, schema) { } } -function inlineLocation(b, location) { +function inlineLocation(global, location) { let key = "\"" + location + "\""; - let i = b.g[key]; + let i = global[key]; if (i !== undefined) { return i; } let inlinedLocation = fromString(location); - b.g[key] = inlinedLocation; + global[key] = inlinedLocation; return inlinedLocation; } @@ -450,29 +466,20 @@ function initialAllocate(v) { b.a = secondAllocate; } -function rootScope(flag, defs) { - let global = { - c: "", - l: "", - a: initialAllocate, - v: -1, - o: flag, - f: "", - e: [], - d: defs - }; - global.g = global; - return global; +function _var() { + return this.i; } -function allocateScope(b) { - ((delete b.a)); - let varsAllocation = b.l; - if (varsAllocation === "") { - return b.f + b.c; - } else { - return b.f + "let " + varsAllocation + ";" + b.c; - } +function _bondVar() { + let val = this; + let bond = val.b; + return bond.v(); +} + +function _prevVar() { + let val = this; + let prev = val.prev; + return prev.v(); } function varWithoutAllocation(global) { @@ -481,658 +488,868 @@ function varWithoutAllocation(global) { return "v" + newCounter; } -function _var(_b) { - return this.i; +function _notVarBeforeValidation() { + let val = this; + let v = varWithoutAllocation(val.g); + val.cp = "let " + v + "=" + val.i + ";"; + val.i = v; + val.v = _var; + return v; +} + +function _notVarAtParent() { + let val = this; + let v = varWithoutAllocation(val.g); + val.p.a(v + "=" + val.i); + val.v = _var; + val.i = v; + return v; } -function _notVar(b) { +function _notVar() { let val = this; - let v = varWithoutAllocation(b.g); + let v = varWithoutAllocation(val.g); + let from = val.prev; + let target = from !== undefined ? from : val; let i = val.i; if (i === "") { - val.b.a(v); - } else if (b.a !== (void 0)) { - b.a(v + "=" + i); + target.a(v); + } else if (val.cp !== "") { + target.a(v); + val.cp = val.cp + v + "=" + i + ";"; } else { - b.c = b.c + (v + "=" + i + ";"); - b.g.a(v); + target.a(v + "=" + i); } val.v = _var; val.i = v; return v; } -function allocateVal(b, schema) { - let v = varWithoutAllocation(b.g); - b.a(v); +function operationArg(schema, expected, flag, defs) { return { - b: b, v: _var, - i: v, + i: "i", + s: schema, + e: expected, f: 0, - type: schema.type + cp: "", + l: "", + a: initialAllocate, + path: "", + g: { + v: -1, + o: flag, + e: [], + d: defs + } }; } -function val(b, initial, schema) { +function failWithArg(b, fn, arg) { + return embed(b, arg => { + let errorDetails = fn(arg); + throw new SuryError(errorDetails); + }) + "(" + arg + ")"; +} + +function makeInvalidConversionDetails(input, to, cause) { + if ((cause&&cause.s===s)) { + if (!cause["p"]) { + cause.path = input.path + cause.path; + } + return cause; + } + let tmp; + if ((cause instanceof Error)) { + let text = ("" + cause); + tmp = text.startsWith("Error: ") ? text.slice(7) : text; + } else { + tmp = stringify(cause); + } return { - b: b, - v: _notVar, - i: initial, - f: 0, - type: schema.type + code: "invalid_conversion", + path: input.path, + reason: tmp, + from: input.s, + to: to, + cause: cause }; } -function constVal(b, schema) { - return { - b: b, - v: _notVar, - i: inlineConst(b, schema), - f: 0, - type: schema.type, - const: schema.const +function makeInvalidInputDetails(expected, received, path, input, includeInput, unionErrors) { + let reasonRef = "Expected " + toExpression(expected) + ", received " + ( + includeInput ? stringify(input) : toExpression(received) + ); + if (unionErrors !== undefined) { + let reasonsDict = {}; + for (let idx = 0, idx_finish = unionErrors.length; idx < idx_finish; ++idx) { + let caseError = unionErrors[idx]; + let caseReason = caseError.reason.split("\n").join("\n "); + let nonEmptyPath = caseError.path; + let location = nonEmptyPath === "" ? "" : "At " + nonEmptyPath + ": "; + let line = "\n- " + location + caseReason; + if (!reasonsDict[line]) { + reasonsDict[line] = 1; + reasonRef = reasonRef + line; + } + + } + } + let details = { + code: "invalid_input", + path: path, + reason: reasonRef, + expected: expected, + received: received, + unionErrors: unionErrors + }; + if (includeInput) { + details.input = input; + } + return details; +} + +function failInvalidType(input) { + let p = input.prev; + let received = p !== undefined ? p.s : input.s; + let path = input.path; + let expected = input.e; + let em = expected.errorMessage; + let override; + if (em !== undefined) { + let m = em["type"]; + override = m !== undefined ? m : em["_"]; + } else { + override = undefined; + } + if (override !== undefined) { + return _value => ({ + code: "custom", + path: path, + reason: override + }); + } else { + return value => makeInvalidInputDetails(expected, received, path, value, true, undefined); + } +} + +function failWithErrorMessage(key, defaultMessage) { + return input => { + let em = input.e.errorMessage; + let override; + if (em !== undefined) { + let m = em[key]; + override = m !== undefined ? m : em["_"]; + } else { + override = undefined; + } + let m$1; + if (override !== undefined) { + m$1 = override; + } else { + if (defaultMessage === undefined) { + return failInvalidType(input); + } + m$1 = defaultMessage; + } + let path = input.path; + return _value => ({ + code: "custom", + path: path, + reason: m$1 + }); }; } -function asyncVal(b, initial) { - return { - b: b, - v: _notVar, - i: initial, - f: 2, - type: "unknown" +function embedInvalidInput(input, expectedOpt) { + let expected = expectedOpt !== undefined ? expectedOpt : input.e; + let p = input.prev; + let received = p !== undefined ? p.s : input.s; + let path = input.path; + return failWithArg(input, value => makeInvalidInputDetails(expected, received, path, value, true, undefined), input.v()); +} + +function emitChecks(val, inputVar) { + let checks = val.vc; + let len = checks.length; + if (len === 1) { + let check = checks[0]; + return check.c(inputVar) + "||" + failWithArg(val, check.f(val), inputVar) + ";"; + } + let out = ""; + let i = 0; + while (i < len) { + let head = checks[i]; + let fail = head.f; + let cond = head.c(inputVar); + i = i + 1 | 0; + while (i < len && checks[i].f === fail) { + cond = cond + "&&" + checks[i].c(inputVar); + i = i + 1 | 0; + }; + out = out + (cond + "||" + failWithArg(val, fail(val), inputVar) + ";"); }; + return out; } -function objectJoin(inlinedLocation, value) { - return inlinedLocation + ":" + value + ","; +function andJoinChecks(checks, inputVar) { + let result = checks[0].c(inputVar); + for (let i = 1, i_finish = checks.length; i < i_finish; ++i) { + result = result + "&&" + checks[i].c(inputVar); + } + return result; } -function arrayJoin(_inlinedLocation, value) { - return value + ","; +function merge(val, hoistCond) { + let current = val; + let code = ""; + while (current !== undefined) { + let val$1 = current; + current = val$1.prev; + let currentCode = ""; + if (val$1.vc) { + let isHoistable = hoistCond !== undefined && ( + val$1.t === true ? val$1.prev.t !== true && val$1.cp === "" : true + ); + if (isHoistable) { + let prev = current; + let condCode = andJoinChecks(val$1.vc, prev.v()); + if (hoistCond.contents) { + hoistCond.contents = condCode + "&&" + hoistCond.contents; + } else { + hoistCond.contents = condCode; + } + } else if (val$1.e.noValidation !== true) { + let prev$1 = current; + currentCode = emitChecks(val$1, prev$1.v()); + } + + } + if (val$1.l !== "") { + currentCode = currentCode + ("let " + val$1.l + ";"); + } + ((delete val$1.a)); + currentCode = val$1.cp + currentCode; + code = currentCode + code; + }; + return code; } -function make(b, isArray) { +function next(prev, initial, schema, expectedOpt) { + let expected = expectedOpt !== undefined ? expectedOpt : prev.e; return { - b: b, v: _notVar, - i: "", + i: initial, + s: schema, + e: expected, + prev: prev, f: 0, - type: isArray ? "array" : "object", - properties: {}, - additionalItems: "strict", - j: isArray ? arrayJoin : objectJoin, - c: 0, - r: "" + d: prev.d, + cp: "", + l: "", + a: initialAllocate, + t: true, + path: prev.path, + g: prev.g }; } -function add(objectVal, location, val) { - let inlinedLocation = inlineLocation(objectVal.b, location); - objectVal.properties[location] = val; - if (val.f & 2) { - objectVal.r = objectVal.r + val.i + ","; - objectVal.i = objectVal.i + objectVal.j(inlinedLocation, "a[" + (objectVal.c++) + "]"); - } else { - objectVal.i = objectVal.i + objectVal.j(inlinedLocation, val.i); +function refine(val, schemaOpt, checks, expectedOpt) { + let schema = schemaOpt !== undefined ? schemaOpt : val.s; + let expected = expectedOpt !== undefined ? expectedOpt : val.e; + let shouldLink = val.v !== _var; + let nextVal = { + v: shouldLink ? _prevVar : _var, + i: val.i, + s: schema, + e: expected, + prev: val, + f: val.f, + d: val.d, + cp: "", + l: "", + a: initialAllocate, + vc: checks, + t: val.t, + path: val.path, + g: val.g + }; + if (shouldLink) { + let valVar = (val.v.bind(val)); + val.v = () => { + let v = valVar(); + nextVal.i = v; + nextVal.v = _var; + return v; + }; } + return nextVal; } -function merge(target, subObjectVal) { - let locations = Object.keys(subObjectVal.properties); - for (let idx = 0, idx_finish = locations.length; idx < idx_finish; ++idx) { - let location = locations[idx]; - add(target, location, subObjectVal.properties[location]); +function pushCheck(val, check) { + let arr = val.vc; + if (arr !== undefined) { + arr.push(check); + } else { + val.vc = [check]; } } -function complete(objectVal, isArray) { - objectVal.i = isArray ? "[" + objectVal.i + "]" : "{" + objectVal.i + "}"; - if (objectVal.c) { - objectVal.f = objectVal.f | 2; - objectVal.i = "Promise.all([" + objectVal.r + "]).then(a=>(" + objectVal.i + "))"; +function hoistChildChecks(parent, child, key) { + if (!child.vc) { + return; } - objectVal.additionalItems = "strict"; - return objectVal; + let inlinedLocation = inlineLocation(parent.g, key); + let pathAppend = "[" + inlinedLocation + "]"; + child.vc.forEach(check => pushCheck(parent, { + c: inputVar => check.c(inputVar + pathAppend), + f: check.f + })); + child.vc = undefined; +} + +function dynamicScope(from, locationVar) { + return { + p: from, + v: _notVarBeforeValidation, + i: from.v() + "[" + locationVar + "]", + s: from.s.additionalItems, + e: from.e.additionalItems, + f: from.f, + cp: "", + l: "", + a: initialAllocate, + path: "", + g: from.g + }; } -function addKey(b, input, key, val) { - return input.v(b) + "[" + key + "]=" + val.i; +function nextConst(from, schema, expected) { + return next(from, inlineConst(from, schema), schema, expected); } -function set(b, input, val) { - if (input === val) { - return ""; - } - let inputVar = input.v(b); - let match = input.f & 2; - let match$1 = val.f & 2; - if (match) { - if (!match$1) { - return inputVar + "=Promise.resolve(" + val.i + ")"; +function asyncVal(from, initial) { + let v = next(from, initial, from.s, undefined); + v.f = 1; + return v; +} + +function add(objectVal, location, val) { + if (objectVal.s.type === arrayTag) { + objectVal.s.items.push(val.s); + } else { + if (!val.o) { + objectVal.s.required.push(location); } - - } else if (match$1) { - input.f = input.f | 2; - return inputVar + "=" + val.i; + objectVal.s.properties[location] = val.s; + } + if (val.f & 1) { + val.v(); + } + objectVal.cp = objectVal.cp + merge(val, undefined); + objectVal.d[location] = val; +} + +function addKey(objVal, key, value) { + return objVal.v() + "[" + key + "]=" + value.i; +} + +function scope(val) { + let shouldLink = val.v !== _var; + let nextVal = { + b: val, + v: shouldLink ? _bondVar : _var, + i: val.i, + s: val.s, + ii: val.ii, + io: val.io, + e: val.e, + f: 0, + d: val.d, + cp: "", + l: "", + a: initialAllocate, + u: false, + t: false, + path: val.path, + g: val.g + }; + if (shouldLink) { + let valVar = (val.v.bind(val)); + val.v = () => { + let v = valVar(); + nextVal.i = v; + nextVal.v = _var; + return v; + }; } - return inputVar + "=" + val.i; + return nextVal; } -function get(b, targetVal, location) { - let properties = targetVal.properties; - let val = properties[location]; - if (val !== undefined) { - return val; +function get(parent, location) { + let d = parent.d; + let vals; + if (d !== undefined) { + vals = d; + } else { + let d$1 = {}; + parent.d = d$1; + vals = d$1; + } + let v = vals[location]; + if (v !== undefined) { + return scope(v); } - let schema = targetVal.additionalItems; - let schema$1; - if (schema === "strip" || schema === "strict") { - if (schema === "strip") { + let locationSchema = parent.s.type === objectTag ? parent.s.properties[location] : parent.s.items[location]; + let schema; + if (locationSchema !== undefined) { + schema = locationSchema; + } else { + let s = parent.s.additionalItems; + if (s === "strip" || s === "strict") { + if (s === "strip") { + throw new Error("[Sury] The schema doesn't have additional items"); + } throw new Error("[Sury] The schema doesn't have additional items"); + } else { + schema = s; } - throw new Error("[Sury] The schema doesn't have additional items"); - } else { - schema$1 = schema; } - let val$1 = { - b: b, - v: _notVar, - i: targetVal.v(b) + ("[" + fromString(location) + "]"), + let inlinedLocation = inlineLocation(parent.g, location); + let pathAppend = "[" + inlinedLocation + "]"; + let item = { + p: parent, + v: _notVarAtParent, + i: constField in schema ? inlineConst(parent, schema) : parent.v() + pathAppend, + s: schema, + e: schema, f: 0, - type: schema$1.type + cp: "", + l: "", + a: initialAllocate, + path: parent.path + pathAppend, + g: parent.g }; - properties[location] = val$1; - return val$1; + vals[location] = item; + return item; } -function setInlined(b, input, inlined) { - return input.v(b) + "=" + inlined; +function embedTransformation(input, fn, isAsync) { + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let output = next(input, outputVar, unknown, input.e.to); + output.v = _var; + if (isAsync) { + if (!(input.g.o & 1)) { + throw new SuryError({ + code: "invalid_operation", + path: "", + reason: "Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead" + }); + } + output.f = output.f | 1; + } + let embededFn = embed(input, fn); + let failure = failWithArg(output, e => makeInvalidConversionDetails(input, unknown, e), "x"); + output.cp = "try{" + outputVar + "=" + embededFn + "(" + input.i + ")" + ( + isAsync ? ".catch(x=>" + failure + ")" : "" + ) + "}catch(x){" + failure + "}"; + return output; } -function map(inlinedFn, input) { +function effectCtx(input) { return { - b: input.b, - v: _notVar, - i: inlinedFn + "(" + input.i + ")", - f: 0, - type: "unknown" + fail: (message, pathOpt) => { + let path = pathOpt !== undefined ? pathOpt : ""; + let error = new SuryError({ + code: "custom", + path: input.path + path, + reason: message + }); + error["p"] = 1; + throw error; + } }; } -function $$throw(b, code, path) { - throw new SuryError(code, b.g.o, path); +function invalidOperation(val, description) { + throw new SuryError({ + code: "invalid_operation", + path: val.path, + reason: description + }); } -function embedSyncOperation(b, input, fn) { - if (input.f & 2) { - return asyncVal(input.b, input.i + ".then(" + embed(b, fn) + ")"); +function mergeWithPathPrepend(val, parent, locationVar, appendSafe) { + if (val.path === "" && locationVar === undefined) { + return merge(val, undefined); } else { - return map(embed(b, fn), input); + let $$catch = errorVar => { + let path = parent.path; + let tmp = path === "" ? "" : fromString(path) + "+"; + return errorVar + ".path=" + tmp + ( + locationVar !== undefined ? "'[\"'+" + locationVar + "+'\"]'+" : "" + ) + errorVar + ".path"; + }; + let valCode = merge(val, undefined); + if (valCode === "" && !(val.f & 1)) { + return valCode + ( + appendSafe !== undefined ? appendSafe() : "" + ); + } + let errorVar = varWithoutAllocation(val.g); + let catchCode = $$catch(errorVar) + ";throw " + errorVar; + if (val.f & 1) { + val.i = val.i + ".catch(" + errorVar + "=>{" + catchCode + "})"; + } + return "try{" + valCode + ( + appendSafe !== undefined ? appendSafe() : "" + ) + "}catch(" + errorVar + "){" + catchCode + "}"; } } -function failWithArg(b, path, fn, arg) { - return embed(b, arg => $$throw(b, fn(arg), path)) + "(" + arg + ")"; +function unsupportedDecode(b, from, target) { + let errorDetails_0 = b.path; + let errorDetails_1 = "Can't decode " + toExpression(from) + " to " + toExpression(target) + ". Use S.to to define a custom decoder"; + let errorDetails = { + code: "unsupported_decode", + path: errorDetails_0, + reason: errorDetails_1, + from: from, + to: target + }; + throw new SuryError(errorDetails); } -function fail(b, message, path) { - return embed(b, () => $$throw(b, { - TAG: "OperationFailed", - _0: message - }, path)) + "()"; +function noopOperation(i) { + return i; } -function effectCtx(b, selfSchema, path) { - return { - schema: selfSchema, - fail: (message, customPathOpt) => { - let customPath = customPathOpt !== undefined ? customPathOpt : ""; - return $$throw(b, { - TAG: "OperationFailed", - _0: message - }, path + customPath); - } - }; +noopOperation.embedded = immutableEmpty$1; + +function inputToString(input) { + return next(input, "\"\"+" + input.i, string, undefined); } -function invalidOperation(b, path, description) { - return $$throw(b, { - TAG: "InvalidOperation", - description: description - }, path); +function int32FormatValidation(inputVar) { + return inputVar + "<=2147483647&&" + inputVar + ">=-2147483648&&" + inputVar + "%1===0"; } -function withPathPrepend(b, input, path, maybeDynamicLocationVar, appendSafe, fn) { - if (path === "" && maybeDynamicLocationVar === undefined) { - return fn(b, input, path); - } - try { - let $$catch = (b, errorVar) => { - b.c = errorVar + ".path=" + fromString(path) + "+" + ( - maybeDynamicLocationVar !== undefined ? "'[\"'+" + maybeDynamicLocationVar + "+'\"]'+" : "" - ) + errorVar + ".path"; - }; - let fn$1 = b => fn(b, input, ""); - let prevCode = b.c; - b.c = ""; - let errorVar = varWithoutAllocation(b.g); - let maybeResolveVal = $$catch(b, errorVar); - let catchCode = "if(" + (errorVar + "&&" + errorVar + ".s===s") + "){" + b.c; - b.c = ""; - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let fnOutput = fn$1(bb); - b.c = b.c + allocateScope(bb); - let isNoop = fnOutput.i === input.i && b.c === ""; - if (appendSafe !== undefined) { - appendSafe(b, fnOutput); - } - if (isNoop) { - return fnOutput; - } - let isAsync = fnOutput.f & 2; - let output = input === fnOutput ? input : ( - appendSafe !== undefined ? fnOutput : ({ - b: b, - v: _notVar, - i: "", - f: isAsync ? 2 : 0, - type: "unknown" - }) - ); - let catchCode$1 = maybeResolveVal !== undefined ? catchLocation => catchCode + ( - catchLocation === 1 ? "return " + maybeResolveVal.i : set(b, output, maybeResolveVal) - ) + ("}else{throw " + errorVar + "}") : param => catchCode + "}throw " + errorVar; - b.c = prevCode + ("try{" + b.c + ( - isAsync ? setInlined(b, output, fnOutput.i + ".catch(" + errorVar + "=>{" + catchCode$1(1) + "})") : set(b, output, fnOutput) - ) + "}catch(" + errorVar + "){" + catchCode$1(0) + "}"); - return output; - } catch (exn) { - let error = getOrRethrow(exn); - throw new SuryError(error.code, error.flag, path + "[]" + error.path); +function numberDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + let checks = [{ + c: inputVar => "typeof " + inputVar + "===\"" + numberTag + "\"", + f: failInvalidType + }]; + let match = input.e.format; + let exit = 0; + if (match === "int32") { + checks.push({ + c: int32FormatValidation, + f: failInvalidType + }); + } else { + exit = 1; + } + if (exit === 1) { + if (!(input.g.o & 2)) { + checks.push({ + c: inputVar => "!Number.isNaN(" + inputVar + ")", + f: failInvalidType + }); + } + + } + return refine(input, input.e, checks, undefined); + } + if (!(inputTagFlag & 2)) { + if (inputTagFlag & 4) { + if (input.s.format !== input.e.format && input.e.format === "int32") { + return refine(input, input.e, [{ + c: int32FormatValidation, + f: failInvalidType + }], undefined); + } else { + return input; + } + } else { + return unsupportedDecode(input, input.s, input.e); + } } + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar + "=+" + input.v()); + let output = next(input, outputVar, input.e, undefined); + output.v = _var; + output.vc = [{ + c: param => { + let match = input.e.format; + if (match !== undefined) { + if (match === "int32") { + return int32FormatValidation(outputVar); + } else { + return "!Number.isNaN(" + outputVar + ")"; + } + } else { + return "!Number.isNaN(" + outputVar + ")"; + } + }, + f: failInvalidType + }]; + return output; } -function validation(b, inputVar, schema, negative) { - let eq = negative ? "!==" : "==="; - let and_ = negative ? "||" : "&&"; - let exp = negative ? "!" : ""; - let tag = schema.type; - let tagFlag = flags[tag]; - if (tagFlag & 2048) { - return exp + ("Number.isNaN(" + inputVar + ")"); - } - if (constField in schema) { - return inputVar + eq + inlineConst(b, schema); - } - if (tagFlag & 4) { - return "typeof " + inputVar + eq + "\"" + tag + "\""; - } - if (tagFlag & 64) { - return "typeof " + inputVar + eq + "\"" + tag + "\"" + and_ + exp + inputVar; - } - if (tagFlag & 128) { - return exp + "Array.isArray(" + inputVar + ")"; - } - if (!(tagFlag & 8192)) { - return "typeof " + inputVar + eq + "\"" + tag + "\""; +float.decoder = numberDecoder; + +int.decoder = numberDecoder; + +function stringDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + return refine(input, input.e, [{ + c: inputVar => "typeof " + inputVar + "===\"" + stringTag + "\"", + f: failInvalidType + }], undefined); } - let c = inputVar + " instanceof " + embed(b, schema.class); - if (negative) { - return "!(" + c + ")"; - } else { - return c; + if (!(inputTagFlag & 3132 && constField in input.s)) { + if (inputTagFlag & 1036) { + return inputToString(input); + } else if (inputTagFlag & 2) { + return input; + } else { + return unsupportedDecode(input, input.s, input.e); + } } + let $$const = (""+input.s.const); + let schema = base(stringTag, false); + schema.const = $$const; + return next(input, "\"" + $$const + "\"", schema, undefined); } -function refinement(b, inputVar, schema, negative) { - let eq = negative ? "!==" : "==="; - let and_ = negative ? "||" : "&&"; - let not_ = negative ? "" : "!"; - let lt = negative ? ">" : "<"; - let gt = negative ? "<" : ">"; - let match = schema.type; - let tag; - let exit = 0; - let match$1 = schema.const; - if (match$1 !== undefined) { - return ""; - } - let match$2 = schema.format; - if (match$2 !== undefined) { - switch (match$2) { - case "int32" : - return and_ + inputVar + lt + "2147483647" + and_ + inputVar + gt + "-2147483648" + and_ + inputVar + "%1" + eq + "0"; - case "port" : - case "json" : - exit = 2; - break; - } - } else { - exit = 2; +string.decoder = stringDecoder; + +function booleanDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + return refine(input, input.e, [{ + c: inputVar => "typeof " + inputVar + "===\"" + booleanTag + "\"", + f: failInvalidType + }], undefined); } - if (exit === 2) { - switch (match) { - case "number" : - if (globalConfig.n) { - return ""; - } else { - return and_ + not_ + "Number.isNaN(" + inputVar + ")"; - } - case "array" : - case "object" : - tag = match; - break; - default: - return ""; - } - } - let additionalItems = schema.additionalItems; - let items = schema.items; - let length = items.length; - let code = tag === "array" ? ( - additionalItems === "strip" || additionalItems === "strict" ? ( - additionalItems === "strip" ? and_ + inputVar + ".length" + gt + length : and_ + inputVar + ".length" + eq + length - ) : "" - ) : ( - additionalItems === "strip" ? "" : and_ + not_ + "Array.isArray(" + inputVar + ")" - ); - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let match$3 = items[idx]; - let location = match$3.location; - let item = match$3.schema; - let itemCode; - if (constField in item || schema.unnest) { - let inlinedLocation = inlineLocation(b, location); - itemCode = validation(b, inputVar + ("[" + inlinedLocation + "]"), item, negative); - } else if (item.items) { - let inlinedLocation$1 = inlineLocation(b, location); - let inputVar$1 = inputVar + ("[" + inlinedLocation$1 + "]"); - itemCode = validation(b, inputVar$1, item, negative) + refinement(b, inputVar$1, item, negative); + if (!(inputTagFlag & 2)) { + if (inputTagFlag & 8) { + return input; } else { - itemCode = ""; + return unsupportedDecode(input, input.s, input.e); } - if (itemCode !== "") { - code = code + and_ + itemCode; + } + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let output = next(input, outputVar, input.e, undefined); + output.v = _var; + let inputVar = input.v(); + output.cp = "(" + output.i + "=" + inputVar + "===\"true\")||" + inputVar + "===\"false\"||" + embedInvalidInput(input, undefined) + ";"; + return output; +} + +bool.decoder = booleanDecoder; + +function bigintDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + return refine(input, input.e, [{ + c: inputVar => "typeof " + inputVar + "===\"" + bigintTag + "\"", + f: failInvalidType + }], undefined); + } + if (!(inputTagFlag & 2)) { + if (inputTagFlag & 4) { + return next(input, "BigInt(" + input.i + ")", input.e, undefined); + } else if (inputTagFlag & 1024) { + return input; + } else { + return unsupportedDecode(input, input.s, input.e); } - } - return code; + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let output = next(input, outputVar, input.e, undefined); + output.v = _var; + output.cp = "try{" + outputVar + "=BigInt(" + input.v() + ")}catch(_){" + embedInvalidInput(input, undefined) + "}"; + return output; } -function makeRefinedOf(b, input, schema) { - let mut = { - b: b, - v: input.v, - i: input.i, - f: input.f, - type: schema.type - }; - let loop = (mut, schema) => { - if (constField in schema) { - mut.const = schema.const; - } - let items = schema.items; - if (items === undefined) { - return; - } - let properties = {}; - items.forEach(item => { - let schema = item.schema; - let isConst = constField in schema; - if (!(isConst || schema.items)) { - return; - } - let tmp; - if (isConst) { - tmp = inlineConst(b, schema); - } else { - let inlinedLocation = inlineLocation(b, item.location); - tmp = mut.v(b) + ("[" + inlinedLocation + "]"); - } - let mut$1 = { - b: mut.b, - v: _notVar, - i: tmp, - f: 0, - type: schema.type - }; - loop(mut$1, schema); - properties[item.location] = mut$1; - }); - mut.properties = properties; - mut.additionalItems = unknown; - }; - loop(mut, schema); - return mut; -} +bigint.decoder = bigintDecoder; -function typeFilterCode(b, schema, input, path) { - if (schema.noValidation || flags[schema.type] & 17153) { - return ""; +function symbolDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + return refine(input, input.e, [{ + c: inputVar => "typeof " + inputVar + "===\"" + symbolTag + "\"", + f: failInvalidType + }], undefined); + } else if (inputTagFlag & 32768) { + return input; + } else { + return unsupportedDecode(input, input.s, input.e); } - let inputVar = input.v(b); - return "if(" + validation(b, inputVar, schema, true) + refinement(b, inputVar, schema, true) + "){" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: schema, - received: input - }), inputVar) + "}"; -} - -function unsupportedTransform(b, from, target, path) { - return $$throw(b, { - TAG: "UnsupportedTransformation", - from: from, - to: target - }, path); } -function noopOperation(i) { - return i; -} +symbol.decoder = symbolDecoder; function setHas(has, tag) { - has[tag === "union" || tag === "ref" ? "unknown" : tag] = true; + has[flags[tag] & 768 ? unknownTag : tag] = true; } let jsonName = "JSON"; let jsonString = shaken("jsonString"); -function inputToString(b, input) { - return val(b, "\"\"+" + input.i, string); +function jsonStringWithSpace(space) { + let mut = copySchema(jsonString); + mut.space = space; + return mut; } -function parse(prevB, schema, inputArg, path) { - let b = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: prevB.g - }; - if (schema.$defs) { - b.g.d = schema.$defs; - } - let input = inputArg; - let isFromLiteral = constField in input; - let isSchemaLiteral = constField in schema; - let isSameTag = input.type === schema.type; - let schemaTagFlag = flags[schema.type]; - let inputTagFlag = flags[input.type]; - let isUnsupported = false; - if (!(schemaTagFlag & 257 || schema.format === "json")) { - if (schema.name === jsonName && !(inputTagFlag & 1)) { - if (!(inputTagFlag & 14)) { - if (inputTagFlag & 1024) { - input = inputToString(b, input); - } else { - isUnsupported = true; - } - } - - } else if (isSchemaLiteral) { - if (isFromLiteral) { - if (input.const !== schema.const) { - input = constVal(b, schema); - } - - } else if (inputTagFlag & 2 && schemaTagFlag & 3132) { - let inputVar = input.v(b); - b.f = schema.noValidation ? "" : input.i + "===\"" + schema.const + "\"||" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: schema, - received: input - }), inputVar) + ";"; - input = constVal(b, schema); - } else if (schema.noValidation) { - input = constVal(b, schema); +let json = shaken("json"); + +function literalDecoder(input) { + let expectedSchema = input.e; + if (expectedSchema.noValidation) { + return nextConst(input, expectedSchema, undefined); + } + if (constField in input.s) { + if (input.s.const === expectedSchema.const) { + return input; + } else { + return nextConst(input, expectedSchema, undefined); + } + } + let schemaTagFlag = flags[expectedSchema.type]; + if (!(flags[input.s.type] & 2 && schemaTagFlag & 3132)) { + if (schemaTagFlag & 2048) { + return refine(input, expectedSchema, [{ + c: inputVar => "Number.isNaN(" + inputVar + ")", + f: failInvalidType + }], undefined); + } else { + return refine(input, expectedSchema, [{ + c: inputVar => inputVar + "===" + inlineConst(input, expectedSchema), + f: failInvalidType + }], undefined); + } + } + let stringConstSchema = base(stringTag, false); + stringConstSchema.const = ("" + expectedSchema.const); + let stringConstVal = nextConst(input, stringConstSchema, stringConstSchema); + stringConstVal.vc = [{ + c: inputVar => inputVar + "===\"" + stringConstSchema.const + "\"", + f: failInvalidType + }]; + return nextConst(stringConstVal, expectedSchema, expectedSchema); +} + +nullLiteral.decoder = literalDecoder; + +unit.decoder = literalDecoder; + +nan.decoder = literalDecoder; + +function parse(value) { + if (value === null) { + return nullLiteral; + } + let $$typeof = typeof value; + if ($$typeof === "object") { + let s = base(instanceTag, true); + s.class = value.constructor; + s.const = value; + s.decoder = literalDecoder; + return s; + } + if ($$typeof === "undefined") { + return unit; + } + if ($$typeof === "number" && Number.isNaN(value)) { + return nan; + } + let s$1 = base($$typeof, true); + s$1.const = value; + s$1.decoder = literalDecoder; + return s$1; +} + +function parse$1(input) { + let valRef = input; + let appliedEncoderRef; + let loopCount = 0; + while (!valRef.io || valRef.e.to) { + let appliedEncoder = appliedEncoderRef; + appliedEncoderRef = undefined; + let loopInput = valRef; + loopCount = loopCount + 1 | 0; + if (loopCount > 50) { + throw (new Error("Loop count exceeded 100")); + } + if (loopInput.e.$defs) { + if (loopInput.g.d) { + Object.assign(loopInput.g.d, loopInput.e.$defs); } else { - b.f = typeFilterCode(prevB, schema, input, path); - input.type = schema.type; - input.const = schema.const; + loopInput.g.d = loopInput.e.$defs; } - } else if (isFromLiteral && !isSchemaLiteral) { - if (!isSameTag) { - if (schemaTagFlag & 2 && inputTagFlag & 3132) { - let $$const = (""+input.const); - input = { - b: b, - v: _notVar, - i: "\"" + $$const + "\"", - f: 0, - type: "string", - const: $$const - }; - } else { - isUnsupported = true; - } + } + if (loopInput.f & 1) { + let operationInputVar = loopInput.v(); + let operationInput = scope(loopInput); + let operationOutput = parse$1(operationInput); + let operationCode = merge(operationOutput, undefined); + valRef = operationInput.i !== operationOutput.i || operationCode !== "" ? next(loopInput, operationInputVar + ".then(" + operationInputVar + "=>{" + operationCode + "return " + operationOutput.i + "})", operationOutput.s, operationOutput.e) : refine(loopInput, operationOutput.s, undefined, operationOutput.e); + valRef.f = valRef.f | 1; + valRef.io = true; + } else if (loopInput.io) { + let to = loopInput.e.to; + let match = loopInput.e; + let parser = match.parser; + valRef = parser !== undefined ? parser(loopInput) : refine(valRef, undefined, undefined, to); + } else { + let maybeEncoder = loopInput.s.encoder; + if (!loopInput.ii && maybeEncoder && maybeEncoder !== appliedEncoder && loopInput.s !== loopInput.e && loopInput.e.type !== unknownTag) { + valRef = maybeEncoder(loopInput, loopInput.e); } - - } else if (inputTagFlag & 1) { - let ref = schema.$ref; - if (ref !== undefined) { - let defs = b.g.d; - let identifier = ref.slice(8); - let def = defs[identifier]; - let flag = schema.noValidation ? (b.g.o | 1) ^ 1 : b.g.o; - let fn = def[flag]; - let recOperation; - if (fn !== undefined) { - let fn$1 = Primitive_option.valFromOption(fn); - recOperation = fn$1 === 0 ? embed(b, def) + ("[" + flag + "]") : embed(b, fn$1); - } else { - def[flag] = 0; - let fn$2 = internalCompile(def, flag, b.g.d); - def[flag] = fn$2; - recOperation = embed(b, fn$2); - } - input = withPathPrepend(b, input, path, undefined, undefined, (param, input, param$1) => { - let output = map(recOperation, input); - if (def.isAsync === undefined) { - let defsMut = copy(defs); - defsMut[identifier] = unknown; - isAsyncInternal(def, defsMut); - } - if (def.isAsync) { - output.f = output.f | 2; - } - return output; - }); - input.v(b); + if (loopInput !== valRef) { + appliedEncoderRef = maybeEncoder; } else { - if (b.g.o & 1) { - b.f = typeFilterCode(prevB, schema, input, path); - } - let refined = makeRefinedOf(b, input, schema); - input.type = refined.type; - input.i = refined.i; - input.v = refined.v; - input.additionalItems = refined.additionalItems; - input.properties = refined.properties; - if (constField in refined) { - input.const = refined.const; + valRef = loopInput.e.decoder(loopInput); + if (!valRef.io) { + let hasInputRefiner = valRef.e.inputRefiner; + let hasRefiner = valRef.e.refiner; + if (hasInputRefiner || hasRefiner) { + let checks = []; + if (hasInputRefiner) { + let arr = valRef.e.inputRefiner(valRef); + for (let i = 0, i_finish = arr.length; i < i_finish; ++i) { + checks.push(arr[i]); + } + } + if (hasRefiner) { + let arr$1 = valRef.e.refiner(valRef); + for (let i$1 = 0, i_finish$1 = arr$1.length; i$1 < i_finish$1; ++i$1) { + checks.push(arr$1[i$1]); + } + } + if (checks.length > 0) { + valRef = refine(valRef, undefined, checks, undefined); + } + + } + valRef.ii = true; + valRef.io = true; } } - } else if (schemaTagFlag & 2 && inputTagFlag & 1036) { - input = inputToString(b, input); - } else if (!isSameTag) { - if (inputTagFlag & 2) { - let inputVar$1 = input.v(b); - if (schemaTagFlag & 8) { - let output = allocateVal(b, schema); - b.c = b.c + ("(" + output.i + "=" + inputVar$1 + "===\"true\")||" + inputVar$1 + "===\"false\"||" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: schema, - received: input - }), inputVar$1) + ";"); - input = output; - } else if (schemaTagFlag & 4) { - let output$1 = val(b, "+" + inputVar$1, schema); - let outputVar = output$1.v(b); - let match = schema.format; - b.c = b.c + ( - match !== undefined ? "(" + refinement(b, outputVar, schema, true).slice(2) + ")" : "Number.isNaN(" + outputVar + ")" - ) + ("&&" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: schema, - received: input - }), inputVar$1) + ";"); - input = output$1; - } else if (schemaTagFlag & 1024) { - let output$2 = allocateVal(b, schema); - b.c = b.c + ("try{" + output$2.i + "=BigInt(" + inputVar$1 + ")}catch(_){" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: schema, - received: input - }), inputVar$1) + "}"); - input = output$2; - } else { - isUnsupported = true; - } - } else if (inputTagFlag & 4 && schemaTagFlag & 1024) { - input = val(b, "BigInt(" + input.i + ")", schema); - } else { - isUnsupported = true; - } - } - - } - if (isUnsupported) { - unsupportedTransform(b, input, schema, path); - } - let compiler = schema.compiler; - if (compiler !== undefined) { - input = compiler(b, input, schema, path); - } - if (input.t !== true) { - let refiner = schema.refiner; - if (refiner !== undefined) { - b.c = b.c + refiner(b, input.v(b), schema, path); - } - - } - let to = schema.to; - if (to !== undefined) { - let parser = schema.parser; - if (parser !== undefined) { - input = parser(b, input, schema, path); - } - if (input.t !== true) { - input = parse(b, to, input, path); } - - } - prevB.c = prevB.c + allocateScope(b); - return input; + }; + return valRef; } function getOutputSchema(_schema) { @@ -1147,44 +1364,14 @@ function getOutputSchema(_schema) { }; } -function jsonableValidation(output, parent, path, flag) { - let tagFlag = flags[output.type]; - if (tagFlag & 48129 || tagFlag & 16 && parent.type !== "object") { - throw new SuryError({ - TAG: "InvalidJsonSchema", - _0: parent - }, flag, path); - } - if (tagFlag & 256) { - output.anyOf.forEach(s => jsonableValidation(s, parent, path, flag)); - return; - } - if (!(tagFlag & 192)) { - return; - } - let additionalItems = output.additionalItems; - if (additionalItems === "strip" || additionalItems === "strict") { - additionalItems === "strip"; - } else { - jsonableValidation(additionalItems, parent, path, flag); - } - let p = output.properties; - if (p !== undefined) { - let keys = Object.keys(p); - for (let idx = 0, idx_finish = keys.length; idx < idx_finish; ++idx) { - let key = keys[idx]; - jsonableValidation(p[key], parent, path, flag); - } - return; - } - output.items.forEach(item => jsonableValidation(item.schema, output, path + ("[" + fromString(item.location) + "]"), flag)); -} - function reverse(schema) { + if (reversedKey in schema) { + return schema[reversedKey]; + } let reversedHead; let current = schema; while (current) { - let mut = copyWithoutCache(current); + let mut = copySchema(current); let next = mut.to; let to = reversedHead; if (to !== undefined) { @@ -1204,6 +1391,18 @@ function reverse(schema) { } else { ((delete mut.serializer)); } + let refiner = mut.refiner; + let inputRefiner = mut.inputRefiner; + if (inputRefiner !== undefined) { + mut.refiner = inputRefiner; + } else { + ((delete mut.refiner)); + } + if (refiner !== undefined) { + mut.inputRefiner = refiner; + } else { + ((delete mut.inputRefiner)); + } let fromDefault = mut.fromDefault; let $$default = mut.default; if ($$default !== undefined) { @@ -1218,28 +1417,21 @@ function reverse(schema) { } let items = mut.items; if (items !== undefined) { - let properties = {}; let newItems = new Array(items.length); for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let item = items[idx]; - let reversed_schema = reverse(item.schema); - let reversed_location = item.location; - let reversed = { - schema: reversed_schema, - location: reversed_location - }; - if (item.r) { - reversed.r = item.r; - } - properties[item.location] = reversed_schema; - newItems[idx] = reversed; + newItems[idx] = reverse(items[idx]); } mut.items = newItems; - let match = mut.properties; - if (match !== undefined) { - mut.properties = properties; + } + let properties = mut.properties; + if (properties !== undefined) { + let newProperties = {}; + let keys = Object.keys(properties); + for (let idx$1 = 0, idx_finish$1 = keys.length; idx$1 < idx_finish$1; ++idx$1) { + let key = keys[idx$1]; + newProperties[key] = reverse(properties[key]); } - + mut.properties = newProperties; } if (typeof mut.additionalItems === "object") { mut.additionalItems = reverse(mut.additionalItems); @@ -1248,11 +1440,11 @@ function reverse(schema) { if (anyOf !== undefined) { let has = {}; let newAnyOf = []; - for (let idx$1 = 0, idx_finish$1 = anyOf.length; idx$1 < idx_finish$1; ++idx$1) { - let s = anyOf[idx$1]; - let reversed$1 = reverse(s); - newAnyOf.push(reversed$1); - setHas(has, reversed$1.type); + for (let idx$2 = 0, idx_finish$2 = anyOf.length; idx$2 < idx_finish$2; ++idx$2) { + let s = anyOf[idx$2]; + let reversed = reverse(s); + newAnyOf.push(reversed); + setHas(has, reversed.type); } mut.has = has; mut.anyOf = newAnyOf; @@ -1260,84 +1452,534 @@ function reverse(schema) { let defs = mut.$defs; if (defs !== undefined) { let reversedDefs = {}; - for (let idx$2 = 0, idx_finish$2 = Object.keys(defs).length; idx$2 < idx_finish$2; ++idx$2) { - let key = Object.keys(defs)[idx$2]; - reversedDefs[key] = reverse(defs[key]); + for (let idx$3 = 0, idx_finish$3 = Object.keys(defs).length; idx$3 < idx_finish$3; ++idx$3) { + let key$1 = Object.keys(defs)[idx$3]; + reversedDefs[key$1] = reverse(defs[key$1]); } mut.$defs = reversedDefs; } reversedHead = mut; current = next; }; - return reversedHead; + let r = reversedHead; + valueOptions[valKey] = r; + d(schema, reversedKey, valueOptions); + valueOptions[valKey] = schema; + d(r, reversedKey, valueOptions); + return r; } -function internalCompile(schema, flag, defs) { - let b = rootScope(flag, defs); - if (flag & 8) { - let output = reverse(schema); - jsonableValidation(output, output, "", flag); - } - let input = { - b: b, - v: _var, - i: "i", - f: 0, - type: "unknown" - }; - let schema$1 = flag & 4 ? updateOutput(schema, mut => { - let t = new Schema(unit.type); - t.const = unit.const; - t.noValidation = true; - mut.to = t; - }) : ( - flag & 16 ? updateOutput(schema, mut => { - mut.to = jsonString; - }) : schema - ); - let output$1 = parse(b, schema$1, input, ""); - let code = allocateScope(b); - let isAsync = has(output$1.f, 2); - schema$1.isAsync = isAsync; - if (code === "" && output$1 === input && !(flag & 2)) { +function parseDynamic(input) { + try { + return parse$1(input); + } catch (exn) { + let error = getOrRethrow(exn); + let p = input.p; + error.path = ( + p !== undefined ? p.path : "" + ) + (input.path + "[]" + error.path); + throw error; + } +} + +function compileDecoder(schema, expected, flag, defs) { + let input = operationArg(constField in schema ? unknown : schema, expected, flag, defs); + let output = parse$1(input); + let code = merge(output, undefined); + let isAsync = has(output.f, 1); + expected.isAsync = isAsync; + let hasTransform = output.t === true; + expected.hasTransform = hasTransform; + if (code === "" && (output === input || output.i === input.i) && !(flag & 1)) { return noopOperation; } - let inlinedOutput = output$1.i; - if (flag & 2 && !isAsync && !defs) { + let inlinedOutput = output.i; + if (flag & 1 && !isAsync && !defs) { inlinedOutput = "Promise.resolve(" + inlinedOutput + ")"; } let inlinedFunction = "i=>{" + code + "return " + inlinedOutput + "}"; - let ctxVarValue1 = b.g.e; - return new Function("e", "s", "return " + inlinedFunction)(ctxVarValue1, s); + let ctxVarValue1 = input.g.e; + let fn = new Function("e", "s", "return " + inlinedFunction)(ctxVarValue1, s); + fn.embedded = input.g.e; + return fn; +} + +function getDecoder(param, param$1) { + let args = arguments; + let idx = 0; + let flag; + let keyRef = ""; + let maxSeq = 0; + let cacheTarget; + while (flag === undefined) { + let arg = args[idx]; + if (arg) { + if (typeof arg === "number") { + let f = arg | globalConfig.f; + flag = f; + keyRef = keyRef + "-" + f; + } else { + let seq = arg.seq; + if (seq > maxSeq) { + maxSeq = seq; + cacheTarget = arg; + } + keyRef = keyRef + seq + "-"; + idx = idx + 1 | 0; + } + } else { + let f$1 = globalConfig.f; + flag = f$1; + keyRef = keyRef + "-" + f$1; + } + }; + let cacheTarget$1 = cacheTarget; + if (cacheTarget$1 !== undefined) { + let key = keyRef; + if (key in cacheTarget$1) { + return cacheTarget$1[key]; + } + let schema = args[idx - 1 | 0]; + for (let i = idx - 2 | 0; i >= 0; --i) { + let to = schema; + schema = updateOutput(args[i], mut => { + mut.to = to; + }); + } + let f$2 = compileDecoder(schema, schema, flag, 0); + valueOptions[valKey] = f$2; + d(cacheTarget$1, key, valueOptions); + return f$2; + } + throw new Error("[Sury] No schema provided for decoder."); } -function isAsyncInternal(schema, defs) { - try { - let b = rootScope(2, defs); - let input = { - b: b, - v: _var, - i: "i", - f: 0, - type: "unknown" - }; - let output = parse(b, schema, input, ""); - let isAsync = has(output.f, 2); - schema.isAsync = isAsync; - return isAsync; - } catch (exn) { - getOrRethrow(exn); - return false; +function makeObjectVal(prev, schema) { + return { + v: _notVar, + i: "", + s: schema.type === arrayTag ? ({ + type: arrayTag, + decoder: arrayDecoder, + additionalItems: "strict", + items: [] + }) : ({ + type: objectTag, + decoder: objectDecoder, + additionalItems: "strict", + required: [], + properties: {} + }), + e: prev.e, + prev: prev, + f: 0, + d: {}, + cp: "", + l: "", + a: initialAllocate, + t: true, + path: prev.path, + g: prev.g + }; +} + +function completeObjectVal(objectVal) { + let isArray = objectVal.s.type === arrayTag; + let inline = ""; + let promiseAllContent = ""; + let optionalSettingCode; + let keys = Object.keys(objectVal.d); + for (let idx = 0, idx_finish = keys.length; idx < idx_finish; ++idx) { + let key = keys[idx]; + let val = objectVal.d[key]; + if (val.f & 1) { + promiseAllContent = promiseAllContent + val.i + ","; + } + if (val.o) { + let existingFn = optionalSettingCode; + optionalSettingCode = objectVar => ( + existingFn !== undefined ? existingFn(objectVar) : "" + ) + ("if(" + val.v() + "!==void 0){" + objectVar + "[" + inlineLocation(objectVal.g, key) + "]=" + val.i + "}"); + } else { + inline = inline + ( + isArray ? val.i : inlineLocation(objectVal.g, key) + ":" + val.i + ) + ","; + } + } + objectVal.i = isArray ? "[" + inline + "]" : "{" + inline + "}"; + if (promiseAllContent) { + let operationInput = scope(objectVal); + operationInput.io = true; + let operationOutput = parse$1(operationInput); + let operationCode = merge(operationOutput, undefined); + if (operationCode === "" && promiseAllContent === operationOutput.i + ",") { + objectVal.i = operationOutput.i; + } else { + objectVal.i = "Promise.all([" + promiseAllContent + "]).then(([" + promiseAllContent + "])=>{" + operationCode + "return " + operationOutput.i + "})"; + } + objectVal.f = objectVal.f | 1; + objectVal.s = operationOutput.s; + objectVal.e = operationOutput.e; + objectVal.io = true; + return objectVal; + } + let fn = optionalSettingCode; + if (fn === undefined) { + return objectVal; + } + let code = fn(objectVal.v()); + let output = refine(objectVal, undefined, undefined, undefined); + output.cp = output.cp + code; + return output; +} + +function array(item) { + let mut = base(arrayTag, item[reversedKey] === item); + mut.additionalItems = item; + mut.items = immutableEmpty$1; + mut.decoder = arrayDecoder; + return mut; +} + +function arrayDecoder(unknownInput) { + let isUnion = unknownInput.u; + let expectedSchema = unknownInput.e; + let unknownInputTagFlag = flags[unknownInput.s.type]; + let expectedItems = expectedSchema.items; + let expectedLength = expectedItems.length; + let input; + if (unknownInputTagFlag & 129) { + let isArrayInput = unknownInputTagFlag & 128; + let schema = isArrayInput ? unknownInput.s : array(unknown); + let checks = []; + if (!isArrayInput) { + checks.push({ + c: inputVar => "Array.isArray(" + inputVar + ")", + f: failInvalidType + }); + } + let match = schema.additionalItems; + let isExactSize; + isExactSize = match === "strip" || match === "strict" ? schema.items.length === expectedLength : false; + if (!isExactSize) { + let match$1 = expectedSchema.additionalItems; + if (match$1 === "strip" || match$1 === "strict") { + if (match$1 === "strip") { + checks.push({ + c: inputVar => inputVar + ".length>=" + expectedLength, + f: failInvalidType + }); + } else { + checks.push({ + c: inputVar => inputVar + ".length===" + expectedLength, + f: failInvalidType + }); + } + } + + } + input = checks.length > 0 ? refine(unknownInput, schema, checks, undefined) : refine(unknownInput, schema, undefined, undefined); + } else { + input = unsupportedDecode(unknownInput, unknownInput.s, expectedSchema); } + let itemSchema = expectedSchema.additionalItems; + if (itemSchema === "strip" || itemSchema === "strict") { + itemSchema === "strip"; + } else { + if (itemSchema === unknown) { + return input; + } + let inputVar = input.v(); + let iteratorVar = varWithoutAllocation(input.g); + let itemInput = dynamicScope(input, iteratorVar); + let itemOutput = parseDynamic(itemInput); + let hasTransform = itemOutput.t; + let output = hasTransform ? next(input, "new Array(" + inputVar + ".length)", expectedSchema, undefined) : refine(input, expectedSchema, undefined, undefined); + let itemCode = mergeWithPathPrepend(itemOutput, input, iteratorVar, hasTransform ? () => addKey(output, iteratorVar, itemOutput) : undefined); + if (hasTransform || itemCode !== "") { + output.cp = output.cp + ("for(let " + iteratorVar + "=" + expectedLength + ";" + iteratorVar + "<" + inputVar + ".length;++" + iteratorVar + "){" + itemCode + "}"); + } + if (itemOutput.f & 1) { + return asyncVal(output, "Promise.all(" + output.i + ")"); + } else { + return output; + } + } + let objectVal = makeObjectVal(input, expectedSchema); + let match$2 = expectedSchema.additionalItems; + let shouldRecreateInput; + if (match$2 === "strip" || match$2 === "strict") { + if (match$2 === "strip") { + let match$3 = input.s.additionalItems; + shouldRecreateInput = match$3 === "strip" || match$3 === "strict" ? input.s.items.length !== expectedLength : true; + } else { + shouldRecreateInput = false; + } + } else { + shouldRecreateInput = true; + } + for (let idx = 0; idx < expectedLength; ++idx) { + let schema$1 = expectedItems[idx]; + let key = idx.toString(); + let itemInput$1 = get(input, key); + itemInput$1.e = schema$1; + itemInput$1.ii = false; + itemInput$1.io = false; + itemInput$1.u = isUnion; + let itemOutput$1 = parse$1(itemInput$1); + if (isUnion && constField in schema$1) { + hoistChildChecks(input, itemOutput$1, key); + } + add(objectVal, key, itemOutput$1); + if (!shouldRecreateInput) { + shouldRecreateInput = itemOutput$1.t; + } + + } + if (shouldRecreateInput) { + return completeObjectVal(objectVal); + } + let o = refine(input, undefined, undefined, undefined); + o.cp = objectVal.cp; + o.d = objectVal.d; + return o; } -function operationFn(s, o) { - if ((o in s)) { - return (s[o]); +function objectDecoder(unknownInput) { + let isUnion = unknownInput.u; + let expectedSchema = unknownInput.e; + let unknownInputTagFlag = flags[unknownInput.s.type]; + let input; + if (unknownInputTagFlag & 65) { + let isObjectInput = unknownInputTagFlag & 64; + let schema; + if (isObjectInput) { + schema = unknownInput.s; + } else { + let mut = base(objectTag, false); + mut.properties = immutableEmpty; + mut.additionalItems = unknown; + schema = mut; + } + let checks = []; + if (!isObjectInput) { + checks.push({ + c: inputVar => "typeof " + inputVar + "===\"" + objectTag + "\"&&" + inputVar, + f: failInvalidType + }); + if (expectedSchema.additionalItems !== "strip") { + checks.push({ + c: inputVar => "!Array.isArray(" + inputVar + ")", + f: failInvalidType + }); + } + + } + input = checks.length > 0 ? refine(unknownInput, schema, checks, undefined) : refine(unknownInput, schema, undefined, undefined); + } else { + input = unsupportedDecode(unknownInput, unknownInput.s, expectedSchema); + } + let itemSchema = expectedSchema.additionalItems; + let exit = 0; + if (itemSchema === "strip" || itemSchema === "strict") { + exit = 1; + } else { + if (itemSchema === unknown) { + return input; + } + let inputVar = input.v(); + let keyVar = varWithoutAllocation(input.g); + let itemInput = dynamicScope(input, keyVar); + let itemOutput = parseDynamic(itemInput); + let hasTransform = itemOutput.t; + let output = hasTransform ? next(input, "{}", expectedSchema, undefined) : refine(input, expectedSchema, undefined, undefined); + let itemCode = mergeWithPathPrepend(itemOutput, input, keyVar, hasTransform ? () => addKey(output, keyVar, itemOutput) : undefined); + if (hasTransform || itemCode !== "") { + output.cp = output.cp + ("for(let " + keyVar + " in " + inputVar + "){" + itemCode + "}"); + } + if (!(itemOutput.f & 1)) { + return output; + } + let resolveVar = varWithoutAllocation(output.g); + let rejectVar = varWithoutAllocation(output.g); + let asyncParseResultVar = varWithoutAllocation(output.g); + let counterVar = varWithoutAllocation(output.g); + let outputVar = output.v(); + return asyncVal(output, "new Promise((" + resolveVar + "," + rejectVar + ")=>{let " + counterVar + "=Object.keys(" + outputVar + ").length;for(let " + keyVar + " in " + outputVar + "){" + outputVar + "[" + keyVar + "].then(" + asyncParseResultVar + "=>{" + outputVar + "[" + keyVar + "]=" + asyncParseResultVar + ";if(" + counterVar + "--===1){" + resolveVar + "(" + outputVar + ")}}," + rejectVar + ")}})"); } - let f = internalCompile(o & 32 ? reverse(s) : s, o, 0); - ((s[o] = f)); - return f; + if (exit === 1) { + let properties = expectedSchema.properties; + let keys = Object.keys(properties); + let keysCount = keys.length; + let objectVal = makeObjectVal(input, expectedSchema); + let match = expectedSchema.additionalItems; + let shouldRecreateInput; + if (match === "strip" || match === "strict") { + if (match === "strip") { + let match$1 = input.s.additionalItems; + let exit$1 = 0; + if (match$1 === "strip" || match$1 === "strict") { + exit$1 = 2; + } else { + shouldRecreateInput = true; + } + if (exit$1 === 2) { + shouldRecreateInput = Object.keys(input.s.properties).length !== keysCount; + } + + } else { + shouldRecreateInput = false; + } + } else { + shouldRecreateInput = true; + } + for (let idx = 0; idx < keysCount; ++idx) { + let key = keys[idx]; + let schema$1 = properties[key]; + let itemInput$1 = get(input, key); + itemInput$1.e = schema$1; + itemInput$1.ii = false; + itemInput$1.io = false; + itemInput$1.u = isUnion; + let itemOutput$1 = parse$1(itemInput$1); + if (isUnion && constField in schema$1) { + hoistChildChecks(input, itemOutput$1, key); + } + add(objectVal, key, itemOutput$1); + if (!shouldRecreateInput) { + shouldRecreateInput = itemOutput$1.t; + } + + } + let tmp = false; + if (expectedSchema.additionalItems === "strict") { + let match$2 = input.s.additionalItems; + let tmp$1; + tmp$1 = match$2 !== "strip" && match$2 !== "strict"; + tmp = tmp$1; + } + if (tmp) { + let keyVar$1 = varWithoutAllocation(objectVal.g); + input.a(keyVar$1); + objectVal.cp = objectVal.cp + ("for(" + keyVar$1 + " in " + input.v() + "){if("); + if (keys.length !== 0) { + for (let idx$1 = 0, idx_finish = keys.length; idx$1 < idx_finish; ++idx$1) { + let key$1 = keys[idx$1]; + if (idx$1 !== 0) { + objectVal.cp = objectVal.cp + "&&"; + } + objectVal.cp = objectVal.cp + (keyVar$1 + "!==" + inlineLocation(input.g, key$1)); + } + } else { + objectVal.cp = objectVal.cp + "true"; + } + objectVal.cp = objectVal.cp + ("){" + failWithArg(input, exccessFieldName => ({ + code: "unrecognized_keys", + path: objectVal.path, + reason: "Unrecognized key \"" + exccessFieldName + "\"", + keys: [exccessFieldName] + }), keyVar$1) + "}}"); + } + if (shouldRecreateInput) { + return completeObjectVal(objectVal); + } + let o = refine(input, undefined, undefined, undefined); + o.cp = objectVal.cp; + o.d = objectVal.d; + return o; + } + +} + +function recursiveDecoder(input) { + let expectedSchema = input.e; + let schemaRef = expectedSchema.$ref; + let defs = input.g.d; + let identifier = schemaRef.slice(8); + let def = defs[identifier]; + let flag = input.g.o; + let inputSchema = input.s.seq === expectedSchema.seq ? def : input.s; + let key = inputSchema.seq + "-" + def.seq + "--" + flag; + let recOperation = ""; + let fn = def[key]; + if (fn !== undefined) { + let fn$1 = Primitive_option.valFromOption(fn); + recOperation = fn$1 === 0 ? embed(input, def) + ("[\"" + key + "\"]") : embed(input, fn$1); + } else { + let assumedHasTransform = Stdlib_Option.getOr(def.hasTransform, false); + let assumedIsAsync = Stdlib_Option.getOr(def.isAsync, false); + let compileNeeded = true; + let finalFn = 0; + while (compileNeeded) { + compileNeeded = false; + if (def.hasTransform === undefined) { + def.hasTransform = assumedHasTransform; + } + if (def.isAsync === undefined) { + def.isAsync = assumedIsAsync; + } + configurableValueOptions[valKey] = 0; + d(def, key, configurableValueOptions); + let fn$2 = compileDecoder(inputSchema, def, flag, defs); + valueOptions[valKey] = fn$2; + d(def, key, valueOptions); + finalFn = fn$2; + let actualHasTransform = def.hasTransform; + let actualIsAsync = def.isAsync; + if (actualHasTransform !== assumedHasTransform || actualIsAsync !== assumedIsAsync) { + assumedHasTransform = actualHasTransform; + assumedIsAsync = actualIsAsync; + ((delete def[key])); + compileNeeded = true; + } + + }; + recOperation = embed(input, finalFn); + } + let hasTransform = def.hasTransform === true; + let isAsync = def.isAsync; + let output; + if (hasTransform || isAsync) { + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let output$1 = next(input, outputVar, expectedSchema, expectedSchema); + output$1.v = _var; + output$1.cp = outputVar + "=" + recOperation + "(" + input.i + ");"; + if (isAsync) { + output$1.f = output$1.f | 1; + } + output = output$1; + } else { + let output$2 = refine(input, expectedSchema, undefined, expectedSchema); + output$2.cp = recOperation + "(" + input.i + ");"; + output = output$2; + } + output.prev = undefined; + output.cp = mergeWithPathPrepend(output, input, undefined, undefined); + output.a = initialAllocate; + output.prev = input; + return output; +} + +function instanceDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 1) { + return refine(input, input.e, [{ + c: inputVar => inputVar + " instanceof " + embed(input, input.e.class), + f: failInvalidType + }], undefined); + } else if (inputTagFlag & 8192 && input.s.class === input.e.class) { + return input; + } else { + return unsupportedDecode(input, input.s, input.e); + } +} + +function instance(class_) { + let mut = base(instanceTag, true); + mut.class = class_; + mut.decoder = instanceDecoder; + return mut; } d(sp, "~standard", { @@ -1349,13 +1991,13 @@ d(sp, "~standard", { validate: input => { try { return { - value: operationFn(schema, 1)(input) + value: getDecoder(unknown, schema)(input) }; } catch (exn) { let error = getOrRethrow(exn); return { issues: [{ - message: reason(error, undefined), + message: error.reason, path: error.path === "" ? undefined : toArray(error.path) }] }; @@ -1365,131 +2007,56 @@ d(sp, "~standard", { } }); -function compile(schema, input, output, mode, typeValidationOpt) { - let typeValidation = typeValidationOpt !== undefined ? typeValidationOpt : true; - let flag = 0; - let exit = 0; - switch (output) { - case "Output" : - case "Input" : - exit = 1; - break; - case "Assert" : - flag = flag | 4; - break; - case "Json" : - flag = flag | 8; - break; - case "JsonString" : - flag = flag | 24; - break; - } - if (exit === 1 && output === input) { - throw new Error("[Sury] Can't compile operation to converting value to self"); - } - if (mode !== "Sync") { - flag = flag | 2; - } - if (typeValidation) { - flag = flag | 1; - } - if (input === "Output") { - flag = flag | 32; - } - let fn = operationFn(schema, flag); - if (input !== "JsonString") { - return fn; - } - let flag$1 = flag; - return jsonString => { - try { - return fn(JSON.parse(jsonString)); - } catch (exn) { - throw new SuryError({ - TAG: "OperationFailed", - _0: exn.message - }, flag$1, ""); - } - }; -} - -function parseOrThrow(any, schema) { - return operationFn(schema, 1)(any); +function parser(schema) { + return getDecoder(unknown, schema); } -function parseJsonStringOrThrow(jsonString, schema) { - let tmp; - try { - tmp = JSON.parse(jsonString); - } catch (exn) { - throw new SuryError({ - TAG: "OperationFailed", - _0: exn.message - }, 1, ""); - } - return parseOrThrow(tmp, schema); +function asyncParser(schema) { + return getDecoder(unknown, schema, 1); } -function parseAsyncOrThrow(any, schema) { - return operationFn(schema, 3)(any); +function decoder(from, to) { + return getDecoder(reverse(from), to); } -function convertOrThrow(input, schema) { - return operationFn(schema, 0)(input); +function asyncDecoder(from, to) { + return getDecoder(reverse(from), to, 1); } -function convertToJsonOrThrow(any, schema) { - return operationFn(schema, 8)(any); +function decoder1(schema) { + return getDecoder(schema, undefined); } -function convertToJsonStringOrThrow(input, schema) { - return operationFn(schema, 24)(input); +function asyncDecoder1(schema) { + return getDecoder(schema, 1); } -function convertAsyncOrThrow(any, schema) { - return operationFn(schema, 2)(any); -} +let assertResult = copySchema(unit); -function reverseConvertOrThrow(value, schema) { - return operationFn(schema, 32)(value); -} +assertResult.noValidation = true; -function reverseConvertToJsonOrThrow(value, schema) { - return operationFn(schema, 40)(value); +function parseOrThrow(any, schema) { + return getDecoder(unknown, schema)(any); } -function reverseConvertToJsonStringOrThrow(value, schema, spaceOpt) { - let space = spaceOpt !== undefined ? spaceOpt : 0; - return JSON.stringify(reverseConvertToJsonOrThrow(value, schema), null, space); +function parseAsyncOrThrow(any, schema) { + return getDecoder(unknown, schema, 1)(any); } function assertOrThrow(any, schema) { - return operationFn(schema, 5)(any); + return getDecoder(unknown, schema, assertResult)(any); } -let $$null = new Schema("null"); +function assertAsyncOrThrow(any, schema) { + return getDecoder(unknown, schema, assertResult, 1)(any); +} -$$null.const = null; +function decodeOrThrow(any, from, to) { + return getDecoder(reverse(from), to)(any); +} -function parse$1(value) { - if (value === null) { - return $$null; - } - let $$typeof = typeof value; - let schema; - if ($$typeof === "object") { - let i = new Schema("instance"); - i.class = value.constructor; - schema = i; - } else { - schema = $$typeof === "undefined" ? unit : ( - $$typeof === "number" ? ( - Number.isNaN(value) ? new Schema("nan") : new Schema($$typeof) - ) : new Schema($$typeof) - ); - } - schema.const = value; - return schema; +function decodeAsyncOrThrow(any, from, to) { + return getDecoder(reverse(from), to, 1)(any); } function isAsync(schema) { @@ -1497,7 +2064,17 @@ function isAsync(schema) { if (v !== undefined) { return v; } else { - return isAsyncInternal(schema, 0); + let defs = 0; + try { + let input = operationArg(unknown, schema, 1, defs); + let output = parse$1(input); + let isAsync$1 = has(output.f, 1); + schema.isAsync = isAsync$1; + return isAsync$1; + } catch (exn) { + getOrRethrow(exn); + return false; + } } } @@ -1550,8 +2127,8 @@ function get$1(schema, id) { return schema[id]; } -function set$1(schema, id, metadata) { - let mut = copyWithoutCache(schema); +function set(schema, id, metadata) { + let mut = copySchema(schema); mut[id] = metadata; return mut; } @@ -1560,9 +2137,10 @@ let defsPath = "#/$defs/"; function recursive(name, fn) { let ref = defsPath + name; - let refSchema = new Schema("ref"); + let refSchema = base(refTag, false); refSchema.$ref = ref; refSchema.name = name; + refSchema.decoder = recursiveDecoder; let isNestedRec = globalConfig.d; if (!isNestedRec) { globalConfig.d = {}; @@ -1577,140 +2155,135 @@ function recursive(name, fn) { if (isNestedRec) { return refSchema; } - let schema = new Schema("ref"); + let schema = base(refTag, false); schema.name = def.name; schema.$ref = ref; schema.$defs = globalConfig.d; + schema.decoder = recursiveDecoder; globalConfig.d = undefined; return schema; } function noValidation(schema, value) { - let mut = copyWithoutCache(schema); + let mut = copySchema(schema); mut.noValidation = value; return mut; } -function appendRefiner(maybeExistingRefiner, refiner) { - if (maybeExistingRefiner !== undefined) { - return (b, inputVar, selfSchema, path) => maybeExistingRefiner(b, inputVar, selfSchema, path) + refiner(b, inputVar, selfSchema, path); - } else { - return refiner; - } -} - -function internalRefine(schema, refiner) { +function internalRefine(schema, makeRefiner) { return updateOutput(schema, mut => { - mut.refiner = appendRefiner(mut.refiner, refiner); + let refiner = makeRefiner(mut); + let existingRefiner = mut.refiner; + if (existingRefiner !== undefined) { + mut.refiner = input => { + let arr = existingRefiner(input); + let next = refiner(input); + for (let i = 0, i_finish = next.length; i < i_finish; ++i) { + arr.push(next[i]); + } + return arr; + }; + } else { + mut.refiner = refiner; + } }); } -function refine(schema, refiner) { - return internalRefine(schema, (b, inputVar, selfSchema, path) => embed(b, refiner(effectCtx(b, selfSchema, path))) + "(" + inputVar + ");"); +function refine$1(schema, refineCheck, error, path) { + let message = error !== undefined ? error : "Refinement failed"; + let extraPath = path !== undefined ? fromArray(path) : ""; + return internalRefine(schema, param => (input => { + let embeddedCheck = embed(input, refineCheck); + return [{ + c: inputVar => embeddedCheck + "(" + inputVar + ")", + f: input => { + let path = extraPath === "" ? input.path : input.path + extraPath; + return _value => ({ + code: "custom", + path: path, + reason: message + }); + } + }]; + })); } -function addRefinement(schema, metadataId, refinement, refiner) { - let refinements = schema[metadataId]; - return internalRefine(set$1(schema, metadataId, refinements !== undefined ? refinements.concat(refinement) : [refinement]), refiner); +function getMutErrorMessage(mut) { + let em = mut.errorMessage ? copy(mut.errorMessage) : ({}); + mut.errorMessage = em; + return em; } function transform(schema, transformer) { return updateOutput(schema, mut => { - mut.parser = (b, input, selfSchema, path) => { - let match = transformer(effectCtx(b, selfSchema, path)); + mut.parser = input => { + let match = transformer(effectCtx(input)); let parser = match.p; if (parser !== undefined) { if (match.a !== undefined) { - return invalidOperation(b, path, "The S.transform doesn't allow parser and asyncParser at the same time. Remove parser in favor of asyncParser"); + return invalidOperation(input, "The S.transform doesn't allow parser and asyncParser at the same time. Remove parser in favor of asyncParser"); } else { - return embedSyncOperation(b, input, parser); + return embedTransformation(input, parser, false); } } let asyncParser = match.a; if (asyncParser !== undefined) { - if (!(b.g.o & 2)) { - $$throw(b, "UnexpectedAsync", ""); - } - let val = embedSyncOperation(b, input, asyncParser); - val.f = val.f | 2; - return val; + return embedTransformation(input, asyncParser, true); } else if (match.s !== undefined) { - return invalidOperation(b, path, "The S.transform parser is missing"); + return invalidOperation(input, "The S.transform parser is missing"); } else { - return input; + return refine(input, undefined, undefined, input.e.to); } }; - let to = new Schema("unknown"); - mut.to = (to.serializer = (b, input, selfSchema, path) => { - let match = transformer(effectCtx(b, selfSchema, path)); + let to = base(unknownTag, false); + mut.to = (to.decoder = noopDecoder, to.serializer = input => { + let match = transformer(effectCtx(input)); let serializer = match.s; if (serializer !== undefined) { - return embedSyncOperation(b, input, serializer); + return embedTransformation(input, serializer, false); } else if (match.a !== undefined || match.p !== undefined) { - return invalidOperation(b, path, "The S.transform serializer is missing"); + return invalidOperation(input, "The S.transform serializer is missing"); } else { - return input; + return refine(input, undefined, undefined, input.e.to); } }, to); ((delete mut.isAsync)); }); } -let nullAsUnit = new Schema("null"); +let nullAsUnit = base(nullTag, false); nullAsUnit.const = null; nullAsUnit.to = unit; -function neverBuilder(b, input, selfSchema, path) { - b.c = b.c + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: selfSchema, - received: input - }), input.i) + ";"; - return input; -} +nullAsUnit.decoder = literalDecoder; + +let never = base(neverTag, true); -let never = new Schema("never"); +function neverBuilder(input) { + let output = refine(input, undefined, undefined, never); + output.cp = embedInvalidInput(input, undefined) + ";"; + return output; +} -never.compiler = neverBuilder; +never.decoder = neverBuilder; let nestedLoc = "BS_PRIVATE_NESTED_SOME_NONE"; -function getItemCode(b, schema, input, output, deopt, path) { - try { - let globalFlag = b.g.o; - if (deopt) { - b.g.o = globalFlag | 1; - } - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let input$1 = deopt ? copy(input) : makeRefinedOf(bb, input, schema); - let itemOutput = parse(bb, schema, input$1, path); - if (itemOutput !== input$1) { - itemOutput.b = bb; - if (itemOutput.f & 2) { - output.f = output.f | 2; - } - bb.c = bb.c + (output.v(b) + "=" + itemOutput.i); - } - b.g.o = globalFlag; - return allocateScope(bb); - } catch (exn) { - return "throw " + embed(b, getOrRethrow(exn)); - } +function factory(item) { + let mut = base(objectTag, item[reversedKey] === item); + mut.properties = immutableEmpty; + mut.additionalItems = item; + mut.decoder = objectDecoder; + return mut; } function isPriority(tagFlag, byKey) { - if (tagFlag & 8320 && "object" in byKey) { + if (tagFlag & 8320 && objectTag in byKey) { return true; } else if (tagFlag & 2048) { - return "number" in byKey; + return numberTag in byKey; } else { return false; } @@ -1719,257 +2292,340 @@ function isPriority(tagFlag, byKey) { function isWiderUnionSchema(schemaAnyOf, inputAnyOf) { return inputAnyOf.every((inputSchema, idx) => { let schema = schemaAnyOf[idx]; - if (schema !== undefined && !(flags[inputSchema.type] & 9152) && inputSchema.type === schema.type) { - return inputSchema.const === schema.const; + if (schema !== undefined && !(flags[inputSchema.type] & 9152) && inputSchema.type === schema.type && inputSchema.const === schema.const) { + return inputSchema.to === undefined; } else { return false; } }); } -function compiler(b, input, selfSchema, path) { +function unionDecoder(input) { + let selfSchema = input.e; let schemas = selfSchema.anyOf; - let inputAnyOf = input.anyOf; - if (inputAnyOf !== undefined) { - if (isWiderUnionSchema(schemas, inputAnyOf)) { - return input; - } else { - return unsupportedTransform(b, input, selfSchema, path); - } + let initialInputTagFlag = flags[input.s.type]; + let match = selfSchema.parser; + let toPerCase; + if (match !== undefined) { + toPerCase = undefined; + } else { + let to = selfSchema.to; + toPerCase = to !== undefined ? to : undefined; + } + if (initialInputTagFlag & 256 && isWiderUnionSchema(schemas, input.s.anyOf) && toPerCase === undefined || input.io && input.e === input.s) { + return input; + } + if (input.s.encoder === undefined && initialInputTagFlag & 768) { + input.s = unknown; } - let fail = caught => embed(b, function () { - let args = arguments; - return $$throw(b, { - TAG: "InvalidType", - expected: selfSchema, - received: args[0], - unionErrors: args.length > 1 ? Array.from(args).slice(1) : undefined - }, path); - }) + "(" + input.v(b) + caught + ")"; - let typeValidation = b.g.o & 1; let initialInline = input.i; - let deoptIdx = -1; - let lastIdx = schemas.length - 1 | 0; - let byKey = {}; - let keys = []; - for (let idx = 0; idx <= lastIdx; ++idx) { - let target = selfSchema.to; - let schema = target !== undefined && !selfSchema.parser && target.type !== "union" ? updateOutput(schemas[idx], mut => { - let refiner = selfSchema.refiner; - if (refiner !== undefined) { - mut.refiner = appendRefiner(mut.refiner, refiner); + let fail = caught => embed(input, function () { + let args = arguments; + let errorDetails = makeInvalidInputDetails(selfSchema, unknown, input.path, args[0], true, args.length > 1 ? Array.from(args).slice(1) : undefined); + throw new SuryError(errorDetails); + }) + "(" + input.v() + caught + ")"; + let output = refine(input, undefined, undefined, undefined); + let outputAnyOf = []; + let getArrItemsCode = (arr, isDeopt) => { + let typeValidationInput = arr[0]; + let typeValidationOutput = arr[1]; + let itemStart = ""; + let itemEnd = ""; + let itemNextElse = false; + let itemNoop = { + contents: "" + }; + let caught = ""; + let byDiscriminant = {}; + let itemIdx = 2; + let lastIdx = arr.length - 1 | 0; + while (itemIdx <= lastIdx) { + let input = scope(typeValidationOutput); + input.u = true; + input.t = typeValidationOutput.t; + input.ii = false; + input.io = false; + input.e = arr[itemIdx]; + let isLast = itemIdx === lastIdx; + let isFirst = itemIdx === 2; + let withExhaustiveCheck = !(isFirst && isLast); + let itemCode = ""; + let itemCond = { + contents: "" + }; + try { + let itemOutput = parse$1(input); + outputAnyOf.push(itemOutput.s); + itemCode = merge(itemOutput, itemCond); + if (itemOutput.t) { + output.t = true; + if (itemOutput.f & 1) { + output.f = output.f | 1; + } + itemCode = itemCode + (typeValidationInput.v() + "=" + itemOutput.i); } - mut.to = target; - }) : schemas[idx]; - let tag = schema.type; - let tagFlag = flags[tag]; - if (!(tagFlag & 16 && "fromDefault" in selfSchema)) { - if (tagFlag & 17153 || !(flags[input.type] & 1) && input.type !== tag) { - deoptIdx = idx; - byKey = {}; - keys = []; - } else { - let key = tagFlag & 8192 ? schema.class.name : tag; - let arr = byKey[key]; - if (arr !== undefined) { - if (tagFlag & 64 && nestedLoc in schema.properties) { - arr.unshift(schema); - } else if (!(tagFlag & 2096)) { - arr.push(schema); + + } catch (exn) { + let errorVar = embed(input, getOrRethrow(exn)); + if (isLast) { + withExhaustiveCheck = false; + } + let tmp; + if (isLast && !isDeopt) { + withExhaustiveCheck = false; + tmp = fail("," + errorVar); + } else { + tmp = "throw " + errorVar; + } + itemCode = tmp; + } + let itemCond$1 = itemCond.contents; + let itemCode$1 = itemCode; + if (itemCond$1) { + if (itemCode$1) { + let match = byDiscriminant[itemCond$1]; + if (match !== undefined) { + if (typeof match === "string") { + byDiscriminant[itemCond$1] = [ + match, + itemCode$1 + ]; + } else { + match.push(itemCode$1); + } + } else { + byDiscriminant[itemCond$1] = itemCode$1; + } + } else { + itemNoop.contents = itemNoop.contents ? itemNoop.contents + "||" + itemCond$1 : itemCond$1; + } + } + if (!itemCond$1 || isLast) { + let accedDiscriminants = Object.keys(byDiscriminant); + for (let idx = 0, idx_finish = accedDiscriminants.length; idx < idx_finish; ++idx) { + let discrim = accedDiscriminants[idx]; + let if_ = itemNextElse ? "else if" : "if"; + itemStart = itemStart + if_ + ("(" + discrim + "){"); + let code = byDiscriminant[discrim]; + if (typeof code === "string") { + itemStart = itemStart + code + "}"; + } else { + let caught$1 = ""; + for (let idx$1 = 0, idx_finish$1 = code.length; idx$1 < idx_finish$1; ++idx$1) { + let code$1 = code[idx$1]; + let errorVar$1 = "e" + idx$1; + itemStart = itemStart + ("try{" + code$1 + "}catch(" + errorVar$1 + "){"); + caught$1 = caught$1 + "," + errorVar$1; + } + itemStart = itemStart + fail(caught$1) + "}".repeat(code.length) + "}"; + } + itemNextElse = true; + } + byDiscriminant = {}; + } + if (!itemCond$1) { + if (itemCode$1) { + if (itemNoop.contents) { + let if_$1 = itemNextElse ? "else if" : "if"; + itemStart = itemStart + if_$1 + ("(!(" + itemNoop.contents + ")){"); + itemEnd = "}" + itemEnd; + itemNoop.contents = ""; + itemNextElse = false; + } + if (isLast && (isDeopt || !withExhaustiveCheck || isFirst)) { + itemStart = itemStart + (( + itemNextElse ? "else{" : "" + ) + itemCode$1); + itemEnd = ( + itemNextElse ? "}" : "" + ) + itemEnd; + } else { + let errorVar$2 = "e" + (itemIdx - 2 | 0); + itemStart = itemStart + (( + itemNextElse ? "else{" : "" + ) + "try{" + itemCode$1 + "}catch(" + errorVar$2 + "){"); + itemEnd = ( + itemNextElse ? "}" : "" + ) + "}" + itemEnd; + caught = caught + "," + errorVar$2; + itemNextElse = false; } - } else { - if (isPriority(tagFlag, byKey)) { - keys.unshift(key); + itemNoop.contents = ""; + itemIdx = lastIdx; + withExhaustiveCheck = false; + } + } + if (isLast) { + if (itemNoop.contents) { + if (itemStart) { + let if_$2 = itemNextElse ? "else if" : "if"; + itemStart = itemStart + if_$2 + ("(!(" + itemNoop.contents + ")){" + fail(caught) + "}"); } else { - keys.push(key); + pushCheck(typeValidationOutput, { + c: param => "(" + itemNoop.contents + ")", + f: failInvalidType + }); } - byKey[key] = [schema]; + } else if (withExhaustiveCheck) { + let errorCode = fail(caught); + itemStart = itemStart + ( + itemNextElse ? "else{" + errorCode + "}" : errorCode + ); } + } - } - - } - let deoptIdx$1 = deoptIdx; - let byKey$1 = byKey; - let keys$1 = keys; + itemIdx = itemIdx + 1; + }; + return itemStart + itemEnd; + }; let start = ""; let end = ""; let caught = ""; let exit = false; - if (deoptIdx$1 !== -1) { - for (let idx$1 = 0; idx$1 <= deoptIdx$1; ++idx$1) { - if (!exit) { - let schema$1 = schemas[idx$1]; - let itemCode = getItemCode(b, schema$1, input, input, true, path); - if (itemCode) { - let errorVar = "e" + idx$1; - start = start + ("try{" + itemCode + "}catch(" + errorVar + "){"); - end = "}" + end; - caught = caught + "," + errorVar; + let lastIdx = schemas.length - 1 | 0; + let byKey = {}; + let keys = []; + let updatedSchemas = []; + for (let idx = 0; idx <= lastIdx; ++idx) { + let schema = toPerCase !== undefined ? updateOutput(schemas[idx], mut => { + mut.to = toPerCase; + }) : schemas[idx]; + updatedSchemas.push(schema); + let tag = schema.type; + let tagFlag = flags[tag]; + let key = tagFlag & 8192 ? schema.class.name : tag; + if (!(tagFlag & 16 && "fromDefault" in selfSchema)) { + let initialArr = byKey[key]; + if (initialArr !== undefined) { + if (tagFlag & 64 && nestedLoc in schema.properties) { + initialArr.splice(initialArr.length - 1 | 0, 0, schema); + } else if (!(tagFlag & 2096)) { + initialArr.push(schema); + } + + } else { + let typeValidationInput = scope(input); + typeValidationInput.e = tagFlag & 32 ? nullLiteral : ( + tagFlag & 16 ? unit : ( + tagFlag & 64 ? factory(unknown) : ( + tagFlag & 128 ? array(unknown) : ( + tagFlag & 8192 ? instance(schema.class) : ( + tagFlag & 2048 ? nan : ( + tagFlag & 2 ? string : ( + tagFlag & 4 ? float : ( + tagFlag & 8 ? bool : ( + tagFlag & 1024 ? bigint : ( + tagFlag & 32768 ? symbol : unknown + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + let typeValidationOutput; + try { + typeValidationOutput = parse$1(typeValidationInput); + } catch (exn) { + typeValidationInput.vc = undefined; + typeValidationOutput = typeValidationInput; + } + if (isPriority(tagFlag, byKey)) { + keys.unshift(key); } else { - exit = true; + keys.push(key); } - } - - } - } - if (!exit) { - let nextElse = false; - let noop = ""; - for (let idx$2 = 0, idx_finish = keys$1.length; idx$2 < idx_finish; ++idx$2) { - let schemas$1 = byKey$1[keys$1[idx$2]]; - let isMultiple = schemas$1.length > 1; - let firstSchema = schemas$1[0]; - let cond = 0; - let body; - if (isMultiple) { - let inputVar = input.v(b); - let itemStart = ""; - let itemEnd = ""; - let itemNextElse = false; - let itemNoop = { - contents: "" + byKey[key] = [ + typeValidationInput, + typeValidationOutput, + schema + ]; + let shouldDeopt = true; + let valRef = typeValidationOutput; + while (valRef !== undefined && shouldDeopt) { + let v = valRef; + valRef = v.prev; + shouldDeopt = !(v.vc && ( + v.t === true ? v.prev.t !== true && v.cp === "" : true + )); }; - let caught$1 = ""; - let byDiscriminant = {}; - let itemIdx = 0; - let lastIdx$1 = schemas$1.length - 1 | 0; - while (itemIdx <= lastIdx$1) { - let schema$2 = schemas$1[itemIdx]; - let itemCond = ( - constField in schema$2 ? validation(b, inputVar, schema$2, false) : "" - ) + refinement(b, inputVar, schema$2, false).slice(2); - let itemCode$1 = getItemCode(b, schema$2, input, input, false, path); - if (itemCond) { - if (itemCode$1) { - let match = byDiscriminant[itemCond]; - if (match !== undefined) { - if (typeof match === "string") { - byDiscriminant[itemCond] = [ - match, - itemCode$1 - ]; - } else { - match.push(itemCode$1); - } + if (shouldDeopt) { + for (let keyIdx = 0, keyIdx_finish = keys.length; keyIdx < keyIdx_finish; ++keyIdx) { + let key$1 = keys[keyIdx]; + if (!exit) { + let arr = byKey[key$1]; + let typeValidationOutput$1 = arr[1]; + let itemsCode = getArrItemsCode(arr, true); + let blockCode = merge(typeValidationOutput$1, undefined) + itemsCode; + if (blockCode) { + let errorVar = "e" + (idx + keyIdx | 0); + start = start + ("try{" + blockCode + "}catch(" + errorVar + "){"); + end = "}" + end; + caught = caught + "," + errorVar; } else { - byDiscriminant[itemCond] = itemCode$1; - } - } else { - itemNoop.contents = itemNoop.contents ? itemNoop.contents + "||" + itemCond : itemCond; - } - } - if (!itemCond || itemIdx === lastIdx$1) { - let accedDiscriminants = Object.keys(byDiscriminant); - for (let idx$3 = 0, idx_finish$1 = accedDiscriminants.length; idx$3 < idx_finish$1; ++idx$3) { - let discrim = accedDiscriminants[idx$3]; - let if_ = itemNextElse ? "else if" : "if"; - itemStart = itemStart + if_ + ("(" + discrim + "){"); - let code = byDiscriminant[discrim]; - if (typeof code === "string") { - itemStart = itemStart + code + "}"; - } else { - let caught$2 = ""; - for (let idx$4 = 0, idx_finish$2 = code.length; idx$4 < idx_finish$2; ++idx$4) { - let code$1 = code[idx$4]; - let errorVar$1 = "e" + idx$4; - itemStart = itemStart + ("try{" + code$1 + "}catch(" + errorVar$1 + "){"); - caught$2 = caught$2 + "," + errorVar$1; - } - itemStart = itemStart + fail(caught$2) + "}".repeat(code.length) + "}"; - } - itemNextElse = true; - } - byDiscriminant = {}; - } - if (!itemCond) { - if (itemCode$1) { - if (itemNoop.contents) { - let if_$1 = itemNextElse ? "else if" : "if"; - itemStart = itemStart + if_$1 + ("(!(" + itemNoop.contents + ")){"); - itemEnd = "}" + itemEnd; - itemNoop.contents = ""; - itemNextElse = false; + exit = true; } - let errorVar$2 = "e" + itemIdx; - itemStart = itemStart + (( - itemNextElse ? "else{" : "" - ) + "try{" + itemCode$1 + "}catch(" + errorVar$2 + "){"); - itemEnd = ( - itemNextElse ? "}" : "" - ) + "}" + itemEnd; - caught$1 = caught$1 + "," + errorVar$2; - itemNextElse = false; - } else { - itemNoop.contents = ""; - itemIdx = lastIdx$1; - } - } - itemIdx = itemIdx + 1; - }; - cond = inputVar => validation(b, inputVar, { - type: firstSchema.type, - parser: 0 - }, false); - if (itemNoop.contents) { - if (itemStart) { - if (typeValidation) { - let if_$2 = itemNextElse ? "else if" : "if"; - itemStart = itemStart + if_$2 + ("(!(" + itemNoop.contents + ")){" + fail(caught$1) + "}"); } - } else { - let condBefore = cond; - cond = inputVar => condBefore(inputVar) + ("&&(" + itemNoop.contents + ")"); } - } else if (typeValidation && itemStart) { - let errorCode = fail(caught$1); - itemStart = itemStart + ( - itemNextElse ? "else{" + errorCode + "}" : errorCode - ); + byKey = {}; + keys = []; } - body = itemStart + itemEnd; - } else { - cond = inputVar => validation(b, inputVar, firstSchema, false) + refinement(b, inputVar, firstSchema, false); - body = getItemCode(b, firstSchema, input, input, false, path); - } - if (body || isPriority(flags[firstSchema.type], byKey$1)) { - let if_$3 = nextElse ? "else if" : "if"; - start = start + if_$3 + ("(" + cond(input.v(b)) + "){" + body + "}"); - nextElse = true; - } else if (typeValidation) { - let cond$1 = cond(input.v(b)); - noop = noop ? noop + "||" + cond$1 : cond$1; + } - } - if (typeValidation || deoptIdx$1 === lastIdx) { - let errorCode$1 = fail(caught); - let tmp; - if (noop) { - let if_$4 = nextElse ? "else if" : "if"; - tmp = if_$4 + ("(!(" + noop + ")){" + errorCode$1 + "}"); + + } + let byKey$1 = byKey; + let keys$1 = keys; + if (!exit) { + let nextElse = false; + let noop = ""; + for (let idx$1 = 0, idx_finish = keys$1.length; idx$1 < idx_finish; ++idx$1) { + let arr$1 = byKey$1[keys$1[idx$1]]; + let typeValidationOutput$2 = arr$1[1]; + let firstSchema = arr$1[2]; + let itemsCode$1 = getArrItemsCode(arr$1, false); + let blockCond = { + contents: "" + }; + let blockCode$1 = merge(typeValidationOutput$2, blockCond) + itemsCode$1; + let blockCond$1 = blockCond.contents; + if (blockCode$1 || isPriority(flags[firstSchema.type], byKey$1)) { + let if_ = nextElse ? "else if" : "if"; + start = start + if_ + ("(" + blockCond$1 + "){" + blockCode$1 + "}"); + nextElse = true; } else { - tmp = nextElse ? "else{" + errorCode$1 + "}" : errorCode$1; + noop = noop ? noop + "||" + blockCond$1 : blockCond$1; } - start = start + tmp; } - + let errorCode = fail(caught); + let tmp; + if (noop) { + let if_$1 = nextElse ? "else if" : "if"; + tmp = if_$1 + ("(!(" + noop + ")){" + errorCode + "}"); + } else { + tmp = nextElse ? "else{" + errorCode + "}" : errorCode; + } + start = start + tmp; + } + output.cp = output.cp + start + end; + if (input.i !== output.i) { + output.i = input.i; } - b.c = b.c + start + end; - let o = input.f & 2 ? asyncVal(b, "Promise.resolve(" + input.i + ")") : ( - input.v === _var ? ( - b.c === "" && input.b.c === "" && (input.b.l === input.i + "=" + initialInline || initialInline === "i") ? (input.b.l = "", input.b.a = initialAllocate, input.v = _notVar, input.i = initialInline, input) : copy(input) - ) : input + let o = output.f & 1 ? (output.i = "Promise.resolve(" + output.i + ")", output.v = _notVar, output) : ( + output.v === _var && input.cp === "" && output.cp === "" && (output.l === output.i + "=" + initialInline || initialInline === "i") ? (input.l = "", input.a = initialAllocate, input.v = _notVar, input.i = initialInline, input) : output ); - o.anyOf = selfSchema.anyOf; - let to = selfSchema.to; - o.type = to !== undefined && to.type !== "union" ? (o.t = true, getOutputSchema(to).type) : "union"; + o.s = outputAnyOf.length ? factory$1(outputAnyOf) : never; + o.e = toPerCase !== undefined ? (o.io = true, getOutputSchema(toPerCase)) : selfSchema; return o; } -function factory(schemas) { +function factory$1(schemas) { let len = schemas.length; if (len === 1) { return schemas[0]; @@ -1979,7 +2635,7 @@ function factory(schemas) { let anyOf = new Set(); for (let idx = 0, idx_finish = schemas.length; idx < idx_finish; ++idx) { let schema = schemas[idx]; - if (schema.type === "union" && schema.to === undefined) { + if (schema.type === unionTag && schema.to === undefined) { schema.anyOf.forEach(item => { anyOf.add(item); }); @@ -1989,9 +2645,9 @@ function factory(schemas) { setHas(has, schema.type); } } - let mut = new Schema("union"); + let mut = base(unionTag, false); mut.anyOf = Array.from(anyOf); - mut.compiler = compiler; + mut.decoder = unionDecoder; mut.has = has; return mut; } @@ -1999,40 +2655,41 @@ function factory(schemas) { } function nestedNone() { - let itemSchema = parse$1(0); - let item = { - schema: itemSchema, - location: nestedLoc - }; + let itemSchema = parse(0); let properties = {}; properties[nestedLoc] = itemSchema; return { - type: "object", - serializer: (b, param, selfSchema, param$1) => constVal(b, selfSchema.to), + type: objectTag, + serializer: input => { + let nextSchema = input.e.to; + return nextConst(input, nextSchema, nextSchema); + }, + decoder: objectDecoder, additionalItems: "strip", - items: [item], + required: [nestedLoc], properties: properties }; } -function parser(b, param, selfSchema, param$1) { - return val(b, "{" + nestedLoc + ":" + getOutputSchema(selfSchema).items[0].schema.const + "}", selfSchema.to); +function parser$1(input) { + let nextSchema = input.e.to; + return next(input, "{" + nestedLoc + ":" + getOutputSchema(input.e).properties[nestedLoc].const + "}", nextSchema, nextSchema); } function nestedOption(item) { return updateOutput(item, mut => { mut.to = nestedNone(); - mut.parser = parser; + mut.parser = parser$1; }); } -function factory$1(item, unitOpt) { +function factory$2(item, unitOpt) { let unit$1 = unitOpt !== undefined ? unitOpt : unit; let match = getOutputSchema(item); let match$1 = match.type; switch (match$1) { case "undefined" : - return factory([ + return factory$1([ unit$1, nestedOption(item) ]); @@ -2056,18 +2713,10 @@ function factory$1(item, unitOpt) { if (properties !== undefined) { let nestedSchema = properties[nestedLoc]; tmp = nestedSchema !== undefined ? updateOutput(schema, mut => { - let newItem_schema = { - type: nestedSchema.type, - parser: nestedSchema.parser, - const: nestedSchema.const + 1 - }; - let newItem = { - schema: newItem_schema, - location: nestedLoc - }; let properties = {}; - properties[nestedLoc] = newItem_schema; - mut.items = [newItem]; + let newrecord = {...nestedSchema}; + newrecord.const = nestedSchema.const + 1; + properties[nestedLoc] = newrecord; mut.properties = properties; }) : schema; } else { @@ -2084,7 +2733,7 @@ function factory$1(item, unitOpt) { mut.has = mutHas; }); default: - return factory([ + return factory$1([ item, unit$1 ]); @@ -2120,48 +2769,26 @@ function getWithDefault(schema, $$default) { let message$1 = "Can't set default for " + toExpression(mut); throw new Error("[Sury] " + message$1); } - mut.parser = (b, input, selfSchema, param) => { - let operation = (b, input) => { - let inputVar = input.v(b); - let tmp; - tmp = $$default.TAG === "Value" ? inlineConst(b, parse$1($$default._0)) : embed(b, $$default._0) + "()"; - return val(b, inputVar + "===void 0?" + tmp + ":" + inputVar, selfSchema.to); - }; - if (!(input.f & 2)) { - return operation(b, input); - } - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let operationInput = { - b: b, - v: _var, - i: varWithoutAllocation(bb.g), - f: 0, - type: "unknown" - }; - let operationOutputVal = operation(bb, operationInput); - let operationCode = allocateScope(bb); - return asyncVal(input.b, input.i + ".then(" + operationInput.v(b) + "=>{" + operationCode + "return " + operationOutputVal.i + "})"); + mut.parser = input => { + let nextSchema = input.e.to; + let inputVar = input.v(); + let tmp; + tmp = $$default.TAG === "Value" ? inlineConst(input, parse($$default._0)) : embed(input, $$default._0) + "()"; + return next(input, inputVar + "===void 0?" + tmp + ":" + inputVar, nextSchema, nextSchema); }; - let to = copyWithoutCache(itemOutputSchema); - let compiler = to.compiler; - if (compiler !== undefined) { - to.serializer = compiler; - ((delete to.compiler)); - } else { - to.serializer = (_b, input, param, param$1) => input; - } + let to = copySchema(itemOutputSchema); + let originalDecoder = to.decoder; + to.serializer = input => { + let nextSchema = reverse(item$1); + return refine(originalDecoder(input), nextSchema, undefined, nextSchema); + }; + to.decoder = noopDecoder; mut.to = to; if ($$default.TAG !== "Value") { return; } try { - mut.default = operationFn(item$1, 32)($$default._0); + mut.default = getDecoder(reverse(item$1), undefined)($$default._0); return; } catch (exn) { return; @@ -2187,81 +2814,37 @@ function getOrWith(schema, defalutCb) { }); } -let metadataId = "m:Array.refinements"; - -function refinements(schema) { - let m = schema[metadataId]; - if (m !== undefined) { - return m; - } else { - return []; - } -} - -function arrayCompiler(b, input, selfSchema, path) { - let item = selfSchema.additionalItems; - let inputVar = input.v(b); - let iteratorVar = varWithoutAllocation(b.g); - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let itemInput = val(bb, inputVar + "[" + iteratorVar + "]", unknown); - let itemOutput = withPathPrepend(bb, itemInput, path, iteratorVar, undefined, (b, input, path) => parse(b, item, input, path)); - let itemCode = allocateScope(bb); - let isTransformed = itemInput !== itemOutput; - let output = isTransformed ? val(b, "new Array(" + inputVar + ".length)", selfSchema) : input; - output.type = selfSchema.type; - output.additionalItems = selfSchema.additionalItems; - if (isTransformed || itemCode !== "") { - b.c = b.c + ("for(let " + iteratorVar + "=0;" + iteratorVar + "<" + inputVar + ".length;++" + iteratorVar + "){" + itemCode + ( - isTransformed ? addKey(b, output, iteratorVar, itemOutput) : "" - ) + "}"); - } - if (itemOutput.f & 2) { - return asyncVal(output.b, "Promise.all(" + output.i + ")"); - } else { - return output; - } -} - -function factory$2(item) { - let mut = new Schema("array"); - mut.additionalItems = item; - mut.items = immutableEmpty$1; - mut.compiler = arrayCompiler; - return mut; -} - function setAdditionalItems(schema, additionalItems, deep) { let currentAdditionalItems = schema.additionalItems; if (currentAdditionalItems === undefined) { return schema; } - let items = schema.items; - if (currentAdditionalItems === additionalItems || typeof currentAdditionalItems === "object") { + if (currentAdditionalItems === additionalItems || typeof currentAdditionalItems === objectTag) { return schema; } - let mut = copyWithoutCache(schema); + let mut = copySchema(schema); mut.additionalItems = additionalItems; if (deep) { - let newItems = []; - let newProperties = {}; - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let item = items[idx]; - let newSchema = setAdditionalItems(item.schema, additionalItems, deep); - let newItem = newSchema === item.schema ? item : ({ - schema: newSchema, - location: item.location - }); - newProperties[item.location] = newSchema; - newItems.push(newItem); + let items = schema.items; + if (items !== undefined) { + let newItems = []; + for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { + let s = items[idx]; + newItems.push(setAdditionalItems(s, additionalItems, deep)); + } + mut.items = newItems; } - mut.items = newItems; - mut.properties = newProperties; + let properties = schema.properties; + if (properties !== undefined) { + let newProperties = {}; + let keys = Object.keys(properties); + for (let idx$1 = 0, idx_finish$1 = keys.length; idx$1 < idx_finish$1; ++idx$1) { + let key = keys[idx$1]; + newProperties[key] = setAdditionalItems(properties[key], additionalItems, deep); + } + mut.properties = newProperties; + } + } return mut; } @@ -2282,109 +2865,198 @@ function deepStrict(schema) { return setAdditionalItems(schema, "strict", true); } -function dictCompiler(b, input, selfSchema, path) { - let item = selfSchema.additionalItems; - let inputVar = input.v(b); - let keyVar = varWithoutAllocation(b.g); - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let itemInput = val(bb, inputVar + "[" + keyVar + "]", unknown); - let itemOutput = withPathPrepend(bb, itemInput, path, keyVar, undefined, (b, input, path) => parse(b, item, input, path)); - let itemCode = allocateScope(bb); - let isTransformed = itemInput !== itemOutput; - let output = isTransformed ? val(b, "{}", selfSchema) : input; - output.type = selfSchema.type; - output.additionalItems = selfSchema.additionalItems; - if (isTransformed || itemCode !== "") { - b.c = b.c + ("for(let " + keyVar + " in " + inputVar + "){" + itemCode + ( - isTransformed ? addKey(b, output, keyVar, itemOutput) : "" - ) + "}"); - } - if (!(itemOutput.f & 2)) { +let Tuple = {}; + +function jsonEncoder(input, target) { + let toTagFlag = flags[target.type]; + if (toTagFlag & 46) { + return parse$1(refine(input, unknown, undefined, target)); + } + if (toTagFlag & 2064) { + let jsonExpected = copySchema(nullLiteral); + jsonExpected.to = target; + return parse$1(refine(input, unknown, undefined, jsonExpected)); + } + if (toTagFlag & 128) { + let jsonExpected$1 = array(unknown); + let output = parse$1(refine(input, unknown, undefined, jsonExpected$1)); + output.s.additionalItems = json; + output.e = target; + output.ii = false; + output.io = false; return output; } - let resolveVar = varWithoutAllocation(b.g); - let rejectVar = varWithoutAllocation(b.g); - let asyncParseResultVar = varWithoutAllocation(b.g); - let counterVar = varWithoutAllocation(b.g); - let outputVar = output.v(b); - return asyncVal(b, "new Promise((" + resolveVar + "," + rejectVar + ")=>{let " + counterVar + "=Object.keys(" + outputVar + ").length;for(let " + keyVar + " in " + outputVar + "){" + outputVar + "[" + keyVar + "].then(" + asyncParseResultVar + "=>{" + outputVar + "[" + keyVar + "]=" + asyncParseResultVar + ";if(" + counterVar + "--===1){" + resolveVar + "(" + outputVar + ")}}," + rejectVar + ")}})"); -} - -function factory$3(item) { - let mut = new Schema("object"); - mut.properties = immutableEmpty; - mut.items = immutableEmpty$1; - mut.additionalItems = item; - mut.compiler = dictCompiler; - return mut; + if (toTagFlag & 64) { + let jsonExpected$2 = factory(unknown); + let output$1 = parse$1(refine(input, unknown, undefined, jsonExpected$2)); + output$1.s.additionalItems = json; + output$1.e = target; + output$1.ii = false; + output$1.io = false; + return output$1; + } + if (toTagFlag & 768) { + return input; + } + try { + let jsonExpected$3 = copySchema(string); + jsonExpected$3.to = target; + return parse$1(refine(input, unknown, undefined, jsonExpected$3)); + } catch (exn) { + return input; + } } -let Tuple = {}; - -let metadataId$1 = "m:String.refinements"; - -function refinements$1(schema) { - let m = schema[metadataId$1]; - if (m !== undefined) { - return m; +function isJsonable(schema) { + let tagFlag = flags[schema.type]; + let tmp = true; + if (!(tagFlag & 46 || schema.$ref === json.$ref || tagFlag & 256 && schema.anyOf.every(isJsonable))) { + let tmp$1 = false; + if (tagFlag & 128) { + let s = schema.additionalItems; + let tmp$2; + tmp$2 = s === "strip" || s === "strict" ? true : isJsonable(s); + tmp$1 = tmp$2; + } + tmp = tmp$1 && schema.items.every(isJsonable); + } + if (tmp) { + return true; + } + let tmp$3 = false; + if (tagFlag & 64) { + let s$1 = schema.additionalItems; + let tmp$4; + tmp$4 = s$1 === "strip" || s$1 === "strict" ? true : isJsonable(s$1); + tmp$3 = tmp$4; + } + if (tmp$3) { + return Object.values(schema.properties).every(isJsonable); } else { - return []; + return false; } } -let cuidRegex = /^c[^\s-]{8,}$/i; - -let uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; - -let emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; - -let datetimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; - -let json = shaken("json"); +function jsonDecoder(input) { + let inputTagFlag = flags[input.s.type]; + if (isJsonable(input.s)) { + return input; + } + if (inputTagFlag & 2064) { + return nextConst(input, nullLiteral, undefined); + } + if (inputTagFlag & 128) { + let expected = base(arrayTag, false); + expected.items = input.s.items.map(param => json); + expected.decoder = arrayDecoder; + let v = input.s.additionalItems; + let tmp; + tmp = v === "strip" || v === "strict" ? v : json; + expected.additionalItems = tmp; + expected.to = input.e.to; + return parse$1(refine(input, undefined, undefined, expected)); + } + if (inputTagFlag & 64) { + let match = input.s.additionalItems; + if (match === "strip" || match === "strict") { + match === "strip"; + } else { + let expected$1 = factory(json); + expected$1.to = input.e.to; + return parse$1(refine(input, undefined, undefined, expected$1)); + } + let jsonVal = makeObjectVal(input, input.s); + jsonVal.e = json; + if (input.e.to) { + jsonVal.e = copySchema(jsonVal.e); + jsonVal.e.to = input.e.to; + } + let keys = Object.keys(input.s.properties); + for (let idx = 0, idx_finish = keys.length; idx < idx_finish; ++idx) { + let key = keys[idx]; + let itemVal = get(input, key); + itemVal.ii = false; + itemVal.io = false; + if (itemVal.s.type === unionTag && itemVal.s.has[undefinedTag]) { + itemVal.e = factory$1([ + unit, + json + ]); + let itemOutput = parse$1(itemVal); + itemOutput.o = true; + add(jsonVal, key, itemOutput); + } else { + itemVal.e = json; + add(jsonVal, key, parse$1(itemVal)); + } + } + return completeObjectVal(jsonVal); + } + if (inputTagFlag & 512) { + return recursiveDecoder(input); + } + if (inputTagFlag & 1) { + let to = input.e.to; + let preEncode = to && !input.e.parser; + if (preEncode) { + input.s = json; + return jsonEncoder(input, input.e); + } else if (input.e.noValidation) { + input.s = json; + return input; + } else { + return recursiveDecoder(input); + } + } + try { + let expected$2 = copySchema(string); + expected$2.to = input.e; + input.e = expected$2; + return parse$1(input); + } catch (exn) { + return unsupportedDecode(input, input.s, json); + } +} function enableJson() { if (!json[shakenRef]) { return; } ((delete json.as)); - let jsonRef = new Schema("ref"); + let jsonRef = base(refTag, true); jsonRef.$ref = defsPath + jsonName; jsonRef.name = jsonName; + jsonRef.decoder = jsonDecoder; + jsonRef.encoder = jsonEncoder; json.type = jsonRef.type; json.$ref = jsonRef.$ref; json.name = jsonName; + json.decoder = jsonDecoder; + json.encoder = jsonEncoder; + let anyOf = [ + string, + bool, + float, + nullLiteral, + factory(jsonRef), + array(jsonRef) + ]; + let has = {}; + anyOf.forEach(schema => { + has[schema.type] = true; + }); + let jsonDef = base(unionTag, true); + jsonDef.anyOf = anyOf; + jsonDef.has = has; + jsonDef.decoder = unionDecoder; + jsonDef.name = jsonName; + jsonDef.type = unionTag; let defs = {}; - defs[jsonName] = { - type: "union", - compiler: compiler, - name: jsonName, - has: { - string: true, - boolean: true, - number: true, - null: true, - object: true, - array: true - }, - anyOf: [ - string, - bool, - float, - $$null, - factory$3(jsonRef), - factory$2(jsonRef) - ] - }; + defs[jsonName] = jsonDef; json.$defs = defs; } -function inlineJsonString(b, schema, selfSchema, path) { +function inlineJsonString(input, schema) { let tagFlag = flags[schema.type]; let $$const = schema.const; if (tagFlag & 48) { @@ -2396,76 +3068,135 @@ function inlineJsonString(b, schema, selfSchema, path) { } else if (tagFlag & 12) { return "\"" + $$const + "\""; } else { - return unsupportedTransform(b, schema, selfSchema, path); + return unsupportedDecode(input, schema, input.e); } } +function constSchemaToJsonStringConst(input, target) { + let tagFlag = flags[target.type]; + let $$const = target.const; + if (tagFlag & 48) { + return "null"; + } else if (tagFlag & 2) { + return fromString($$const); + } else if (tagFlag & 1024) { + return "\"" + $$const + "\""; + } else if (tagFlag & 12) { + return (""+$$const); + } else { + return unsupportedDecode(input, input.s, target); + } +} + +function jsonStringEncoder(input, target) { + if (target.format === "json") { + return input; + } + if (constField in target) { + let jsonStringConstSchema = base(stringTag, true); + jsonStringConstSchema.const = constSchemaToJsonStringConst(input, target); + jsonStringConstSchema.to = target; + jsonStringConstSchema.decoder = literalDecoder; + return refine(input, undefined, undefined, jsonStringConstSchema); + } + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let nextSchema = copySchema(json); + nextSchema.to = target; + let output = next(input, outputVar, nextSchema, nextSchema); + output.ii = true; + output.io = true; + output.v = _var; + let inputVar = input.v(); + output.cp = "try{" + outputVar + "=JSON.parse(" + inputVar + ")}catch(t){" + embedInvalidInput(input, input.s) + "}"; + return output; +} + +function jsonStringDecoder(input) { + let inputTagFlag = flags[input.s.type]; + let expectedSchema = input.e; + if (inputTagFlag & 1) { + let to = expectedSchema.to; + let preEncode = to && to.type !== unknownTag && !expectedSchema.parser && !expectedSchema.refiner; + let stringVal = stringDecoder(input); + stringVal.s = expectedSchema; + stringVal.e = expectedSchema; + if (preEncode) { + return jsonStringEncoder(stringVal, to); + } + let stringVar = stringVal.v(); + let output = refine(stringVal, expectedSchema, undefined, undefined); + output.cp = "try{JSON.parse(" + stringVar + ")}catch(t){" + embedInvalidInput(stringVal, undefined) + "}"; + return output; + } + if (input.s.format === "json") { + return input; + } + if (constField in input.s) { + return next(input, inlineJsonString(input, input.s), expectedSchema, undefined); + } + if (inputTagFlag & 2) { + return next(input, "JSON.stringify(" + input.i + ")", expectedSchema, undefined); + } + if (inputTagFlag & 12) { + let output$1 = inputToString(input); + output$1.s = expectedSchema; + return output$1; + } + if (inputTagFlag & 1024) { + return next(input, "\"\\\"\"+" + input.i + "+\"\\\"\"", expectedSchema, undefined); + } + if (!(inputTagFlag & 192)) { + return unsupportedDecode(input, input.s, expectedSchema); + } + let jsonVal = parse$1(refine(input, undefined, undefined, json)); + let v = expectedSchema.space; + return next(jsonVal, "JSON.stringify(" + jsonVal.i + ( + v !== undefined && v !== 0 ? ",null," + v : "" + ) + ")", expectedSchema, expectedSchema); +} + function enableJsonString() { if (jsonString[shakenRef]) { ((delete jsonString.as)); - jsonString.type = "string"; + enableJson(); + jsonString.type = stringTag; jsonString.format = "json"; jsonString.name = jsonName + " string"; - jsonString.compiler = (b, inputArg, selfSchema, path) => { - let inputTagFlag = flags[inputArg.type]; + jsonString.encoder = jsonStringEncoder; + jsonString.decoder = jsonStringDecoder; + return; + } + +} + +let uint8Array = shaken("uint8Array"); + +function enableUint8Array() { + if (uint8Array[shakenRef]) { + ((delete uint8Array.as)); + uint8Array.type = instanceTag; + uint8Array.class = (Uint8Array); + uint8Array.decoder = inputArg => { + let inputTagFlag = flags[inputArg.s.type]; let input = inputArg; - if (inputTagFlag & 1) { - let to = selfSchema.to; - if (to && constField in to) { - let inputVar = input.v(b); - b.f = inputVar + "===" + inlineJsonString(b, to, selfSchema, path) + "||" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: to, - received: input - }), inputVar) + ";"; - input = constVal(b, to); - } else if (!(to && to.format === "json")) { - let inputVar$1 = input.v(b); - let withTypeValidation = b.g.o & 1; - if (withTypeValidation) { - b.f = typeFilterCode(b, string, input, path); - } - if (to || withTypeValidation) { - let tmp; - if (to) { - jsonableValidation(to, to, path, b.g.o); - let targetVal = allocateVal(b, unknown); - input = targetVal; - tmp = targetVal.i + "="; - } else { - tmp = ""; - } - b.c = b.c + ("try{" + tmp + "JSON.parse(" + inputVar$1 + ")}catch(t){" + failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: selfSchema, - received: input - }), inputVar$1) + "}"); - } - - } - - } else { - if (constField in input) { - input = val(b, inlineJsonString(b, input, selfSchema, path), string); - } else if (inputTagFlag & 2) { - if (input.format !== "json") { - input = val(b, "JSON.stringify(" + input.i + ")", string); - } - - } else if (inputTagFlag & 12) { - input = inputToString(b, input); - } else if (inputTagFlag & 1024) { - input = val(b, "\"\\\"\"+" + input.i + "+\"\\\"\"", string); - } else if (inputTagFlag & 192) { - jsonableValidation(input, input, path, b.g.o); - let v = selfSchema.space; - input = val(b, "JSON.stringify(" + input.i + ( - v !== undefined && v !== 0 ? ",null," + v : "" - ) + ")", string); - } else { - unsupportedTransform(b, input, selfSchema, path); - } - input.format = "json"; + if (inputTagFlag & 2) { + input = next(input, embed(input, (new TextEncoder())) + ".encode(" + input.i + ")", uint8Array, undefined); + } else if (inputTagFlag & 8193) { + input = instanceDecoder(input); + } + let match = inputArg.e; + let match$1 = match.parser; + if (match$1 !== undefined) { + return input; + } + let to = match.to; + if (to === undefined) { + return input; + } + let toTagFlag = flags[to.type]; + if (toTagFlag & 2) { + input = next(input, embed(input, (new TextDecoder())) + ".decode(" + input.i + ")", string, undefined); } return input; }; @@ -2474,34 +3205,142 @@ function enableJsonString() { } -function jsonStringWithSpace(space) { - let mut = copyWithoutCache(jsonString); - mut.space = space; - return mut; +let isoDateTime = shaken("isoDateTime"); + +function enableIsoDateTime() { + if (!isoDateTime[shakenRef]) { + return; + } + ((delete isoDateTime.as)); + let datetimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; + isoDateTime.type = stringTag; + isoDateTime.decoder = string.decoder; + isoDateTime.format = "date-time"; + isoDateTime.refiner = input => [{ + c: inputVar => embed(input, datetimeRe) + ".test(" + inputVar + ")", + f: failWithErrorMessage("format", "Invalid datetime string! Expected UTC") + }]; +} + +let port = shaken("port"); + +function enablePort() { + if (port[shakenRef]) { + ((delete port.as)); + port.type = numberTag; + port.decoder = int.decoder; + port.format = "port"; + port.refiner = param => [{ + c: inputVar => inputVar + ">0&&" + inputVar + "<65536&&" + inputVar + "%1===0", + f: failWithErrorMessage("format", undefined) + }]; + return; + } + } -let metadataId$2 = "m:Int.refinements"; +let email = shaken("email"); -function refinements$2(schema) { - let m = schema[metadataId$2]; - if (m !== undefined) { - return m; - } else { - return []; +function enableEmail() { + if (!email[shakenRef]) { + return; } + ((delete email.as)); + let emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; + email.type = stringTag; + email.decoder = string.decoder; + email.format = "email"; + email.refiner = input => [{ + c: inputVar => embed(input, emailRegex) + ".test(" + inputVar + ")", + f: failWithErrorMessage("format", undefined) + }]; } -let metadataId$3 = "m:Float.refinements"; +let uuid = shaken("uuid"); -function refinements$3(schema) { - let m = schema[metadataId$3]; - if (m !== undefined) { - return m; - } else { - return []; +function enableUuid() { + if (!uuid[shakenRef]) { + return; + } + ((delete uuid.as)); + let uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; + uuid.type = stringTag; + uuid.decoder = string.decoder; + uuid.format = "uuid"; + uuid.refiner = input => [{ + c: inputVar => embed(input, uuidRegex) + ".test(" + inputVar + ")", + f: failWithErrorMessage("format", undefined) + }]; +} + +let cuid = shaken("cuid"); + +function enableCuid() { + if (!cuid[shakenRef]) { + return; + } + ((delete cuid.as)); + let cuidRegex = /^c[^\s-]{8,}$/i; + cuid.type = stringTag; + cuid.decoder = string.decoder; + cuid.format = "cuid"; + cuid.refiner = input => [{ + c: inputVar => embed(input, cuidRegex) + ".test(" + inputVar + ")", + f: failWithErrorMessage("format", undefined) + }]; +} + +let url = shaken("url"); + +function enableUrl() { + if (!url[shakenRef]) { + return; } + ((delete url.as)); + let urlValidator = (s=>{try{new URL(s);return true}catch(_){return false}}); + url.type = stringTag; + url.decoder = string.decoder; + url.format = "url"; + url.refiner = input => [{ + c: inputVar => embed(input, urlValidator) + "(" + inputVar + ")", + f: failWithErrorMessage("format", undefined) + }]; } +function invalidDateRefine(input) { + return refine(input, input.e, [{ + c: inputVar => "!Number.isNaN(" + inputVar + ".getTime())", + f: failInvalidType + }], undefined); +} + +let mut = base(instanceTag, true); + +mut.class = Date; + +mut.decoder = input => { + let inputTagFlag = flags[input.s.type]; + if (inputTagFlag & 2) { + return invalidDateRefine(next(input, "new Date(" + input.i + ")", mut, undefined)); + } else if (inputTagFlag & 1) { + return invalidDateRefine(instanceDecoder(input)); + } else if (inputTagFlag & 8192 && input.s.class === mut.class) { + return input; + } else { + return unsupportedDecode(input, input.s, input.e); + } +}; + +mut.encoder = (input, target) => { + let toTagFlag = flags[target.type]; + if (!(toTagFlag & 2)) { + return input; + } + let dateTimeString = copySchema(string); + dateTimeString.format = "date-time"; + return parse$1(next(input, input.i + ".toISOString()", dateTimeString, target)); +}; + function to(from, target) { if (from === target) { return from; @@ -2513,20 +3352,14 @@ function to(from, target) { } function list(schema) { - return transform(factory$2(schema), param => ({ + return transform(array(schema), param => ({ p: Belt_List.fromArray, - s: Belt_List.toArray - })); -} - -function instance(class_) { - let mut = new Schema("instance"); - mut.class = class_; - return mut; + s: Belt_List.toArray + })); } function meta(schema, data) { - let mut = copyWithoutCache(schema); + let mut = copySchema(schema); let name = data.name; if (name !== undefined) { if (name === "") { @@ -2558,134 +3391,327 @@ function meta(schema, data) { let examples = data.examples; if (examples !== undefined) { if (examples.length !== 0) { - mut.examples = examples.map(operationFn(schema, 32)); + mut.examples = examples.map(getDecoder(reverse(schema), undefined)); } else { mut.examples = undefined; } } + let em = data.errorMessage; + if (em !== undefined) { + if (Object.keys(em).length === 0) { + mut.errorMessage = undefined; + } else { + mut.errorMessage = em; + } + } return mut; } function brand(schema, id) { - let mut = copyWithoutCache(schema); + let mut = copySchema(schema); mut.name = id; return mut; } -function getFullDitemPath(ditem) { - switch (ditem.k) { - case 0 : - return "[" + fromString(ditem.location) + "]"; - case 1 : - return getFullDitemPath(ditem.of) + ditem.p; - case 2 : - return ditem.p; - } +function toEmbededItem(definition) { + return definition[itemSymbol]; } -function definitionToOutput(b, definition, getItemOutput, outputSchema) { - if (constField in outputSchema) { - return constVal(b, outputSchema); +function proxifyShapedSchema(schema, from, fromFlattened) { + let mut = copySchema(getOutputSchema(schema)); + mut.from = from; + if (fromFlattened !== undefined) { + mut.fromFlattened = fromFlattened; } - let item = definition[itemSymbol]; - if (item !== undefined) { - return getItemOutput(item); - } - let isArray = flags[outputSchema.type] & 128; - let objectVal = make(b, isArray); - outputSchema.items.forEach(item => add(objectVal, item.location, definitionToOutput(b, definition[item.location], getItemOutput, item.schema))); - return complete(objectVal, isArray); + return new Proxy(mut, { + get: (target, prop) => { + if (prop === itemSymbol) { + return target; + } + let items = target.items; + let properties = target.properties; + let maybeField = properties !== undefined ? properties[prop] : ( + items !== undefined ? items[prop] : undefined + ); + if (maybeField === undefined) { + let message = "Cannot read property \"" + prop + "\" of " + toExpression(target); + throw new Error("[Sury] " + message); + } + return proxifyShapedSchema(maybeField, target.from.concat(prop), target.fromFlattened); + } + }); } -function objectStrictModeCheck(b, input, items, selfSchema, path) { - if (!(selfSchema.type === "object" && selfSchema.additionalItems === "strict" && b.g.o & 1)) { - return; +function getShapedSerializerOutput(input, acc, targetSchema, path) { + let exit = 0; + if (acc !== undefined) { + let val = acc.val; + if (val !== undefined) { + let v = scope(val); + v.t = true; + v.s = targetSchema; + v.e = targetSchema; + return parse$1(v); + } + exit = 1; + } else { + exit = 1; } - let key = allocateVal(b, unknown); - let keyVar = key.i; - b.c = b.c + ("for(" + keyVar + " in " + input.v(b) + "){if("); - if (items.length !== 0) { - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let match = items[idx]; - if (idx !== 0) { - b.c = b.c + "&&"; + if (exit === 1) { + if (constField in targetSchema) { + let v$1 = nextConst(input, targetSchema, targetSchema); + v$1.prev = undefined; + v$1.p = input; + v$1.v = _notVarAtParent; + v$1.io = true; + return parse$1(v$1); + } + let resolvedTargetSchema = acc === undefined ? getOutputSchema(targetSchema) : targetSchema; + let v$2 = makeObjectVal(input, resolvedTargetSchema); + v$2.e = resolvedTargetSchema; + v$2.io = true; + v$2.prev = undefined; + v$2.p = input; + v$2.v = _notVarAtParent; + let flattened = resolvedTargetSchema.flattened; + let items = resolvedTargetSchema.items; + let exit$1 = 0; + let exit$2 = 0; + if (items !== undefined && (acc !== undefined || typeof resolvedTargetSchema.additionalItems !== objectTag)) { + for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { + let location = idx.toString(); + let tmp; + if (acc !== undefined) { + let properties = acc.properties; + tmp = properties !== undefined ? properties[location] : undefined; + } else { + tmp = undefined; + } + let inlinedLocation = inlineLocation(input.g, location); + add(v$2, location, getShapedSerializerOutput(input, tmp, items[idx], path + ("[" + inlinedLocation + "]"))); } - b.c = b.c + (keyVar + "!==" + inlineLocation(b, match.location)); + } else { + exit$2 = 3; } - } else { - b.c = b.c + "true"; + if (exit$2 === 3) { + let properties$1 = resolvedTargetSchema.properties; + if (properties$1 !== undefined && (acc !== undefined || typeof resolvedTargetSchema.additionalItems !== objectTag)) { + if (flattened !== undefined && acc !== undefined) { + let flattenedAcc = acc.flattened; + if (flattenedAcc !== undefined) { + flattenedAcc.forEach((acc, idx) => { + let flattenedOutput = getShapedSerializerOutput(input, acc, reverse(flattened[idx]), path); + let vals = flattenedOutput.d; + let locations = Object.keys(vals); + for (let idx$1 = 0, idx_finish = locations.length; idx$1 < idx_finish; ++idx$1) { + let location = locations[idx$1]; + add(v$2, location, vals[location]); + } + }); + } + + } + let keys = Object.keys(properties$1); + for (let idx$1 = 0, idx_finish$1 = keys.length; idx$1 < idx_finish$1; ++idx$1) { + let location$1 = keys[idx$1]; + if (!(location$1 in v$2.d)) { + let tmp$1; + if (acc !== undefined) { + let properties$2 = acc.properties; + tmp$1 = properties$2 !== undefined ? properties$2[location$1] : undefined; + } else { + tmp$1 = undefined; + } + let inlinedLocation$1 = inlineLocation(input.g, location$1); + add(v$2, location$1, getShapedSerializerOutput(input, tmp$1, properties$1[location$1], path + ("[" + inlinedLocation$1 + "]"))); + } + + } + } else { + exit$1 = 2; + } + } + if (exit$1 === 2) { + let from = targetSchema.from; + let path$1 = from !== undefined ? path + from.map(item => "[\"" + item + "\"]").join("") : path; + let tmp$2 = path$1 === "" ? "" : " at " + path$1; + invalidOperation(input, "Missing input for " + toExpression(targetSchema) + tmp$2); + } + return completeObjectVal(v$2); } - b.c = b.c + ("){" + failWithArg(b, path, exccessFieldName => ({ - TAG: "ExcessField", - _0: exccessFieldName - }), keyVar) + "}}"); + } -function proxify(item) { - return new Proxy(immutableEmpty, { - get: (param, prop) => { - if (prop === itemSymbol) { - return item; +function getShapedParserOutput(input, targetSchema) { + let from = targetSchema.from; + let fromFlattened = targetSchema.fromFlattened; + let v; + if (fromFlattened !== undefined) { + v = scope(getValByFrom(input.fv[fromFlattened], targetSchema.from, 0)); + } else if (from !== undefined) { + v = scope(getValByFrom(input, from, 0)); + } else if (constField in targetSchema) { + v = nextConst(input, targetSchema, undefined); + } else { + let output = makeObjectVal(input, targetSchema); + output.io = true; + let items = targetSchema.items; + if (items !== undefined) { + for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { + let location = idx.toString(); + add(output, location, getShapedParserOutput(input, items[idx])); } - let inlinedLocation = fromString(prop); - let targetReversed = getOutputSchema(item.schema); - let items = targetReversed.items; - let properties = targetReversed.properties; - let maybeField; + } else { + let properties = targetSchema.properties; if (properties !== undefined) { - maybeField = properties[prop]; - } else if (items !== undefined) { - let i = items[prop]; - maybeField = i !== undefined ? i.schema : undefined; + let keys = Object.keys(properties); + for (let idx$1 = 0, idx_finish$1 = keys.length; idx$1 < idx_finish$1; ++idx$1) { + let location$1 = keys[idx$1]; + add(output, location$1, getShapedParserOutput(input, properties[location$1])); + } } else { - maybeField = undefined; - } - if (maybeField === undefined) { - let message = "Cannot read property " + inlinedLocation + " of " + toExpression(targetReversed); + let message = "Don't know where the value is coming from: " + toExpression(targetSchema); throw new Error("[Sury] " + message); } - return proxify({ - k: 1, - location: prop, - schema: maybeField, - of: item, - p: "[" + inlinedLocation + "]" - }); } - }); + v = completeObjectVal(output); + } + v.prev = undefined; + v.e = targetSchema; + return v; } -function schemaCompiler(b, input, selfSchema, path) { - let additionalItems = selfSchema.additionalItems; - let items = selfSchema.items; - let isArray = flags[selfSchema.type] & 128; - if (b.g.o & 64) { - let objectVal = make(b, isArray); - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let match = items[idx]; - let location = match.location; - add(objectVal, location, input.properties[location]); - } - return complete(objectVal, isArray); - } - let objectVal$1 = make(b, isArray); - for (let idx$1 = 0, idx_finish$1 = items.length; idx$1 < idx_finish$1; ++idx$1) { - let match$1 = items[idx$1]; - let location$1 = match$1.location; - let itemInput = get(b, input, location$1); - let inlinedLocation = inlineLocation(b, location$1); - let path$1 = path + ("[" + inlinedLocation + "]"); - add(objectVal$1, location$1, parse(b, match$1.schema, itemInput, path$1)); - } - objectStrictModeCheck(b, input, items, selfSchema, path); - if ((additionalItems !== "strip" || b.g.o & 32) && items.every(item => objectVal$1.properties[item.location] === input.properties[item.location])) { - input.additionalItems = "strip"; - return input; - } else { - return complete(objectVal$1, isArray); +function getValByFrom(_input, from, _idx) { + while (true) { + let idx = _idx; + let input = _input; + let key = from[idx]; + if (key === undefined) { + return input; + } + _idx = idx + 1 | 0; + _input = input.d[key]; + continue; + }; +} + +function shapedSerializer(input) { + let acc = {}; + prepareShapedSerializerAcc(acc, input); + let targetSchema = input.e.to; + let output = getShapedSerializerOutput(input, acc, targetSchema, ""); + output.t = true; + output.prev = input; + return output; +} + +function traverseDefinition(definition, onNode) { + if (typeof definition !== "object" || definition === null) { + return parse(definition); + } + let s = onNode(definition); + if (s !== undefined) { + return s; + } + if (Array.isArray(definition)) { + for (let idx = 0, idx_finish = definition.length; idx < idx_finish; ++idx) { + let schema = traverseDefinition(definition[idx], onNode); + definition[idx] = schema; + } + let mut = base(arrayTag, false); + mut.items = definition; + mut.additionalItems = "strict"; + mut.decoder = arrayDecoder; + return mut; + } + let cnstr = definition.constructor; + if (cnstr && cnstr !== Object) { + let mut$1 = base(instanceTag, true); + mut$1.class = cnstr; + mut$1.const = definition; + mut$1.decoder = literalDecoder; + return mut$1; + } + let fieldNames = Object.keys(definition); + let length = fieldNames.length; + for (let idx$1 = 0; idx$1 < length; ++idx$1) { + let location = fieldNames[idx$1]; + let schema$1 = traverseDefinition(definition[location], onNode); + definition[location] = schema$1; + } + let mut$2 = base(objectTag, false); + mut$2.required = fieldNames; + mut$2.properties = definition; + mut$2.additionalItems = globalConfig.a; + mut$2.decoder = objectDecoder; + return mut$2; +} + +function prepareShapedSerializerAcc(acc, input) { + let match = input.e; + let from = match.from; + if (from !== undefined) { + let fromFlattened = match.fromFlattened; + let accAtFrom; + if (fromFlattened !== undefined) { + if (acc.flattened === undefined) { + acc.flattened = []; + } + let acc$1 = acc.flattened[fromFlattened]; + if (acc$1 !== undefined) { + accAtFrom = acc$1; + } else { + let newAcc = {}; + acc.flattened[fromFlattened] = newAcc; + accAtFrom = newAcc; + } + } else { + accAtFrom = acc; + } + for (let idx = 0, idx_finish = from.length; idx < idx_finish; ++idx) { + let key = from[idx]; + let p = accAtFrom.properties; + let p$1; + if (p !== undefined) { + p$1 = p; + } else { + let p$2 = {}; + accAtFrom.properties = p$2; + p$1 = p$2; + } + let acc$2 = p$1[key]; + let tmp; + if (acc$2 !== undefined) { + tmp = acc$2; + } else { + let newAcc$1 = {}; + p$1[key] = newAcc$1; + tmp = newAcc$1; + } + accAtFrom = tmp; + } + accAtFrom.val = input; + return; + } + let vals = input.d; + if (vals === undefined) { + return; + } + let keys = Object.keys(vals); + for (let idx$1 = 0, idx_finish$1 = keys.length; idx$1 < idx_finish$1; ++idx$1) { + prepareShapedSerializerAcc(acc, vals[keys[idx$1]]); } } +function definitionToSchema(definition) { + return traverseDefinition(definition, node => { + if (node["~standard"]) { + return node; + } + + }); +} + function nested(fieldName) { let parentCtx = this; let cacheId = "~" + fieldName; @@ -2693,38 +3719,28 @@ function nested(fieldName) { if (ctx !== undefined) { return Primitive_option.valFromOption(ctx); } - let schemas = []; let properties = {}; - let items = []; - let schema = new Schema("object"); - schema.items = items; + let required = []; + let schema = base(objectTag, false); + schema.required = required; schema.properties = properties; schema.additionalItems = globalConfig.a; - schema.compiler = schemaCompiler; - let target = parentCtx.f(fieldName, schema)[itemSymbol]; + schema.decoder = objectDecoder; + let parentSchema = parentCtx.f(fieldName, schema)[itemSymbol]; let field = (fieldName, schema) => { let inlinedLocation = fromString(fieldName); if (fieldName in properties) { throw new Error("[Sury] " + ("The field " + inlinedLocation + " defined twice")); } - let ditem_3 = "[" + inlinedLocation + "]"; - let ditem = { - k: 1, - location: fieldName, - schema: schema, - of: target, - p: ditem_3 - }; + required.push(fieldName); properties[fieldName] = schema; - items.push(ditem); - schemas.push(schema); - return proxify(ditem); + return proxifyShapedSchema(schema, parentSchema.from.concat(fieldName), parentSchema.fromFlattened); }; let tag = (tag$1, asValue) => { field(tag$1, definitionToSchema(asValue)); }; let fieldOr = (fieldName, schema, or) => { - let schema$1 = factory$1(schema, undefined); + let schema$1 = factory$2(schema, undefined); return field(fieldName, getWithDefault(schema$1, { TAG: "Value", _0: or @@ -2734,15 +3750,16 @@ function nested(fieldName) { let match = schema.type; if (match === "object") { let to = schema.to; - let flattenedItems = schema.items; + let flattenedProperties = schema.properties; if (to) { let message = "Unsupported nested flatten for transformed object schema " + toExpression(schema); throw new Error("[Sury] " + message); } + let flattenedKeys = Object.keys(flattenedProperties); let result = {}; - for (let idx = 0, idx_finish = flattenedItems.length; idx < idx_finish; ++idx) { - let item = flattenedItems[idx]; - result[item.location] = field(item.location, item.schema); + for (let idx = 0, idx_finish = flattenedKeys.length; idx < idx_finish; ++idx) { + let key = flattenedKeys[idx]; + result[key] = field(key, flattenedProperties[key]); } return result; } @@ -2761,314 +3778,72 @@ function nested(fieldName) { return ctx$1; } -function definitionToSchema(definition) { - if (typeof definition !== "object" || definition === null) { - return parse$1(definition); - } - if (definition["~standard"]) { - return definition; - } - if (Array.isArray(definition)) { - for (let idx = 0, idx_finish = definition.length; idx < idx_finish; ++idx) { - let schema = definitionToSchema(definition[idx]); - let location = idx.toString(); - definition[idx] = { - schema: schema, - location: location - }; - } - let mut = new Schema("array"); - mut.items = definition; - mut.additionalItems = "strict"; - mut.compiler = schemaCompiler; - return mut; - } - let cnstr = definition.constructor; - if (cnstr && cnstr !== Object) { - return { - type: "instance", - const: definition, - class: cnstr - }; - } - let fieldNames = Object.keys(definition); - let length = fieldNames.length; - let items = []; - for (let idx$1 = 0; idx$1 < length; ++idx$1) { - let location$1 = fieldNames[idx$1]; - let schema$1 = definitionToSchema(definition[location$1]); - let item = { - schema: schema$1, - location: location$1 - }; - definition[location$1] = schema$1; - items[idx$1] = item; - } - let mut$1 = new Schema("object"); - mut$1.items = items; - mut$1.properties = definition; - mut$1.additionalItems = globalConfig.a; - mut$1.compiler = schemaCompiler; - return mut$1; -} - -function definitionToRitem(definition, path, ritemsByItemPath) { - if (typeof definition !== "object" || definition === null) { - return { - k: 1, - p: path, - s: copyWithoutCache(parse$1(definition)) - }; - } - let item = definition[itemSymbol]; - if (item !== undefined) { - let ritemSchema = copyWithoutCache(getOutputSchema(item.schema)); - ((delete ritemSchema.serializer)); - let ritem = { - k: 0, - p: path, - s: ritemSchema - }; - item.r = ritem; - ritemsByItemPath[getFullDitemPath(item)] = ritem; - return ritem; - } - if (Array.isArray(definition)) { - let items = []; - for (let idx = 0, idx_finish = definition.length; idx < idx_finish; ++idx) { - let location = idx.toString(); - let inlinedLocation = "\"" + location + "\""; - let ritem$1 = definitionToRitem(definition[idx], path + ("[" + inlinedLocation + "]"), ritemsByItemPath); - let item_schema = ritem$1.s; - let item$1 = { - schema: item_schema, - location: location - }; - items[idx] = item$1; +function definitionToShapedSchema(definition) { + let s = copySchema(traverseDefinition(definition, toEmbededItem)); + s.serializer = shapedSerializer; + return s; +} + +function shapedParser(input) { + let flattened = input.e.flattened; + if (flattened !== undefined) { + let flattenedVals = []; + for (let idx = 0, idx_finish = flattened.length; idx < idx_finish; ++idx) { + let flattenedSchema = flattened[idx]; + let flattenedInput = scope(input); + flattenedInput.e = flattenedSchema; + flattenedInput.io = false; + flattenedInput.ii = false; + let flattenedVal = parse$1(flattenedInput); + flattenedVals.push(flattenedVal); + input.cp = input.cp + merge(flattenedVal, undefined); } - let mut = new Schema("array"); - return { - k: 2, - p: path, - s: (mut.items = items, mut.additionalItems = "strict", mut.serializer = neverBuilder, mut) - }; - } - let fieldNames = Object.keys(definition); - let properties = {}; - let items$1 = []; - for (let idx$1 = 0, idx_finish$1 = fieldNames.length; idx$1 < idx_finish$1; ++idx$1) { - let location$1 = fieldNames[idx$1]; - let inlinedLocation$1 = fromString(location$1); - let ritem$2 = definitionToRitem(definition[location$1], path + ("[" + inlinedLocation$1 + "]"), ritemsByItemPath); - let item_schema$1 = ritem$2.s; - let item$2 = { - schema: item_schema$1, - location: location$1 - }; - items$1[idx$1] = item$2; - properties[location$1] = item_schema$1; + input.fv = flattenedVals; } - let mut$1 = new Schema("object"); - return { - k: 2, - p: path, - s: (mut$1.items = items$1, mut$1.properties = properties, mut$1.additionalItems = globalConfig.a, mut$1.serializer = neverBuilder, mut$1) - }; -} - -function definitionToTarget(definition, to, flattened) { - let ritemsByItemPath = {}; - let ritem = definitionToRitem(definition, "", ritemsByItemPath); - let mut = ritem.s; - ((delete mut.refiner)); - ((delete mut.compiler)); - mut.serializer = (b, input, selfSchema, path) => { - let getRitemInput = ritem => { - let ritemPath = ritem.p; - if (ritemPath === "") { - return input; - } - let _input = input; - let _locations = toArray(ritemPath); - while (true) { - let locations = _locations; - let input$1 = _input; - if (locations.length === 0) { - return input$1; - } - let location = locations[0]; - _locations = locations.slice(1); - _input = get(b, input$1, location); - continue; - }; - }; - let schemaToOutput = (schema, originalPath) => { - let outputSchema = getOutputSchema(schema); - if (constField in outputSchema) { - return constVal(b, outputSchema); - } - if (constField in schema) { - return parse(b, schema, constVal(b, schema), path); - } - let tag = outputSchema.type; - let additionalItems = outputSchema.additionalItems; - let items = outputSchema.items; - if (items !== undefined && typeof additionalItems === "string") { - let isArray = flags[tag] & 128; - let objectVal = make(b, isArray); - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let item = items[idx]; - let inlinedLocation = inlineLocation(b, item.location); - let itemPath = originalPath + ("[" + inlinedLocation + "]"); - let ritem = ritemsByItemPath[itemPath]; - let itemInput = ritem !== undefined ? parse(b, item.schema, getRitemInput(ritem), ritem.p) : schemaToOutput(item.schema, itemPath); - add(objectVal, item.location, itemInput); - } - return complete(objectVal, isArray); - } - let tmp = originalPath === "" ? "Schema isn't registered" : "Schema for " + originalPath + " isn't registered"; - return invalidOperation(b, path, tmp); - }; - let getItemOutput = (item, itemPath, shouldReverse) => { - let ritem = item.r; - if (ritem === undefined) { - return schemaToOutput(item.schema, itemPath); - } - let targetSchema = shouldReverse ? reverse(item.schema) : ( - itemPath === "" ? getOutputSchema(item.schema) : item.schema - ); - let itemInput = getRitemInput(ritem); - let path$1 = path + ritem.p; - return parse(b, targetSchema, itemInput, path$1); - }; - if (to !== undefined) { - return getItemOutput(to, "", false); - } - let originalSchema = selfSchema.to; - objectStrictModeCheck(b, input, selfSchema.items, selfSchema, path); - let isArray = originalSchema.type === "array"; - let items = originalSchema.items; - let objectVal = make(b, isArray); - if (flattened !== undefined) { - for (let idx = 0, idx_finish = flattened.length; idx < idx_finish; ++idx) { - merge(objectVal, getItemOutput(flattened[idx], "", true)); - } - } - for (let idx$1 = 0, idx_finish$1 = items.length; idx$1 < idx_finish$1; ++idx$1) { - let item = items[idx$1]; - if (!(item.location in objectVal.properties)) { - let inlinedLocation = inlineLocation(b, item.location); - add(objectVal, item.location, getItemOutput(item, "[" + inlinedLocation + "]", false)); - } - - } - return complete(objectVal, isArray); - }; - return mut; -} - -function advancedBuilder(definition, flattened) { - return (b, input, selfSchema, path) => { - let isFlatten = b.g.o & 64; - let outputs = isFlatten ? input.properties : ({}); - if (!isFlatten) { - let items = selfSchema.items; - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let match = items[idx]; - let location = match.location; - let itemInput = get(b, input, location); - let inlinedLocation = inlineLocation(b, location); - let path$1 = path + ("[" + inlinedLocation + "]"); - outputs[location] = parse(b, match.schema, itemInput, path$1); - } - objectStrictModeCheck(b, input, items, selfSchema, path); - } - if (flattened !== undefined) { - let prevFlag = b.g.o; - b.g.o = prevFlag | 64; - for (let idx$1 = 0, idx_finish$1 = flattened.length; idx$1 < idx_finish$1; ++idx$1) { - let item = flattened[idx$1]; - outputs[item.i] = parse(b, item.schema, input, path); - } - b.g.o = prevFlag; - } - let getItemOutput = item => { - switch (item.k) { - case 0 : - return outputs[item.location]; - case 1 : - return get(b, getItemOutput(item.of), item.location); - case 2 : - return outputs[item.i]; - } - }; - return definitionToOutput(b, definition, getItemOutput, selfSchema.to); - }; + let targetSchema = input.e.to; + let output = getShapedParserOutput(input, targetSchema); + output.t = true; + output.prev = input; + return output; } function shape(schema, definer) { return updateOutput(schema, mut => { - let ditem = { - k: 2, - schema: schema, - p: "", - i: 0 - }; - let definition = definer(proxify(ditem)); - mut.parser = (b, input, selfSchema, param) => { - let getItemOutput = item => { - switch (item.k) { - case 1 : - return get(b, getItemOutput(item.of), item.location); - case 0 : - case 2 : - return input; - } - }; - return definitionToOutput(b, definition, getItemOutput, selfSchema.to); - }; - mut.to = definitionToTarget(definition, ditem, undefined); + let fromProxy = proxifyShapedSchema(mut, immutableEmpty$1, undefined); + let definition = definer(fromProxy); + if (definition === fromProxy) { + return; + } else { + mut.parser = shapedParser; + mut.to = definitionToShapedSchema(definition); + return; + } }); } function object(definer) { let flattened = (void 0); - let items = []; let properties = {}; let flatten = schema => { let match = schema.type; if (match === "object") { - let flattenedItems = schema.items; - for (let idx = 0, idx_finish = flattenedItems.length; idx < idx_finish; ++idx) { - let match$1 = flattenedItems[idx]; - let location = match$1.location; - let flattenedSchema = match$1.schema; - let schema$1 = properties[location]; + let flattenedProperties = schema.properties; + let flattenedKeys = Object.keys(flattenedProperties); + for (let idx = 0, idx_finish = flattenedKeys.length; idx < idx_finish; ++idx) { + let key = flattenedKeys[idx]; + let flattenedSchema = flattenedProperties[key]; + let schema$1 = properties[key]; if (schema$1 !== undefined) { if (schema$1 !== flattenedSchema) { - throw new Error("[Sury] " + ("The field \"" + location + "\" defined twice with incompatible schemas")); + throw new Error("[Sury] " + ("The field \"" + key + "\" defined twice with incompatible schemas")); } } else { - let item = { - k: 0, - schema: flattenedSchema, - location: location - }; - items.push(item); - properties[location] = flattenedSchema; + properties[key] = flattenedSchema; } } let f = (flattened || (flattened = [])); - let item_2 = f.length; - let item$1 = { - k: 2, - schema: schema, - p: "", - i: item_2 - }; - f.push(item$1); - return proxify(item$1); + return proxifyShapedSchema(schema, immutableEmpty$1, f.push(schema) - 1 | 0); } let message = "The '" + toExpression(schema) + "' schema can't be flattened"; throw new Error("[Sury] " + message); @@ -3077,20 +3852,14 @@ function object(definer) { if (fieldName in properties) { throw new Error("[Sury] " + ("The field \"" + fieldName + "\" defined twice with incompatible schemas")); } - let ditem = { - k: 0, - schema: schema, - location: fieldName - }; properties[fieldName] = schema; - items.push(ditem); - return proxify(ditem); + return proxifyShapedSchema(schema, [fieldName], undefined); }; let tag = (tag$1, asValue) => { field(tag$1, definitionToSchema(asValue)); }; let fieldOr = (fieldName, schema, or) => { - let schema$1 = factory$1(schema, undefined); + let schema$1 = factory$2(schema, undefined); return field(fieldName, getWithDefault(schema$1, { TAG: "Value", _0: or @@ -3105,12 +3874,16 @@ function object(definer) { flatten: flatten }; let definition = definer(ctx); - let mut = new Schema("object"); - mut.items = items; + let mut = base(objectTag, false); + mut.required = Object.keys(properties); mut.properties = properties; mut.additionalItems = globalConfig.a; - mut.parser = advancedBuilder(definition, flattened); - mut.to = definitionToTarget(definition, undefined, flattened); + mut.decoder = objectDecoder; + mut.parser = shapedParser; + mut.to = definitionToShapedSchema(definition); + if (flattened !== undefined) { + mut.flattened = flattened; + } return mut; } @@ -3121,13 +3894,8 @@ function tuple(definer) { if (items[idx]) { throw new Error("[Sury] " + ("The item [" + location + "] is defined multiple times")); } - let ditem = { - k: 0, - schema: schema, - location: location - }; - items[idx] = ditem; - return proxify(ditem); + items[idx] = schema; + return proxifyShapedSchema(schema, [idx.toString()], undefined); }; let tag = (idx, asValue) => { item(idx, definitionToSchema(asValue)); @@ -3139,20 +3907,16 @@ function tuple(definer) { let definition = definer(ctx); for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { if (!items[idx]) { - let location = idx.toString(); - let ditem = { - schema: unit, - location: location - }; - items[idx] = ditem; + items[idx] = unit; } } - let mut = new Schema("array"); + let mut = base(arrayTag, false); mut.items = items; mut.additionalItems = "strict"; - mut.parser = advancedBuilder(definition, undefined); - mut.to = definitionToTarget(definition, undefined, undefined); + mut.decoder = arrayDecoder; + mut.parser = shapedParser; + mut.to = definitionToShapedSchema(definition); return mut; } @@ -3164,130 +3928,207 @@ let ctx = { m: matches }; -function factory$4(definer) { +function factory$3(definer) { return definitionToSchema(definer(ctx)); } -function factory$5(item) { - return factory$1(item, nullAsUnit); -} - let js_schema = definitionToSchema; function $$enum(values) { - return factory(values.map(js_schema)); + return factory$1(values.map(js_schema)); } -function unnestSerializer(b, input, selfSchema, path) { - let schema = selfSchema.additionalItems; - let items = schema.items; - let inputVar = input.v(b); - let iteratorVar = varWithoutAllocation(b.g); - let outputVar = varWithoutAllocation(b.g); - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let itemInput = { - b: bb, - v: _var, - i: inputVar + "[" + iteratorVar + "]", - f: 0, - type: "unknown" - }; - let itemOutput = withPathPrepend(bb, itemInput, path, iteratorVar, (bb, output) => { - let initialArraysCode = ""; - let settingCode = ""; - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let toItem = items[idx]; - initialArraysCode = initialArraysCode + ("new Array(" + inputVar + ".length),"); - settingCode = settingCode + (outputVar + "[" + idx + "][" + iteratorVar + "]=" + get(b, output, toItem.location).i + ";"); - } - b.a(outputVar + "=[" + initialArraysCode + "]"); - bb.c = bb.c + settingCode; - }, (b, input, path) => parse(b, schema, input, path)); - let itemCode = allocateScope(bb); - b.c = b.c + ("for(let " + iteratorVar + "=0;" + iteratorVar + "<" + inputVar + ".length;++" + iteratorVar + "){" + itemCode + "}"); - if (itemOutput.f & 2) { - return { - b: b, - v: _notVar, - i: "Promise.all(" + outputVar + ")", - f: 2, - type: "array" - }; +function compactColumnsDecoder(input) { + let selfSchema = input.e; + let isUnknownInput = flags[input.s.type] & 1; + let match = selfSchema.to; + let forwardProps; + if (match !== undefined) { + let match$1 = match.additionalItems; + forwardProps = match$1 !== undefined && match$1 !== "strip" && match$1 !== "strict" ? match$1.properties : undefined; } else { - return { - b: b, - v: _var, - i: outputVar, - f: 0, - type: "array" - }; + forwardProps = undefined; } -} - -function unnest(schema) { - if (schema.type === "object") { - let items = schema.items; - if (items.length === 0) { - throw new Error("[Sury] Invalid empty object for S.unnest schema."); - } - let mut = new Schema("array"); - mut.items = items.map((item, idx) => { - let location = idx.toString(); - return { - schema: factory$2(item.schema), - location: location - }; - }); - mut.additionalItems = "strict"; - mut.parser = (b, input, selfSchema, path) => { - let inputVar = input.v(b); - let iteratorVar = varWithoutAllocation(b.g); - let bb = { - c: "", - l: "", - a: initialAllocate, - f: "", - g: b.g - }; - let itemInput = make(bb, false); + let maybeProperties; + if (forwardProps) { + maybeProperties = forwardProps; + } else { + let match$2 = input.s.additionalItems; + maybeProperties = match$2 !== undefined && match$2 !== "strip" && match$2 !== "strict" ? match$2.properties : undefined; + } + if (maybeProperties !== undefined) { + let keys = Object.keys(maybeProperties); + let keysLen = keys.length; + let outputSchema; + if (forwardProps) { + outputSchema = base(arrayTag, false); + } else { + let s = array(array(unknown)); + s.to = selfSchema.to; + outputSchema = s; + } + if (keysLen === 0) { + let input$1 = isUnknownInput ? refine(input, undefined, [{ + c: inputVar => "Array.isArray(" + inputVar + ")&&" + inputVar + ".length===0", + f: failInvalidType + }], undefined) : input; + let output = next(input$1, "[]", outputSchema, outputSchema); + output.io = true; + return output; + } + if (forwardProps) { + let input$2 = isUnknownInput ? refine(input, undefined, [{ + c: inputVar => { + let check = "Array.isArray(" + inputVar + ")&&" + inputVar + ".length===" + keysLen; + for (let idx = 0; idx < keysLen; ++idx) { + check = check + ("&&Array.isArray(" + inputVar + "[" + idx + "])"); + } + return check; + }, + f: failInvalidType + }], undefined) : input; + let inputVar = input$2.v(); + let iteratorVar = varWithoutAllocation(input$2.g); + let outputVar = varWithoutAllocation(input$2.g); + let innerArray = selfSchema.additionalItems; + let declaredItemSchema = innerArray.additionalItems; + let runtimeItemSchema = isUnknownInput ? unknown : input$2.s.additionalItems.additionalItems; let lengthCode = ""; - for (let idx = 0, idx_finish = items.length; idx < idx_finish; ++idx) { - let item = items[idx]; - add(itemInput, item.location, val(bb, inputVar + "[" + idx + "][" + iteratorVar + "]", unknown)); + let itemBuildCode = ""; + let itemParseCode = ""; + let asyncInlines = ""; + let hasAsync = false; + for (let idx = 0; idx < keysLen; ++idx) { + let key = keys[idx]; + let rawValueCode = inputVar + "[" + idx + "][" + iteratorVar + "]"; + let fieldSchema = maybeProperties[key]; + let itemExpected; + if (declaredItemSchema !== runtimeItemSchema) { + let chained = copySchema(declaredItemSchema); + chained.to = fieldSchema; + itemExpected = chained; + } else { + itemExpected = fieldSchema; + } + let itemInput = scope(input$2); + itemInput.i = rawValueCode; + itemInput.s = runtimeItemSchema; + itemInput.e = itemExpected; + itemInput.v = _notVarBeforeValidation; + itemInput.ii = false; + itemInput.io = false; + let inlinedLocation = inlineLocation(input$2.g, key); + itemInput.path = "[" + inlinedLocation + "]"; + let itemOutput = parse$1(itemInput); + if (itemOutput.f & 1) { + hasAsync = true; + } + itemParseCode = itemParseCode + merge(itemOutput, undefined); lengthCode = lengthCode + (inputVar + "[" + idx + "].length,"); + asyncInlines = asyncInlines + (itemOutput.i + ","); + itemBuildCode = itemBuildCode + (fromString(key) + ":" + itemOutput.i + ","); } - let output = val(b, "new Array(Math.max(" + lengthCode + "))", selfSchema.to); - let outputVar = output.v(b); - let itemOutput = withPathPrepend(bb, complete(itemInput, false), path, iteratorVar, (bb, itemOutput) => { - bb.c = bb.c + addKey(bb, output, iteratorVar, itemOutput) + ";"; - }, (b, input, path) => parse(b, schema, input, path)); - let itemCode = allocateScope(bb); - b.c = b.c + ("for(let " + iteratorVar + "=0;" + iteratorVar + "<" + outputVar + ".length;++" + iteratorVar + "){" + itemCode + "}"); - if (itemOutput.f & 2) { - return asyncVal(output.b, "Promise.all(" + output.i + ")"); + input$2.a(outputVar + "=new Array(Math.max(" + lengthCode + "))"); + let output$1 = next(input$2, outputVar, outputSchema, outputSchema); + output$1.v = _var; + output$1.io = true; + let rowAssign; + if (hasAsync) { + let rowResultVar = varWithoutAllocation(input$2.g); + let asyncBuildCode = ""; + for (let idx$1 = 0; idx$1 < keysLen; ++idx$1) { + let key$1 = keys[idx$1]; + asyncBuildCode = asyncBuildCode + (fromString(key$1) + ":" + rowResultVar + "[" + idx$1 + "],"); + } + rowAssign = outputVar + "[" + iteratorVar + "]=Promise.all([" + asyncInlines + "]).then(" + rowResultVar + "=>({" + asyncBuildCode + "}));"; } else { - return output; + rowAssign = outputVar + "[" + iteratorVar + "]={" + itemBuildCode + "};"; } - }; - let to = new Schema("array"); - to.items = immutableEmpty$1; - to.additionalItems = schema; - to.serializer = unnestSerializer; - mut.unnest = true; - mut.to = to; - return mut; + let rowBody = itemParseCode + rowAssign; + let wrappedBody; + if (itemParseCode === "") { + wrappedBody = rowBody; + } else { + let errorVar = varWithoutAllocation(input$2.g); + wrappedBody = "try{" + rowBody + "}catch(" + errorVar + "){" + errorVar + ".path='[\"'+" + iteratorVar + "+'\"]'+" + errorVar + ".path;throw " + errorVar + "}"; + } + output$1.cp = output$1.cp + ("for(let " + iteratorVar + "=0;" + iteratorVar + "<" + outputVar + ".length;++" + iteratorVar + "){" + wrappedBody + "}"); + if (hasAsync) { + return asyncVal(output$1, "Promise.all(" + outputVar + ")"); + } else { + return output$1; + } + } + let inputVar$1 = input.v(); + let iteratorVar$1 = varWithoutAllocation(input.g); + let outputVar$1 = varWithoutAllocation(input.g); + let innerArray$1 = selfSchema.additionalItems; + let declaredItemSchema$1 = innerArray$1.additionalItems; + let needsPerFieldTransform = declaredItemSchema$1 !== unknown; + let initialArraysCode = ""; + let settingCode = ""; + let perFieldCode = ""; + for (let idx$2 = 0; idx$2 < keysLen; ++idx$2) { + let key$2 = keys[idx$2]; + initialArraysCode = initialArraysCode + ("new Array(" + inputVar$1 + ".length),"); + if (needsPerFieldTransform) { + let fieldSchema$1 = maybeProperties[key$2]; + let rawValueCode$1 = inputVar$1 + "[" + iteratorVar$1 + "][" + fromString(key$2) + "]"; + let itemInput$1 = scope(input); + itemInput$1.i = rawValueCode$1; + itemInput$1.s = fieldSchema$1; + itemInput$1.e = declaredItemSchema$1; + itemInput$1.v = _notVarBeforeValidation; + itemInput$1.ii = false; + itemInput$1.io = false; + let inlinedLocation$1 = inlineLocation(input.g, key$2); + itemInput$1.path = "[" + inlinedLocation$1 + "]"; + let itemOutput$1 = parse$1(itemInput$1); + perFieldCode = perFieldCode + merge(itemOutput$1, undefined); + settingCode = settingCode + (outputVar$1 + "[" + idx$2 + "][" + iteratorVar$1 + "]=" + itemOutput$1.i + ";"); + } else { + settingCode = settingCode + (outputVar$1 + "[" + idx$2 + "][" + iteratorVar$1 + "]=" + inputVar$1 + "[" + iteratorVar$1 + "][" + fromString(key$2) + "];"); + } + } + input.a(outputVar$1 + "=[" + initialArraysCode + "]"); + let output$2 = next(input, outputVar$1, outputSchema, outputSchema); + output$2.v = _var; + output$2.io = true; + let loopBody = perFieldCode + settingCode; + let wrappedBody$1; + if (needsPerFieldTransform && perFieldCode !== "") { + let errorVar$1 = varWithoutAllocation(input.g); + wrappedBody$1 = "try{" + loopBody + "}catch(" + errorVar$1 + "){" + errorVar$1 + ".path='[\"'+" + iteratorVar$1 + "+'\"]'+" + errorVar$1 + ".path;throw " + errorVar$1 + "}"; + } else { + wrappedBody$1 = loopBody; + } + output$2.cp = output$2.cp + ("for(let " + iteratorVar$1 + "=0;" + iteratorVar$1 + "<" + inputVar$1 + ".length;++" + iteratorVar$1 + "){" + wrappedBody$1 + "}"); + return output$2; } - throw new Error("[Sury] S.unnest supports only object schemas."); + throw new Error("[Sury] S.compactColumns supports only object schemas. Use S.compactColumns(S.unknown)->S.to(S.array(objectSchema))."); +} + +function compactColumns(inputSchema) { + let innerArray = array(inputSchema); + let mut = array(innerArray); + mut.format = "compactColumns"; + mut.decoder = compactColumnsDecoder; + return mut; +} + +function nullAsOption(item) { + return factory$2(item, nullAsUnit); +} + +function $$null(item) { + return factory$1([ + item, + nullLiteral + ]); } function option(item) { - return factory$1(item, unit); + return factory$2(item, unit); } function tuple1(v0) { @@ -3309,168 +4150,140 @@ function tuple3(v0, v1, v2) { ]); } +function assertNumber(fnName, n) { + if (!(typeof n !== "number" || (Number.isNaN(n)))) { + return; + } + throw new SuryError({ + code: "invalid_operation", + path: "", + reason: "[S." + fnName + "] Expected number, received " + stringify(n) + }); +} + function intMin(schema, minValue, maybeMessage) { + assertNumber("min", minValue); let message = maybeMessage !== undefined ? maybeMessage : "Number must be greater than or equal to " + minValue; - return addRefinement(schema, metadataId$2, { - kind: { - TAG: "Min", - value: minValue - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + "<" + embed(b, minValue) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.minimum = minValue; + getMutErrorMessage(mut)["minimum"] = message; + return param => [{ + c: inputVar => inputVar + ">" + (minValue - 1 | 0), + f: failWithErrorMessage("minimum", message) + }]; + }); } function intMax(schema, maxValue, maybeMessage) { + assertNumber("max", maxValue); let message = maybeMessage !== undefined ? maybeMessage : "Number must be lower than or equal to " + maxValue; - return addRefinement(schema, metadataId$2, { - kind: { - TAG: "Max", - value: maxValue - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ">" + embed(b, maxValue) + "){" + fail(b, message, path) + "}"); -} - -function port(schema, message) { - let mutStandard = internalRefine(schema, (b, inputVar, selfSchema, path) => inputVar + ">0&&" + inputVar + "<65536&&" + inputVar + "%1===0||" + ( - message !== undefined ? fail(b, message, path) : failWithArg(b, path, input => ({ - TAG: "InvalidType", - expected: selfSchema, - received: input - }), inputVar) - ) + ";"); - mutStandard.format = "port"; - reverse(mutStandard).format = "port"; - return mutStandard; + return internalRefine(schema, mut => { + mut.maximum = maxValue; + getMutErrorMessage(mut)["maximum"] = message; + return param => [{ + c: inputVar => inputVar + "<" + (maxValue + 1 | 0), + f: failWithErrorMessage("maximum", message) + }]; + }); } function floatMin(schema, minValue, maybeMessage) { + assertNumber("min", minValue); let message = maybeMessage !== undefined ? maybeMessage : "Number must be greater than or equal to " + minValue; - return addRefinement(schema, metadataId$3, { - kind: { - TAG: "Min", - value: minValue - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + "<" + embed(b, minValue) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.minimum = minValue; + getMutErrorMessage(mut)["minimum"] = message; + return input => [{ + c: inputVar => inputVar + ">=" + embed(input, minValue), + f: failWithErrorMessage("minimum", message) + }]; + }); } function floatMax(schema, maxValue, maybeMessage) { + assertNumber("max", maxValue); let message = maybeMessage !== undefined ? maybeMessage : "Number must be lower than or equal to " + maxValue; - return addRefinement(schema, metadataId$3, { - kind: { - TAG: "Max", - value: maxValue - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ">" + embed(b, maxValue) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.maximum = maxValue; + getMutErrorMessage(mut)["maximum"] = message; + return input => [{ + c: inputVar => inputVar + "<=" + embed(input, maxValue), + f: failWithErrorMessage("maximum", message) + }]; + }); } function arrayMinLength(schema, length, maybeMessage) { + assertNumber("min", length); let message = maybeMessage !== undefined ? maybeMessage : "Array must be " + length + " or more items long"; - return addRefinement(schema, metadataId, { - kind: { - TAG: "Min", - length: length - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ".length<" + embed(b, length) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.minItems = length; + getMutErrorMessage(mut)["minItems"] = message; + return param => [{ + c: inputVar => inputVar + ".length>" + (length - 1 | 0), + f: failWithErrorMessage("minItems", message) + }]; + }); } function arrayMaxLength(schema, length, maybeMessage) { + assertNumber("max", length); let message = maybeMessage !== undefined ? maybeMessage : "Array must be " + length + " or fewer items long"; - return addRefinement(schema, metadataId, { - kind: { - TAG: "Max", - length: length - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ".length>" + embed(b, length) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.maxItems = length; + getMutErrorMessage(mut)["maxItems"] = message; + return param => [{ + c: inputVar => inputVar + ".length<" + (length + 1 | 0), + f: failWithErrorMessage("maxItems", message) + }]; + }); } function stringMinLength(schema, length, maybeMessage) { + assertNumber("min", length); let message = maybeMessage !== undefined ? maybeMessage : "String must be " + length + " or more characters long"; - return addRefinement(schema, metadataId$1, { - kind: { - TAG: "Min", - length: length - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ".length<" + embed(b, length) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.minLength = length; + getMutErrorMessage(mut)["minLength"] = message; + return param => [{ + c: inputVar => inputVar + ".length>" + (length - 1 | 0), + f: failWithErrorMessage("minLength", message) + }]; + }); } function stringMaxLength(schema, length, maybeMessage) { + assertNumber("max", length); let message = maybeMessage !== undefined ? maybeMessage : "String must be " + length + " or fewer characters long"; - return addRefinement(schema, metadataId$1, { - kind: { - TAG: "Max", - length: length - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ".length>" + embed(b, length) + "){" + fail(b, message, path) + "}"); -} - -function email(schema, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid email address"; - return addRefinement(schema, metadataId$1, { - kind: "Email", - message: message - }, (b, inputVar, param, path) => "if(!" + embed(b, emailRegex) + ".test(" + inputVar + ")){" + fail(b, message, path) + "}"); -} - -function uuid(schema, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid UUID"; - return addRefinement(schema, metadataId$1, { - kind: "Uuid", - message: message - }, (b, inputVar, param, path) => "if(!" + embed(b, uuidRegex) + ".test(" + inputVar + ")){" + fail(b, message, path) + "}"); -} - -function cuid(schema, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid CUID"; - return addRefinement(schema, metadataId$1, { - kind: "Cuid", - message: message - }, (b, inputVar, param, path) => "if(!" + embed(b, cuidRegex) + ".test(" + inputVar + ")){" + fail(b, message, path) + "}"); -} - -function url(schema, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid url"; - return addRefinement(schema, metadataId$1, { - kind: "Url", - message: message - }, (b, inputVar, param, path) => "try{new URL(" + inputVar + ")}catch(_){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.maxLength = length; + getMutErrorMessage(mut)["maxLength"] = message; + return param => [{ + c: inputVar => inputVar + ".length<" + (length + 1 | 0), + f: failWithErrorMessage("maxLength", message) + }]; + }); } function pattern(schema, re, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid"; - return addRefinement(schema, metadataId$1, { - kind: { - TAG: "Pattern", - re: re - }, - message: message - }, (b, inputVar, param, path) => ( - re.global ? embed(b, re) + ".lastIndex=0;" : "" - ) + ("if(!" + embed(b, re) + ".test(" + inputVar + ")){" + fail(b, message, path) + "}")); -} - -function datetime(schema, messageOpt) { - let message = messageOpt !== undefined ? messageOpt : "Invalid datetime string! Expected UTC"; - let refinement = { - kind: "Datetime", - message: message - }; - let refinements = schema[metadataId$1]; - return transform(set$1(schema, metadataId$1, refinements !== undefined ? refinements.concat(refinement) : [refinement]), s => ({ - p: string => { - if (!datetimeRe.test(string)) { - s.fail(message, undefined); - } - return new Date(string); - }, - s: date => date.toISOString() - })); + let message = messageOpt !== undefined ? messageOpt : "Invalid pattern"; + return internalRefine(schema, mut => { + mut.pattern = re; + getMutErrorMessage(mut)["pattern"] = message; + return input => { + let embededRe = embed(input, re); + return [{ + c: inputVar => { + if (re.global) { + return "(" + embededRe + ".lastIndex=0," + embededRe + ".test(" + inputVar + "))"; + } else { + return embededRe + ".test(" + inputVar + ")"; + } + }, + f: failWithErrorMessage("pattern", message) + }]; + }; + }); } function trim(schema) { @@ -3482,49 +4295,112 @@ function trim(schema) { } function nullable(schema) { - return factory([ + return factory$1([ schema, unit, - $$null + nullLiteral ]); } function nullableAsOption(schema) { - return factory([ + return factory$1([ schema, unit, nullAsUnit ]); } +let js_parser = ((...args) => getDecoder(unknown, ...args)); + +let js_asyncParser = ((...args) => getDecoder(unknown, ...args, 1)); + +let js_asyncDecoder = ((...args) => getDecoder(...args, 1)); + +let js_encoder = ((...args) => getDecoder(...args.map(reverse))); + +let js_asyncEncoder = ((...args) => getDecoder(...args.map(reverse), 1)); + +function js_assert(schema, data) { + return getDecoder(unknown, schema, assertResult)(data); +} + function js_union(values) { - return factory(values.map(definitionToSchema)); + return factory$1(values.map(definitionToSchema)); } -function js_transform(schema, maybeParser, maybeSerializer) { - return transform(schema, s => ({ - p: maybeParser !== undefined ? v => maybeParser(v, s) : undefined, - s: maybeSerializer !== undefined ? v => maybeSerializer(v, s) : undefined - })); +function customBuilder(fn) { + return input => { + let target = input.e.to; + let outputVar = varWithoutAllocation(input.g); + input.a(outputVar); + let output = next(input, outputVar, target, target); + output.v = _var; + output.cp = "try{" + output.i + "=" + embed(input, fn) + "(" + input.i + ")}catch(x){" + failWithArg(output, e => makeInvalidConversionDetails(input, target, e), "x") + "}"; + return output; + }; +} + +function js_to(schema, target, maybeDecoder, maybeEncoder) { + return updateOutput(schema, mut => { + if (maybeEncoder !== undefined) { + let targetMut = copySchema(target); + targetMut.serializer = customBuilder(maybeEncoder); + mut.to = targetMut; + } else { + mut.to = target; + } + if (maybeDecoder !== undefined) { + mut.parser = customBuilder(maybeDecoder); + return; + } + + }); } -function js_refine(schema, refiner) { - return refine(schema, s => (v => refiner(v, s))); +function js_refine(schema, refineCheck, refineOptions) { + let message; + if (refineOptions !== undefined) { + let e = Primitive_option.valFromOption(refineOptions).error; + message = e !== undefined ? Primitive_option.valFromOption(e) : "Refinement failed"; + } else { + message = "Refinement failed"; + } + let extraPath; + if (refineOptions !== undefined) { + let p = Primitive_option.valFromOption(refineOptions).path; + extraPath = p !== undefined ? fromArray(Primitive_option.valFromOption(p)) : ""; + } else { + extraPath = ""; + } + return internalRefine(schema, param => (input => { + let embeddedCheck = embed(input, refineCheck); + return [{ + c: inputVar => embeddedCheck + "(" + inputVar + ")", + f: input => { + let path = extraPath === "" ? input.path : input.path + extraPath; + return _value => ({ + code: "custom", + path: path, + reason: message + }); + } + }]; + })); } function noop(a) { return a; } -function js_asyncParserRefine(schema, refine) { - return transform(schema, s => ({ - a: v => refine(v, s).then(() => v), +function js_asyncDecoderAssert(schema, assertFn) { + return transform(schema, param => ({ + a: v => assertFn(v).then(() => v), s: noop })); } function js_optional(schema, maybeOr) { - let schema$1 = factory([ + let schema$1 = factory$1([ schema, unit ]); @@ -3546,7 +4422,7 @@ function js_optional(schema, maybeOr) { } function js_nullable(schema, maybeOr) { - let schema$1 = factory([ + let schema$1 = factory$1([ schema, nullAsUnit ]); @@ -3572,35 +4448,18 @@ function js_merge(s1, s2) { if (s1.type === "object" && s2.type === "object") { let additionalItems1 = s1.additionalItems; if (typeof additionalItems1 === "string" && typeof s2.additionalItems === "string" && !s1.to && !s2.to) { - let items2 = s2.items; - let items1 = s1.items; - let properties = {}; - let locations = []; - let items = []; - for (let idx = 0, idx_finish = items1.length; idx < idx_finish; ++idx) { - let item = items1[idx]; - locations.push(item.location); - properties[item.location] = item.schema; - } - for (let idx$1 = 0, idx_finish$1 = items2.length; idx$1 < idx_finish$1; ++idx$1) { - let item$1 = items2[idx$1]; - if (!(item$1.location in properties)) { - locations.push(item$1.location); - } - properties[item$1.location] = item$1.schema; + let properties2 = s2.properties; + let properties = copy(s1.properties); + let keys2 = Object.keys(properties2); + for (let idx = 0, idx_finish = keys2.length; idx < idx_finish; ++idx) { + let key = keys2[idx]; + properties[key] = properties2[key]; } - for (let idx$2 = 0, idx_finish$2 = locations.length; idx$2 < idx_finish$2; ++idx$2) { - let location = locations[idx$2]; - items.push({ - schema: properties[location], - location: location - }); - } - let mut = new Schema("object"); - mut.items = items; + let mut = base(objectTag, false); + mut.required = Object.keys(properties); mut.properties = properties; mut.additionalItems = additionalItems1; - mut.compiler = schemaCompiler; + mut.decoder = objectDecoder; s = mut; } else { s = undefined; @@ -3617,216 +4476,13 @@ function js_merge(s1, s2) { function global(override) { let defaultAdditionalItems = override.defaultAdditionalItems; globalConfig.a = defaultAdditionalItems !== undefined ? defaultAdditionalItems : "strip"; - let prevDisableNanNumberCheck = globalConfig.n; - let disableNanNumberValidation = override.disableNanNumberValidation; - globalConfig.n = disableNanNumberValidation !== undefined ? disableNanNumberValidation : false; - if (prevDisableNanNumberCheck !== globalConfig.n) { - return resetCacheInPlace(float); - } - + let match = override.disableNanNumberValidation; + globalConfig.f = match !== undefined && match ? 2 : 0; } let jsonSchemaMetadataId = "m:JSONSchema"; -function internalToJSONSchema(schema, defs) { - let jsonSchema = {}; - switch (schema.type) { - case "never" : - jsonSchema.not = {}; - break; - case "unknown" : - break; - case "string" : - let $$const = schema.const; - jsonSchema.type = "string"; - refinements$1(schema).forEach(refinement => { - let match = refinement.kind; - if (typeof match !== "object") { - switch (match) { - case "Email" : - jsonSchema.format = "email"; - return; - case "Uuid" : - jsonSchema.format = "uuid"; - return; - case "Cuid" : - return; - case "Url" : - jsonSchema.format = "uri"; - return; - case "Datetime" : - jsonSchema.format = "date-time"; - return; - } - } else { - switch (match.TAG) { - case "Min" : - jsonSchema.minLength = match.length; - return; - case "Max" : - jsonSchema.maxLength = match.length; - return; - case "Length" : - let length = match.length; - jsonSchema.minLength = length; - jsonSchema.maxLength = length; - return; - case "Pattern" : - jsonSchema.pattern = String(match.re); - return; - } - } - }); - if ($$const !== undefined) { - jsonSchema.const = $$const; - } - break; - case "number" : - let format = schema.format; - let $$const$1 = schema.const; - if (format !== undefined) { - if (format === "int32") { - jsonSchema.type = "integer"; - refinements$2(schema).forEach(refinement => { - let match = refinement.kind; - if (match.TAG === "Min") { - jsonSchema.minimum = match.value; - } else { - jsonSchema.maximum = match.value; - } - }); - } else { - jsonSchema.type = "integer"; - jsonSchema.maximum = 65535; - jsonSchema.minimum = 0; - } - } else { - jsonSchema.type = "number"; - refinements$3(schema).forEach(refinement => { - let match = refinement.kind; - if (match.TAG === "Min") { - jsonSchema.minimum = match.value; - } else { - jsonSchema.maximum = match.value; - } - }); - } - if ($$const$1 !== undefined) { - jsonSchema.const = $$const$1; - } - break; - case "boolean" : - let $$const$2 = schema.const; - jsonSchema.type = "boolean"; - if ($$const$2 !== undefined) { - jsonSchema.const = $$const$2; - } - break; - case "null" : - jsonSchema.type = "null"; - break; - case "array" : - let additionalItems = schema.additionalItems; - let exit = 0; - if (additionalItems === "strip" || additionalItems === "strict") { - exit = 1; - } else { - jsonSchema.items = internalToJSONSchema(additionalItems, defs); - jsonSchema.type = "array"; - refinements(schema).forEach(refinement => { - let match = refinement.kind; - switch (match.TAG) { - case "Min" : - jsonSchema.minItems = match.length; - return; - case "Max" : - jsonSchema.maxItems = match.length; - return; - case "Length" : - let length = match.length; - jsonSchema.maxItems = length; - jsonSchema.minItems = length; - return; - } - }); - } - if (exit === 1) { - let items = schema.items.map(item => (internalToJSONSchema(item.schema, defs))); - let itemsNumber = items.length; - jsonSchema.items = Primitive_option.some(items); - jsonSchema.type = "array"; - jsonSchema.minItems = itemsNumber; - jsonSchema.maxItems = itemsNumber; - } - break; - case "object" : - let additionalItems$1 = schema.additionalItems; - let exit$1 = 0; - if (additionalItems$1 === "strip" || additionalItems$1 === "strict") { - exit$1 = 1; - } else { - jsonSchema.type = "object"; - jsonSchema.additionalProperties = internalToJSONSchema(additionalItems$1, defs); - } - if (exit$1 === 1) { - let properties = {}; - let required = []; - schema.items.forEach(item => { - let fieldSchema = internalToJSONSchema(item.schema, defs); - if (!isOptional(item.schema)) { - required.push(item.location); - } - properties[item.location] = fieldSchema; - }); - jsonSchema.type = "object"; - jsonSchema.properties = properties; - let tmp; - tmp = additionalItems$1 === "strip" || additionalItems$1 === "strict" ? additionalItems$1 === "strip" : true; - jsonSchema.additionalProperties = tmp; - if (required.length !== 0) { - jsonSchema.required = required; - } - - } - break; - case "union" : - let literals = []; - let items$1 = []; - schema.anyOf.forEach(childSchema => { - if (childSchema.type === "undefined") { - return; - } - items$1.push(internalToJSONSchema(childSchema, defs)); - if (constField in childSchema) { - literals.push(childSchema.const); - return; - } - - }); - let itemsNumber$1 = items$1.length; - let $$default = schema.default; - if ($$default !== undefined) { - jsonSchema.default = Primitive_option.valFromOption($$default); - } - if (itemsNumber$1 === 1) { - Object.assign(jsonSchema, items$1[0]); - } else if (literals.length === itemsNumber$1) { - jsonSchema.enum = literals; - } else { - jsonSchema.anyOf = items$1; - } - break; - case "ref" : - let ref = schema.$ref; - if (ref === defsPath + jsonName) { - - } else { - jsonSchema.$ref = ref; - } - break; - default: - throw new Error("[Sury] Unexpected schema type"); - } +function applyMetadataOverlay(jsonSchema, schema, defs) { let m = schema.description; if (m !== undefined) { jsonSchema.description = m; @@ -3850,19 +4506,229 @@ function internalToJSONSchema(schema, defs) { let metadataRawSchema = schema[jsonSchemaMetadataId]; if (metadataRawSchema !== undefined) { Object.assign(jsonSchema, metadataRawSchema); + return; + } + +} + +function encodeToJsonSchema(schema, path, defs, parent) { + let reversed = reverse(schema); + let input = operationArg(unknown, reversed, 0, 0); + try { + let output = parse$1(input); + return internalToJSONSchema(output.s, path, defs, parent); + } catch (exn) { + getOrRethrow(exn); + return; + } +} + +function internalToJSONSchema(schema, path, defs, parent) { + let hasUserTo = schema.to && !(flags[schema.type] & 448); + let encoded = hasUserTo ? encodeToJsonSchema(schema, path, defs, parent) : undefined; + if (encoded !== undefined) { + applyMetadataOverlay(encoded, schema, defs); + return encoded; + } else { + let jsonSchema = {}; + switch (schema.type) { + case "never" : + jsonSchema.not = {}; + break; + case "string" : + let format = schema.format; + let $$const = schema.const; + jsonSchema.type = "string"; + if (format !== undefined) { + switch (format) { + case "date-time" : + jsonSchema.format = "date-time"; + break; + case "email" : + jsonSchema.format = "email"; + break; + case "uuid" : + jsonSchema.format = "uuid"; + break; + case "json" : + case "cuid" : + break; + case "url" : + jsonSchema.format = "uri"; + break; + } + } + let v = schema.minLength; + if (v !== undefined) { + jsonSchema.minLength = v; + } + let v$1 = schema.maxLength; + if (v$1 !== undefined) { + jsonSchema.maxLength = v$1; + } + let re = schema.pattern; + if (re !== undefined) { + jsonSchema.pattern = re.source; + } + if ($$const !== undefined) { + jsonSchema.const = $$const; + } + break; + case "number" : + let format$1 = schema.format; + let $$const$1 = schema.const; + if (format$1 !== undefined) { + if (format$1 === "int32") { + jsonSchema.type = "integer"; + jsonSchema.minimum = -2147483648; + jsonSchema.maximum = 2147483647; + } else { + jsonSchema.type = "integer"; + jsonSchema.minimum = 0; + jsonSchema.maximum = 65535; + } + } else { + jsonSchema.type = "number"; + } + let v$2 = schema.minimum; + if (v$2 !== undefined) { + jsonSchema.minimum = v$2; + } + let v$3 = schema.maximum; + if (v$3 !== undefined) { + jsonSchema.maximum = v$3; + } + if ($$const$1 !== undefined) { + jsonSchema.const = $$const$1; + } + break; + case "boolean" : + let $$const$2 = schema.const; + jsonSchema.type = "boolean"; + if ($$const$2 !== undefined) { + jsonSchema.const = $$const$2; + } + break; + case "null" : + jsonSchema.type = "null"; + break; + case "array" : + let additionalItems = schema.additionalItems; + let exit = 0; + if (additionalItems === "strip" || additionalItems === "strict") { + exit = 1; + } else { + jsonSchema.items = internalToJSONSchema(additionalItems, path + "[]", defs, schema); + jsonSchema.type = "array"; + let v$4 = schema.minItems; + if (v$4 !== undefined) { + jsonSchema.minItems = v$4; + } + let v$5 = schema.maxItems; + if (v$5 !== undefined) { + jsonSchema.maxItems = v$5; + } + + } + if (exit === 1) { + let items = schema.items.map((itemSchema, idx) => { + let location = idx.toString(); + return internalToJSONSchema(itemSchema, path + ("[" + fromString(location) + "]"), defs, schema); + }); + let itemsNumber = items.length; + jsonSchema.items = Primitive_option.some(items); + jsonSchema.type = "array"; + jsonSchema.minItems = itemsNumber; + jsonSchema.maxItems = itemsNumber; + } + break; + case "object" : + let additionalItems$1 = schema.additionalItems; + let properties = schema.properties; + let exit$1 = 0; + if (additionalItems$1 === "strip" || additionalItems$1 === "strict") { + exit$1 = 1; + } else { + jsonSchema.type = "object"; + let childJsonSchema = internalToJSONSchema(additionalItems$1, path + "[]", defs, schema); + jsonSchema.additionalProperties = Object.keys(childJsonSchema).length === 0 ? true : childJsonSchema; + } + if (exit$1 === 1) { + let required = []; + let keys = Object.keys(properties); + let jsonProperties = {}; + for (let idx = 0, idx_finish = keys.length; idx < idx_finish; ++idx) { + let key = keys[idx]; + let itemSchema = properties[key]; + let fieldSchema = internalToJSONSchema(itemSchema, path + ("[" + fromString(key) + "]"), defs, schema); + if (!isOptional(itemSchema)) { + required.push(key); + } + jsonProperties[key] = fieldSchema; + } + jsonSchema.type = "object"; + jsonSchema.properties = jsonProperties; + if (additionalItems$1 === "strict") { + jsonSchema.additionalProperties = false; + } + if (required.length !== 0) { + jsonSchema.required = required; + } + + } + break; + case "union" : + let literals = []; + let items$1 = []; + schema.anyOf.forEach(childSchema => { + if (childSchema.type === "undefined" && parent.type === objectTag) { + return; + } + items$1.push(internalToJSONSchema(childSchema, path, defs, schema)); + if (constField in childSchema) { + literals.push(childSchema.const); + return; + } + + }); + let itemsNumber$1 = items$1.length; + let $$default = schema.default; + if ($$default !== undefined) { + jsonSchema.default = Primitive_option.valFromOption($$default); + } + if (itemsNumber$1 === 1) { + Object.assign(jsonSchema, items$1[0]); + } else if (literals.length === itemsNumber$1) { + jsonSchema.enum = literals; + } else { + jsonSchema.anyOf = items$1; + } + break; + case "ref" : + let ref = schema.$ref; + if (ref === defsPath + jsonName) { + + } else { + jsonSchema.$ref = ref; + } + break; + default: + throw new SuryError(makeInvalidInputDetails(json, flags[parent.type] & 256 ? parent : schema, path, 0, false, undefined)); + } + applyMetadataOverlay(jsonSchema, schema, defs); + return jsonSchema; } - return jsonSchema; } function toJSONSchema(schema) { - jsonableValidation(schema, schema, "", 8); let defs = {}; - let jsonSchema = internalToJSONSchema(schema, defs); + let jsonSchema = internalToJSONSchema(schema, "", defs, schema); ((delete defs.JSON)); let defsKeys = Object.keys(defs); if (defsKeys.length) { defsKeys.forEach(key => { - defs[key] = internalToJSONSchema(defs[key], 0); + let schema = defs[key]; + defs[key] = internalToJSONSchema(schema, "", 0, schema); }); jsonSchema.$defs = defs; } @@ -3871,10 +4737,10 @@ function toJSONSchema(schema) { function extendJSONSchema(schema, jsonSchema) { let existingSchemaExtend = schema[jsonSchemaMetadataId]; - return set$1(schema, jsonSchemaMetadataId, existingSchemaExtend !== undefined ? Object.assign({}, existingSchemaExtend, jsonSchema) : jsonSchema); + return set(schema, jsonSchemaMetadataId, existingSchemaExtend !== undefined ? Object.assign({}, existingSchemaExtend, jsonSchema) : jsonSchema); } -let primitiveToSchema = parse$1; +let primitiveToSchema = parse; function toIntSchema(jsonSchema) { let minimum = jsonSchema.minimum; @@ -3922,7 +4788,7 @@ function fromJSONSchema(jsonSchema) { let exit = 0; let exit$1 = 0; if (jsonSchema.nullable) { - schema = factory$5(fromJSONSchema(Object.assign({}, jsonSchema, { + schema = $$null(fromJSONSchema(Object.assign({}, jsonSchema, { nullable: false }))); } else if (type_ !== undefined) { @@ -3930,43 +4796,40 @@ function fromJSONSchema(jsonSchema) { if (type_$1 === "object") { let properties = jsonSchema.properties; if (properties !== undefined) { - let schema$1 = object(s => { - let obj = {}; - Object.keys(properties).forEach(key => { - let property = properties[key]; - let propertySchema = definitionToSchema$1(property); - let r = jsonSchema.required; - let propertySchema$1; - let exit = 0; - if (r !== undefined && r.includes(key)) { - propertySchema$1 = propertySchema; + let obj = {}; + let schema$1 = definitionToSchema((Object.keys(properties).forEach(key => { + let property = properties[key]; + let propertySchema = definitionToSchema$1(property); + let r = jsonSchema.required; + let propertySchema$1; + let exit = 0; + if (r !== undefined && r.includes(key)) { + propertySchema$1 = propertySchema; + } else { + exit = 1; + } + if (exit === 1) { + let defaultValue = definitionToDefaultValue(property); + if (defaultValue !== undefined) { + let schema = option(propertySchema); + propertySchema$1 = getWithDefault(schema, { + TAG: "Value", + _0: defaultValue + }); } else { - exit = 1; - } - if (exit === 1) { - let defaultValue = definitionToDefaultValue(property); - if (defaultValue !== undefined) { - let schema = option(propertySchema); - propertySchema$1 = getWithDefault(schema, { - TAG: "Value", - _0: defaultValue - }); - } else { - propertySchema$1 = option(propertySchema); - } + propertySchema$1 = option(propertySchema); } - obj[key] = s.f(key, propertySchema$1); - }); - return obj; - }); + } + obj[key] = propertySchema$1; + }), obj)); let additionalProperties = jsonSchema.additionalProperties; schema = additionalProperties === false ? strict(schema$1) : schema$1; } else { let additionalProperties$1 = jsonSchema.additionalProperties; schema = additionalProperties$1 !== undefined ? ( typeof additionalProperties$1 !== "object" ? ( - additionalProperties$1 === false ? strict(object(param => {})) : factory$3(json) - ) : factory$3(fromJSONSchema(additionalProperties$1)) + additionalProperties$1 === false ? strict(object(param => {})) : factory(json) + ) : factory(fromJSONSchema(additionalProperties$1)) ) : definitionToSchema(); } } else if (type_$1 === "array") { @@ -3975,13 +4838,13 @@ function fromJSONSchema(jsonSchema) { if (items !== undefined) { let single = JSONSchema.Arrayable.classify(Primitive_option.valFromOption(items)); if (single.TAG === "Single") { - schema$2 = factory$2(definitionToSchema$1(single._0)); + schema$2 = array(definitionToSchema$1(single._0)); } else { - let array = single._0; - schema$2 = tuple(s => array.map((d, idx) => s.item(idx, definitionToSchema$1(d)))); + let array$1 = single._0; + schema$2 = tuple(s => array$1.map((d, idx) => s.item(idx, definitionToSchema$1(d)))); } } else { - schema$2 = factory$2(json); + schema$2 = array(json); } let min = jsonSchema.minItems; let schema$3 = min !== undefined ? arrayMinLength(schema$2, min, undefined) : schema$2; @@ -4000,109 +4863,97 @@ function fromJSONSchema(jsonSchema) { if (definitions$1 !== undefined) { let len = definitions$1.length; schema = len !== 1 ? ( - len !== 0 ? factory(definitions$1.map(definitionToSchema$1)) : json + len !== 0 ? factory$1(definitions$1.map(definitionToSchema$1)) : json ) : definitionToSchema$1(definitions$1[0]); } else if (definitions !== undefined) { let len$1 = definitions.length; schema = len$1 !== 1 ? ( - len$1 !== 0 ? refine(json, s => (data => { - definitions.forEach(d => { - try { - return assertOrThrow(data, definitionToSchema$1(d)); - } catch (exn) { - return s.fail("Should pass for all schemas of the allOf property.", undefined); - } - }); - })) : json + len$1 !== 0 ? refine$1(json, data => definitions.every(d => { + try { + assertOrThrow(data, definitionToSchema$1(d)); + return true; + } catch (exn) { + return false; + } + }), "Should pass for all schemas of the allOf property.", undefined) : json ) : definitionToSchema$1(definitions[0]); } else { let definitions$2 = jsonSchema.oneOf; if (definitions$2 !== undefined) { let len$2 = definitions$2.length; schema = len$2 !== 1 ? ( - len$2 !== 0 ? refine(json, s => (data => { - let hasOneValidRef = { - contents: false + len$2 !== 0 ? refine$1(json, data => { + let validCount = { + contents: 0 }; definitions$2.forEach(d => { - let passed; try { assertOrThrow(data, definitionToSchema$1(d)); - passed = true; + validCount.contents = validCount.contents + 1 | 0; + return; } catch (exn) { - passed = false; - } - if (passed) { - if (hasOneValidRef.contents) { - s.fail("Should pass single schema according to the oneOf property.", undefined); - } - hasOneValidRef.contents = true; return; } - }); - if (!hasOneValidRef.contents) { - return s.fail("Should pass at least one schema according to the oneOf property.", undefined); - } - - })) : json + return validCount.contents === 1; + }, "Should pass exactly one schema according to the oneOf property.", undefined) : json ) : definitionToSchema$1(definitions$2[0]); } else { let not = jsonSchema.not; if (not !== undefined) { - schema = refine(json, s => (data => { - let passed; + schema = refine$1(json, data => { try { assertOrThrow(data, definitionToSchema$1(not)); - passed = true; + return false; } catch (exn) { - passed = false; - } - if (passed) { - return s.fail("Should NOT be valid against schema in the not property.", undefined); + return true; } - - })); + }, "Should NOT be valid against schema in the not property.", undefined); } else if (primitives !== undefined) { let len$3 = primitives.length; schema = len$3 !== 1 ? ( - len$3 !== 0 ? factory(primitives.map(primitiveToSchema)) : json - ) : parse$1(primitives[0]); + len$3 !== 0 ? factory$1(primitives.map(primitiveToSchema)) : json + ) : parse(primitives[0]); } else { let $$const = jsonSchema.const; if ($$const !== undefined) { - schema = parse$1($$const); + schema = parse($$const); } else if (type_ !== undefined) { let type_$2 = Primitive_option.valFromOption(type_); let exit$2 = 0; let exit$3 = 0; if (Array.isArray(type_$2)) { - schema = factory(type_$2.map(type_ => fromJSONSchema(Object.assign({}, jsonSchema, { + schema = factory$1(type_$2.map(type_ => fromJSONSchema(Object.assign({}, jsonSchema, { type: Primitive_option.some(type_) })))); } else if (type_$2 === "string") { - let p = jsonSchema.pattern; - let schema$4 = p !== undefined ? pattern(string, new RegExp(p), undefined) : string; - let minLength = jsonSchema.minLength; - let schema$5 = minLength !== undefined ? stringMinLength(schema$4, minLength, undefined) : schema$4; - let maxLength = jsonSchema.maxLength; - let schema$6 = maxLength !== undefined ? stringMaxLength(schema$5, maxLength, undefined) : schema$5; + let schema$4; switch (jsonSchema.format) { case "date-time" : - schema = datetime(schema$6, undefined); + enableIsoDateTime(); + schema$4 = isoDateTime; break; case "email" : - schema = email(schema$6, undefined); + enableEmail(); + schema$4 = email; break; case "uri" : - schema = url(schema$6, undefined); + enableUrl(); + schema$4 = url; break; case "uuid" : - schema = uuid(schema$6, undefined); + enableUuid(); + schema$4 = uuid; break; default: - schema = schema$6; + schema$4 = string; } + let p = jsonSchema.pattern; + let schema$5 = p !== undefined ? pattern(schema$4, new RegExp(p), undefined) : schema$4; + let minLength = jsonSchema.minLength; + let schema$6 = minLength !== undefined ? stringMinLength(schema$5, minLength, undefined) : schema$5; + let maxLength = jsonSchema.maxLength; + schema = maxLength !== undefined ? stringMaxLength(schema$6, maxLength, undefined) : schema$6; } else if (type_$2 === "integer" || jsonSchema.format === "int64" && type_$2 === "number") { schema = toIntSchema(jsonSchema); } else { @@ -4150,6 +5001,7 @@ function fromJSONSchema(jsonSchema) { } if (exit === 1) { let if_ = jsonSchema.if; + let exit$4 = 0; if (if_ !== undefined) { let then = jsonSchema.then; if (then !== undefined) { @@ -4158,7 +5010,7 @@ function fromJSONSchema(jsonSchema) { let ifSchema = definitionToSchema$1(if_); let thenSchema = definitionToSchema$1(then); let elseSchema = definitionToSchema$1(else_); - schema = refine(json, param => (data => { + schema = refine$1(json, data => { let passed; try { assertOrThrow(data, ifSchema); @@ -4166,21 +5018,33 @@ function fromJSONSchema(jsonSchema) { } catch (exn) { passed = false; } - if (passed) { - return assertOrThrow(data, thenSchema); - } else { - return assertOrThrow(data, elseSchema); + try { + if (passed) { + assertOrThrow(data, thenSchema); + } else { + assertOrThrow(data, elseSchema); + } + return true; + } catch (exn$1) { + return false; } - })); + }, "Should pass the if/then/else schema validation.", undefined); } else { - schema = json; + exit$4 = 2; } } else { - schema = json; + exit$4 = 2; } } else { + exit$4 = 2; + } + if (exit$4 === 2) { + if (jsonSchema.type !== undefined) { + throw new Error("[Sury] Unknown JSON Schema type: " + jsonSchema.type); + } schema = json; } + } if (jsonSchema.description === undefined && jsonSchema.deprecated === undefined && jsonSchema.examples === undefined && jsonSchema.title === undefined) { return schema; @@ -4232,23 +5096,33 @@ function max(schema, maxValue, maybeMessage) { function length(schema, length$1, maybeMessage) { switch (schema.type) { case "string" : + assertNumber("length", length$1); let message = maybeMessage !== undefined ? maybeMessage : "String must be exactly " + length$1 + " characters long"; - return addRefinement(schema, metadataId$1, { - kind: { - TAG: "Length", - length: length$1 - }, - message: message - }, (b, inputVar, param, path) => "if(" + inputVar + ".length!==" + embed(b, length$1) + "){" + fail(b, message, path) + "}"); + return internalRefine(schema, mut => { + mut.minLength = length$1; + mut.maxLength = length$1; + let em = getMutErrorMessage(mut); + em["minLength"] = message; + em["maxLength"] = message; + return param => [{ + c: inputVar => inputVar + ".length===" + length$1, + f: failWithErrorMessage("minLength", message) + }]; + }); case "array" : + assertNumber("length", length$1); let message$1 = maybeMessage !== undefined ? maybeMessage : "Array must be exactly " + length$1 + " items long"; - return addRefinement(schema, metadataId, { - kind: { - TAG: "Length", - length: length$1 - }, - message: message$1 - }, (b, inputVar, param, path) => "if(" + inputVar + ".length!==" + embed(b, length$1) + "){" + fail(b, message$1, path) + "}"); + return internalRefine(schema, mut => { + mut.minItems = length$1; + mut.maxItems = length$1; + let em = getMutErrorMessage(mut); + em["minItems"] = message$1; + em["maxItems"] = message$1; + return param => [{ + c: inputVar => inputVar + ".length===" + length$1, + f: failWithErrorMessage("minItems", message$1) + }]; + }); default: let message$2 = "S.length is not supported for " + toExpression(schema) + " schema. Coerce the schema to string or array using S.to first."; throw new Error("[Sury] " + message$2); @@ -4266,30 +5140,20 @@ let Path = { let Flag = { none: 0, - typeValidation: 1, - async: 2, - assertOutput: 4, - jsonableOutput: 8, - jsonStringOutput: 16, - reverse: 32, - has: has + async: 1 }; -let literal = js_schema; - -let array = factory$2; +let date = mut; -let dict = factory$3; - -let $$null$1 = factory$5; +let literal = js_schema; -let union = factory; +let dict = factory; -let parseJsonOrThrow = parseOrThrow; +let union = factory$1; let Schema$1 = {}; -let schema = factory$4; +let schema = factory$3; let $$Object = {}; @@ -4298,44 +5162,16 @@ let Option = { getOrWith: getOrWith }; -let String_Refinement = {}; - -let $$String$1 = { - Refinement: String_Refinement, - refinements: refinements$1 -}; - -let Int_Refinement = {}; - -let Int = { - Refinement: Int_Refinement, - refinements: refinements$2 -}; - -let Float_Refinement = {}; - -let Float = { - Refinement: Float_Refinement, - refinements: refinements$3 -}; - -let Array_Refinement = {}; - -let $$Array$1 = { - Refinement: Array_Refinement, - refinements: refinements -}; - let Metadata = { Id: Id, get: get$1, - set: set$1 + set: set }; export { Path, - $$Error, Flag, + Exn, never, unknown, unit, @@ -4351,36 +5187,51 @@ export { jsonString, jsonStringWithSpace, enableJsonString, + uint8Array, + enableUint8Array, + isoDateTime, + enableIsoDateTime, + port, + enablePort, + email, + enableEmail, + uuid, + enableUuid, + cuid, + enableCuid, + url, + enableUrl, + date, literal, array, - unnest, + compactColumns, list, instance, dict, option, - $$null$1 as $$null, + $$null, + nullAsOption, nullable, nullableAsOption, union, $$enum, meta, transform, - refine, + refine$1 as refine, shape, to, - compile, + parser, + asyncParser, + decoder, + asyncDecoder, + decoder1, + asyncDecoder1, parseOrThrow, - parseJsonOrThrow, - parseJsonStringOrThrow, parseAsyncOrThrow, - convertOrThrow, - convertToJsonOrThrow, - convertToJsonStringOrThrow, - convertAsyncOrThrow, - reverseConvertOrThrow, - reverseConvertToJsonOrThrow, - reverseConvertToJsonStringOrThrow, assertOrThrow, + assertAsyncOrThrow, + decodeOrThrow, + decodeAsyncOrThrow, isAsync, recursive, noValidation, @@ -4399,39 +5250,36 @@ export { tuple2, tuple3, Option, - $$String$1 as $$String, - Int, - Float, - $$Array$1 as $$Array, Metadata, reverse, - ErrorClass, + $$Error$1 as $$Error, min, floatMin, max, floatMax, length, - port, - email, - uuid, - cuid, - url, pattern, - datetime, trim, toJSONSchema, fromJSONSchema, extendJSONSchema, global, brand, + js_parser, + js_asyncParser, + getDecoder, + js_asyncDecoder, + js_encoder, + js_asyncEncoder, + js_assert, js_safe, js_safeAsync, js_union, js_optional, js_nullable, - js_asyncParserRefine, + js_asyncDecoderAssert, js_refine, - js_transform, + js_to, js_schema, js_merge, } diff --git a/packages/sury/src/Sury.resi b/packages/sury/src/Sury.resi index b6a2197fd..76e0b5078 100644 --- a/packages/sury/src/Sury.resi +++ b/packages/sury/src/Sury.resi @@ -21,9 +21,16 @@ module Path: { } type numberFormat = | @as("int32") Int32 | @as("port") Port -type stringFormat = | @as("json") JSON +type stringFormat = + | @as("json") JSON + | @as("date-time") DateTime + | @as("email") Email + | @as("uuid") Uuid + | @as("cuid") Cuid + | @as("url") Url +type arrayFormat = | @as("compactColumns") CompactColumns -type format = | ...numberFormat | ...stringFormat +type format = | ...numberFormat | ...stringFormat | ...arrayFormat @unboxed type additionalItemsMode = | @as("strip") Strip | @as("strict") Strict @@ -49,7 +56,7 @@ type tag = @tag("type") type rec t<'value> = private - | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool}) + | @as("never") Never({name?: string, title?: string, description?: string, deprecated?: bool, errorMessage?: schemaErrorMessage}) | @as("unknown") Unknown({ name?: string, @@ -58,6 +65,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) | @as("string") String({ @@ -69,6 +77,10 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: string, + minLength?: int, + maxLength?: int, + pattern?: Js.Re.t, + errorMessage?: schemaErrorMessage, }) | @as("number") Number({ @@ -80,6 +92,9 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: float, + minimum?: float, + maximum?: float, + errorMessage?: schemaErrorMessage, }) | @as("bigint") BigInt({ @@ -90,6 +105,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bigint, + errorMessage?: schemaErrorMessage, }) | @as("boolean") Boolean({ @@ -100,6 +116,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: bool, + errorMessage?: schemaErrorMessage, }) | @as("symbol") Symbol({ @@ -110,6 +127,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.symbol, + errorMessage?: schemaErrorMessage, }) | @as("null") Null({ @@ -118,6 +136,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("undefined") Undefined({ @@ -126,6 +145,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("nan") NaN({ @@ -134,6 +154,7 @@ type rec t<'value> = title?: string, description?: string, deprecated?: bool, + errorMessage?: schemaErrorMessage, }) | @as("function") Function({ @@ -144,6 +165,7 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.function_val, + errorMessage?: schemaErrorMessage, }) | @as("instance") Instance({ @@ -155,31 +177,36 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: Js.Types.obj_val, + errorMessage?: schemaErrorMessage, }) | @as("array") Array({ - items: array, + items: array>, additionalItems: additionalItems, - unnest?: bool, + format?: arrayFormat, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: array, + minItems?: int, + maxItems?: int, + errorMessage?: schemaErrorMessage, }) | @as("object") Object({ - items: array, properties: dict>, additionalItems: additionalItems, + required?: array, name?: string, title?: string, description?: string, deprecated?: bool, examples?: array>, default?: dict, - }) // TODO: Add const for Object and Tuple + errorMessage?: schemaErrorMessage, + }) | @as("union") Union({ anyOf: array>, @@ -190,19 +217,36 @@ type rec t<'value> = deprecated?: bool, examples?: array, default?: unknown, + errorMessage?: schemaErrorMessage, }) - | @as("ref") Ref({@as("$ref") ref: string}) + | @as("ref") Ref({@as("$ref") ref: string, errorMessage?: schemaErrorMessage}) and schema<'a> = t<'a> +and schemaErrorMessage = { + @as("_") + catchAll?: string, + format?: string, + @as("type") + type_?: string, + minimum?: string, + maximum?: string, + minLength?: string, + maxLength?: string, + minItems?: string, + maxItems?: string, + pattern?: string, +} and meta<'value> = { name?: string, title?: string, description?: string, deprecated?: bool, examples?: array<'value>, + errorMessage?: schemaErrorMessage, } and untagged = private { @as("type") tag: tag, + seq: float, @as("$ref") ref?: string, @as("$defs") @@ -216,9 +260,9 @@ and untagged = private { deprecated?: bool, examples?: array, default?: unknown, - unnest?: bool, noValidation?: bool, - items?: array, + items?: array>, + required?: array, properties?: dict>, additionalItems?: additionalItems, anyOf?: array>, @@ -226,10 +270,6 @@ and untagged = private { to?: t, } @unboxed and additionalItems = | ...additionalItemsMode | Schema(t) -and item = { - schema: t, - location: string, -} and has = { string?: bool, number?: bool, @@ -251,38 +291,51 @@ and error = private { message: string, reason: string, path: Path.t, - code: errorCode, - flag: flag, -} -and errorCode = - | OperationFailed(string) - | InvalidOperation({description: string}) - | InvalidType({expected: schema, received: unknown, unionErrors?: array}) - | UnsupportedTransformation({from: schema, to: schema}) - | ExcessField(string) - | UnexpectedAsync - | InvalidJsonSchema(schema) - -type exn += private Error(error) - -type s<'value> = { - schema: t<'value>, - fail: 'a. (string, ~path: Path.t=?) => 'a, } +@tag("code") +and errorDetails = + // When received input doesn't match the expected schema + | @as("invalid_input") + InvalidInput({ + path: Path.t, + reason: string, + expected: schema, + received: schema, + input?: unknown, + unionErrors?: array, + }) + // When an operation fails, because it's impossible or called incorrectly + | @as("invalid_operation") InvalidOperation({path: Path.t, reason: string}) + // When the value decoding between two schemas is not supported + | @as("unsupported_decode") + UnsupportedDecode({ + path: Path.t, + reason: string, + from: schema, + to: schema, + }) + // When a decoder/encoder fails + | @as("invalid_conversion") + InvalidConversion({ + path: Path.t, + reason: string, + from: schema, + to: schema, + cause?: exn, + }) + | @as("unrecognized_keys") UnrecognizedKeys({path: Path.t, reason: string, keys: array}) + | @as("custom") Custom({path: Path.t, reason: string}) module Flag: { - @inline let none: flag - @inline let typeValidation: flag - @inline let async: flag - @inline let assertOutput: flag - @inline let jsonableOutput: flag - @inline let jsonStringOutput: flag - @inline let reverse: flag - + let none: flag + let async: flag external with: (flag, flag) => flag = "%orint" - let has: (flag, flag) => bool } +type exn += private Exn(error) + +type s<'value> = {fail: 'a. (string, ~path: Path.t=?) => 'a} + let never: t let unknown: t let unit: t @@ -301,15 +354,39 @@ let jsonString: t let jsonStringWithSpace: int => t let enableJsonString: unit => unit +let uint8Array: t +let enableUint8Array: unit => unit + +let isoDateTime: t +let enableIsoDateTime: unit => unit + +let port: t +let enablePort: unit => unit + +let email: t +let enableEmail: unit => unit + +let uuid: t +let enableUuid: unit => unit + +let cuid: t +let enableCuid: unit => unit + +let url: t +let enableUrl: unit => unit + +let date: t + let literal: 'value => t<'value> let array: t<'value> => t> -let unnest: t<'value> => t> +let compactColumns: t<'value> => t>> let list: t<'value> => t> let instance: unknown => t let dict: t<'value> => t> let option: t<'value> => t> -let null: t<'value> => t> -let nullable: t<'value> => t> +let null: t<'value> => t> +let nullAsOption: t<'value> => t> +let nullable: t<'value> => t> let nullableAsOption: t<'value> => t> let union: array> => t<'value> let enum: array<'value> => t<'value> @@ -326,51 +403,25 @@ type transformDefinition<'input, 'output> = { } let transform: (t<'input>, s<'output> => transformDefinition<'input, 'output>) => t<'output> -let refine: (t<'value>, s<'value> => 'value => unit) => t<'value> +let refine: (t<'value>, 'value => bool, ~error: string=?, ~path: array=?) => t<'value> let shape: (t<'value>, 'value => 'shape) => t<'shape> let to: (t<'from>, t<'to>) => t<'to> -type rec input<'value, 'computed> = - | @as("Output") Value: input<'value, 'value> - | @as("Input") Unknown: input<'value, unknown> - | Any: input<'value, 'any> - | Json: input<'value, Js.Json.t> - | JsonString: input<'value, string> -type rec output<'value, 'computed> = - | @as("Output") Value: output<'value, 'value> - | @as("Input") Unknown: output<'value, unknown> - | Assert: output<'value, unit> - | Json: output<'value, Js.Json.t> - | JsonString: output<'value, string> -type rec mode<'output, 'computed> = - | Sync: mode<'output, 'output> - | Async: mode<'output, promise<'output>> - -let compile: ( - t<'value>, - ~input: input<'value, 'input>, - ~output: output<'value, 'transformedOutput>, - ~mode: mode<'transformedOutput, 'output>, - ~typeValidation: bool=?, -) => 'input => 'output - -let parseOrThrow: ('any, t<'value>) => 'value -let parseJsonOrThrow: (Js.Json.t, t<'value>) => 'value -let parseJsonStringOrThrow: (string, t<'value>) => 'value -let parseAsyncOrThrow: ('any, t<'value>) => promise<'value> - -let convertOrThrow: ('any, t<'value>) => 'value -let convertToJsonOrThrow: ('any, t<'value>) => Js.Json.t -let convertToJsonStringOrThrow: ('any, t<'value>) => string -let convertAsyncOrThrow: ('any, t<'value>) => promise<'value> +let parser: (~to: t<'value>) => 'any => 'value +let asyncParser: (~to: t<'value>) => 'any => promise<'value> +let decoder: (~from: t<'from>, ~to: t<'to>) => 'from => 'to +let asyncDecoder: (~from: t<'from>, ~to: t<'to>) => 'from => promise<'to> +let decoder1: t<'value> => unknown => 'value +let asyncDecoder1: t<'value> => unknown => promise<'value> -let reverseConvertOrThrow: ('value, t<'value>) => unknown -let reverseConvertToJsonOrThrow: ('value, t<'value>) => Js.Json.t -let reverseConvertToJsonStringOrThrow: ('value, t<'value>, ~space: int=?) => string - -let assertOrThrow: ('any, t<'value>) => unit +let parseOrThrow: ('any, ~to: t<'value>) => 'value +let parseAsyncOrThrow: ('any, ~to: t<'value>) => promise<'value> +let assertOrThrow: ('any, ~to: t<'value>) => unit +let assertAsyncOrThrow: ('any, ~to: t<'value>) => promise +let decodeOrThrow: ('from, ~from: t<'from>, ~to: t<'to>) => 'to +let decodeAsyncOrThrow: ('from, ~from: t<'from>, ~to: t<'to>) => promise<'to> let isAsync: t<'value> => bool @@ -423,71 +474,6 @@ module Option: { let getOrWith: (t>, unit => 'value) => t<'value> } -module String: { - module Refinement: { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - | Email - | Uuid - | Cuid - | Url - | Pattern({re: Js.Re.t}) - | Datetime - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Int: { - module Refinement: { - type kind = - | Min({value: int}) - | Max({value: int}) - - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Float: { - module Refinement: { - type kind = - | Min({value: float}) - | Max({value: float}) - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - -module Array: { - module Refinement: { - type kind = - | Min({length: int}) - | Max({length: int}) - | Length({length: int}) - type t = { - kind: kind, - message: string, - } - } - - let refinements: t<'value> => array -} - module Metadata: { module Id: { type t<'metadata> @@ -501,12 +487,14 @@ module Metadata: { let reverse: t<'value> => t -module ErrorClass: { - type t +module Error: { + type class + + let class: class - let value: t + let make: errorDetails => error - let constructor: (~code: errorCode, ~flag: flag, ~path: Path.t) => error + external classify: error => errorDetails = "%identity" } // ============= @@ -521,13 +509,7 @@ let floatMax: (t, float, ~message: string=?) => t let length: (t<'value>, int, ~message: string=?) => t<'value> -let port: (t, ~message: string=?) => t -let email: (t, ~message: string=?) => t -let uuid: (t, ~message: string=?) => t -let cuid: (t, ~message: string=?) => t -let url: (t, ~message: string=?) => t let pattern: (t, Js.Re.t, ~message: string=?) => t -let datetime: (t, ~message: string=?) => t let trim: t => t let toJSONSchema: t<'value> => JSONSchema.t @@ -550,6 +532,14 @@ type jsResult<'value> let brand: (t<'value>, string) => t<'value> +let js_parser: t => unknown => unknown +let js_asyncParser: t => unknown => unknown +let getDecoder: (~s1: t, ~flag: flag=?) => 'from => 'to +let js_asyncDecoder: t => unknown => unknown +let js_encoder: t => unknown => unknown +let js_asyncEncoder: t => unknown => unknown +let js_assert: (t, unknown) => unit + let js_safe: (unit => 'v) => jsResult<'v> let js_safeAsync: (unit => promise<'v>) => promise> @@ -558,14 +548,15 @@ let js_union: array => t<'value> let js_optional: (t<'v>, option) => t> let js_nullable: (t<'v>, option) => t> -let js_asyncParserRefine: (t<'output>, ('output, s<'output>) => promise) => t<'output> -let js_refine: (t<'output>, ('output, s<'output>) => unit) => t<'output> +let js_asyncDecoderAssert: (t<'output>, 'output => promise) => t<'output> +let js_refine: (t<'output>, 'output => bool, option<{..}>) => t<'output> -let js_transform: ( - t<'output>, - ~parser: ('output, s<'transformed>) => 'transformed=?, - ~serializer: ('transformed, s<'transformed>) => 'output=?, -) => t<'transformed> +let js_to: ( + t<'value>, + t<'target>, + ~decoder: 'value => 'target=?, + ~encoder: 'target => 'value=?, +) => t<'target> let js_schema: unknown => t diff --git a/packages/sury/tests/Example_test.res b/packages/sury/tests/Example_test.res index b1c94c88d..72371b348 100644 --- a/packages/sury/tests/Example_test.res +++ b/packages/sury/tests/Example_test.res @@ -1,5 +1,7 @@ open Ava +S.enableJson() + @dead type rating = | @as("G") GeneralAudiences @@ -37,7 +39,7 @@ let filmSchema = S.object(s => { test("Example", t => { t->Assert.deepEqual( - %raw(`{"Id": 1, "Title": "My first film", "Rating": "R", "Age": 17}`)->S.parseOrThrow( + %raw(`{"Id": 1, "Title": "My first film", "Rating": "R", "Age": 17}`)->S.parseOrThrow(~to= filmSchema, ), { @@ -55,22 +57,27 @@ test("Example", t => { title: "Sad & sed", rating: ParentalStronglyCautioned, deprecatedAgeRestriction: None, - }->S.reverseConvertToJsonOrThrow(filmSchema), + }->S.decodeOrThrow(~from=filmSchema, ~to=S.json), %raw(`{ "Id": 2, "Title": "Sad & sed", "Rating": "PG13", "Tags": ["Loved"], - "Age": undefined, }`), ) + // FIXME: This can be improved, currently we run unknown->json decoder for optional deprecatedAgeRestriction + t->U.assertCompiledCode( + ~schema=filmSchema, + ~op=#ReverseConvertToJson, + `i=>{let v0=i["tags"],v3=i["deprecatedAgeRestriction"];let v5;try{v3===void 0||e[0](v3);}catch(e1){try{try{e[1](v3);}catch(v4){v4.path="[\\"deprecatedAgeRestriction\\"]"+v4.path;throw v4}}catch(e2){e[2](v3,e1,e2)}}v5={"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":i["rating"],};if(v3!==void 0){v5["Age"]=v3}return v5}`, + ) }) test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema=filmSchema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["Id"],v1=i["Title"];if(typeof v0!=="number"||Number.isNaN(v0)){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}let v6=i["Tags"];if(Array.isArray(v6)){let v2=i["Tags"];for(let v3=0;v3-2147483648&&v8%1===0||v8===void 0)){e[7](v8)}return {"id":v0,"title":v1,"tags":v6===void 0?e[5]:v6,"rating":v7,"deprecatedAgeRestriction":v8,}}`, + `i=>{typeof i==="object"&&i||e[7](i);let v0=i["Id"],v1=i["Title"],v2=i["Tags"],v6=i["Rating"],v7=i["Age"];typeof v0==="number"&&!Number.isNaN(v0)||e[0](v0);typeof v1==="string"||e[1](v1);if(Array.isArray(v2)){for(let v3=0;v3=-2147483648&&v7%1===0)||v7===void 0)){e[6](v7)}return {"id":v0,"title":v1,"tags":v2===void 0?e[4]:v2,"rating":v6,"deprecatedAgeRestriction":v7,}}`, ) }) @@ -78,7 +85,7 @@ test("Compiled serialize code snapshot", t => { t->U.assertCompiledCode( ~schema=filmSchema, ~op=#ReverseConvert, - `i=>{let v0=i["tags"];let v5=i["rating"];return {"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":v5,"Age":i["deprecatedAgeRestriction"],}}`, + `i=>{let v0=i["tags"];return {"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":i["rating"],"Age":i["deprecatedAgeRestriction"],}}`, ) }) @@ -92,7 +99,7 @@ test("Custom schema", t => { ->Obj.magic ->Set.forEach( item => { - output->Set.add(S.parseOrThrow(item, itemSchema)) + output->Set.add(S.parseOrThrow(item, ~to=itemSchema)) }, ) output @@ -104,15 +111,15 @@ test("Custom schema", t => { let intSetSchema = mySet(S.int) t->Assert.deepEqual( - S.parseOrThrow(%raw(`new Set([1, 2, 3])`), intSetSchema), + S.parseOrThrow(%raw(`new Set([1, 2, 3])`), ~to=intSetSchema), Set.fromArray([1, 2, 3]), ) t->U.assertThrowsMessage( - () => S.parseOrThrow(%raw(`new Set([1, 2, "3"])`), intSetSchema), - `Failed parsing: Expected int32, received "3"`, + () => S.parseOrThrow(%raw(`new Set([1, 2, "3"])`), ~to=intSetSchema), + `Expected int32, received "3"`, ) t->U.assertThrowsMessage( - () => S.parseOrThrow(%raw(`[1, 2, 3]`), intSetSchema), - `Failed parsing: Expected Set.t, received [1, 2, 3]`, + () => S.parseOrThrow(%raw(`[1, 2, 3]`), ~to=intSetSchema), + `Expected Set.t, received [1, 2, 3]`, ) }) diff --git a/packages/sury/tests/S_Array_length_test.res b/packages/sury/tests/S_Array_length_test.res index b111a6119..c121c824d 100644 --- a/packages/sury/tests/S_Array_length_test.res +++ b/packages/sury/tests/S_Array_length_test.res @@ -3,71 +3,59 @@ open Ava test("Successfully parses valid data", t => { let schema = S.array(S.int)->S.length(1) - t->Assert.deepEqual([1]->S.parseOrThrow(schema), [1]) + t->Assert.deepEqual([1]->S.parseOrThrow(~to=schema), [1]) }) test("Fails to parse invalid data", t => { let schema = S.array(S.int)->S.length(1) - t->U.assertThrows( - () => []->S.parseOrThrow(schema), - { - code: OperationFailed("Array must be exactly 1 items long"), - operation: Parse, - path: S.Path.empty, - }, - ) - t->U.assertThrows( - () => [1, 2, 3, 4]->S.parseOrThrow(schema), - { - code: OperationFailed("Array must be exactly 1 items long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage(() => []->S.parseOrThrow(~to=schema), `Array must be exactly 1 items long`) + t->U.assertThrowsMessage( + () => [1, 2, 3, 4]->S.parseOrThrow(~to=schema), + `Array must be exactly 1 items long`, ) }) test("Successfully serializes valid value", t => { let schema = S.array(S.int)->S.length(1) - t->Assert.deepEqual([1]->S.reverseConvertOrThrow(schema), %raw(`[1]`)) + t->Assert.deepEqual([1]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[1]`)) }) test("Fails to serialize invalid value", t => { let schema = S.array(S.int)->S.length(1) - t->U.assertThrows( - () => []->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Array must be exactly 1 items long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => []->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Array must be exactly 1 items long`, ) - t->U.assertThrows( - () => [1, 2, 3, 4]->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Array must be exactly 1 items long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => [1, 2, 3, 4]->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Array must be exactly 1 items long`, ) }) test("Returns custom error message", t => { let schema = S.array(S.int)->S.length(~message="Custom", 1) - t->U.assertThrows( - () => []->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => []->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.array(S.int)->S.length(1) - t->Assert.deepEqual( - schema->S.Array.refinements, - [{kind: Length({length: 1}), message: "Array must be exactly 1 items long"}], - ) + switch schema { + | Array({minItems, maxItems, errorMessage}) => { + t->Assert.deepEqual(minItems, 1) + t->Assert.deepEqual(maxItems, 1) + t->Assert.deepEqual( + errorMessage, + { + minItems: "Array must be exactly 1 items long", + maxItems: "Array must be exactly 1 items long", + }, + ) + } + | _ => t->Assert.fail("Expected Array schema with minItems and maxItems") + } }) diff --git a/packages/sury/tests/S_Array_max_test.res b/packages/sury/tests/S_Array_max_test.res index e5e34367e..9ad413918 100644 --- a/packages/sury/tests/S_Array_max_test.res +++ b/packages/sury/tests/S_Array_max_test.res @@ -3,57 +3,52 @@ open Ava test("Successfully parses valid data", t => { let schema = S.array(S.int)->S.max(1) - t->Assert.deepEqual([1]->S.parseOrThrow(schema), [1]) - t->Assert.deepEqual([]->S.parseOrThrow(schema), []) + t->Assert.deepEqual([1]->S.parseOrThrow(~to=schema), [1]) + t->Assert.deepEqual([]->S.parseOrThrow(~to=schema), []) }) test("Fails to parse invalid data", t => { let schema = S.array(S.int)->S.max(1) - t->U.assertThrows( - () => [1, 2, 3, 4]->S.parseOrThrow(schema), - { - code: OperationFailed("Array must be 1 or fewer items long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => [1, 2, 3, 4]->S.parseOrThrow(~to=schema), + `Array must be 1 or fewer items long`, ) }) test("Successfully serializes valid value", t => { let schema = S.array(S.int)->S.max(1) - t->Assert.deepEqual([1]->S.reverseConvertOrThrow(schema), %raw(`[1]`)) - t->Assert.deepEqual([]->S.reverseConvertOrThrow(schema), %raw(`[]`)) + t->Assert.deepEqual([1]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[1]`)) + t->Assert.deepEqual([]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[]`)) }) test("Fails to serialize invalid value", t => { let schema = S.array(S.int)->S.max(1) - t->U.assertThrows( - () => [1, 2, 3, 4]->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Array must be 1 or fewer items long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => [1, 2, 3, 4]->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Array must be 1 or fewer items long`, ) }) test("Returns custom error message", t => { let schema = S.array(S.int)->S.max(~message="Custom", 1) - t->U.assertThrows( - () => [1, 2]->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => [1, 2]->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.array(S.int)->S.max(1) - t->Assert.deepEqual( - schema->S.Array.refinements, - [{kind: Max({length: 1}), message: "Array must be 1 or fewer items long"}], - ) + switch schema { + | Array({maxItems, errorMessage}) => { + t->Assert.deepEqual(maxItems, 1) + t->Assert.deepEqual( + errorMessage, + {maxItems: "Array must be 1 or fewer items long"}, + ) + } + | _ => t->Assert.fail("Expected Array schema with maxItems") + } }) diff --git a/packages/sury/tests/S_Array_min_test.res b/packages/sury/tests/S_Array_min_test.res index be24676c2..20a8fa669 100644 --- a/packages/sury/tests/S_Array_min_test.res +++ b/packages/sury/tests/S_Array_min_test.res @@ -3,57 +3,49 @@ open Ava test("Successfully parses valid data", t => { let schema = S.array(S.int)->S.min(1) - t->Assert.deepEqual([1]->S.parseOrThrow(schema), [1]) - t->Assert.deepEqual([1, 2, 3, 4]->S.parseOrThrow(schema), [1, 2, 3, 4]) + t->Assert.deepEqual([1]->S.parseOrThrow(~to=schema), [1]) + t->Assert.deepEqual([1, 2, 3, 4]->S.parseOrThrow(~to=schema), [1, 2, 3, 4]) }) test("Fails to parse invalid data", t => { let schema = S.array(S.int)->S.min(1) - t->U.assertThrows( - () => []->S.parseOrThrow(schema), - { - code: OperationFailed("Array must be 1 or more items long"), - operation: Parse, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => []->S.parseOrThrow(~to=schema), `Array must be 1 or more items long`) }) test("Successfully serializes valid value", t => { let schema = S.array(S.int)->S.min(1) - t->Assert.deepEqual([1]->S.reverseConvertOrThrow(schema), %raw(`[1]`)) - t->Assert.deepEqual([1, 2, 3, 4]->S.reverseConvertOrThrow(schema), %raw(`[1,2,3,4]`)) + t->Assert.deepEqual([1]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[1]`)) + t->Assert.deepEqual([1, 2, 3, 4]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[1,2,3,4]`)) }) test("Fails to serialize invalid value", t => { let schema = S.array(S.int)->S.min(1) - t->U.assertThrows( - () => []->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Array must be 1 or more items long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => []->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Array must be 1 or more items long`, ) }) test("Returns custom error message", t => { let schema = S.array(S.int)->S.min(~message="Custom", 1) - t->U.assertThrows( - () => []->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => []->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.array(S.int)->S.min(1) - t->Assert.deepEqual( - schema->S.Array.refinements, - [{kind: Min({length: 1}), message: "Array must be 1 or more items long"}], - ) + switch schema { + | Array({minItems, errorMessage}) => { + t->Assert.deepEqual(minItems, 1) + t->Assert.deepEqual( + errorMessage, + {minItems: "Array must be 1 or more items long"}, + ) + } + | _ => t->Assert.fail("Expected Array schema with minItems") + } }) diff --git a/packages/sury/tests/S_Error_message_test.res b/packages/sury/tests/S_Error_message_test.res deleted file mode 100644 index 96372cc73..000000000 --- a/packages/sury/tests/S_Error_message_test.res +++ /dev/null @@ -1,250 +0,0 @@ -open Ava - -test("OperationFailed error", t => { - t->Assert.is( - U.error({ - code: OperationFailed("Should be positive"), - operation: Parse, - path: S.Path.empty, - }).message, - "Failed parsing: Should be positive", - ) -}) - -test("Error with Serializing operation", t => { - t->Assert.is( - U.error({ - code: OperationFailed("Should be positive"), - operation: ReverseConvert, - path: S.Path.empty, - }).message, - "Failed converting: Should be positive", - ) -}) - -test("Error with path", t => { - t->Assert.is( - U.error({ - code: OperationFailed("Should be positive"), - operation: Parse, - path: S.Path.fromArray(["0", "foo"]), - }).message, - `Failed parsing at ["0"]["foo"]: Should be positive`, - ) -}) - -test("InvalidOperation error", t => { - t->Assert.is( - U.error({ - code: InvalidOperation({description: "The S.transform serializer is missing"}), - operation: Parse, - path: S.Path.empty, - }).message, - "Failed parsing: The S.transform serializer is missing", - ) -}) - -test("InvalidType error", t => { - t->Assert.is( - U.error({ - code: InvalidType({expected: S.string->S.castToUnknown, received: Obj.magic(true)}), - operation: Parse, - path: S.Path.empty, - }).message, - "Failed parsing: Expected string, received true", - ) -}) - -test("UnexpectedAsync error", t => { - t->Assert.is( - U.error({ - code: UnexpectedAsync, - operation: Parse, - path: S.Path.empty, - }).message, - "Failed parsing: Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead", - ) -}) - -test("InvalidType with literal error", t => { - t->Assert.is( - U.error({ - code: InvalidType({expected: S.literal(false)->S.castToUnknown, received: true->Obj.magic}), - operation: Parse, - path: S.Path.empty, - }).message, - "Failed parsing: Expected false, received true", - ) -}) - -test("ExcessField error", t => { - t->Assert.is( - U.error({ - code: ExcessField("unknownKey"), - operation: Parse, - path: S.Path.empty, - }).message, - `Failed parsing: Unrecognized key "unknownKey"`, - ) -}) - -test("InvalidType error (replacement for InvalidTupleSize)", t => { - t->Assert.is( - U.error({ - code: InvalidType({ - expected: S.tuple2(S.bool, S.int)->S.castToUnknown, - received: (1, 2, "foo")->Obj.magic, - }), - operation: Parse, - path: S.Path.empty, - }).message, - `Failed parsing: Expected [boolean, int32], received [1, 2, "foo"]`, - ) -}) - -test("InvalidType error with union errors", t => { - t->Assert.is( - U.error({ - code: InvalidType({ - expected: S.unknown, - received: "foo"->Obj.magic, - unionErrors: [ - U.error({ - code: InvalidType({ - expected: S.literal("circle")->S.castToUnknown, - received: "oval"->Obj.magic, - }), - operation: Parse, - path: S.Path.fromArray(["kind"]), - }), - U.error({ - code: InvalidType({ - expected: S.literal("square")->S.castToUnknown, - received: "oval"->Obj.magic, - }), - operation: Parse, - path: S.Path.fromArray(["kind"]), - }), - U.error({ - code: InvalidType({ - expected: S.literal("triangle")->S.castToUnknown, - received: "oval"->Obj.magic, - }), - operation: Parse, - path: S.Path.fromArray(["kind"]), - }), - ], - }), - operation: Parse, - path: S.Path.empty, - }).message, - `Failed parsing: Expected unknown, received "foo" -- At ["kind"]: Expected "circle", received "oval" -- At ["kind"]: Expected "square", received "oval" -- At ["kind"]: Expected "triangle", received "oval"`, - ) -}) - -test("InvalidUnion filters similar reasons", t => { - t->Assert.is( - U.error({ - code: InvalidType({ - expected: S.unknown, - received: "foo"->Obj.magic, - unionErrors: [ - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - ], - }), - operation: Parse, - path: S.Path.empty, - }).message, - `Failed parsing: Expected unknown, received "foo" -- Expected boolean, received "Hello world!"`, - ) -}) - -test("Nested InvalidUnion error", t => { - t->Assert.is( - U.error({ - code: InvalidType({ - expected: S.unknown, - received: "foo"->Obj.magic, - unionErrors: [ - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: "foo"->Obj.magic, - unionErrors: [ - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - ], - }), - operation: Parse, - path: S.Path.empty, - }), - ], - }), - operation: Parse, - path: S.Path.empty, - }).message, - `Failed parsing: Expected unknown, received "foo" -- Expected boolean, received "foo" - - Expected boolean, received "Hello world!"`, - ) -}) - -test("InvalidJsonSchema error", t => { - t->Assert.is( - U.error({ - code: InvalidJsonSchema(S.option(S.literal(true))->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }).message, - `Failed converting to JSON: true | undefined is not valid JSON`, - ) -}) diff --git a/packages/sury/tests/S_Error_raise_test.res b/packages/sury/tests/S_Error_raise_test.res deleted file mode 100644 index 24c3840c6..000000000 --- a/packages/sury/tests/S_Error_raise_test.res +++ /dev/null @@ -1,35 +0,0 @@ -open Ava - -test( - "Raised error is instance of S.Error and displayed with a nice error message when not caught", - t => { - t->Assert.throws( - () => { - S.ErrorClass.constructor( - ~code=OperationFailed("Should be positive"), - ~flag=S.Flag.typeValidation, - ~path=S.Path.empty, - )->U.throwError - }, - ~expectations={ - message: "Failed parsing: Should be positive", - instanceOf: S.ErrorClass.value->(U.magic: S.ErrorClass.t => 'instanceOf), - }, - ) - }, -) - -test("Raised error is also the S.Error exeption and can be caught with catch", t => { - let error = S.ErrorClass.constructor( - ~code=OperationFailed("Should be positive"), - ~flag=S.Flag.typeValidation, - ~path=S.Path.empty, - ) - t->ExecutionContext.plan(1) - try { - let _ = U.throwError(error) - t->Assert.fail("Should throw before the line") - } catch { - | S.Error(throwdError) => t->Assert.is(error, throwdError) - } -}) diff --git a/packages/sury/tests/S_Error_received_test.res b/packages/sury/tests/S_Error_received_test.res new file mode 100644 index 000000000..498899f2d --- /dev/null +++ b/packages/sury/tests/S_Error_received_test.res @@ -0,0 +1,127 @@ +open Ava + +// Tests that InvalidInput.received reports the pre-narrowing schema, +// not the post-narrowing target. Without the fix, received === expected +// because B.refine sets val.schema = expected before checks run. + +test("InvalidInput error has correct received schema for unknown-to-string type mismatch", t => { + switch 123->S.parseOrThrow(~to=S.string) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "string", ~message="expected schema") + t->Assert.is(received->S.toExpression, "unknown", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error has correct received schema for unknown-to-float type mismatch", t => { + switch "hello"->S.parseOrThrow(~to=S.float) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "number", ~message="expected schema") + t->Assert.is(received->S.toExpression, "unknown", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error has correct received schema for unknown-to-bool type mismatch", t => { + switch 42->S.parseOrThrow(~to=S.bool) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "boolean", ~message="expected schema") + t->Assert.is(received->S.toExpression, "unknown", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error has correct received schema for unknown-to-object type mismatch", t => { + switch "not an object"->S.parseOrThrow(~to=S.object(s => s.field("x", S.string))) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(received->S.toExpression, "unknown", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error received differs from expected (not equal)", t => { + switch true->S.parseOrThrow(~to=S.string) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.notDeepEqual( + expected->S.toExpression, + received->S.toExpression, + ~message="received should differ from expected", + ) + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error has correct received schema for nested field type mismatch", t => { + let schema = S.object(s => s.field("age", S.float)) + switch {"age": true}->S.parseOrThrow(~to=schema) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "number", ~message="expected schema") + t->Assert.is(received->S.toExpression, "unknown", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +// Cases where received is a specific type (not unknown) + +test("InvalidInput error reports number as received when float fails int32 format check", t => { + switch 1.5->S.decodeOrThrow(~from=S.float, ~to=S.int) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "int32", ~message="expected schema") + t->Assert.is(received->S.toExpression, "number", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error reports string as received when string-to-number coercion produces NaN", t => { + switch "abc"->S.decodeOrThrow(~from=S.string, ~to=S.float) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, "number", ~message="expected schema") + t->Assert.is(received->S.toExpression, "string", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) + +test("InvalidInput error reports string as received when string doesn't match literal", t => { + switch "wrong"->S.decodeOrThrow(~from=S.string, ~to=S.literal("apple")) { + | _ => t->Assert.fail("Should have thrown") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidInput({expected, received}) => + t->Assert.is(expected->S.toExpression, `"apple"`, ~message="expected schema") + t->Assert.is(received->S.toExpression, "string", ~message="received schema") + | _ => t->Assert.fail("Expected InvalidInput error") + } + } +}) diff --git a/packages/sury/tests/S_Error_throw_test.res b/packages/sury/tests/S_Error_throw_test.res new file mode 100644 index 000000000..c5c1ea080 --- /dev/null +++ b/packages/sury/tests/S_Error_throw_test.res @@ -0,0 +1,37 @@ +open Ava + +test( + "Raised error is instance of S.Error and displayed with a nice error message when not caught", + t => { + t->Assert.throws( + () => { + S.Error.make( + Custom({ + reason: "Should be positive", + path: S.Path.empty, + }), + )->U.throwError + }, + ~expectations={ + message: "Should be positive", + instanceOf: S.Error.class->(U.magic: S.Error.class => 'instanceOf), + }, + ) + }, +) + +test("Raised error is also the S.Error exeption and can be caught with catch", t => { + let error = S.Error.make( + Custom({ + reason: "Should be positive", + path: S.Path.empty, + }), + ) + t->ExecutionContext.plan(1) + try { + let _ = U.throwError(error) + t->Assert.fail("Should throw before the line") + } catch { + | S.Exn(throwdError) => t->Assert.is(error, throwdError) + } +}) diff --git a/packages/sury/tests/S_Float_max_test.res b/packages/sury/tests/S_Float_max_test.res index d95a4d2e6..d91d98c83 100644 --- a/packages/sury/tests/S_Float_max_test.res +++ b/packages/sury/tests/S_Float_max_test.res @@ -3,59 +3,54 @@ open Ava test("Successfully parses valid data", t => { let schema = S.float->S.floatMax(1.) - t->Assert.deepEqual(1->S.parseOrThrow(schema), 1.) - t->Assert.deepEqual(-1->S.parseOrThrow(schema), -1.) + t->Assert.deepEqual(1->S.parseOrThrow(~to=schema), 1.) + t->Assert.deepEqual(-1->S.parseOrThrow(~to=schema), -1.) }) test("Fails to parse invalid data", t => { let schema = S.float->S.floatMax(1.) - t->U.assertThrows( - () => 1234->S.parseOrThrow(schema), - { - code: OperationFailed("Number must be lower than or equal to 1"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 1234->S.parseOrThrow(~to=schema), + `Number must be lower than or equal to 1`, ) }) test("Successfully serializes valid value", t => { let schema = S.float->S.floatMax(1.) - t->Assert.deepEqual(1.->S.reverseConvertOrThrow(schema), %raw(`1`)) - t->Assert.deepEqual(-1.->S.reverseConvertOrThrow(schema), %raw(`-1`)) + t->Assert.deepEqual(1.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1`)) + t->Assert.deepEqual(-1.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`-1`)) }) test("Fails to serialize invalid value", t => { let schema = S.float->S.floatMax(1.) - t->U.assertThrows( - () => 1234.->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Number must be lower than or equal to 1"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 1234.->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Number must be lower than or equal to 1`, ) }) test("Returns custom error message", t => { let schema = S.float->S.floatMax(~message="Custom", 1.) - t->U.assertThrows( - () => 12.->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => 12.->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.float->S.floatMax(1.) - t->Assert.deepEqual( - schema->S.Float.refinements, - [{kind: Max({value: 1.}), message: "Number must be lower than or equal to 1"}], - ) + switch schema { + | Number({maximum, errorMessage}) => { + t->Assert.deepEqual(maximum, 1.) + t->Assert.deepEqual( + errorMessage, + {maximum: "Number must be lower than or equal to 1"}, + ) + } + | _ => t->Assert.fail("Expected Number schema with maximum") + } }) test("Compiled parse code snapshot", t => { @@ -64,6 +59,6 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="number"||Number.isNaN(i)){e[0](i)}if(i>e[1]){e[2]()}return i}`, + `i=>{typeof i==="number"&&!Number.isNaN(i)||e[2](i);i<=e[0]||e[1](i);return i}`, ) }) diff --git a/packages/sury/tests/S_Float_min_test.res b/packages/sury/tests/S_Float_min_test.res index a9a26f0f6..29469f315 100644 --- a/packages/sury/tests/S_Float_min_test.res +++ b/packages/sury/tests/S_Float_min_test.res @@ -3,57 +3,52 @@ open Ava test("Successfully parses valid data", t => { let schema = S.float->S.floatMin(1.) - t->Assert.deepEqual(1.->S.parseOrThrow(schema), 1.) - t->Assert.deepEqual(1234.->S.parseOrThrow(schema), 1234.) + t->Assert.deepEqual(1.->S.parseOrThrow(~to=schema), 1.) + t->Assert.deepEqual(1234.->S.parseOrThrow(~to=schema), 1234.) }) test("Fails to parse invalid data", t => { let schema = S.float->S.floatMin(1.) - t->U.assertThrows( - () => 0->S.parseOrThrow(schema), - { - code: OperationFailed("Number must be greater than or equal to 1"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 0->S.parseOrThrow(~to=schema), + `Number must be greater than or equal to 1`, ) }) test("Successfully serializes valid value", t => { let schema = S.float->S.floatMin(1.) - t->Assert.deepEqual(1.->S.reverseConvertOrThrow(schema), %raw(`1`)) - t->Assert.deepEqual(1234.->S.reverseConvertOrThrow(schema), %raw(`1234`)) + t->Assert.deepEqual(1.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1`)) + t->Assert.deepEqual(1234.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1234`)) }) test("Fails to serialize invalid value", t => { let schema = S.float->S.floatMin(1.) - t->U.assertThrows( - () => 0.->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Number must be greater than or equal to 1"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 0.->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Number must be greater than or equal to 1`, ) }) test("Returns custom error message", t => { let schema = S.float->S.floatMin(~message="Custom", 1.) - t->U.assertThrows( - () => 0.->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => 0.->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.float->S.floatMin(1.) - t->Assert.deepEqual( - schema->S.Float.refinements, - [{kind: Min({value: 1.}), message: "Number must be greater than or equal to 1"}], - ) + switch schema { + | Number({minimum, errorMessage}) => { + t->Assert.deepEqual(minimum, 1.) + t->Assert.deepEqual( + errorMessage, + {minimum: "Number must be greater than or equal to 1"}, + ) + } + | _ => t->Assert.fail("Expected Number schema with minimum") + } }) diff --git a/packages/sury/tests/S_Int_max_test.res b/packages/sury/tests/S_Int_max_test.res index 83c5e44d6..8af504265 100644 --- a/packages/sury/tests/S_Int_max_test.res +++ b/packages/sury/tests/S_Int_max_test.res @@ -3,57 +3,52 @@ open Ava test("Successfully parses valid data", t => { let schema = S.int->S.max(1) - t->Assert.deepEqual(1->S.parseOrThrow(schema), 1) - t->Assert.deepEqual(-1->S.parseOrThrow(schema), -1) + t->Assert.deepEqual(1->S.parseOrThrow(~to=schema), 1) + t->Assert.deepEqual(-1->S.parseOrThrow(~to=schema), -1) }) test("Fails to parse invalid data", t => { let schema = S.int->S.max(1) - t->U.assertThrows( - () => 1234->S.parseOrThrow(schema), - { - code: OperationFailed("Number must be lower than or equal to 1"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 1234->S.parseOrThrow(~to=schema), + `Number must be lower than or equal to 1`, ) }) test("Successfully serializes valid value", t => { let schema = S.int->S.max(1) - t->Assert.deepEqual(1->S.reverseConvertOrThrow(schema), %raw(`1`)) - t->Assert.deepEqual(-1->S.reverseConvertOrThrow(schema), %raw(`-1`)) + t->Assert.deepEqual(1->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1`)) + t->Assert.deepEqual(-1->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`-1`)) }) test("Fails to serialize invalid value", t => { let schema = S.int->S.max(1) - t->U.assertThrows( - () => 1234->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Number must be lower than or equal to 1"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 1234->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Number must be lower than or equal to 1`, ) }) test("Returns custom error message", t => { let schema = S.int->S.max(~message="Custom", 1) - t->U.assertThrows( - () => 12->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => 12->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.int->S.max(1) - t->Assert.deepEqual( - schema->S.Int.refinements, - [{kind: Max({value: 1}), message: "Number must be lower than or equal to 1"}], - ) + switch schema { + | Number({maximum, errorMessage}) => { + t->Assert.deepEqual(maximum, 1.) + t->Assert.deepEqual( + errorMessage, + {maximum: "Number must be lower than or equal to 1"}, + ) + } + | _ => t->Assert.fail("Expected Number schema with maximum") + } }) diff --git a/packages/sury/tests/S_Int_min_test.res b/packages/sury/tests/S_Int_min_test.res index f0265a0d9..d78dd4a7a 100644 --- a/packages/sury/tests/S_Int_min_test.res +++ b/packages/sury/tests/S_Int_min_test.res @@ -3,57 +3,59 @@ open Ava test("Successfully parses valid data", t => { let schema = S.int->S.min(1) - t->Assert.deepEqual(1->S.parseOrThrow(schema), 1) - t->Assert.deepEqual(1234->S.parseOrThrow(schema), 1234) + t->Assert.deepEqual(1->S.parseOrThrow(~to=schema), 1) + t->Assert.deepEqual(1234->S.parseOrThrow(~to=schema), 1234) }) test("Fails to parse invalid data", t => { let schema = S.int->S.min(1) - t->U.assertThrows( - () => 0->S.parseOrThrow(schema), - { - code: OperationFailed("Number must be greater than or equal to 1"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 0->S.parseOrThrow(~to=schema), + `Number must be greater than or equal to 1`, ) }) test("Successfully serializes valid value", t => { let schema = S.int->S.min(1) - t->Assert.deepEqual(1->S.reverseConvertOrThrow(schema), %raw(`1`)) - t->Assert.deepEqual(1234->S.reverseConvertOrThrow(schema), %raw(`1234`)) + t->Assert.deepEqual(1->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1`)) + t->Assert.deepEqual(1234->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`1234`)) }) test("Fails to serialize invalid value", t => { let schema = S.int->S.min(1) - t->U.assertThrows( - () => 0->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Number must be greater than or equal to 1"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => 0->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Number must be greater than or equal to 1`, ) }) test("Returns custom error message", t => { let schema = S.int->S.min(~message="Custom", 1) - t->U.assertThrows( - () => 0->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, + t->U.assertThrowsMessage(() => 0->S.parseOrThrow(~to=schema), `Custom`) +}) + +test("Throws when called with a non-number value", t => { + t->U.assertThrowsMessage( + () => S.int->S.min(%raw(`"abc"`)), + `[S.min] Expected number, received "abc"`, ) }) test("Returns refinement", t => { let schema = S.int->S.min(1) - t->Assert.deepEqual( - schema->S.Int.refinements, - [{kind: Min({value: 1}), message: "Number must be greater than or equal to 1"}], - ) + switch schema { + | Number({minimum, errorMessage}) => { + t->Assert.deepEqual(minimum, 1.) + t->Assert.deepEqual( + errorMessage, + {minimum: "Number must be greater than or equal to 1"}, + ) + } + | _ => t->Assert.fail("Expected Number schema with minimum") + } }) diff --git a/packages/sury/tests/S_Int_port_test.res b/packages/sury/tests/S_Int_port_test.res index 7d40e5aed..23cc84605 100644 --- a/packages/sury/tests/S_Int_port_test.res +++ b/packages/sury/tests/S_Int_port_test.res @@ -1,46 +1,42 @@ open Ava +S.enablePort() + test("Successfully parses valid data", t => { - let schema = S.int->S.port + let schema = S.port - t->Assert.deepEqual(8080->S.parseOrThrow(schema), 8080) + t->Assert.deepEqual(8080->S.parseOrThrow(~to=schema), 8080) }) test("Fails to parse invalid data", t => { - let schema = S.int->S.port + let schema = S.port - t->U.assertThrowsMessage( - () => 65536->S.parseOrThrow(schema), - `Failed parsing: Expected port, received 65536`, - ) + t->U.assertThrowsMessage(() => 65536->S.parseOrThrow(~to=schema), `Expected port, received 65536`) }) test("Successfully serializes valid value", t => { - let schema = S.int->S.port + let schema = S.port - t->Assert.deepEqual(8080->S.reverseConvertOrThrow(schema), %raw(`8080`)) + t->Assert.deepEqual(8080->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`8080`)) }) test("Fails to serialize invalid value", t => { - let schema = S.int->S.port + let schema = S.port t->U.assertThrowsMessage( - () => -80->S.reverseConvertOrThrow(schema), - `Failed converting: Expected port, received -80`, + () => -80->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected port, received -80`, ) }) -test("Returns custom error message", t => { - let schema = S.int->S.port(~message="Custom") +test("Custom error message via S.meta", t => { + let schema = S.port->S.meta({errorMessage: {format: "Custom"}}) - t->U.assertThrows( - () => 400000->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => 400000->S.parseOrThrow(~to=schema), `Custom`) }) -test("Reflects refinement on schema", t => { - let schema = S.int->S.port +test("Reflects format on schema", t => { + let schema = S.port t->Assert.deepEqual((schema->S.untag).format, Some(Port)) switch schema { diff --git a/packages/sury/tests/S_Option_getOrWith_test.res b/packages/sury/tests/S_Option_getOrWith_test.res index 3aaee2180..2f682e3bd 100644 --- a/packages/sury/tests/S_Option_getOrWith_test.res +++ b/packages/sury/tests/S_Option_getOrWith_test.res @@ -6,55 +6,51 @@ test("Uses default value when parsing optional unknown primitive", t => { let schema = S.float->S.option->S.Option.getOrWith(() => value) - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Uses default value when nullable optional unknown primitive", t => { let value = 123. let any = %raw(`null`) - let schema = S.float->S.null->S.Option.getOrWith(() => value) + let schema = S.float->S.nullAsOption->S.Option.getOrWith(() => value) - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Successfully parses with default when provided JS undefined", t => { let schema = S.bool->S.option->S.Option.getOrWith(() => false) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), false) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), false) }) test("Successfully parses with default when provided primitive", t => { let schema = S.bool->S.option->S.Option.getOrWith(() => false) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), true) }) test("Successfully parses nested option with default value", t => { t->Assert.throws(() => { let schema = S.option(S.bool)->S.option->S.Option.getOrWith(() => Some(true)) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), Some(true)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), Some(true)) }, ~expectations={message: "[Sury] Can\'t set default for boolean | undefined | undefined"}) }) test("Fails to parse data with default", t => { let schema = S.bool->S.option->S.Option.getOrWith(() => false) - t->U.assertThrows( - () => %raw(`"string"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"string"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"string"`)->S.parseOrThrow(~to=schema), + `Expected boolean | undefined, received "string"`, ) }) test("Successfully serializes schema with transformation", t => { let schema = S.string->S.trim->S.option->S.Option.getOrWith(() => "default") - t->Assert.deepEqual(" abc"->S.reverseConvertOrThrow(schema), %raw(`"abc"`)) + t->Assert.deepEqual(" abc"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"abc"`)) }) test("Compiled parse code snapshot", t => { @@ -77,7 +73,7 @@ test("Compiled async parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i==="boolean"){i=e[0](i)}else if(!(i===void 0)){e[1](i)}return Promise.resolve(i).then(v0=>{return v0===void 0?e[2]():v0})}`, + `i=>{let v1;if(typeof i==="boolean"){let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}else if(!(i===void 0)){e[2](i)}v1=Promise.resolve(i);return v1.then(v1=>{return v1===void 0?e[3]():v1})}`, ) }) diff --git a/packages/sury/tests/S_Option_getOr_test.res b/packages/sury/tests/S_Option_getOr_test.res index bf50b914e..278d2b82f 100644 --- a/packages/sury/tests/S_Option_getOr_test.res +++ b/packages/sury/tests/S_Option_getOr_test.res @@ -6,28 +6,28 @@ test("Uses default value when parsing optional unknown primitive", t => { let schema = S.float->S.option->S.Option.getOr(value) - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Uses default value when nullable optional unknown primitive", t => { let value = 123. let any = %raw(`null`) - let schema = S.float->S.null->S.Option.getOr(value) + let schema = S.float->S.nullAsOption->S.Option.getOr(value) - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Successfully parses with default when provided JS undefined", t => { let schema = S.bool->S.option->S.Option.getOr(false) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), false) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), false) }) test("Successfully parses with default when provided primitive", t => { let schema = S.bool->S.option->S.Option.getOr(false) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), true) }) test("Successfully serializes nested option with default value", t => { @@ -38,12 +38,12 @@ test("Successfully serializes nested option with default value", t => { ) t->Assert.deepEqual( - Some(Some(Some(Some(None))))->S.reverseConvertOrThrow(schema), + Some(Some(Some(Some(None))))->S.decodeOrThrow(~from=schema, ~to=S.unknown), Some(Some(Some(Some(None))))->Obj.magic, ) // FIXME: I'm not sure this is correct - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) }, ~expectations={ message: `[Sury] Can\'t set default for boolean | undefined | undefined | undefined`, @@ -54,13 +54,9 @@ test("Successfully serializes nested option with default value", t => { test("Fails to parse data with default", t => { let schema = S.bool->S.option->S.Option.getOr(false) - t->U.assertThrows( - () => %raw(`"string"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"string"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"string"`)->S.parseOrThrow(~to=schema), + `Expected boolean | undefined, received "string"`, ) }) @@ -79,18 +75,18 @@ test("Successfully parses schema with transformation", t => { ->S.to(S.option(S.string)) ->S.Option.getOr("not positive") - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), "not positive") + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), "not positive") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)||i===void 0)){e[0](i)}let v0=e[1](i===void 0?-123:i);if(!(typeof v0==="string"||v0===void 0)){e[2](v0)}return v0===void 0?"not positive":v0}`, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)||i===void 0)){e[0](i)}let v0;try{v0=e[1](i===void 0?-123:i)}catch(x){e[2](x)}if(!(typeof v0==="string"||v0===void 0)){e[3](v0)}return v0===void 0?"not positive":v0}`, ) }) test("Successfully serializes schema with transformation", t => { let schema = S.string->S.trim->S.option->S.Option.getOr("default") - t->Assert.deepEqual(" abc"->S.reverseConvertOrThrow(schema), %raw(`"abc"`)) + t->Assert.deepEqual(" abc"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"abc"`)) }) test("Compiled parse code snapshot", t => { @@ -110,11 +106,11 @@ asyncTest("Compiled async parse code snapshot", async t => { ) t->Assert.deepEqual(schema->S.isAsync, true) - t->Assert.deepEqual(await None->S.parseAsyncOrThrow(schema), false) + t->Assert.deepEqual(await None->S.parseAsyncOrThrow(~to=schema), false) t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i==="boolean"){i=e[0](i)}else if(!(i===void 0)){e[1](i)}return Promise.resolve(i).then(v0=>{return v0===void 0?false:v0})}`, + `i=>{let v1;if(typeof i==="boolean"){let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}else if(!(i===void 0)){e[2](i)}v1=Promise.resolve(i);return v1.then(v1=>{return v1===void 0?false:v1})}`, ) let schema = @@ -122,11 +118,11 @@ asyncTest("Compiled async parse code snapshot", async t => { ->S.Option.getOr(false) ->S.transform(_ => {asyncParser: i => Promise.resolve(i)}) - t->Assert.deepEqual(await None->S.parseAsyncOrThrow(schema), false) + t->Assert.deepEqual(await None->S.parseAsyncOrThrow(~to=schema), false) t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(!(typeof i==="boolean"||i===void 0)){e[0](i)}return e[1](i===void 0?false:i)}`, + `i=>{if(!(typeof i==="boolean"||i===void 0)){e[0](i)}let v0;try{v0=e[1](i===void 0?false:i).catch(x=>e[2](x))}catch(x){e[2](x)}return v0}`, ) }) diff --git a/packages/sury/tests/S_String_cuid_test.res b/packages/sury/tests/S_String_cuid_test.res index fc8c35be3..797031ce3 100644 --- a/packages/sury/tests/S_String_cuid_test.res +++ b/packages/sury/tests/S_String_cuid_test.res @@ -1,52 +1,52 @@ open Ava +S.enableCuid() + test("Successfully parses valid data", t => { - let schema = S.string->S.cuid + let schema = S.cuid t->Assert.deepEqual( - "ckopqwooh000001la8mbi2im9"->S.parseOrThrow(schema), + "ckopqwooh000001la8mbi2im9"->S.parseOrThrow(~to=schema), "ckopqwooh000001la8mbi2im9", ) }) test("Fails to parse invalid data", t => { - let schema = S.string->S.cuid + let schema = S.cuid - t->U.assertThrows( - () => "cifjhdsfhsd-invalid-cuid"->S.parseOrThrow(schema), - {code: OperationFailed("Invalid CUID"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "cifjhdsfhsd-invalid-cuid"->S.parseOrThrow(~to=schema), `Expected cuid, received "cifjhdsfhsd-invalid-cuid"`) }) test("Successfully serializes valid value", t => { - let schema = S.string->S.cuid + let schema = S.cuid t->Assert.deepEqual( - "ckopqwooh000001la8mbi2im9"->S.reverseConvertOrThrow(schema), + "ckopqwooh000001la8mbi2im9"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"ckopqwooh000001la8mbi2im9"`), ) }) test("Fails to serialize invalid value", t => { - let schema = S.string->S.cuid + let schema = S.cuid - t->U.assertThrows( - () => "cifjhdsfhsd-invalid-cuid"->S.reverseConvertOrThrow(schema), - {code: OperationFailed("Invalid CUID"), operation: ReverseConvert, path: S.Path.empty}, + t->U.assertThrowsMessage( + () => "cifjhdsfhsd-invalid-cuid"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected cuid, received "cifjhdsfhsd-invalid-cuid"`, ) }) -test("Returns custom error message", t => { - let schema = S.string->S.cuid(~message="Custom") +test("Custom error message via S.meta", t => { + let schema = S.cuid->S.meta({errorMessage: {format: "Custom"}}) - t->U.assertThrows( - () => "cifjhdsfhsd-invalid-cuid"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "cifjhdsfhsd-invalid-cuid"->S.parseOrThrow(~to=schema), `Custom`) }) -test("Returns refinement", t => { - let schema = S.string->S.cuid +test("Reflects format on schema", t => { + let schema = S.cuid - t->Assert.deepEqual(schema->S.String.refinements, [{kind: Cuid, message: "Invalid CUID"}]) + t->Assert.deepEqual((schema->S.untag).format, Some(Cuid)) + switch schema { + | String({format}) => t->Assert.deepEqual(format, Cuid) + | _ => t->Assert.fail("Expected String with format Cuid") + } }) diff --git a/packages/sury/tests/S_String_datetime_test.res b/packages/sury/tests/S_String_datetime_test.res deleted file mode 100644 index 88e30476d..000000000 --- a/packages/sury/tests/S_String_datetime_test.res +++ /dev/null @@ -1,80 +0,0 @@ -open Ava - -test("Successfully parses valid data", t => { - let schema = S.string->S.datetime - - t->Assert.deepEqual( - "2020-01-01T00:00:00Z"->S.parseOrThrow(schema), - Date.fromString("2020-01-01T00:00:00Z"), - ) - t->Assert.deepEqual( - "2020-01-01T00:00:00.123Z"->S.parseOrThrow(schema), - Date.fromString("2020-01-01T00:00:00.123Z"), - ) - t->Assert.deepEqual( - "2020-01-01T00:00:00.123456Z"->S.parseOrThrow(schema), - Date.fromString("2020-01-01T00:00:00.123456Z"), - ) -}) - -test("Fails to parse non UTC date string", t => { - let schema = S.string->S.datetime - - t->U.assertThrows( - () => "Thu Apr 20 2023 10:45:48 GMT+0400"->S.parseOrThrow(schema), - { - code: OperationFailed("Invalid datetime string! Expected UTC"), - operation: Parse, - path: S.Path.empty, - }, - ) -}) - -test("Fails to parse UTC date with timezone offset", t => { - let schema = S.string->S.datetime - - t->U.assertThrows( - () => "2020-01-01T00:00:00+02:00"->S.parseOrThrow(schema), - { - code: OperationFailed("Invalid datetime string! Expected UTC"), - operation: Parse, - path: S.Path.empty, - }, - ) -}) - -test("Uses custom message on failure", t => { - let schema = S.string->S.datetime(~message="Invalid date") - - t->U.assertThrows( - () => "Thu Apr 20 2023 10:45:48 GMT+0400"->S.parseOrThrow(schema), - {code: OperationFailed("Invalid date"), operation: Parse, path: S.Path.empty}, - ) -}) - -test("Successfully serializes valid value", t => { - let schema = S.string->S.datetime - - t->Assert.deepEqual( - Date.fromString("2020-01-01T00:00:00.123Z")->S.reverseConvertOrThrow(schema), - %raw(`"2020-01-01T00:00:00.123Z"`), - ) -}) - -test("Trims precision to 3 digits when serializing", t => { - let schema = S.string->S.datetime - - t->Assert.deepEqual( - Date.fromString("2020-01-01T00:00:00.123456Z")->S.reverseConvertOrThrow(schema), - %raw(`"2020-01-01T00:00:00.123Z"`), - ) -}) - -test("Returns refinement", t => { - let schema = S.string->S.datetime - - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Datetime, message: "Invalid datetime string! Expected UTC"}], - ) -}) diff --git a/packages/sury/tests/S_String_email_test.res b/packages/sury/tests/S_String_email_test.res index e63e91fd3..db8805305 100644 --- a/packages/sury/tests/S_String_email_test.res +++ b/packages/sury/tests/S_String_email_test.res @@ -1,60 +1,51 @@ open Ava +S.enableEmail() + test("Successfully parses valid data", t => { - let schema = S.string->S.email + let schema = S.email - t->Assert.deepEqual("dzakh.dev@gmail.com"->S.parseOrThrow(schema), "dzakh.dev@gmail.com") + t->Assert.deepEqual("dzakh.dev@gmail.com"->S.parseOrThrow(~to=schema), "dzakh.dev@gmail.com") }) test("Fails to parse invalid data", t => { - let schema = S.string->S.email - - t->U.assertThrows( - () => "dzakh.dev"->S.parseOrThrow(schema), - { - code: OperationFailed("Invalid email address"), - operation: Parse, - path: S.Path.empty, - }, - ) + let schema = S.email + + t->U.assertThrowsMessage(() => "dzakh.dev"->S.parseOrThrow(~to=schema), `Expected email, received "dzakh.dev"`) }) test("Successfully serializes valid value", t => { - let schema = S.string->S.email + let schema = S.email t->Assert.deepEqual( - "dzakh.dev@gmail.com"->S.reverseConvertOrThrow(schema), + "dzakh.dev@gmail.com"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"dzakh.dev@gmail.com"`), ) }) test("Fails to serialize invalid value", t => { - let schema = S.string->S.email - - t->U.assertThrows( - () => "dzakh.dev"->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Invalid email address"), - operation: ReverseConvert, - path: S.Path.empty, - }, + let schema = S.email + + t->U.assertThrowsMessage( + () => "dzakh.dev"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected email, received "dzakh.dev"`, ) }) -test("Returns custom error message", t => { - let schema = S.string->S.email(~message="Custom") +test("Reflects format on schema", t => { + let schema = S.email - t->U.assertThrows( - () => "dzakh.dev"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->Assert.deepEqual((schema->S.untag).format, Some(Email)) + switch schema { + | String({format}) => t->Assert.deepEqual(format, Email) + | _ => t->Assert.fail("Expected String with format Email") + } }) -test("Returns refinement", t => { - let schema = S.string->S.email +test("Custom error message via S.meta", t => { + let schema = S.email->S.meta({errorMessage: {format: "Custom"}}) - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Email, message: "Invalid email address"}], - ) + t->U.assertThrowsMessage(() => "dzakh.dev"->S.parseOrThrow(~to=schema), `Custom`) + // Original singleton is not mutated + t->U.assertThrowsMessage(() => "dzakh.dev"->S.parseOrThrow(~to=S.email), `Expected email, received "dzakh.dev"`) }) diff --git a/packages/sury/tests/S_String_length_test.res b/packages/sury/tests/S_String_length_test.res index 6aabacee3..7f31904ea 100644 --- a/packages/sury/tests/S_String_length_test.res +++ b/packages/sury/tests/S_String_length_test.res @@ -3,71 +3,62 @@ open Ava test("Successfully parses valid data", t => { let schema = S.string->S.length(1) - t->Assert.deepEqual("1"->S.parseOrThrow(schema), "1") + t->Assert.deepEqual("1"->S.parseOrThrow(~to=schema), "1") }) test("Fails to parse invalid data", t => { let schema = S.string->S.length(1) - t->U.assertThrows( - () => ""->S.parseOrThrow(schema), - { - code: OperationFailed("String must be exactly 1 characters long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => ""->S.parseOrThrow(~to=schema), + `String must be exactly 1 characters long`, ) - t->U.assertThrows( - () => "1234"->S.parseOrThrow(schema), - { - code: OperationFailed("String must be exactly 1 characters long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "1234"->S.parseOrThrow(~to=schema), + `String must be exactly 1 characters long`, ) }) test("Successfully serializes valid value", t => { let schema = S.string->S.length(1) - t->Assert.deepEqual("1"->S.reverseConvertOrThrow(schema), %raw(`"1"`)) + t->Assert.deepEqual("1"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1"`)) }) test("Fails to serialize invalid value", t => { let schema = S.string->S.length(1) - t->U.assertThrows( - () => ""->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("String must be exactly 1 characters long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => ""->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `String must be exactly 1 characters long`, ) - t->U.assertThrows( - () => "1234"->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("String must be exactly 1 characters long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "1234"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `String must be exactly 1 characters long`, ) }) test("Returns custom error message", t => { let schema = S.string->S.length(~message="Custom", 12) - t->U.assertThrows( - () => "123"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "123"->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.string->S.length(4) - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Length({length: 4}), message: "String must be exactly 4 characters long"}], - ) + switch schema { + | String({minLength, maxLength, errorMessage}) => { + t->Assert.deepEqual(minLength, 4) + t->Assert.deepEqual(maxLength, 4) + t->Assert.deepEqual( + errorMessage, + { + minLength: "String must be exactly 4 characters long", + maxLength: "String must be exactly 4 characters long", + }, + ) + } + | _ => t->Assert.fail("Expected String schema with minLength and maxLength") + } }) diff --git a/packages/sury/tests/S_String_max_test.res b/packages/sury/tests/S_String_max_test.res index 20cd5f564..9fd84c17b 100644 --- a/packages/sury/tests/S_String_max_test.res +++ b/packages/sury/tests/S_String_max_test.res @@ -3,57 +3,52 @@ open Ava test("Successfully parses valid data", t => { let schema = S.string->S.max(1) - t->Assert.deepEqual("1"->S.parseOrThrow(schema), "1") - t->Assert.deepEqual(""->S.parseOrThrow(schema), "") + t->Assert.deepEqual("1"->S.parseOrThrow(~to=schema), "1") + t->Assert.deepEqual(""->S.parseOrThrow(~to=schema), "") }) test("Fails to parse invalid data", t => { let schema = S.string->S.max(1) - t->U.assertThrows( - () => "1234"->S.parseOrThrow(schema), - { - code: OperationFailed("String must be 1 or fewer characters long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "1234"->S.parseOrThrow(~to=schema), + `String must be 1 or fewer characters long`, ) }) test("Successfully serializes valid value", t => { let schema = S.string->S.max(1) - t->Assert.deepEqual("1"->S.reverseConvertOrThrow(schema), %raw(`"1"`)) - t->Assert.deepEqual(""->S.reverseConvertOrThrow(schema), %raw(`""`)) + t->Assert.deepEqual("1"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1"`)) + t->Assert.deepEqual(""->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`""`)) }) test("Fails to serialize invalid value", t => { let schema = S.string->S.max(1) - t->U.assertThrows( - () => "1234"->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("String must be 1 or fewer characters long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "1234"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `String must be 1 or fewer characters long`, ) }) test("Returns custom error message", t => { let schema = S.string->S.max(~message="Custom", 1) - t->U.assertThrows( - () => "1234"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "1234"->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.string->S.max(1) - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Max({length: 1}), message: "String must be 1 or fewer characters long"}], - ) + switch schema { + | String({maxLength, errorMessage}) => { + t->Assert.deepEqual(maxLength, 1) + t->Assert.deepEqual( + errorMessage, + {maxLength: "String must be 1 or fewer characters long"}, + ) + } + | _ => t->Assert.fail("Expected String schema with maxLength") + } }) diff --git a/packages/sury/tests/S_String_min_test.res b/packages/sury/tests/S_String_min_test.res index eb461d904..0624fea2d 100644 --- a/packages/sury/tests/S_String_min_test.res +++ b/packages/sury/tests/S_String_min_test.res @@ -3,57 +3,83 @@ open Ava test("Successfully parses valid data", t => { let schema = S.string->S.min(1) - t->Assert.deepEqual("1"->S.parseOrThrow(schema), "1") - t->Assert.deepEqual("1234"->S.parseOrThrow(schema), "1234") + t->Assert.deepEqual("1"->S.parseOrThrow(~to=schema), "1") + t->Assert.deepEqual("1234"->S.parseOrThrow(~to=schema), "1234") }) test("Fails to parse invalid data", t => { let schema = S.string->S.min(1) - t->U.assertThrows( - () => ""->S.parseOrThrow(schema), - { - code: OperationFailed("String must be 1 or more characters long"), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => ""->S.parseOrThrow(~to=schema), + `String must be 1 or more characters long`, ) }) test("Successfully serializes valid value", t => { let schema = S.string->S.min(1) - t->Assert.deepEqual("1"->S.reverseConvertOrThrow(schema), %raw(`"1"`)) - t->Assert.deepEqual("1234"->S.reverseConvertOrThrow(schema), %raw(`"1234"`)) + t->Assert.deepEqual("1"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1"`)) + t->Assert.deepEqual("1234"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1234"`)) }) test("Fails to serialize invalid value", t => { let schema = S.string->S.min(1) - t->U.assertThrows( - () => ""->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("String must be 1 or more characters long"), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => ""->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `String must be 1 or more characters long`, ) }) test("Returns custom error message", t => { let schema = S.string->S.min(~message="Custom", 1) - t->U.assertThrows( - () => ""->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => ""->S.parseOrThrow(~to=schema), `Custom`) }) test("Returns refinement", t => { let schema = S.string->S.min(1) - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Min({length: 1}), message: "String must be 1 or more characters long"}], - ) + switch schema { + | String({minLength, errorMessage}) => { + t->Assert.deepEqual(minLength, 1) + t->Assert.deepEqual( + errorMessage, + {minLength: "String must be 1 or more characters long"}, + ) + } + | _ => t->Assert.fail("Expected String schema with minLength") + } +}) + +test("Chaining refinements does not mutate the original schema", t => { + let schema1 = S.string->S.min(1) + let schema2 = schema1->S.max(10) + + switch schema1 { + | String({minLength, ?maxLength, errorMessage}) => { + t->Assert.deepEqual(minLength, 1) + t->Assert.deepEqual(maxLength, None) + t->Assert.deepEqual( + errorMessage, + {minLength: "String must be 1 or more characters long"}, + ) + } + | _ => t->Assert.fail("Expected String schema with minLength only") + } + switch schema2 { + | String({minLength, maxLength, errorMessage}) => { + t->Assert.deepEqual(minLength, 1) + t->Assert.deepEqual(maxLength, 10) + t->Assert.deepEqual( + errorMessage, + { + minLength: "String must be 1 or more characters long", + maxLength: "String must be 10 or fewer characters long", + }, + ) + } + | _ => t->Assert.fail("Expected String schema with minLength and maxLength") + } }) diff --git a/packages/sury/tests/S_String_pattern_test.res b/packages/sury/tests/S_String_pattern_test.res index 3f90b2a29..ac8c30a83 100644 --- a/packages/sury/tests/S_String_pattern_test.res +++ b/packages/sury/tests/S_String_pattern_test.res @@ -3,88 +3,81 @@ open Ava test("Successfully parses valid data", t => { let schema = S.string->S.pattern(/[0-9]/) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), "123") + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), "123") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}if(!e[1].test(i)){e[2]()}return i}`, + `i=>{typeof i==="string"||e[2](i);e[0].test(i)||e[1](i);return i}`, ) }) test("Successfully parses valid data with global flag", t => { - let schema = S.string->S.pattern(%re(`/[0-9]/g`)) + let schema = S.string->S.pattern(/[0-9]/g) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), "123") + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), "123") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}e[1].lastIndex=0;if(!e[2].test(i)){e[3]()}return i}`, + `i=>{typeof i==="string"||e[2](i);(e[0].lastIndex=0,e[0].test(i))||e[1](i);return i}`, ) }) test("Fails to parse invalid data", t => { let schema = S.string->S.pattern(/[0-9]/) - t->U.assertThrows( - () => "abc"->S.parseOrThrow(schema), - {code: OperationFailed("Invalid"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Invalid pattern`) }) test("Successfully serializes valid value", t => { let schema = S.string->S.pattern(/[0-9]/) - t->Assert.deepEqual("123"->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual("123"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) }) test("Fails to serialize invalid value", t => { let schema = S.string->S.pattern(/[0-9]/) - t->U.assertThrows( - () => "abc"->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Invalid"), - operation: ReverseConvert, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => "abc"->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Invalid pattern`) }) test("Returns custom error message", t => { let schema = S.string->S.pattern(~message="Custom", /[0-9]/) - t->U.assertThrows( - () => "abc"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Custom`) }) -test("Returns refinement", t => { +test("Reflects pattern on schema", t => { let schema = S.string->S.pattern(/[0-9]/) - t->Assert.deepEqual( - schema->S.String.refinements, - [{kind: Pattern({re: /[0-9]/}), message: "Invalid"}], - ) + switch schema { + | String({pattern}) => t->Assert.deepEqual(pattern, /[0-9]/) + | _ => t->Assert.fail("Expected String with pattern") + } }) -test("Returns multiple refinement", t => { - let schema1 = S.string - let schema2 = schema1->S.pattern(~message="Should have digit", /[0-9]+/) - let schema3 = schema2->S.pattern(~message="Should have text", /\w+/) +test("Reflects errorMessage on schema", t => { + let schema = S.string->S.pattern(~message="Custom", /[0-9]/) - t->Assert.deepEqual(schema1->S.String.refinements, []) - t->Assert.deepEqual( - schema2->S.String.refinements, - [{kind: Pattern({re: /[0-9]+/}), message: "Should have digit"}], - ) - t->Assert.deepEqual( - schema3->S.String.refinements, - [ - {kind: Pattern({re: /[0-9]+/}), message: "Should have digit"}, - {kind: Pattern({re: /\w+/}), message: "Should have text"}, - ], - ) + switch schema { + | String({errorMessage}) => + t->Assert.deepEqual(errorMessage, {pattern: "Custom"}) + | _ => t->Assert.fail("Expected String") + } +}) + +test("Chaining patterns overwrites pattern but keeps last", t => { + let schema = S.string->S.pattern(~message="Should have digit", /[0-9]+/)->S.pattern(~message="Should have text", /\w+/) + + switch schema { + | String({pattern, errorMessage}) => { + t->Assert.deepEqual(pattern, /\w+/) + t->Assert.deepEqual( + errorMessage, + {pattern: "Should have text"}, + ) + } + | _ => t->Assert.fail("Expected String with pattern") + } }) diff --git a/packages/sury/tests/S_String_trim_test.res b/packages/sury/tests/S_String_trim_test.res index 2e975152f..7ae90aa2d 100644 --- a/packages/sury/tests/S_String_trim_test.res +++ b/packages/sury/tests/S_String_trim_test.res @@ -3,11 +3,11 @@ open Ava test("Successfully parses", t => { let schema = S.string->S.trim - t->Assert.deepEqual(" Hello world!"->S.parseOrThrow(schema), "Hello world!") + t->Assert.deepEqual(" Hello world!"->S.parseOrThrow(~to=schema), "Hello world!") }) test("Successfully serializes", t => { let schema = S.string->S.trim - t->Assert.deepEqual(" Hello world!"->S.reverseConvertOrThrow(schema), %raw(`"Hello world!"`)) + t->Assert.deepEqual(" Hello world!"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"Hello world!"`)) }) diff --git a/packages/sury/tests/S_String_url_test.res b/packages/sury/tests/S_String_url_test.res index 7e2167099..e177489f8 100644 --- a/packages/sury/tests/S_String_url_test.res +++ b/packages/sury/tests/S_String_url_test.res @@ -1,43 +1,46 @@ open Ava +S.enableUrl() + test("Successfully parses valid data", t => { - let schema = S.string->S.url + let schema = S.url - t->Assert.deepEqual("http://dzakh.dev"->S.parseOrThrow(schema), "http://dzakh.dev") + t->Assert.deepEqual("http://dzakh.dev"->S.parseOrThrow(~to=schema), "http://dzakh.dev") }) test("Fails to parse invalid data", t => { - let schema = S.string->S.url + let schema = S.url - t->U.assertThrows( - () => "cifjhdsfhsd"->S.parseOrThrow(schema), - {code: OperationFailed("Invalid url"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "cifjhdsfhsd"->S.parseOrThrow(~to=schema), `Expected url, received "cifjhdsfhsd"`) }) test("Successfully serializes valid value", t => { - let schema = S.string->S.url + let schema = S.url t->Assert.deepEqual( - "http://dzakh.dev"->S.reverseConvertOrThrow(schema), + "http://dzakh.dev"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"http://dzakh.dev"`), ) }) test("Fails to serialize invalid value", t => { - let schema = S.string->S.url + let schema = S.url - t->U.assertThrows( - () => "cifjhdsfhsd"->S.reverseConvertOrThrow(schema), - {code: OperationFailed("Invalid url"), operation: ReverseConvert, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "cifjhdsfhsd"->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Expected url, received "cifjhdsfhsd"`) }) -test("Returns custom error message", t => { - let schema = S.string->S.url(~message="Custom") +test("Custom error message via S.meta", t => { + let schema = S.url->S.meta({errorMessage: {format: "Custom"}}) - t->U.assertThrows( - () => "abc"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Custom`) +}) + +test("Reflects format on schema", t => { + let schema = S.url + + t->Assert.deepEqual((schema->S.untag).format, Some(Url)) + switch schema { + | String({format}) => t->Assert.deepEqual(format, Url) + | _ => t->Assert.fail("Expected String with format Url") + } }) diff --git a/packages/sury/tests/S_String_uuid_test.res b/packages/sury/tests/S_String_uuid_test.res index 7f695b65d..ab6c6fd26 100644 --- a/packages/sury/tests/S_String_uuid_test.res +++ b/packages/sury/tests/S_String_uuid_test.res @@ -1,55 +1,58 @@ open Ava +S.enableUuid() + test("Successfully parses valid data", t => { - let schema = S.string->S.uuid + let schema = S.uuid t->Assert.deepEqual( - "123e4567-e89b-12d3-a456-426614174000"->S.parseOrThrow(schema), + "123e4567-e89b-12d3-a456-426614174000"->S.parseOrThrow(~to=schema), "123e4567-e89b-12d3-a456-426614174000", ) }) test("Successfully parses uuid V7", t => { - let schema = S.string->S.uuid + let schema = S.uuid t->Assert.deepEqual( - "019122ba-bb79-75ef-9a97-190f1effbb54"->S.parseOrThrow(schema), + "019122ba-bb79-75ef-9a97-190f1effbb54"->S.parseOrThrow(~to=schema), "019122ba-bb79-75ef-9a97-190f1effbb54", ) }) test("Fails to parse invalid data", t => { - let schema = S.string->S.uuid + let schema = S.uuid - t->U.assertThrows( - () => "123e4567"->S.parseOrThrow(schema), - {code: OperationFailed("Invalid UUID"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "123e4567"->S.parseOrThrow(~to=schema), `Expected uuid, received "123e4567"`) }) test("Successfully serializes valid value", t => { - let schema = S.string->S.uuid + let schema = S.uuid t->Assert.deepEqual( - "123e4567-e89b-12d3-a456-426614174000"->S.reverseConvertOrThrow(schema), + "123e4567-e89b-12d3-a456-426614174000"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123e4567-e89b-12d3-a456-426614174000"`), ) }) test("Fails to serialize invalid value", t => { - let schema = S.string->S.uuid + let schema = S.uuid - t->U.assertThrows( - () => "123e4567"->S.reverseConvertOrThrow(schema), - {code: OperationFailed("Invalid UUID"), operation: ReverseConvert, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "123e4567"->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Expected uuid, received "123e4567"`) }) -test("Returns custom error message", t => { - let schema = S.string->S.uuid(~message="Custom") +test("Custom error message via S.meta", t => { + let schema = S.uuid->S.meta({errorMessage: {format: "Custom"}}) - t->U.assertThrows( - () => "abc"->S.parseOrThrow(schema), - {code: OperationFailed("Custom"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Custom`) +}) + +test("Reflects format on schema", t => { + let schema = S.uuid + + t->Assert.deepEqual((schema->S.untag).format, Some(Uuid)) + switch schema { + | String({format}) => t->Assert.deepEqual(format, Uuid) + | _ => t->Assert.fail("Expected String with format Uuid") + } }) diff --git a/packages/sury/tests/S_additionalItems_test.res b/packages/sury/tests/S_additionalItems_test.res index 19d168508..820f3838f 100644 --- a/packages/sury/tests/S_additionalItems_test.res +++ b/packages/sury/tests/S_additionalItems_test.res @@ -7,7 +7,7 @@ test("Successfully parses Object with unknown keys by default", t => { let schema = S.object(s => s.field("key", S.string)) - t->Assert.deepEqual(any->S.parseOrThrow(schema), "value") + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), "value") }) test("Fails fast and shows only one excees key in the error message", t => { @@ -17,10 +17,10 @@ test("Fails fast and shows only one excees key in the error message", t => { } )->S.strict - t->U.assertThrows( + t->U.assertThrowsMessage( () => - %raw(`{key: "value", unknownKey: "value2", unknownKey2: "value2"}`)->S.parseOrThrow(schema), - {code: ExcessField("unknownKey"), operation: Parse, path: S.Path.empty}, + %raw(`{key: "value", unknownKey: "value2", unknownKey2: "value2"}`)->S.parseOrThrow(~to=schema), + `Unrecognized key "unknownKey"`, ) }) @@ -30,7 +30,7 @@ test("Successfully parses Object with unknown keys when Strip strategy applyed", let schema = S.object(s => s.field("key", S.string))->S.strip - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Works correctly when the same unknown keys strategy applyed multiple times", t => { @@ -39,7 +39,7 @@ test("Works correctly when the same unknown keys strategy applyed multiple times let schema = S.object(s => s.field("key", S.string))->S.strip->S.strip->S.strip - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Doesn't throw an error when unknown keys strategy applyed to a non Object schema", t => { @@ -56,10 +56,7 @@ test("Can reset unknown keys strategy applying Strict strategy", t => { let schema = S.object(s => s.field("key", S.string))->S.strip->S.strict - t->U.assertThrows( - () => any->S.parseOrThrow(schema), - {code: ExcessField("unknownKey"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => any->S.parseOrThrow(~to=schema), `Unrecognized key "unknownKey"`) }) test("Ignores additional items override for S.array and S.dict", t => { diff --git a/packages/sury/tests/S_array_test.res b/packages/sury/tests/S_array_test.res index d6e91b210..58b468654 100644 --- a/packages/sury/tests/S_array_test.res +++ b/packages/sury/tests/S_array_test.res @@ -10,39 +10,31 @@ module CommonWithNested = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected string[], received true`, ) }) test("Fails to parse nested", t => { let schema = factory() - t->U.assertThrows( - () => nestedInvalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: 1->Obj.magic}), - operation: Parse, - path: S.Path.fromArray(["1"]), - }, + t->U.assertThrowsMessage( + () => nestedInvalidAny->S.parseOrThrow(~to=schema), + `Failed at ["1"]: Expected string, received 1`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { @@ -51,7 +43,7 @@ module CommonWithNested = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)){e[0](i)}for(let v0=0;v0{Array.isArray(i)||e[1](i);for(let v0=0;v0U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(!Array.isArray(i)){e[0](i)}let v3=new Array(i.length);for(let v0=0;v0{if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1})}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v3[v0]=v2}return Promise.all(v3)}`, + `i=>{Array.isArray(i)||e[2](i);let v3=new Array(i.length);for(let v0=0;v0e[1](x))}catch(x){e[1](x)}v3[v0]=v1.catch(v2=>{v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2})}catch(v2){v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2}}return Promise.all(v3)}`, ) }) @@ -74,12 +66,12 @@ module CommonWithNested = { }) test("Compiled serialize code snapshot with transform", t => { - let schema = S.array(S.null(S.string)) + let schema = S.array(S.nullAsOption(S.string)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{let v4=new Array(i.length);for(let v0=0;v0{let v3=new Array(i.length);for(let v0=0;v0 { - let schema = S.array(S.null(S.string)) + let schema = S.array(S.nullAsOption(S.string)) t->U.assertEqualSchemas( schema->S.reverse, @@ -107,7 +99,7 @@ test("Successfully parses matrix", t => { let schema = S.array(S.array(S.string)) t->Assert.deepEqual( - %raw(`[["a", "b"], ["c", "d"]]`)->S.parseOrThrow(schema), + %raw(`[["a", "b"], ["c", "d"]]`)->S.parseOrThrow(~to=schema), [["a", "b"], ["c", "d"]], ) }) @@ -115,13 +107,9 @@ test("Successfully parses matrix", t => { test("Fails to parse matrix", t => { let schema = S.array(S.array(S.string)) - t->U.assertThrows( - () => %raw(`[["a", 1], ["c", "d"]]`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`1`)}), - operation: Parse, - path: S.Path.fromArray(["0", "1"]), - }, + t->U.assertThrowsMessage( + () => %raw(`[["a", 1], ["c", "d"]]`)->S.parseOrThrow(~to=schema), + `Failed at ["0"]["1"]: Expected string, received 1`, ) }) @@ -129,7 +117,7 @@ test("Successfully parses array of optional items", t => { let schema = S.array(S.option(S.string)) t->Assert.deepEqual( - %raw(`["a", undefined, undefined, "b"]`)->S.parseOrThrow(schema), + %raw(`["a", undefined, undefined, "b"]`)->S.parseOrThrow(~to=schema), [Some("a"), None, None, Some("b")], ) }) diff --git a/packages/sury/tests/S_bigint_test.res b/packages/sury/tests/S_bigint_test.res index 0ed425e96..a5afcdcdd 100644 --- a/packages/sury/tests/S_bigint_test.res +++ b/packages/sury/tests/S_bigint_test.res @@ -1,5 +1,7 @@ open Ava +S.enableJson() + module Common = { let value = 123n let any = %raw(`123n`) @@ -9,29 +11,22 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected bigint, received 123.45`, ) }) - test("Fails to convert to Json", t => { + test("Decodes to Json", t => { let schema = factory() - t->U.assertThrowsMessage( - () => value->S.convertToJsonOrThrow(schema), - "Failed converting to JSON: bigint is not valid JSON", - ) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.json), "123"->Obj.magic) }) test("BigInt name", t => { @@ -42,13 +37,13 @@ module Common = { test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="bigint"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="bigint"||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { diff --git a/packages/sury/tests/S_bool_test.res b/packages/sury/tests/S_bool_test.res index bf7e884d5..82ea18322 100644 --- a/packages/sury/tests/S_bool_test.res +++ b/packages/sury/tests/S_bool_test.res @@ -9,32 +9,28 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse ", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected boolean, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="boolean"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="boolean"||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { @@ -58,11 +54,11 @@ module Common = { test("Parses bool when JSON is true", t => { let schema = S.bool - t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(~to=schema), true) }) test("Parses bool when JSON is false", t => { let schema = S.bool - t->Assert.deepEqual(JSON.Encode.bool(false)->S.parseOrThrow(schema), false) + t->Assert.deepEqual(JSON.Encode.bool(false)->S.parseOrThrow(~to=schema), false) }) diff --git a/packages/sury/tests/S_compactColumns_test.res b/packages/sury/tests/S_compactColumns_test.res new file mode 100644 index 000000000..2a8637453 --- /dev/null +++ b/packages/sury/tests/S_compactColumns_test.res @@ -0,0 +1,477 @@ +open Ava + +test("Successfully parses and reverse converts a simple object with compactColumns", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.int), + } + ), + ), + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{Array.isArray(i)&&i.length===2&&Array.isArray(i[0])&&Array.isArray(i[1])||e[2](i);let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0=-2147483648&&v3%1===0||e[1](v3);v1[v0]={"foo":v2,"bar":v3,};}catch(v4){v4.path='["'+v0+'"]'+v4.path;throw v4}}return v1}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{for(let v0=0;v0Assert.deepEqual( + %raw(`[["a", "b"], [0, 1]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": 1}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": 1}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", "b"], [0, 1]]`), + ) +}) + +test("Transforms nullable fields", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.nullAsOption(S.int)), + } + ), + ), + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{Array.isArray(i)&&i.length===2&&Array.isArray(i[0])&&Array.isArray(i[1])||e[2](i);let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0=-2147483648&&v3%1===0))){e[1](v3)}v1[v0]={"foo":v2,"bar":v3,};}catch(v4){v4.path='["'+v0+'"]'+v4.path;throw v4}}return v1}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v4=new Array(i.length);for(let v0=0;v0=-2147483648&&v2%1===0))){e[0](v2)}v4[v0]={"foo":v1["foo"],"bar":v2,}}catch(v3){v3.path='["'+v0+'"]'+v3.path;throw v3}}let v6=[new Array(v4.length),new Array(v4.length),];for(let v5=0;v5Assert.deepEqual( + %raw(`[["a", "b"], [0, null]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": undefined}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": undefined}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", "b"], [0, null]]`), + ) +}) + +test("Case with missing item at the end", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.option(S.string)), + "bar": s.matches(S.bool), + } + ), + ), + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{Array.isArray(i)&&i.length===2&&Array.isArray(i[0])&&Array.isArray(i[1])||e[2](i);let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{for(let v0=0;v0Assert.deepEqual( + %raw(`[["a", "b"], [true, true, false]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": true}, {"foo": "b", "bar": true}, {"foo": undefined, "bar": false}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"foo": "a", "bar": true}, {"foo": "b", "bar": true}, {"foo": undefined, "bar": false}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", "b", undefined], [true, true, false]]`), + ) +}) + +test("Handles empty objects", t => { + let schema = S.compactColumns(S.unknown)->S.to(S.array(S.object(_ => ()))) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{Array.isArray(i)&&i.length===0||e[0](i);return []}`, + ) + + // Parse empty columnar input to empty array + t->Assert.deepEqual(%raw(`[]`)->S.parseOrThrow(~to=schema), %raw(`[]`)) +}) + +test("Handles non-object schemas", t => { + let schema = S.compactColumns(S.unknown)->S.to(S.array(S.tuple2(S.string, S.int))) + t->Assert.throws( + () => { + %raw(`[["a"], [0]]`)->S.parseOrThrow(~to=schema) + }, + ~expectations={ + message: "[Sury] S.compactColumns supports only object schemas. Use S.compactColumns(S.unknown)->S.to(S.array(objectSchema)).", + }, + ) +}) + +test("Schema has format field set to compactColumns", t => { + let schema = S.compactColumns(S.unknown) + t->Assert.deepEqual((schema->S.untag).format, Some(CompactColumns)) +}) + +test("Typed input schema (non-unknown inputSchema branch)", t => { + // Exercises the non-unknown branch of itemSchema derivation, + // where input.schema.additionalItems is walked twice. + let schema = + S.compactColumns(S.string)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.string), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[["a", "b"], ["c", "d"]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": "c"}, {"foo": "b", "bar": "d"}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"foo": "a", "bar": "c"}, {"foo": "b", "bar": "d"}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", "b"], ["c", "d"]]`), + ) +}) + +test("Invalid field value reports error with path", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.int), + } + ), + ), + ) + + // Second row, bar column contains a non-int value. + t->U.assertThrowsMessage( + () => %raw(`[["a", "b"], [0, "not-an-int"]]`)->S.parseOrThrow(~to=schema), + `Failed at ["1"]["bar"]: Expected int32, received "not-an-int"`, + ) +}) + +test("Error path reporting for invalid column value", t => { + // Asserts that validation errors carry a useful path to the offending cell. + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.int), + } + ), + ), + ) + + t->U.assertThrowsMessage( + () => %raw(`[["a"], ["not-an-int"]]`)->S.parseOrThrow(~to=schema), + `Failed at ["0"]["bar"]: Expected int32, received "not-an-int"`, + ) +}) + +asyncTest("Async field schema", async t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string->S.transform(_ => {asyncParser: async i => i})), + "bar": s.matches(S.int), + } + ), + ), + ) + + t->Assert.deepEqual( + await %raw(`[["a", "b"], [0, 1]]`)->S.parseAsyncOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": 1}]`), + ) +}) + +test("Field schema with S.transform", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string->S.transform(_ => {parser: v => v->Js.String2.toUpperCase})), + "bar": s.matches(S.int), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[["a", "b"], [0, 1]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "A", "bar": 0}, {"foo": "B", "bar": 1}]`), + ) +}) + +test("Nullable field (null | undefined)", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.nullable(S.string)), + "bar": s.matches(S.int), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[["a", null], [0, 1]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": "a", "bar": 0}, {"foo": null, "bar": 1}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"foo": "a", "bar": 0}, {"foo": null, "bar": 1}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", null], [0, 1]]`), + ) +}) + +test("More than 2 fields", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "a": s.matches(S.string), + "b": s.matches(S.int), + "c": s.matches(S.bool), + "d": s.matches(S.float), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[["x", "y"], [1, 2], [true, false], [1.5, 2.5]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"a": "x", "b": 1, "c": true, "d": 1.5}, {"a": "y", "b": 2, "c": false, "d": 2.5}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"a": "x", "b": 1, "c": true, "d": 1.5}, {"a": "y", "b": 2, "c": false, "d": 2.5}]`)->S.decodeOrThrow( + ~from=schema, + ~to=S.unknown, + ), + %raw(`[["x", "y"], [1, 2], [true, false], [1.5, 2.5]]`), + ) +}) + +test("Single-field object", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "only": s.matches(S.string), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[["a", "b", "c"]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"only": "a"}, {"only": "b"}, {"only": "c"}]`), + ) + + t->Assert.deepEqual( + %raw(`[{"only": "a"}, {"only": "b"}, {"only": "c"}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["a", "b", "c"]]`), + ) +}) + +test("Union field", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.union([S.int->S.castToUnknown, S.string->S.castToUnknown])), + "bar": s.matches(S.bool), + } + ), + ), + ) + + t->Assert.deepEqual( + %raw(`[[1, "two"], [true, false]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"foo": 1, "bar": true}, {"foo": "two", "bar": false}]`), + ) +}) + +test("Field with S.refine", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "age": s.matches(S.int->S.refine(age => age >= 0, ~error="Age must be non-negative")), + "name": s.matches(S.string), + } + ), + ), + ) + + // Valid row parses successfully. + t->Assert.deepEqual( + %raw(`[[10, 20], ["alice", "bob"]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"age": 10, "name": "alice"}, {"age": 20, "name": "bob"}]`), + ) + + // Negative age triggers the refinement error. + t->U.assertThrowsMessage( + () => %raw(`[[-5], ["bad"]]`)->S.parseOrThrow(~to=schema), + `Failed at ["0"]["age"]: Age must be non-negative`, + ) +}) + +test("reverseConvertToJsonOrThrow with nullable field", t => { + S.enableJson() + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.nullAsOption(S.int)), + } + ), + ), + ) + + let value = %raw(`[{"foo": "a", "bar": 0}, {"foo": "b", "bar": undefined}]`) + t->Assert.deepEqual( + value->S.decodeOrThrow(~from=schema, ~to=S.json), + %raw(`[["a", "b"], [0, null]]`), + ) +}) + +test("Roundtrip: parse -> reverseConvert -> parse", t => { + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.nullAsOption(S.int)), + } + ), + ), + ) + + let columnar = %raw(`[["a", "b", "c"], [0, null, 2]]`) + let rows = columnar->S.parseOrThrow(~to=schema) + let roundtripped = rows->S.decodeOrThrow(~from=schema, ~to=S.unknown)->S.parseOrThrow(~to=schema) + t->Assert.deepEqual(rows, roundtripped) +}) + +test("reverseConvertToJsonOrThrow validates non-JSON-able unknown field values", t => { + S.enableJson() + let schema = + S.compactColumns(S.unknown)->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.unknown), + } + ), + ), + ) + + // JSON-compatible values round-trip through the columnar form unchanged. + t->Assert.deepEqual( + %raw(`[{"foo": "hello"}, {"foo": 42}]`)->S.decodeOrThrow(~from=schema, ~to=S.json), + %raw(`[["hello", 42]]`), + ) + + // Non-JSON-able values (e.g. bigint) are rejected by the json step that + // runs after the rows → columnar conversion. The path ["0"]["0"] points + // at column 0, row 0 of the columnar output (i.e. the "foo" value of the + // first row). + t->U.assertThrowsMessage( + () => %raw(`[{"foo": 123n}]`)->S.decodeOrThrow(~from=schema, ~to=S.json), + `Failed at ["0"]["0"]: Expected JSON, received 123n`, + ) +}) + +test("Json source with bigint field converts string↔bigint", t => { + S.enableJson() + let schema = + S.compactColumns(S.json)->S.to( + S.array( + S.schema(s => + { + "id": s.matches(S.string), + "amount": s.matches(S.bigint), + } + ), + ), + ) + + // Forward: json strings are converted to bigint via BigInt() + t->Assert.deepEqual( + %raw(`[["0", "1"], ["12345678901234567890", "98765432109876543210"]]`)->S.parseOrThrow(~to=schema), + %raw(`[{"id": "0", "amount": 12345678901234567890n}, {"id": "1", "amount": 98765432109876543210n}]`), + ) + + // Reverse: bigint values are converted back to strings for json + t->Assert.deepEqual( + %raw(`[{"id": "0", "amount": 12345678901234567890n}, {"id": "1", "amount": 98765432109876543210n}]`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`[["0", "1"], ["12345678901234567890", "98765432109876543210"]]`), + ) +}) + +test("Json source roundtrip with bigint", t => { + S.enableJson() + let schema = + S.compactColumns(S.json)->S.to( + S.array( + S.schema(s => + { + "id": s.matches(S.string), + "amount": s.matches(S.bigint), + } + ), + ), + ) + + let columnar = %raw(`[["0", "1"], ["12345678901234567890", "98765432109876543210"]]`) + let rows = columnar->S.parseOrThrow(~to=schema) + let roundtripped = rows->S.decodeOrThrow(~from=schema, ~to=S.unknown)->S.parseOrThrow(~to=schema) + t->Assert.deepEqual(rows, roundtripped) +}) diff --git a/packages/sury/tests/S_compile_test.res b/packages/sury/tests/S_compile_test.res index e90ca900a..1632e1327 100644 --- a/packages/sury/tests/S_compile_test.res +++ b/packages/sury/tests/S_compile_test.res @@ -7,47 +7,38 @@ let assertCode = (t, fn: 'a => 'b, code) => { } test("Schema with empty code optimised to use precompiled noop function", t => { - let schema = S.string - let fn = schema->S.compile(~input=Any, ~output=Unknown, ~mode=Sync, ~typeValidation=false) + let fn = S.decoder(~from=S.string, ~to=S.unknown) t->assertCode(fn, U.noopOpCode) }) test("Doesn't compile primitive unknown with assert output to noop", t => { - let schema = S.unknown - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=true) + let fn = S.decoder(~from=S.unknown, ~to=S.unknown->S.to(S.literal()->S.noValidation(true))) t->assertCode(fn, `i=>{return void 0}`) }) test("Doesn't compile to noop when primitive converted to json string", t => { - let schema = S.bool - let fn = schema->S.compile(~input=Any, ~output=JsonString, ~mode=Sync, ~typeValidation=false) + let fn = S.decoder(~from=S.bool, ~to=S.jsonString) t->assertCode(fn, `i=>{return ""+i}`) }) test("JsonString output with Async mode", t => { - let schema = S.string - let fn = schema->S.compile(~input=Any, ~output=JsonString, ~mode=Async, ~typeValidation=false) + let fn = S.asyncDecoder(~from=S.string, ~to=S.jsonString) t->assertCode(fn, `i=>{return Promise.resolve(JSON.stringify(i))}`) }) test("TypeValidation=false works with assert output", t => { - let schema = S.string - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=true) - t->assertCode(fn, `i=>{if(typeof i!=="string"){e[0](i)}return void 0}`) - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=false) + let fn = S.decoder(~from=S.unknown, ~to=S.string->S.to(S.literal()->S.noValidation(true))) + t->assertCode(fn, `i=>{typeof i==="string"||e[0](i);return void 0}`) + let fn = S.decoder(~from=S.string, ~to=S.string->S.to(S.literal()->S.noValidation(true))) t->assertCode(fn, `i=>{return void 0}`) }) test("Assert output with Async mode", t => { - let schema = S.string - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Async, ~typeValidation=true) - t->assertCode(fn, `i=>{if(typeof i!=="string"){e[0](i)}return Promise.resolve(void 0)}`) + let fn = S.asyncDecoder(~from=S.unknown, ~to=S.string->S.to(S.literal()->S.noValidation(true))) + t->assertCode(fn, `i=>{typeof i==="string"||e[0](i);return Promise.resolve(void 0)}`) }) -test("Immitate assert output with S.to and literal", t => { - let fn = - S.string - ->S.to(S.literal(true)->S.noValidation(true)) - ->S.compile(~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=true) - t->assertCode(fn, `i=>{if(typeof i!=="string"){e[0](i)}return true}`) +test("Immitate assert returning true with S.to and literal", t => { + let fn = S.decoder(~from=S.unknown, ~to=S.string->S.to(S.literal(true)->S.noValidation(true))) + t->assertCode(fn, `i=>{typeof i==="string"||e[0](i);return true}`) }) diff --git a/packages/sury/tests/S_date_test.res b/packages/sury/tests/S_date_test.res new file mode 100644 index 000000000..1179d3cb8 --- /dev/null +++ b/packages/sury/tests/S_date_test.res @@ -0,0 +1,181 @@ +open Ava + +test("Successfully parses valid Date", t => { + let date = Date.fromString("2024-01-01T00:00:00Z") + t->Assert.deepEqual(date->S.parseOrThrow(~to=S.date), date) +}) + +test("Successfully parses Date with time", t => { + let date = Date.fromString("2024-06-15T12:30:45.123Z") + t->Assert.deepEqual(date->S.parseOrThrow(~to=S.date), date) +}) + +test("Fails to parse string", t => { + t->U.assertThrowsMessage( + () => %raw(`"2024-01-01"`)->S.parseOrThrow(~to=S.date), + `Expected Date, received "2024-01-01"`, + ) +}) + +test("Fails to parse number", t => { + t->U.assertThrowsMessage( + () => %raw(`123`)->S.parseOrThrow(~to=S.date), + `Expected Date, received 123`, + ) +}) + +test("Fails to parse Invalid Date", t => { + t->U.assertThrowsMessage( + () => %raw(`new Date("invalid")`)->S.parseOrThrow(~to=S.date), + `Expected Date, received [object Date]`, + ) +}) + +test("Successfully reverse converts", t => { + let date = Date.fromString("2024-01-01T00:00:00Z") + t->Assert.deepEqual(date->S.decodeOrThrow(~from=S.date, ~to=S.unknown), date->Obj.magic) +}) + +test("Schema has instance tag and Date class", t => { + switch S.date->Obj.magic { + | S.Instance({class: c}) => t->Assert.is(c, %raw(`Date`)) + | _ => t->Assert.fail("Expected instance schema") + } +}) + +// S.string->S.to(S.date) conversion tests + +test("Successfully parses string to Date with S.to", t => { + let schema = S.string->S.to(S.date) + t->Assert.deepEqual( + "2024-01-01T00:00:00.000Z"->S.parseOrThrow(~to=schema), + Date.fromString("2024-01-01T00:00:00.000Z"), + ) +}) + +test("Successfully parses ISO string with fractional seconds to Date with S.to", t => { + let schema = S.string->S.to(S.date) + t->Assert.deepEqual( + "2024-06-15T12:30:45.123Z"->S.parseOrThrow(~to=schema), + Date.fromString("2024-06-15T12:30:45.123Z"), + ) +}) + +test("Fails to parse invalid string to Date with S.to", t => { + let schema = S.string->S.to(S.date) + t->U.assertThrowsMessage( + () => "invalid"->S.parseOrThrow(~to=schema), + `Expected Date, received [object Date]`, + ) +}) + +test("Successfully reverse converts string-to-date schema", t => { + let schema = S.string->S.to(S.date) + let date = Date.fromString("2024-01-01T00:00:00.000Z") + t->Assert.deepEqual( + date->S.decodeOrThrow(~from=schema, ~to=S.unknown), + "2024-01-01T00:00:00.000Z"->Obj.magic, + ) +}) + +// S.date->S.to(S.string) conversion tests + +test("Successfully converts Date to string with S.to", t => { + let schema = S.date->S.to(S.string) + let date = Date.fromString("2024-01-01T00:00:00.000Z") + t->Assert.deepEqual( + S.decoder1(schema)(date->Obj.magic), + "2024-01-01T00:00:00.000Z"->Obj.magic, + ) +}) + +test("Successfully reverse converts date-to-string schema", t => { + let schema = S.date->S.to(S.string) + t->Assert.deepEqual( + "2024-01-01T00:00:00.000Z"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + Date.fromString("2024-01-01T00:00:00.000Z")->Obj.magic, + ) +}) + +// JSON → Date conversion tests + +S.enableJson() +S.enableJsonString() + +test("Successfully decodes JSON string to Date", t => { + let date = Date.fromString("2024-01-01T00:00:00.000Z") + let decoder = S.decoder(~from=S.json, ~to=S.date) + t->Assert.deepEqual( + decoder(JSON.Encode.string("2024-01-01T00:00:00.000Z")), + date, + ) +}) + +test("Successfully decodes JSON object with date field", t => { + let dateSchema = S.schema(s => + { + "field": s.matches(S.date), + } + ) + let date = Date.fromString("2024-01-01T00:00:00.000Z") + let decoder = S.decoder(~from=S.json, ~to=dateSchema) + t->Assert.deepEqual( + decoder(%raw(`{"field":"2024-01-01T00:00:00.000Z"}`)), + {"field": date}, + ) +}) + +test("Fails to decode non-string JSON value to Date", t => { + let decoder = S.decoder(~from=S.json, ~to=S.date) + t->U.assertThrowsMessage( + () => decoder(%raw(`123`)), + `Expected string, received 123`, + ) +}) + +test("Fails to decode invalid date string from JSON", t => { + let decoder = S.decoder(~from=S.json, ~to=S.date) + t->U.assertThrowsMessage( + () => decoder(JSON.Encode.string("invalid")), + `Expected Date, received [object Date]`, + ) +}) + +test("Successfully decodes JSON string to Date via jsonString", t => { + let dateSchema = S.schema(s => + { + "field": s.matches(S.date), + } + ) + let date = Date.fromString("2024-01-01T00:00:00.000Z") + t->Assert.deepEqual( + `{"field":"2024-01-01T00:00:00.000Z"}`->S.decodeOrThrow(~from=S.jsonString, ~to=dateSchema), + {"field": date}, + ) +}) + +test("Successfully decodes JSON array of dates", t => { + let schema = S.array(S.date) + let decoder = S.decoder(~from=S.json, ~to=schema) + let d1 = Date.fromString("2024-01-01T00:00:00.000Z") + let d2 = Date.fromString("2024-06-15T12:30:45.123Z") + t->Assert.deepEqual( + decoder(%raw(`["2024-01-01T00:00:00.000Z", "2024-06-15T12:30:45.123Z"]`)), + [d1, d2], + ) +}) + +test("Successfully round-trips date through JSON", t => { + let dateSchema = S.schema(s => + { + "field": s.matches(S.date), + } + ) + let date = Date.fromString("2024-06-15T12:30:45.123Z") + let toJson = S.decoder(~from=dateSchema, ~to=S.json) + let fromJson = S.decoder(~from=S.json, ~to=dateSchema) + t->Assert.deepEqual( + fromJson(toJson({"field": date})), + {"field": date}, + ) +}) diff --git a/packages/sury/tests/S_deprecated_test.res b/packages/sury/tests/S_deprecated_test.res index d8caec4a8..19b32f8d4 100644 --- a/packages/sury/tests/S_deprecated_test.res +++ b/packages/sury/tests/S_deprecated_test.res @@ -30,5 +30,5 @@ test("Transforms don't remove deprecation", t => { test("Deprecated is a metadata only and doesn't make the field optional", t => { let schema = S.string->S.meta({deprecated: true, description: "Use number instead."}) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="string"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="string"||e[0](i);return i}`) }) diff --git a/packages/sury/tests/S_dict_test.res b/packages/sury/tests/S_dict_test.res index e779f514f..f34090ea9 100644 --- a/packages/sury/tests/S_dict_test.res +++ b/packages/sury/tests/S_dict_test.res @@ -10,38 +10,30 @@ module CommonWithNested = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected { [key: string]: string; }, received true`, ) }) test("Fails to parse nested", t => { let schema = factory() - t->U.assertThrows( - () => nestedInvalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`true`)}), - operation: Parse, - path: S.Path.fromArray(["key2"]), - }, + t->U.assertThrowsMessage( + () => nestedInvalidAny->S.parseOrThrow(~to=schema), + `Failed at ["key2"]: Expected string, received true`, ) }) @@ -51,7 +43,7 @@ module CommonWithNested = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}for(let v0 in i){try{let v2=i[v0];if(typeof v2!=="string"){e[1](v2)}}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}}return i}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[1](i);for(let v0 in i){try{let v1=i[v0];typeof v1==="string"||e[0](v1);}catch(v2){v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2}}return i}`, ) }) @@ -61,7 +53,7 @@ module CommonWithNested = { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}let v3={};for(let v0 in i){let v2;try{v2=e[1](i[v0]).catch(v1=>{if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1})}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v3[v0]=v2}return new Promise((v4,v5)=>{let v7=Object.keys(v3).length;for(let v0 in v3){v3[v0].then(v6=>{v3[v0]=v6;if(v7--===1){v4(v3)}},v5)}})}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[2](i);let v3={};for(let v0 in i){try{let v1;try{v1=e[0](i[v0]).catch(x=>e[1](x))}catch(x){e[1](x)}v3[v0]=v1.catch(v2=>{v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2})}catch(v2){v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2}}return new Promise((v4,v5)=>{let v7=Object.keys(v3).length;for(let v0 in v3){v3[v0].then(v6=>{v3[v0]=v6;if(v7--===1){v4(v3)}},v5)}})}`, ) }) @@ -74,12 +66,12 @@ module CommonWithNested = { }) test("Compiled serialize code snapshot with transform", t => { - let schema = S.dict(S.null(S.string)) + let schema = S.dict(S.nullAsOption(S.string)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{let v4={};for(let v0 in i){let v3;try{let v2=i[v0];if(v2===void 0){v2=null}v3=v2}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v4[v0]=v3}return v4}`, + `i=>{let v3={};for(let v0 in i){try{let v1=i[v0];if(v1===void 0){v1=null}else if(!(typeof v1==="string")){e[0](v1)}v3[v0]=v1}catch(v2){v2.path=\'["\'+v0+\'"]\'+v2.path;throw v2}}return v3}`, ) }) @@ -95,7 +87,7 @@ module CommonWithNested = { } test("Reverse child schema", t => { - let schema = S.dict(S.null(S.string)) + let schema = S.dict(S.nullAsOption(S.string)) t->U.assertEqualSchemas( schema->S.reverse, S.dict(S.union([S.string->S.castToUnknown, S.nullAsUnit->S.reverse]))->S.castToUnknown, @@ -106,7 +98,7 @@ test("Successfully parses dict with int keys", t => { let schema = S.dict(S.string) t->Assert.deepEqual( - %raw(`{1:"b",2:"d"}`)->S.parseOrThrow(schema), + %raw(`{1:"b",2:"d"}`)->S.parseOrThrow(~to=schema), Dict.fromArray([("1", "b"), ("2", "d")]), ) }) @@ -117,7 +109,7 @@ test("Applies operation for each item on serializing", t => { let schema = S.dict(S.jsonString->S.to(S.int)) t->Assert.deepEqual( - Dict.fromArray([("a", 1), ("b", 2)])->S.reverseConvertOrThrow(schema), + Dict.fromArray([("a", 1), ("b", 2)])->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{ "a": "1", "b": "2", @@ -126,15 +118,11 @@ test("Applies operation for each item on serializing", t => { }) test("Fails to serialize dict item", t => { - let schema = S.dict(S.string->S.refine(s => _ => s.fail("User error"))) - - t->U.assertThrows( - () => Dict.fromArray([("a", "aa"), ("b", "bb")])->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromLocation("a"), - }, + let schema = S.dict(S.string->S.refine(_ => false, ~error="User error")) + + t->U.assertThrowsMessage( + () => Dict.fromArray([("a", "aa"), ("b", "bb")])->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Failed at ["a"]: User error`, ) }) @@ -142,7 +130,7 @@ test("Successfully parses dict with optional items", t => { let schema = S.dict(S.option(S.string)) t->Assert.deepEqual( - %raw(`{"key1":"value1","key2":undefined}`)->S.parseOrThrow(schema), + %raw(`{"key1":"value1","key2":undefined}`)->S.parseOrThrow(~to=schema), Dict.fromArray([("key1", Some("value1")), ("key2", None)]), ) }) diff --git a/packages/sury/tests/S_failWithError_test.res b/packages/sury/tests/S_failWithError_test.res index f137f1e7d..da23f81ee 100644 --- a/packages/sury/tests/S_failWithError_test.res +++ b/packages/sury/tests/S_failWithError_test.res @@ -5,22 +5,19 @@ test("Keeps operation of the error passed to S.Error.throw", t => { S.string->S.transform(_ => { parser: _ => U.throwError( - U.error({ - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["a", "b"]), - }), + S.Error.make( + Custom({ + reason: "User error", + path: S.Path.fromArray(["a", "b"]), + }), + ), ), }), ) - t->U.assertThrows( - () => ["Hello world!"]->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["0", "a", "b"]), - }, + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + `Failed at ["0"]["a"]["b"]: User error`, ) }) @@ -28,17 +25,17 @@ test("Works with failing outside of the parser", t => { let schema = S.object(s => s.field( "field", - S.string->S.transform(s => s.fail("User error", ~path=S.Path.fromArray(["a", "b"]))), + S.string->S.transform( + s => { + s.fail("User error", ~path=S.Path.fromArray(["a", "b"])) + }, + ), ) ) - t->U.assertThrows( - () => ["Hello world!"]->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromLocation("field")->S.Path.concat(S.Path.fromArray(["a", "b"])), - }, + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + `Failed at ["field"]["a"]["b"]: User error`, ) }) @@ -46,18 +43,18 @@ test("Works with failing outside of the parser inside of array", t => { let schema = S.object(s => s.field( "field", - S.array(S.string->S.transform(s => s.fail("User error", ~path=S.Path.fromArray(["a", "b"])))), + S.array( + S.string->S.transform( + s => { + s.fail("User error", ~path=S.Path.fromArray(["a", "b"])) + }, + ), + ), ) ) - t->U.assertThrows( - () => ["Hello world!"]->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromLocation("field") - ->S.Path.concat(S.Path.dynamic) - ->S.Path.concat(S.Path.fromArray(["a", "b"])), - }, + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + `Failed at ["field"][]["a"]["b"]: User error`, ) }) diff --git a/packages/sury/tests/S_float_test.res b/packages/sury/tests/S_float_test.res index c8170a92d..a5da126da 100644 --- a/packages/sury/tests/S_float_test.res +++ b/packages/sury/tests/S_float_test.res @@ -9,26 +9,22 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected number, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { @@ -37,7 +33,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="number"||Number.isNaN(i)){e[0](i)}return i}`, + `i=>{typeof i==="number"&&!Number.isNaN(i)||e[0](i);return i}`, ) }) @@ -62,18 +58,14 @@ module Common = { test("Successfully parses number with a fractional part", t => { let schema = S.float - t->Assert.deepEqual(%raw(`123.123`)->S.parseOrThrow(schema), 123.123) + t->Assert.deepEqual(%raw(`123.123`)->S.parseOrThrow(~to=schema), 123.123) }) test("Fails to parse NaN", t => { let schema = S.float - t->U.assertThrows( - () => %raw(`NaN`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`NaN`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`NaN`)->S.parseOrThrow(~to=schema), + `Expected number, received NaN`, ) }) diff --git a/packages/sury/tests/S_fromJSONSchema.res.mjs b/packages/sury/tests/S_fromJSONSchema.res.mjs deleted file mode 100644 index 5d76afeea..000000000 --- a/packages/sury/tests/S_fromJSONSchema.res.mjs +++ /dev/null @@ -1,496 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as S from "../src/S.res.mjs"; -import Ava from "ava"; - -function roundTrip(schema) { - return S.fromJSONSchema(S.toJSONSchema(schema)); -} - -function jsonRoundTrip(js) { - return S.toJSONSchema(S.fromJSONSchema(js)); -} - -function parse(schema, value) { - return S.parseOrThrow(value, schema); -} - -function eq(a, b) { - return JSON.stringify(a) === JSON.stringify(b); -} - -Ava("fromJSONSchema: string", t => { - let js_type = "string"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo", schema), "foo"); - t.throws(() => S.parseOrThrow(123, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: number", t => { - let js_type = "number"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(1.5, schema), 1.5); - t.throws(() => S.parseOrThrow("foo", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: integer", t => { - let js_type = "integer"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(42, schema), 42); - t.throws(() => S.parseOrThrow(1.5, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: boolean", t => { - let js_type = "boolean"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(true, schema), true); - t.throws(() => S.parseOrThrow(0, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: null", t => { - let js_type = "null"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(null, schema), null); - t.throws(() => S.parseOrThrow(0, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: const", t => { - let js_const = "foo"; - let js = { - const: js_const - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo", schema), "foo"); - t.throws(() => S.parseOrThrow("bar", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: enum", t => { - let js_enum = [ - "a", - "b", - "c" - ]; - let js = { - enum: js_enum - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("a", schema), "a"); - t.throws(() => S.parseOrThrow("z", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: array of string", t => { - let js_type = "array"; - let js_items = { - type: "string" - }; - let js = { - type: js_type, - items: js_items - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow([ - "a", - "b" - ], schema), [ - "a", - "b" - ]); - t.throws(() => S.parseOrThrow([ - 1, - 2 - ], schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: array with minItems/maxItems", t => { - let js_type = "array"; - let js_items = { - type: "number" - }; - let js_maxItems = 3; - let js_minItems = 2; - let js = { - type: js_type, - items: js_items, - maxItems: js_maxItems, - minItems: js_minItems - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow([ - 1, - 2 - ], schema), [ - 1, - 2 - ]); - t.throws(() => S.parseOrThrow([1], schema)); - t.throws(() => S.parseOrThrow([ - 1, - 2, - 3, - 4 - ], schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: tuple", t => { - let js_type = "array"; - let js_items = [ - { - type: "string" - }, - { - type: "number" - } - ]; - let js_maxItems = 2; - let js_minItems = 2; - let js = { - type: js_type, - items: js_items, - maxItems: js_maxItems, - minItems: js_minItems - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow([ - "a", - 1 - ], schema), [ - "a", - 1 - ]); - t.throws(() => S.parseOrThrow([ - 1, - "a" - ], schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: object with properties", t => { - let js_type = "object"; - let js_required = ["foo"]; - let js_properties = Object.fromEntries([ - [ - "foo", - { - type: "string" - } - ], - [ - "bar", - { - type: "number" - } - ] - ]); - let js = { - type: js_type, - required: js_required, - properties: js_properties - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow({ - foo: "hi", - bar: 1 - }, schema), { - foo: "hi", - bar: 1 - }); - t.throws(() => S.parseOrThrow({ - bar: 1 - }, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: object with additionalProperties false", t => { - let js_type = "object"; - let js_properties = Object.fromEntries([[ - "foo", - { - type: "string" - } - ]]); - let js_additionalProperties = false; - let js = { - type: js_type, - properties: js_properties, - additionalProperties: js_additionalProperties - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow({ - foo: "hi" - }, schema), { - foo: "hi" - }); - t.throws(() => S.parseOrThrow({ - foo: "hi", - bar: 1 - }, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: object with additionalProperties true", t => { - let js_type = "object"; - let js_additionalProperties = true; - let js = { - type: js_type, - additionalProperties: js_additionalProperties - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow({ - foo: 1, - bar: 2 - }, schema), { - foo: 1, - bar: 2 - }); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: anyOf", t => { - let js_anyOf = [ - { - type: "string" - }, - { - type: "number" - } - ]; - let js = { - anyOf: js_anyOf - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("hi", schema), "hi"); - t.deepEqual(S.parseOrThrow(1, schema), 1); - t.throws(() => S.parseOrThrow(true, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: oneOf", t => { - let js_oneOf = [ - { - type: "string" - }, - { - type: "number" - } - ]; - let js = { - oneOf: js_oneOf - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("hi", schema), "hi"); - t.deepEqual(S.parseOrThrow(1, schema), 1); - t.throws(() => S.parseOrThrow(true, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: allOf", t => { - let js_allOf = [ - { - type: "number", - minimum: 0 - }, - { - type: "number", - maximum: 10 - } - ]; - let js = { - allOf: js_allOf - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(5, schema), 5); - t.throws(() => S.parseOrThrow(20, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: not", t => { - let js_not = { - type: "string" - }; - let js = { - not: js_not - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow(1, schema), 1); - t.throws(() => S.parseOrThrow("hi", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: nullable true", t => { - let js_type = "string"; - let js_nullable = true; - let js = { - type: js_type, - nullable: js_nullable - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("hi", schema), "hi"); - t.deepEqual(S.parseOrThrow(null, schema), null); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: nullable false", t => { - let js_type = "string"; - let js_nullable = false; - let js = { - type: js_type, - nullable: js_nullable - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("hi", schema), "hi"); - t.throws(() => S.parseOrThrow(null, schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: string format email", t => { - let js_type = "string"; - let js_format = "email"; - let js = { - type: js_type, - format: js_format - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo@bar.com", schema), "foo@bar.com"); - t.throws(() => S.parseOrThrow("not-an-email", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: string format uuid", t => { - let js_type = "string"; - let js_format = "uuid"; - let js = { - type: js_type, - format: js_format - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("123e4567-e89b-12d3-a456-426614174000", schema), "123e4567-e89b-12d3-a456-426614174000"); - t.throws(() => S.parseOrThrow("not-a-uuid", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: string format date-time", t => { - let js_type = "string"; - let js_format = "date-time"; - let js = { - type: js_type, - format: js_format - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("2020-01-01T00:00:00Z", schema), "2020-01-01T00:00:00Z"); - t.throws(() => S.parseOrThrow("not-a-date", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: string pattern", t => { - let js_type = "string"; - let js_pattern = "^foo$"; - let js = { - type: js_type, - pattern: js_pattern - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo", schema), "foo"); - t.throws(() => S.parseOrThrow("bar", schema)); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: title, description, deprecated, examples", t => { - let js_type = "string"; - let js_title = "title"; - let js_description = "desc"; - let js_deprecated = true; - let js_examples = [ - "a", - "b" - ]; - let js = { - type: js_type, - title: js_title, - description: js_description, - deprecated: js_deprecated, - examples: js_examples - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(schema.title, "title"); - t.deepEqual(schema.description, "desc"); - t.deepEqual(schema.deprecated, true); - t.deepEqual(schema.examples, [ - "a", - "b" - ]); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: empty schema is any", t => { - let js = {}; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo", schema), "foo"); - t.deepEqual(S.parseOrThrow(1, schema), 1); - t.deepEqual(S.parseOrThrow(true, schema), true); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: unknown type is any", t => { - let js_type = "unknownType"; - let js = { - type: js_type - }; - let schema = S.fromJSONSchema(js); - t.deepEqual(S.parseOrThrow("foo", schema), "foo"); - t.deepEqual(S.parseOrThrow(1, schema), 1); - t.deepEqual(S.parseOrThrow(true, schema), true); - t.deepEqual(S.toJSONSchema(S.fromJSONSchema(js)), js); -}); - -Ava("fromJSONSchema: round-trip for string schema", t => { - let round = S.fromJSONSchema(S.toJSONSchema(S.string)); - t.deepEqual(S.parseOrThrow("foo", round), "foo"); - t.throws(() => S.parseOrThrow(1, round)); - t.deepEqual(S.toJSONSchema(round), S.toJSONSchema(S.string)); -}); - -Ava("fromJSONSchema: round-trip for object schema", t => { - let orig = S.object(s => s.f("foo", S.string)); - let round = S.fromJSONSchema(S.toJSONSchema(orig)); - t.deepEqual(S.parseOrThrow({ - foo: "bar" - }, round), { - foo: "bar" - }); - t.throws(() => S.parseOrThrow({ - foo: 1 - }, round)); - t.deepEqual(S.toJSONSchema(round), S.toJSONSchema(orig)); -}); - -export { - roundTrip, - jsonRoundTrip, - parse, - eq, -} -/* Not a pure module */ diff --git a/packages/sury/tests/S_fromJSONSchema.res b/packages/sury/tests/S_fromJSONSchema_test.res similarity index 82% rename from packages/sury/tests/S_fromJSONSchema.res rename to packages/sury/tests/S_fromJSONSchema_test.res index e9c38f292..406c38469 100644 --- a/packages/sury/tests/S_fromJSONSchema.res +++ b/packages/sury/tests/S_fromJSONSchema_test.res @@ -1,6 +1,8 @@ open Ava open JSONSchema +S.enableJson() + // Helper for round-trip: S -> toJSONSchema -> fromJSONSchema -> S let roundTrip = schema => schema->S.toJSONSchema->S.fromJSONSchema @@ -8,7 +10,7 @@ let roundTrip = schema => schema->S.toJSONSchema->S.fromJSONSchema let jsonRoundTrip = js => js->S.fromJSONSchema->S.toJSONSchema // Helper for parsing -let parse = (schema, value) => value->S.parseOrThrow(schema)->Obj.magic +let parse = (schema, value) => value->S.parseOrThrow(~to=schema)->Obj.magic // Helper for deepEqual let eq = (a, b) => JSON.stringify(a) == JSON.stringify(b) @@ -36,7 +38,10 @@ test("fromJSONSchema: integer", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, 42), 42) t->Assert.throws(() => parse(schema, 1.5)) - t->Assert.deepEqual(jsonRoundTrip(js), js) + t->Assert.deepEqual( + jsonRoundTrip(js), + {type_: Arrayable.single(#integer), minimum: -2147483648., maximum: 2147483647.}, + ) }) test("fromJSONSchema: boolean", t => { @@ -62,7 +67,8 @@ test("fromJSONSchema: const", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, "foo"), "foo") t->Assert.throws(() => parse(schema, "bar")) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // toJSONSchema adds type for literal schemas + t->Assert.deepEqual(jsonRoundTrip(js), {...js, type_: Arrayable.single(#string)}) }) test("fromJSONSchema: enum", t => { @@ -176,7 +182,8 @@ test("fromJSONSchema: oneOf", t => { t->Assert.deepEqual(parse(schema, "hi"), "hi") t->Assert.deepEqual(parse(schema, 1), 1) t->Assert.throws(() => parse(schema, true)) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // refine-based oneOf can't round-trip the structural info + t->Assert.deepEqual(jsonRoundTrip(js), {}) }) test("fromJSONSchema: allOf", t => { @@ -189,7 +196,8 @@ test("fromJSONSchema: allOf", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, 5), 5) t->Assert.throws(() => parse(schema, 20)) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // refine-based allOf can't round-trip the structural info + t->Assert.deepEqual(jsonRoundTrip(js), {}) }) test("fromJSONSchema: not", t => { @@ -197,7 +205,8 @@ test("fromJSONSchema: not", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, 1), 1) t->Assert.throws(() => parse(schema, "hi")) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // refine-based not can't round-trip the structural info + t->Assert.deepEqual(jsonRoundTrip(js), {}) }) // 6. Nullable @@ -207,7 +216,11 @@ test("fromJSONSchema: nullable true", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, "hi"), "hi") t->Assert.deepEqual(parse(schema, %raw("null")), %raw("null")) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // toJSONSchema uses anyOf style for nullable + t->Assert.deepEqual( + jsonRoundTrip(js), + {anyOf: [Schema({type_: Arrayable.single(#string)}), Schema({type_: Arrayable.single(#null)})]}, + ) }) test("fromJSONSchema: nullable false", t => { @@ -215,7 +228,8 @@ test("fromJSONSchema: nullable false", t => { let schema = S.fromJSONSchema(js) t->Assert.deepEqual(parse(schema, "hi"), "hi") t->Assert.throws(() => parse(schema, %raw("null"))) - t->Assert.deepEqual(jsonRoundTrip(js), js) + // nullable: false is the default, so toJSONSchema omits it + t->Assert.deepEqual(jsonRoundTrip(js), {type_: Arrayable.single(#string)}) }) // 7. Format @@ -247,6 +261,29 @@ test("fromJSONSchema: string format date-time", t => { t->Assert.deepEqual(jsonRoundTrip(js), js) }) +test("Round-trip S.string->S.to(S.date) through toJSONSchema/fromJSONSchema", t => { + let schema = S.string->S.to(S.date) + let js = schema->S.toJSONSchema + t->Assert.deepEqual(js, %raw(`{"type": "string", "format": "date-time"}`)) + // fromJSONSchema then toJSONSchema should preserve the format + t->Assert.deepEqual(js->S.fromJSONSchema->S.toJSONSchema, js) +}) + +// All format schemas (including date-time) compose with sibling constraints. +test("fromJSONSchema: format date-time composes with sibling minLength/maxLength", t => { + let js = { + type_: Arrayable.single(#string), + format: "date-time", + minLength: 10, + maxLength: 30, + } + let schema = S.fromJSONSchema(js) + // A valid ISO datetime within length bounds parses. + t->Assert.deepEqual(parse(schema, "2020-01-01T00:00:00Z"), "2020-01-01T00:00:00Z"->Obj.magic) + // A non-ISO string still fails — the datetime validator runs. + t->Assert.throws(() => parse(schema, "not-a-date")) +}) + test("fromJSONSchema: string pattern", t => { let js = {type_: Arrayable.single(#string), pattern: "^foo$"} let schema = S.fromJSONSchema(js) @@ -284,13 +321,9 @@ test("fromJSONSchema: empty schema is any", t => { t->Assert.deepEqual(jsonRoundTrip(js), js) }) -test("fromJSONSchema: unknown type is any", t => { +test("fromJSONSchema: unknown type throws", t => { let js = {type_: Arrayable.single((Obj.magic("unknownType"): typeName))} - let schema = S.fromJSONSchema(js) - t->Assert.deepEqual(parse(schema, "foo"), "foo") - t->Assert.deepEqual(parse(schema, 1), 1) - t->Assert.deepEqual(parse(schema, true), true) - t->Assert.deepEqual(jsonRoundTrip(js), js) + t->Assert.throws(() => S.fromJSONSchema(js), ~expectations={message: "[Sury] Unknown JSON Schema type: unknownType"}) }) // 10. Round-trip S -> toJSONSchema -> fromJSONSchema -> S diff --git a/packages/sury/tests/S_inline.res b/packages/sury/tests/S_inline.res index 44158d4c1..693720eec 100644 --- a/packages/sury/tests/S_inline.res +++ b/packages/sury/tests/S_inline.res @@ -312,8 +312,8 @@ // }) // test("Supports Null", t => { -// let schema = S.null(S.string) -// t->Assert.deepEqual(schema->S.inline, `S.null(S.string)`) +// let schema = S.nullAsOption(S.string) +// t->Assert.deepEqual(schema->S.inline, `S.nullAsOption(S.string)`) // }) // test("Supports Array", t => { @@ -566,10 +566,10 @@ // // }) // // test("Supports Null schemas in union", t => { -// // let schema = S.union([S.null(S.literalVariant(String("123"), 123.)), S.null(S.float)]) +// // let schema = S.union([S.nullAsOption(S.literalVariant(String("123"), 123.)), S.nullAsOption(S.float)]) // // let schemaInlineResult = S.union([ -// // S.null(S.literal(String("123")))->S.shape(v => #NullOf123(v)), -// // S.null(S.float)->S.shape(v => #NullOfFloat(v)), +// // S.nullAsOption(S.literal(String("123")))->S.shape(v => #NullOf123(v)), +// // S.nullAsOption(S.float)->S.shape(v => #NullOfFloat(v)), // // ]) // // schemaInlineResult->( @@ -583,7 +583,7 @@ // // t->Assert.deepEqual( // // schema->S.inline, -// // `S.union([S.null(S.literal(String("123")))->S.shape(v => #"NullOf123"(v)), S.null(S.float)->S.shape(v => #"NullOfFloat"(v))])`, +// // `S.union([S.nullAsOption(S.literal(String("123")))->S.shape(v => #"NullOf123"(v)), S.nullAsOption(S.float)->S.shape(v => #"NullOfFloat"(v))])`, // // (), // // ) // // }) diff --git a/packages/sury/tests/S_int_test.res b/packages/sury/tests/S_int_test.res index 744277d34..4413dd621 100644 --- a/packages/sury/tests/S_int_test.res +++ b/packages/sury/tests/S_int_test.res @@ -9,26 +9,22 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected int32, received 123.45`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { @@ -37,7 +33,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="number"||i>2147483647||i<-2147483648||i%1!==0){e[0](i)}return i}`, + `i=>{typeof i==="number"&&i<=2147483647&&i>=-2147483648&&i%1===0||e[0](i);return i}`, ) }) @@ -62,40 +58,28 @@ module Common = { test("Fails to parse int when JSON is a number bigger than +2^31", t => { let schema = S.int - t->U.assertThrows( - () => %raw(`2147483648`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`2147483648`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`2147483648`)->S.parseOrThrow(~to=schema), + `Expected int32, received 2147483648`, ) - t->Assert.deepEqual(%raw(`2147483647`)->S.parseOrThrow(schema), 2147483647) + t->Assert.deepEqual(%raw(`2147483647`)->S.parseOrThrow(~to=schema), 2147483647) }) test("Fails to parse int when JSON is a number lower than -2^31", t => { let schema = S.int - t->U.assertThrows( - () => %raw(`-2147483649`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`-2147483649`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`-2147483649`)->S.parseOrThrow(~to=schema), + `Expected int32, received -2147483649`, ) - t->Assert.deepEqual(%raw(`-2147483648`)->S.parseOrThrow(schema), -2147483648) + t->Assert.deepEqual(%raw(`-2147483648`)->S.parseOrThrow(~to=schema), -2147483648) }) test("Fails to parse NaN", t => { let schema = S.int - t->U.assertThrows( - () => %raw(`NaN`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`NaN`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`NaN`)->S.parseOrThrow(~to=schema), + `Expected int32, received NaN`, ) }) diff --git a/packages/sury/tests/S_isAsync_test.res b/packages/sury/tests/S_isAsync_test.res index f1a5a7754..6830ef01e 100644 --- a/packages/sury/tests/S_isAsync_test.res +++ b/packages/sury/tests/S_isAsync_test.res @@ -1,5 +1,7 @@ open Ava +S.enableJsonString() + test("Returns false for schema with NoOperation", t => { t->Assert.is(S.unknown->S.isAsync, false) }) @@ -17,7 +19,7 @@ test("Returns true for async schema", t => { test("Returns true for async schema after running a serializer", t => { let schema = S.string->S.transform(_ => {asyncParser: i => Promise.resolve(i), serializer: i => i}) - t->Assert.deepEqual("abc"->S.reverseConvertToJsonOrThrow(schema), %raw(`"abc"`)) + t->Assert.deepEqual("abc"->S.decodeOrThrow(~from=schema, ~to=S.json), %raw(`"abc"`)) t->Assert.is(schema->S.isAsync, true) }) diff --git a/packages/sury/tests/S_isoDateTime_test.res b/packages/sury/tests/S_isoDateTime_test.res new file mode 100644 index 000000000..7a00b190f --- /dev/null +++ b/packages/sury/tests/S_isoDateTime_test.res @@ -0,0 +1,63 @@ +open Ava + +S.enableIsoDateTime() + +test("Successfully parses valid data", t => { + let schema = S.isoDateTime + + t->Assert.deepEqual("2020-01-01T00:00:00Z"->S.parseOrThrow(~to=schema), "2020-01-01T00:00:00Z") + t->Assert.deepEqual( + "2020-01-01T00:00:00.123Z"->S.parseOrThrow(~to=schema), + "2020-01-01T00:00:00.123Z", + ) + t->Assert.deepEqual( + "2020-01-01T00:00:00.123456Z"->S.parseOrThrow(~to=schema), + "2020-01-01T00:00:00.123456Z", + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[2](i);e[0].test(i)||e[1](i);return i}`, + ) +}) + +test("Fails to parse non UTC date string", t => { + let schema = S.isoDateTime + + t->U.assertThrowsMessage( + () => "Thu Apr 20 2023 10:45:48 GMT+0400"->S.parseOrThrow(~to=schema), + `Invalid datetime string! Expected UTC`, + ) +}) + +test("Fails to parse UTC date with timezone offset", t => { + let schema = S.isoDateTime + + t->U.assertThrowsMessage( + () => "2020-01-01T00:00:00+02:00"->S.parseOrThrow(~to=schema), + `Invalid datetime string! Expected UTC`, + ) +}) + +test("Successfully serializes valid value", t => { + let schema = S.isoDateTime + + t->Assert.deepEqual( + "2020-01-01T00:00:00.123Z"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + %raw(`"2020-01-01T00:00:00.123Z"`), + ) +}) + +test("Can be combined with S.to(S.date) for string-to-Date decoding", t => { + let schema = S.isoDateTime->S.to(S.date) + + t->Assert.deepEqual( + "2020-01-01T00:00:00.123Z"->S.parseOrThrow(~to=schema), + Date.fromString("2020-01-01T00:00:00.123Z"), + ) + t->U.assertThrowsMessage( + () => "not-a-date"->S.parseOrThrow(~to=schema), + `Invalid datetime string! Expected UTC`, + ) +}) diff --git a/packages/sury/tests/S_jsonString_test.res b/packages/sury/tests/S_jsonString_test.res index 113eda0df..68247ab00 100644 --- a/packages/sury/tests/S_jsonString_test.res +++ b/packages/sury/tests/S_jsonString_test.res @@ -5,16 +5,16 @@ S.enableJsonString() test("Parses JSON string without transformation", t => { let schema = S.jsonString - t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(schema), `"Foo"`) + t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(~to=schema), `"Foo"`) t->U.assertThrowsMessage( - () => `Foo`->S.parseOrThrow(schema), - `Failed parsing: Expected JSON string, received "Foo"`, + () => `Foo`->S.parseOrThrow(~to=schema), + `Expected JSON string, received "Foo"`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}try{JSON.parse(i)}catch(t){e[1](i)}return i}`, + `i=>{typeof i==="string"||e[1](i);try{JSON.parse(i)}catch(t){e[0](i)}return i}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#Convert) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) @@ -23,28 +23,25 @@ test("Parses JSON string without transformation", t => { test("Parses JSON string to string", t => { let schema = S.jsonString->S.to(S.string) - t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(schema), "Foo") + t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(~to=schema), "Foo") t->U.assertThrowsMessage( - () => `Foo`->S.parseOrThrow(schema), - `Failed parsing: Expected JSON string, received "Foo"`, - ) - t->U.assertThrowsMessage( - () => `123`->S.parseOrThrow(schema), - `Failed parsing: Expected string, received 123`, + () => `Foo`->S.parseOrThrow(~to=schema), + `Expected JSON string, received "Foo"`, ) + t->U.assertThrowsMessage(() => `123`->S.parseOrThrow(~to=schema), `Expected string, received 123`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="string"){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="string"||e[1](v0);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - "i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}return v0}", + `i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="string"||e[1](v0);return v0}`, ) - t->Assert.deepEqual(`"Foo`->S.reverseConvertOrThrow(schema), %raw(`'"\\"Foo"'`)) + t->Assert.deepEqual(`"Foo`->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`'"\\"Foo"'`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return JSON.stringify(i)}`) }) @@ -52,52 +49,51 @@ test("Parses JSON string to string", t => { test("Parses JSON string to string literal", t => { let schema = S.jsonString->S.to(S.literal("Foo")) - t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(schema), "Foo") - t->U.assertThrowsMessage( - () => `123`->S.parseOrThrow(schema), - `Failed parsing: Expected "Foo", received "123"`, - ) + t->Assert.deepEqual(`"Foo"`->S.parseOrThrow(~to=schema), "Foo") + t->U.assertThrowsMessage(() => `123`->S.parseOrThrow(~to=schema), `Expected ""Foo"", received "123"`) + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Expected JSON string, received 123`) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="\\"Foo\\""||e[0](i);return "Foo"}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="\\"Foo\\""||e[0](i);return "Foo"}`, + ) t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{i==="\\"Foo\\""||e[0](i);return "Foo"}`) - t->Assert.deepEqual(`Foo`->S.reverseConvertOrThrow(schema), %raw(`'"Foo"'`)) + t->Assert.deepEqual(`Foo`->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`'"Foo"'`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!=="Foo"){e[0](i)}return "\\"Foo\\""}`, + `i=>{i==="Foo"||e[0](i);return "\\"Foo\\""}`, ) let schema = S.jsonString->S.to(S.literal("\"Foo")) - t->Assert.deepEqual(`"Foo`->S.reverseConvertOrThrow(schema), %raw(`'"\\"Foo"'`)) + t->Assert.deepEqual(`"Foo`->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`'"\\"Foo"'`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!=="\\"Foo"){e[0](i)}return "\\"\\\\\\"Foo\\""}`, + `i=>{i==="\\"Foo"||e[0](i);return "\\"\\\\\\"Foo\\""}`, ) }) test("Parses JSON string to float", t => { let schema = S.jsonString->S.to(S.float) - t->Assert.deepEqual(`1.23`->S.parseOrThrow(schema), 1.23) - t->U.assertThrowsMessage( - () => `null`->S.parseOrThrow(schema), - `Failed parsing: Expected number, received null`, - ) + t->Assert.deepEqual(`1.23`->S.parseOrThrow(~to=schema), 1.23) + t->U.assertThrowsMessage(() => `null`->S.parseOrThrow(~to=schema), `Expected number, received null`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="number"||Number.isNaN(v0)){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="number"&&!Number.isNaN(v0)||e[1](v0);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - "i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}return v0}", + `i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="number"&&!Number.isNaN(v0)||e[1](v0);return v0}`, ) - t->Assert.deepEqual(1.23->S.reverseConvertOrThrow(schema), %raw(`"1.23"`)) + t->Assert.deepEqual(1.23->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1.23"`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) }) @@ -105,40 +101,38 @@ test("Parses JSON string to float", t => { test("Parses JSON string to float literal", t => { let schema = S.jsonString->S.to(S.literal(1.23)) - t->Assert.deepEqual(`1.23`->S.parseOrThrow(schema), 1.23) - t->U.assertThrowsMessage( - () => `null`->S.parseOrThrow(schema), - `Failed parsing: Expected 1.23, received "null"`, - ) + t->Assert.deepEqual(`1.23`->S.parseOrThrow(~to=schema), 1.23) + t->U.assertThrowsMessage(() => `null`->S.parseOrThrow(~to=schema), `Expected "1.23", received "null"`) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="1.23"||e[0](i);return 1.23}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="1.23"||e[0](i);return 1.23}`, + ) - t->Assert.deepEqual(1.23->S.reverseConvertOrThrow(schema), %raw(`"1.23"`)) + t->Assert.deepEqual(1.23->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"1.23"`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==1.23){e[0](i)}return "1.23"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===1.23||e[0](i);return "1.23"}`) }) test("Parses JSON string to bool", t => { let schema = S.jsonString->S.to(S.bool) - t->Assert.deepEqual(`true`->S.parseOrThrow(schema), true) - t->U.assertThrowsMessage( - () => `"t"`->S.parseOrThrow(schema), - `Failed parsing: Expected boolean, received "t"`, - ) + t->Assert.deepEqual(`true`->S.parseOrThrow(~to=schema), true) + t->U.assertThrowsMessage(() => `"t"`->S.parseOrThrow(~to=schema), `Expected boolean, received "t"`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="boolean"){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="boolean"||e[1](v0);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - "i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}return v0}", + `i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="boolean"||e[1](v0);return v0}`, ) - t->Assert.deepEqual(true->S.reverseConvertOrThrow(schema), %raw(`"true"`)) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) }) @@ -146,41 +140,39 @@ test("Parses JSON string to bool", t => { test("Parses JSON string to bool literal", t => { let schema = S.jsonString->S.to(S.literal(true)) - t->Assert.deepEqual(`true`->S.parseOrThrow(schema), true) - t->U.assertThrowsMessage( - () => `null`->S.parseOrThrow(schema), - `Failed parsing: Expected true, received "null"`, - ) + t->Assert.deepEqual(`true`->S.parseOrThrow(~to=schema), true) + t->U.assertThrowsMessage(() => `null`->S.parseOrThrow(~to=schema), `Expected "true", received "null"`) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="true"||e[0](i);return true}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="true"||e[0](i);return true}`, + ) - t->Assert.deepEqual(true->S.reverseConvertOrThrow(schema), %raw(`"true"`)) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==true){e[0](i)}return "true"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===true||e[0](i);return "true"}`) }) test("Parses JSON string to bigint", t => { let schema = S.jsonString->S.to(S.bigint) - t->U.assertThrowsMessage( - () => `123`->S.parseOrThrow(schema), - `Failed parsing: bigint is not valid JSON`, - ) + t->U.assertThrowsMessage(() => `123`->S.parseOrThrow(~to=schema), `Expected string, received 123`) - // t->Assert.deepEqual(`123`->S.parseOrThrow(schema), 123n) + t->Assert.deepEqual(`"123"`->S.parseOrThrow(~to=schema), 123n) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#Parse, - // `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="boolean"){e[2](v0)}return v0}`, - // ) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#Convert, - // "i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}return v0}", - // ) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[3](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="string"||e[2](v0);let v1;try{v1=BigInt(v0)}catch(_){e[1](v0)}return v1}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#Convert, + `i=>{let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="string"||e[2](v0);let v1;try{v1=BigInt(v0)}catch(_){e[1](v0)}return v1}`, + ) - t->Assert.deepEqual(123n->S.reverseConvertOrThrow(schema), %raw(`"\"123\""`)) + t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"\"123\""`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return "\\""+i+"\\""}`) }) @@ -188,36 +180,38 @@ test("Parses JSON string to bigint", t => { test("Parses JSON string to bigint literal", t => { let schema = S.jsonString->S.to(S.literal(123n)) - t->Assert.deepEqual(`"123"`->S.parseOrThrow(schema), 123n) - t->U.assertThrowsMessage( - () => `123`->S.parseOrThrow(schema), - `Failed parsing: Expected 123n, received "123"`, - ) + t->Assert.deepEqual(`"123"`->S.parseOrThrow(~to=schema), 123n) + t->U.assertThrowsMessage(() => `123`->S.parseOrThrow(~to=schema), `Expected ""123"", received "123"`) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="\\"123\\""||e[0](i);return 123n}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="\\"123\\""||e[0](i);return 123n}`, + ) - t->Assert.deepEqual(123n->S.reverseConvertOrThrow(schema), %raw(`'"123"'`)) + t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`'"123"'`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!==123n){e[0](i)}return "\\"123\\""}`, + `i=>{i===123n||e[0](i);return "\\"123\\""}`, ) }) test("Parses JSON string to symbol literal", t => { let symbol = %raw(`Symbol("foo")`) + // TODO: Test that it works with literal having noValidation let schema = S.jsonString->S.to(S.literal(symbol)) t->U.assertThrowsMessage( - () => `true`->S.parseOrThrow(schema), - `Failed parsing: Unsupported transformation from Symbol(foo) to JSON string`, + () => `true`->S.parseOrThrow(~to=schema), + `Can't decode JSON string to Symbol(foo). Use S.to to define a custom decoder`, ) t->U.assertThrowsMessage( - () => symbol->S.reverseConvertOrThrow(schema), - `Failed converting: Unsupported transformation from Symbol(foo) to JSON string`, + () => symbol->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Can't decode Symbol(foo) to JSON string. Use S.to to define a custom decoder`, ) }) @@ -225,52 +219,64 @@ test("Parses JSON string to null literal", t => { let nullVal = %raw(`null`) let schema = S.jsonString->S.to(S.literal(nullVal)) - t->Assert.deepEqual("null"->S.parseOrThrow(schema), nullVal) + t->Assert.deepEqual("null"->S.parseOrThrow(~to=schema), nullVal) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="null"||e[0](i);return null}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="null"||e[0](i);return null}`, + ) - t->Assert.deepEqual(nullVal->S.reverseConvertOrThrow(schema), %raw(`"null"`)) + t->Assert.deepEqual(nullVal->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"null"`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==null){e[0](i)}return "null"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===null||e[0](i);return "null"}`) }) test("Parses JSON string to nullAsUnit", t => { let schema = S.jsonString->S.to(S.nullAsUnit) - t->Assert.deepEqual(`null`->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(`null`->S.parseOrThrow(~to=schema), ()) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="null"||e[0](i);return void 0}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="null"||e[0](i);return void 0}`, + ) - t->Assert.deepEqual(()->S.reverseConvertOrThrow(schema), %raw(`"null"`)) + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"null"`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==void 0){e[0](i)}return "null"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===void 0||e[0](i);return "null"}`) }) test("Parses JSON string to unit", t => { let schema = S.jsonString->S.to(S.unit) - t->Assert.deepEqual(`null`->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(`null`->S.parseOrThrow(~to=schema), ()) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i==="null"||e[0](i);return void 0}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[1](i);i==="null"||e[0](i);return void 0}`, + ) - t->Assert.deepEqual(()->S.reverseConvertOrThrow(schema), %raw(`"null"`)) + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"null"`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==void 0){e[0](i)}return "null"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===void 0||e[0](i);return "null"}`) }) test("Parses JSON string to dict", t => { let value = Dict.fromArray([("foo", true)]) let schema = S.jsonString->S.to(S.dict(S.bool)) - t->Assert.deepEqual(`{"foo": true}`->S.parseOrThrow(schema), value) + t->Assert.deepEqual(`{"foo": true}`->S.parseOrThrow(~to=schema), value) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="object"||!v0||Array.isArray(v0)){e[2](v0)}for(let v1 in v0){try{let v3=v0[v1];if(typeof v3!=="boolean"){e[3](v3)}}catch(v2){if(v2&&v2.s===s){v2.path=""+\'["\'+v1+\'"]\'+v2.path}throw v2}}return v0}`, + `i=>{typeof i==="string"||e[3](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="object"&&v0&&!Array.isArray(v0)||e[2](v0);for(let v1 in v0){try{let v2=v0[v1];typeof v2==="boolean"||e[1](v2);}catch(v3){v3.path=\'["\'+v1+\'"]\'+v3.path;throw v3}}return v0}`, ) - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), `{"foo":true}`->Obj.magic) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), `{"foo":true}`->Obj.magic) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return JSON.stringify(i)}`) }) @@ -279,15 +285,15 @@ test("Parses JSON string to array", t => { let value = [true, false] let schema = S.jsonString->S.to(S.array(S.bool)) - t->Assert.deepEqual(`[true, false]`->S.parseOrThrow(schema), value) + t->Assert.deepEqual(`[true, false]`->S.parseOrThrow(~to=schema), value) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(!Array.isArray(v0)){e[2](v0)}for(let v1=0;v1{typeof i==="string"||e[3](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}Array.isArray(v0)||e[2](v0);for(let v1=0;v1Assert.deepEqual(value->S.reverseConvertOrThrow(schema), `[true,false]`->Obj.magic) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), `[true,false]`->Obj.magic) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return JSON.stringify(i)}`) }) @@ -295,90 +301,91 @@ test("Parses JSON string to array", t => { test("A chain of JSON string schemas should do nothing", t => { let schema = S.jsonString->S.to(S.jsonString)->S.to(S.jsonString)->S.to(S.bool) - t->Assert.deepEqual(`true`->S.parseOrThrow(schema), true) + t->Assert.deepEqual(`true`->S.parseOrThrow(~to=schema), true) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="boolean"){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="boolean"||e[1](v0);return v0}`, ) - t->Assert.deepEqual(true->S.reverseConvertOrThrow(schema), %raw(`"true"`)) - + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) }) -Failing.test("Nested JSON string", t => { +test("A S.unknown in the S.jsonString chain should do nothing", t => { let schema = S.jsonString->S.to(S.unknown)->S.to(S.jsonString)->S.to(S.bool) - t->Assert.deepEqual(`"true"`->S.parseOrThrow(schema), true) - + t->Assert.deepEqual("true"->S.parseOrThrow(~to=schema), true) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="boolean"){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[3](i);try{JSON.parse(i)}catch(t){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}typeof v0==="boolean"||e[2](v0);return v0}`, ) - t->Assert.deepEqual(true->S.reverseConvertOrThrow(schema), %raw(`'"true"'`)) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) }) -test("Parses JSON string to object", t => { +test("Parses JSON string to object with bigint", t => { let value = { "foo": "bar", - "bar": [1, 3], + "bar": (1n, true), } let schema = S.jsonString->S.to( - S.schema(_ => + S.schema(s => { "foo": "bar", - "bar": [1, 3], + "bar": (s.matches(S.bigint), s.matches(S.bool)), } ), ) - t->Assert.deepEqual(`{"foo":"bar","bar":[1,3]}`->S.parseOrThrow(schema), value) + t->Assert.deepEqual(`{"foo":"bar","bar":["1",true]}`->S.parseOrThrow(~to=schema), value) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="object"||!v0||v0["foo"]!=="bar"||!Array.isArray(v0["bar"])||v0["bar"].length!==2||v0["bar"]["0"]!==1||v0["bar"]["1"]!==3){e[2](v0)}return {"foo":"bar","bar":v0["bar"],}}`, + `i=>{typeof i==="string"||e[8](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="object"&&v0&&!Array.isArray(v0)||e[7](v0);let v1=v0["foo"],v2=v0["bar"];v1==="bar"||e[1](v1);Array.isArray(v2)||e[6](v2);v2.length===2||e[5](v2);let v4=v2["0"],v5=v2["1"];typeof v4==="string"||e[3](v4);let v3;try{v3=BigInt(v4)}catch(_){e[2](v4)}typeof v5==="boolean"||e[4](v5);return {"foo":v1,"bar":[v3,v5,],}}`, ) t->Assert.deepEqual( - value->S.reverseConvertOrThrow(schema), - `{"foo":"bar","bar":[1,3]}`->Obj.magic, + value->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `{"foo":"bar","bar":["1",true]}`->Obj.magic, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return JSON.stringify(i)}`) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v0=i["bar"];return JSON.stringify({"foo":"bar","bar":[""+v0["0"],v0["1"],],})}`, + ) }) test("Parses JSON string to option", t => { let schema = S.jsonString->S.to(S.option(S.bool)) t->U.assertThrowsMessage( - () => `"foo"`->S.parseOrThrow(schema), - `Failed parsing: boolean | undefined is not valid JSON`, + () => `"foo"`->S.parseOrThrow(~to=schema), + `Expected boolean | undefined, received "foo"`, ) - // t->Assert.deepEqual(`null`->S.parseOrThrow(schema), None) - // t->Assert.deepEqual(`true`->S.parseOrThrow(schema), Some(true)) + t->Assert.deepEqual(`null`->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(`true`->S.parseOrThrow(~to=schema), Some(true)) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#Parse, - // `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="object"||!v0||v0["foo"]!=="bar"||!Array.isArray(v0["bar"])||v0["bar"].length!==2||v0["bar"]["0"]!==1||v0["bar"]["1"]!==3){e[2](v0)}return {"foo":"bar","bar":v0["bar"],}}`, - // ) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}if(v0===null){v0=void 0}else if(!(typeof v0==="boolean")){e[1](v0)}return v0}`, + ) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), `null`->Obj.magic) - t->Assert.deepEqual(Some(true)->S.reverseConvertOrThrow(schema), `true`->Obj.magic) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), `null`->Obj.magic) + t->Assert.deepEqual(Some(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), `true`->Obj.magic) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="boolean"){i=""+i}else if(i===void 0){i="null"}return i}`, + `i=>{if(typeof i==="boolean"){i=""+i}else if(i===void 0){i="null"}else{e[0](i)}return i}`, ) }) @@ -394,22 +401,30 @@ test("Successfully serializes JSON object with space", t => { { "foo": "bar", "baz": [1, 3], - }->S.reverseConvertOrThrow(S.jsonStringWithSpace(2)->S.to(schema)), + }->S.decodeOrThrow(~from=S.jsonStringWithSpace(2)->S.to(schema), ~to=S.unknown), %raw(`'{\n "foo": "bar",\n "baz": [\n 1,\n 3\n ]\n}'`), ) }) -test( - "Create schema when passing non-jsonable schema to S.jsonString, but fails to serialize", - t => { - let schema = S.jsonString->S.to(S.object(s => s.field("foo", S.unknown))) +test("Converts JSON string to object with unknown field", t => { + let schema = S.jsonString->S.to(S.object(s => s.field("foo", S.unknown))) - t->U.assertThrowsMessage( - () => %raw(`"foo"`)->S.reverseConvertOrThrow(S.jsonString->S.to(schema)), - `Failed converting: { foo: unknown; } is not valid JSON`, - ) - }, -) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="object"&&v0&&!Array.isArray(v0)||e[1](v0);return v0["foo"]}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{e[0](i);return JSON.stringify({"foo":i,})}`, + ) + + t->Assert.deepEqual(%raw(`"foo"`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`'{"foo":"foo"}'`)) + t->U.assertThrowsMessage(() => { + %raw(`123n`)->S.decodeOrThrow(~from=schema, ~to=S.unknown) + }, `Expected JSON, received 123n`) +}) test("Compiled async parse code snapshot", t => { let schema = S.jsonString->S.to(S.bool->S.transform(_ => {asyncParser: i => Promise.resolve(i)})) @@ -417,59 +432,43 @@ test("Compiled async parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="boolean"){e[2](v0)}return e[3](v0)}`, + `i=>{typeof i==="string"||e[4](i);let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}typeof v0==="boolean"||e[3](v0);let v1;try{v1=e[1](v0).catch(x=>e[2](x))}catch(x){e[2](x)}return v1}`, ) }) test("Can apply refinement to JSON string", t => { - let schema = S.jsonString->S.refine(s => - v => - if v !== "123" { - s.fail("Expected 123") - } - ) + let schema = S.jsonString->S.refine(v => v === "123", ~error="Expected 123") - t->U.assertThrowsMessage(() => `124`->S.parseOrThrow(schema), `Failed parsing: Expected 123`) + t->U.assertThrowsMessage(() => `124`->S.parseOrThrow(~to=schema), `Expected 123`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}try{JSON.parse(i)}catch(t){e[1](i)}e[2](i);return i}`, + `i=>{typeof i==="string"||e[3](i);try{JSON.parse(i)}catch(t){e[0](i)}e[1](i)||e[2](i);return i}`, ) }) test("Can apply refinement to JSON string with S.to after", t => { let schema = S.jsonString - ->S.refine(s => - v => - if v !== "123" { - s.fail("Expected 123") - } - ) + ->S.refine(v => v === "123", ~error="Expected 123") ->S.to(S.int) - t->U.assertThrowsMessage(() => `124`->S.parseOrThrow(schema), `Failed parsing: Expected 123`) + t->U.assertThrowsMessage(() => `124`->S.parseOrThrow(~to=schema), `Expected 123`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}e[2](v0);if(typeof v0!=="number"||v0>2147483647||v0<-2147483648||v0%1!==0){e[3](v0)}return v0}`, + // TODO: Can be improved to perform JSON.parse only once + `i=>{typeof i==="string"||e[5](i);try{JSON.parse(i)}catch(t){e[0](i)}e[1](i)||e[4](i);let v0;try{v0=JSON.parse(i)}catch(t){e[2](i)}typeof v0==="number"&&v0<=2147483647&&v0>=-2147483648&&v0%1===0||e[3](v0);return v0}`, ) }) test("Can apply refinement to JSON string with S.to before", t => { - let schema = S.int->S.to( - S.jsonString->S.refine(s => - v => - if v !== "123" { - s.fail("Expected 123") - } - ), - ) + let schema = S.int->S.to(S.jsonString->S.refine(v => v === "123", ~error="Expected 123")) - t->U.assertThrowsMessage(() => 124->S.parseOrThrow(schema), `Failed parsing: Expected 123`) + t->U.assertThrowsMessage(() => 124->S.parseOrThrow(~to=schema), `Expected 123`) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="number"||i>2147483647||i<-2147483648||i%1!==0){e[0](i)}let v0=""+i;e[1](v0);return v0}`, + `i=>{typeof i==="number"&&i<=2147483647&&i>=-2147483648&&i%1===0||e[2](i);let v0=""+i;e[0](v0)||e[1](v0);return v0}`, ) }) diff --git a/packages/sury/tests/S_json_test.res b/packages/sury/tests/S_json_test.res index 6eb024513..3350bd3f8 100644 --- a/packages/sury/tests/S_json_test.res +++ b/packages/sury/tests/S_json_test.res @@ -6,40 +6,40 @@ test("Supports String", t => { let schema = S.json let data = JSON.Encode.string("Foo") - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Supports Number", t => { let schema = S.json let data = JSON.Encode.float(123.) - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Supports Bool", t => { let schema = S.json let data = JSON.Encode.bool(true) - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Supports Null", t => { let schema = S.json let data = JSON.Encode.null - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Supports Array", t => { let schema = S.json let data = JSON.Encode.array([JSON.Encode.string("foo"), JSON.Encode.null]) - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Supports Object", t => { @@ -48,8 +48,8 @@ test("Supports Object", t => { [("bar", JSON.Encode.string("foo")), ("baz", JSON.Encode.null)]->Dict.fromArray, ) - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(data->S.reverseConvertToJsonOrThrow(schema), data) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(data->S.decodeOrThrow(~from=schema, ~to=S.json), data) }) test("Fails to parse Object field", t => { @@ -58,13 +58,9 @@ test("Fails to parse Object field", t => { [("bar", %raw(`undefined`)), ("baz", JSON.Encode.null)]->Dict.fromArray, ) - t->U.assertThrows( - () => data->S.parseOrThrow(schema), - { - code: InvalidType({received: %raw(`undefined`), expected: schema->S.castToUnknown}), - operation: Parse, - path: S.Path.fromLocation("bar"), - }, + t->U.assertThrowsMessage( + () => data->S.parseOrThrow(~to=schema), + `Failed at ["bar"]: Expected JSON, received undefined`, ) }) @@ -72,62 +68,38 @@ test("Fails to parse matrix field", t => { let schema = S.json let data = %raw(`[1,[undefined]]`) - t->U.assertThrows( - () => data->S.parseOrThrow(schema), - { - code: InvalidType({received: %raw(`undefined`), expected: schema->S.castToUnknown}), - operation: Parse, - path: S.Path.fromArray(["1", "0"]), - }, + t->U.assertThrowsMessage( + () => data->S.parseOrThrow(~to=schema), + `Failed at ["1"]["0"]: Expected JSON, received undefined`, ) }) test("Fails to parse NaN", t => { let schema = S.json - t->U.assertThrows( - () => %raw(`NaN`)->S.parseOrThrow(schema), - { - code: InvalidType({received: %raw(`NaN`), expected: schema->S.castToUnknown}), - operation: Parse, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => %raw(`NaN`)->S.parseOrThrow(~to=schema), `Expected JSON, received NaN`) }) test("Fails to parse undefined", t => { let schema = S.json t->U.assertThrowsMessage( - () => %raw(`undefined`)->S.parseOrThrow(schema), - `Failed parsing: Expected JSON, received undefined`, + () => %raw(`undefined`)->S.parseOrThrow(~to=schema), + `Expected JSON, received undefined`, ) }) +let jsonParseCode = `i=>{e[0](i);return i} +JSON: i=>{if(Array.isArray(i)){for(let v0=0;v0JSON--0"](i[v0]);}catch(v1){v1.path='["'+v0+'"]'+v1.path;throw v1}}}else if(typeof i==="object"&&i&&!Array.isArray(i)){for(let v2 in i){try{e[1]["unknown->JSON--0"](i[v2]);}catch(v3){v3.path='["'+v2+'"]'+v3.path;throw v3}}}else if(!(typeof i==="string"||typeof i==="boolean"||typeof i==="number"&&!Number.isNaN(i)||i===null)){e[2](i)}return i}` test("Compiled parse code snapshot", t => { let schema = S.json - t->U.assertCompiledCode( - ~schema, - ~op=#Parse, - `i=>{let v0=e[0](i);return v0} -JSON: i=>{if(Array.isArray(i)){let v4=new Array(i.length);for(let v0=0;v0U.assertCompiledCode( - ~schema, - ~op=#Convert, - `i=>{let v0=e[0](i);return v0} -JSON: i=>{if(Array.isArray(i)){let v4=new Array(i.length);for(let v0=0;v0U.assertCompiledCode(~schema, ~op=#Parse, jsonParseCode) + t->U.assertCompiledCodeIsNoop(~schema, ~op=#Convert) }) test("Compiled serialize code snapshot", t => { let schema = S.json - t->U.assertCompiledCode( - ~schema=schema->S.reverse, - ~op=#Convert, - `i=>{let v0=e[0](i);return v0} -JSON: i=>{if(Array.isArray(i)){let v4=new Array(i.length);for(let v0=0;v0U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{let v0=e[0](i);return v0}`) + t->U.assertCompiledCodeIsNoop(~schema=schema->S.reverse, ~op=#Convert) + t->U.assertCompiledCode(~schema, ~op=#ReverseParse, jsonParseCode) }) test("Reverse schema to S.json", t => { diff --git a/packages/sury/tests/S_list_test.res b/packages/sury/tests/S_list_test.res index cf2c69ed8..3227ced17 100644 --- a/packages/sury/tests/S_list_test.res +++ b/packages/sury/tests/S_list_test.res @@ -10,45 +10,31 @@ module CommonWithNested = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - switch invalidAny->S.parseOrThrow(schema) { - | _ => t->Assert.fail("Unexpected result.") - | exception S.Error(e) => { - t->Assert.deepEqual(e.flag, S.Flag.typeValidation) - t->Assert.deepEqual(e.path, S.Path.empty) - switch e.code { - | InvalidType({expected, received}) => { - t->Assert.deepEqual(received, invalidAny) - t->U.unsafeAssertEqualSchemas(expected, schema) - } - | _ => t->Assert.fail("Unexpected code.") - } - } - } + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected string[], received true`, + ) }) test("Fails to parse nested", t => { let schema = factory() - t->U.assertThrows( - () => nestedInvalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: 1->Obj.magic}), - operation: Parse, - path: S.Path.fromArray(["1"]), - }, + t->U.assertThrowsMessage( + () => nestedInvalidAny->S.parseOrThrow(~to=schema), + `Failed at ["1"]: Expected string, received 1`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) } @@ -56,7 +42,7 @@ test("Successfully parses list of optional items", t => { let schema = S.list(S.option(S.string)) t->Assert.deepEqual( - %raw(`["a", undefined, undefined, "b"]`)->S.parseOrThrow(schema), + %raw(`["a", undefined, undefined, "b"]`)->S.parseOrThrow(~to=schema), list{Some("a"), None, None, Some("b")}, ) }) diff --git a/packages/sury/tests/S_literal_Array_test.res b/packages/sury/tests/S_literal_Array_test.res index 4ac33c85a..774e78ff0 100644 --- a/packages/sury/tests/S_literal_Array_test.res +++ b/packages/sury/tests/S_literal_Array_test.res @@ -8,75 +8,54 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(value->S.parseOrThrow(schema), value) + t->Assert.deepEqual(value->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid", t => { let schema = factory() - t->U.assertThrows( - () => invalid->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(("bar", true))->S.castToUnknown, - received: invalid, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalid->S.parseOrThrow(~to=schema), + `Expected ["bar", true], received 123`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), value->U.castAnyToUnknown) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), value->U.castAnyToUnknown) }) test("Fails to serialize invalid", t => { let schema = factory() t->Assert.is( - invalid->S.reverseConvertOrThrow(schema), + invalid->S.decodeOrThrow(~from=schema, ~to=S.unknown), invalid, ~message="Convert operation doesn't validate anything and assumes a valid input", ) t->U.assertThrowsMessage( - () => invalid->S.parseOrThrow(schema->S.reverse), - `Failed parsing: Expected ["bar", true], received 123`, + () => invalid->S.parseOrThrow(~to=schema->S.reverse), + `Expected ["bar", true], received 123`, ) }) test("Fails to parse array like object", t => { let schema = factory() - t->U.assertThrows( - () => %raw(`{0: "bar",1:true}`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(("bar", true))->S.castToUnknown, - received: %raw(`{0: "bar",1:true}`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`{0: "bar",1:true}`)->S.parseOrThrow(~to=schema), + `Expected ["bar", true], received { 0: "bar"; 1: true; }`, ) }) test("Fails to parse array with excess item", t => { let schema = factory() - t->U.assertThrows( - () => %raw(`["bar", true, false]`)->S.parseOrThrow(schema->S.strict), - { - code: InvalidType({ - expected: S.literal(("bar", true))->S.castToUnknown, - received: %raw(`["bar", true, false]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["bar", true, false]`)->S.parseOrThrow(~to=schema->S.strict), + `Expected ["bar", true], received ["bar", true, false]`, ) }) @@ -86,7 +65,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2||i["0"]!=="bar"||i["1"]!==true){e[0](i)}return i}`, + `i=>{Array.isArray(i)&&i.length===2||e[2](i);let v0=i["0"],v1=i["1"];v0==="bar"||e[0](v0);v1===true||e[1](v1);return i}`, ) }) @@ -116,37 +95,30 @@ module EmptyArray = { test("Successfully parses empty array literal schema", t => { let schema = factory() - t->Assert.deepEqual(value->S.parseOrThrow(schema), value) + t->Assert.deepEqual(value->S.parseOrThrow(~to=schema), value) }) test("Ignores extra items in strip mode and prevents in strict (default)", t => { let schema = factory() - t->Assert.deepEqual(invalid->S.parseOrThrow(schema->S.strip), []) + t->Assert.deepEqual(invalid->S.parseOrThrow(~to=schema->S.strip), []) - t->U.assertThrows( - () => invalid->S.parseOrThrow(schema->S.strict), - { - code: InvalidType({ - expected: S.literal([])->S.castToUnknown, - received: invalid->U.castAnyToUnknown, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalid->S.parseOrThrow(~to=schema->S.strict), + `Expected [], received ["abc"]`, ) }) test("Successfully serializes empty array literal schema", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), value->U.castAnyToUnknown) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), value->U.castAnyToUnknown) }) test("Serialize array with excess item in strict mode and it passes through", t => { let schema = factory() - t->Assert.deepEqual(invalid->S.reverseConvertOrThrow(schema->S.strict), invalid->Obj.magic) + t->Assert.deepEqual(invalid->S.decodeOrThrow(~from=schema->S.strict, ~to=S.unknown), invalid->Obj.magic) }) test("Compiled parse code snapshot of empty array literal schema", t => { @@ -155,7 +127,7 @@ module EmptyArray = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==0){e[0](i)}return i}`, + `i=>{Array.isArray(i)&&i.length===0||e[0](i);return i}`, ) }) diff --git a/packages/sury/tests/S_literal_Boolean_test.res b/packages/sury/tests/S_literal_Boolean_test.res index 5a77195a1..d956bc58b 100644 --- a/packages/sury/tests/S_literal_Boolean_test.res +++ b/packages/sury/tests/S_literal_Boolean_test.res @@ -11,64 +11,52 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.literal(false)->S.castToUnknown, received: true->Obj.magic}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected false, received true`, ) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.literal(false)->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected false, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({expected: S.literal(false)->S.castToUnknown, received: invalidValue}), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected false, received true`, ) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==false){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===false||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==false){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===false||e[0](i);return i}`) }) test("Reverse schema to self", t => { diff --git a/packages/sury/tests/S_literal_NaN_test.res b/packages/sury/tests/S_literal_NaN_test.res index ed869e921..e30562596 100644 --- a/packages/sury/tests/S_literal_NaN_test.res +++ b/packages/sury/tests/S_literal_NaN_test.res @@ -10,51 +10,37 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`NaN`))->S.castToUnknown, - received: invalidTypeAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected NaN, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`NaN`))->S.castToUnknown, - received: invalidValue, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected NaN, received 123`, ) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(!Number.isNaN(i)){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{Number.isNaN(i)||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { @@ -63,7 +49,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(!Number.isNaN(i)){e[0](i)}return i}`, + `i=>{Number.isNaN(i)||e[0](i);return i}`, ) }) diff --git a/packages/sury/tests/S_literal_Null_test.res b/packages/sury/tests/S_literal_Null_test.res index 2447c32b4..e88d2d1e8 100644 --- a/packages/sury/tests/S_literal_Null_test.res +++ b/packages/sury/tests/S_literal_Null_test.res @@ -10,57 +10,43 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`null`))->S.castToUnknown, - received: invalidTypeAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected null, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`null`))->S.castToUnknown, - received: invalidValue, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected null, received 123`, ) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==null){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===null||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==null){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===null||e[0](i);return i}`) }) test("Reverse schema to self", t => { diff --git a/packages/sury/tests/S_literal_Number_test.res b/packages/sury/tests/S_literal_Number_test.res index b65c963b3..298ab2c20 100644 --- a/packages/sury/tests/S_literal_Number_test.res +++ b/packages/sury/tests/S_literal_Number_test.res @@ -11,64 +11,49 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.literal(123.)->S.castToUnknown, received: 444.->Obj.magic}), - operation: Parse, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => invalidAny->S.parseOrThrow(~to=schema), `Expected 123, received 444`) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.literal(123.)->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected 123, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({expected: S.literal(123.)->S.castToUnknown, received: invalidValue}), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected 123, received 444`, ) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==123){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===123||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==123){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===123||e[0](i);return i}`) }) test("Reverse schema to self", t => { @@ -85,15 +70,8 @@ module Common = { test("Formatting of negative number with a decimal point in an error message", t => { let schema = S.literal(-123.567) - t->U.assertThrows( - () => %raw(`"foo"`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(-123.567)->S.castToUnknown, - received: "foo"->Obj.magic, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"foo"`)->S.parseOrThrow(~to=schema), + `Expected -123.567, received "foo"`, ) }) diff --git a/packages/sury/tests/S_literal_Object_test.res b/packages/sury/tests/S_literal_Object_test.res index 835e4f1b1..142a0a043 100644 --- a/packages/sury/tests/S_literal_Object_test.res +++ b/packages/sury/tests/S_literal_Object_test.res @@ -20,57 +20,43 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(value->S.parseOrThrow(schema), value) + t->Assert.deepEqual(value->S.parseOrThrow(~to=schema), value) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), value->U.castAnyToUnknown) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), value->U.castAnyToUnknown) }) test("Fails to serialize invalid", t => { let schema = factory() t->Assert.is( - invalid->S.reverseConvertOrThrow(schema), + invalid->S.decodeOrThrow(~from=schema, ~to=S.unknown), invalid, ~message=`Convert operation doesn't validate anything and assumes a valid input`, ) - t->U.assertThrows( - () => invalid->S.parseOrThrow(schema->S.reverse), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`123`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalid->S.parseOrThrow(~to=schema->S.reverse), + `Expected { foo: "bar"; }, received 123`, ) }) test("Fails to parse null", t => { let schema = factory() - t->U.assertThrows( - () => %raw(`null`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(Dict.fromArray([("foo", "bar")]))->S.castToUnknown, - received: %raw(`null`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`null`)->S.parseOrThrow(~to=schema), + `Expected { foo: "bar"; }, received null`, ) }) test("Can parse object instances, reduces it to normal object by default", t => { let schema = factory() - t->Assert.deepEqual(makeNotPlainValue()->S.parseOrThrow(schema), {"foo": "bar"}) + t->Assert.deepEqual(makeNotPlainValue()->S.parseOrThrow(~to=schema), {"foo": "bar"}) }) test("Compiled parse code snapshot", t => { @@ -79,7 +65,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||i["foo"]!=="bar"){e[0](i)}return {"foo":"bar",}}`, + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];v0==="bar"||e[0](v0);return {"foo":v0,}}`, ) }) @@ -109,25 +95,25 @@ module EmptyDict = { test("Successfully parses empty dict literal schema", t => { let schema = factory() - t->Assert.deepEqual(value->S.parseOrThrow(schema), value) + t->Assert.deepEqual(value->S.parseOrThrow(~to=schema), value) }) test("Strips extra fields passed to empty dict literal schema", t => { let schema = factory() - t->Assert.deepEqual(invalid->S.parseOrThrow(schema), Dict.make()) + t->Assert.deepEqual(invalid->S.parseOrThrow(~to=schema), Dict.make()) }) test("Successfully serializes empty dict literal schema", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), value->U.castAnyToUnknown) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), value->U.castAnyToUnknown) }) test("Ignores extra fields during conversion of empty object literal", t => { let schema = factory() - t->Assert.is(invalid->S.reverseConvertOrThrow(schema), invalid->Obj.magic) + t->Assert.is(invalid->S.decodeOrThrow(~from=schema, ~to=S.unknown), invalid->Obj.magic) }) test("Compiled parse code snapshot of empty dict literal schema", t => { @@ -136,12 +122,12 @@ module EmptyDict = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}return {}}`, + `i=>{typeof i==="object"&&i||e[0](i);return {}}`, ) t->U.assertCompiledCode( ~schema=schema->S.strict, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}let v0;for(v0 in i){if(true){e[1](v0)}}return i}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[1](i);let v0;for(v0 in i){if(true){e[0](v0)}}return i}`, ) }) diff --git a/packages/sury/tests/S_literal_String_test.res b/packages/sury/tests/S_literal_String_test.res index 464582c47..4c563a3cf 100644 --- a/packages/sury/tests/S_literal_String_test.res +++ b/packages/sury/tests/S_literal_String_test.res @@ -11,60 +11,39 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal("ReScript is Great!")->S.castToUnknown, - received: "Hello world!"->Obj.magic, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected "ReScript is Great!", received "Hello world!"`, ) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal("ReScript is Great!")->S.castToUnknown, - received: invalidTypeAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected "ReScript is Great!", received true`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({ - expected: S.literal("ReScript is Great!")->S.castToUnknown, - received: "Hello world!"->Obj.magic, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected "ReScript is Great!", received "Hello world!"`, ) }) @@ -74,7 +53,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(i!=="ReScript is Great!"){e[0](i)}return i}`, + `i=>{i==="ReScript is Great!"||e[0](i);return i}`, ) }) @@ -84,7 +63,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!=="ReScript is Great!"){e[0](i)}return i}`, + `i=>{i==="ReScript is Great!"||e[0](i);return i}`, ) }) diff --git a/packages/sury/tests/S_literal_Undefined_test.res b/packages/sury/tests/S_literal_Undefined_test.res index a59b83cec..b190d8c4c 100644 --- a/packages/sury/tests/S_literal_Undefined_test.res +++ b/packages/sury/tests/S_literal_Undefined_test.res @@ -10,51 +10,43 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.literal(None)->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected undefined, received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Fails to serialize invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidValue->S.reverseConvertOrThrow(schema), - { - code: InvalidType({expected: S.literal(None)->S.castToUnknown, received: invalidValue}), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected undefined, received 123`, ) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==void 0){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===void 0||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==void 0){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===void 0||e[0](i);return i}`) }) test("Reverse schema to self", t => { diff --git a/packages/sury/tests/S_meta_errorMessage_test.res b/packages/sury/tests/S_meta_errorMessage_test.res new file mode 100644 index 000000000..5e16e018d --- /dev/null +++ b/packages/sury/tests/S_meta_errorMessage_test.res @@ -0,0 +1,95 @@ +open Ava + +S.enableEmail() + +test("Catch-all _ overrides any constraint message", t => { + let schema = S.email->S.meta({errorMessage: {catchAll: "Bad input"}}) + + t->U.assertThrowsMessage(() => "invalid"->S.parseOrThrow(~to=schema), `Bad input`) +}) + +test("Specific key takes precedence over catch-all _", t => { + let schema = S.email->S.meta({errorMessage: {catchAll: "Fallback", format: "Must be email"}}) + + t->U.assertThrowsMessage(() => "invalid"->S.parseOrThrow(~to=schema), `Must be email`) +}) + +test("Empty errorMessage deletes the field from schema", t => { + let schema = S.string->S.min(1)->S.meta({errorMessage: {}}) + + switch schema { + | String({?errorMessage}) => t->Assert.deepEqual(errorMessage, None) + | _ => t->Assert.fail("Expected String") + } +}) + +test("S.meta errorMessage overwrites, not merges", t => { + let schema = S.string->S.min(1)->S.max(10)->S.meta({errorMessage: {minLength: "Custom min"}}) + + switch schema { + | String({errorMessage}) => + t->Assert.deepEqual(errorMessage, {minLength: "Custom min"}) + | _ => t->Assert.fail("Expected String") + } + // maxLength message is gone since we overwrote + t->U.assertThrowsMessage(() => ""->S.parseOrThrow(~to=schema), `Custom min`) +}) + +test("Override constraint message via S.meta on string min", t => { + let schema = S.string->S.min(5)->S.meta({errorMessage: {minLength: "Too short"}}) + + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Too short`) +}) + +test("Override constraint message via S.meta on int max", t => { + let schema = S.int->S.max(10)->S.meta({errorMessage: {maximum: "Number too large"}}) + + t->U.assertThrowsMessage(() => 100->S.parseOrThrow(~to=schema), `Number too large`) +}) + +test("Override works on serialization path too", t => { + let schema = S.email->S.meta({errorMessage: {format: "Bad email"}}) + + t->U.assertThrowsMessage( + () => "invalid"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Bad email`, + ) +}) + +test("Catch-all _ works on constraint refiners", t => { + let schema = S.string->S.min(5)->S.meta({errorMessage: {catchAll: "Bad"}}) + + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Bad`) +}) + +test("Override pattern message via S.meta", t => { + let schema = S.string->S.pattern(~message="Original", /^\d+$/)->S.meta({errorMessage: {pattern: "Override"}}) + + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Override`) +}) + +test("Override type error message via S.meta on S.string", t => { + let schema = S.string->S.meta({errorMessage: {type_: "Must be a string"}}) + + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Must be a string`) +}) + +test("Override type error message via S.meta on S.int", t => { + let schema = S.int->S.meta({errorMessage: {type_: "Must be an integer"}}) + + t->U.assertThrowsMessage(() => "abc"->S.parseOrThrow(~to=schema), `Must be an integer`) +}) + +test("Catch-all _ is used for type error when no specific type_ is set", t => { + let schema = S.string->S.meta({errorMessage: {catchAll: "Bad value"}}) + + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Bad value`) +}) + +test("S.meta does not mutate the original schema", t => { + let original = S.string->S.min(1) + let _ = original->S.meta({errorMessage: {minLength: "Custom"}}) + + // Original still uses default message + t->U.assertThrowsMessage(() => ""->S.parseOrThrow(~to=original), `String must be 1 or more characters long`) +}) diff --git a/packages/sury/tests/S_never_test.res b/packages/sury/tests/S_never_test.res index c7b5452b1..d8c1d80e0 100644 --- a/packages/sury/tests/S_never_test.res +++ b/packages/sury/tests/S_never_test.res @@ -4,29 +4,18 @@ module Common = { let any = %raw(`true`) let factory = () => S.never - test("Fails to ", t => { + test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => any->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.never->S.castToUnknown, received: any}), - operation: Parse, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => any->S.parseOrThrow(~to=schema), `Expected never, received true`) }) test("Fails to serialize ", t => { let schema = factory() - t->U.assertThrows( - () => any->S.reverseConvertOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: any}), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => any->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected never, received true`, ) }) @@ -58,13 +47,9 @@ module ObjectField = { } ) - t->U.assertThrows( - () => %raw(`{"key":"value"}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.never->S.castToUnknown, received: %raw(`undefined`)}), - operation: Parse, - path: S.Path.fromArray(["oldKey"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{"key":"value"}`)->S.parseOrThrow(~to=schema), + `Failed at ["oldKey"]: Expected never, received undefined`, ) }) @@ -82,7 +67,7 @@ module ObjectField = { ) t->Assert.deepEqual( - %raw(`{"key":"value"}`)->S.parseOrThrow(schema), + %raw(`{"key":"value"}`)->S.parseOrThrow(~to=schema), { "key": "value", "oldKey": None, diff --git a/packages/sury/tests/S_noValidation_test.res b/packages/sury/tests/S_noValidation_test.res index 4ddd379c2..95b5fb3a0 100644 --- a/packages/sury/tests/S_noValidation_test.res +++ b/packages/sury/tests/S_noValidation_test.res @@ -4,35 +4,43 @@ test("Successfully parses", t => { let schema = S.string let schemaWithoutTypeValidation = schema->S.noValidation(true) - t->U.assertThrows( - () => 1->S.parseOrThrow(schema), - { - code: S.InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`1`), - }), - operation: Parse, - path: S.Path.empty, - }, - ) - t->Assert.deepEqual(1->S.parseOrThrow(schemaWithoutTypeValidation), %raw(`1`)) + t->U.assertThrowsMessage(() => 1->S.parseOrThrow(~to=schema), `Expected string, received 1`) + t->Assert.deepEqual(1->S.parseOrThrow(~to=schemaWithoutTypeValidation), %raw(`1`)) }) test("Works for literals", t => { let schema = S.literal("foo") let schemaWithoutTypeValidation = schema->S.noValidation(true) - t->U.assertThrows( - () => 1->S.parseOrThrow(schema), - { - code: S.InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`1`), - }), - operation: Parse, - path: S.Path.empty, - }, - ) - t->Assert.deepEqual(1->S.parseOrThrow(schemaWithoutTypeValidation), "foo") + t->U.assertThrowsMessage(() => 1->S.parseOrThrow(~to=schema), `Expected "foo", received 1`) + t->Assert.deepEqual(1->S.parseOrThrow(~to=schemaWithoutTypeValidation), "foo") t->U.assertCompiledCode(~schema=schemaWithoutTypeValidation, ~op=#Parse, `i=>{return "foo"}`) }) + +// KNOWN BUG: `noValidation` on a union case silently breaks dispatch. +// +// `literalDecoder` short-circuits when `expectedSchema.noValidation` is set +// (Sury.res:2001) and emits no check at all — so there's nothing for the +// union discriminant hoister to lift, and that case becomes a catch-all +// that swallows every input ahead of subsequent cases. +// +// The `noValidation`-bypass comment on `emitValidation` is at the wrong +// layer to fix this (by the time union codegen runs, the literal's val has +// no checks to preserve). Proper fix would be for `literalDecoder` to emit +// its equality check regardless of `noValidation` when the val ends up in +// a union, or for `S.noValidation` on a literal inside a union to be +// rejected at schema construction time. +test("Union dispatch still works when a case has noValidation", t => { + let schema = S.union([ + S.literal("a")->S.noValidation(true), + S.literal("b"), + ]) + + t->Assert.deepEqual("a"->S.parseOrThrow(~to=schema), "a") + // BUG: returns "a" instead of "b" — first case becomes catch-all. + t->Assert.deepEqual("b"->S.parseOrThrow(~to=schema), "b") + t->U.assertThrowsMessage( + () => "c"->S.parseOrThrow(~to=schema), + `Expected "a" | "b", received "c"`, + ) +}) diff --git a/packages/sury/tests/S_null_test.res b/packages/sury/tests/S_null_test.res index f40a9a1b4..1e8c3db7b 100644 --- a/packages/sury/tests/S_null_test.res +++ b/packages/sury/tests/S_null_test.res @@ -1,34 +1,33 @@ open Ava +S.enableJson() +S.enableJsonString() + module Common = { let value = None let any = %raw(`null`) let invalidAny = %raw(`123.45`) - let factory = () => S.null(S.string) + let factory = () => S.nullAsOption(S.string) test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected string | null, received 123.45`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled code snapshot", t => { @@ -39,16 +38,20 @@ module Common = { ~op=#Parse, `i=>{if(i===null){i=void 0}else if(!(typeof i==="string")){e[0](i)}return i}`, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i===void 0){i=null}return i}`) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(i===void 0){i=null}else if(!(typeof i==="string")){e[0](i)}return i}`, + ) }) test("Compiled async parse code snapshot", t => { - let schema = S.null(S.unknown->S.transform(_ => {asyncParser: i => Promise.resolve(i)})) + let schema = S.nullAsOption(S.unknown->S.transform(_ => {asyncParser: i => Promise.resolve(i)})) t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{try{i=e[0](i)}catch(e0){if(i===null){i=void 0}else{e[1](i,e0)}}return Promise.resolve(i)}`, + `i=>{try{let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}catch(e0){if(i===null){i=void 0}else{e[2](i,e0)}}return Promise.resolve(i)}`, ) }) @@ -73,68 +76,56 @@ module Common = { } test("Successfully parses primitive", t => { - let schema = S.null(S.bool) + let schema = S.nullAsOption(S.bool) - t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(schema), Some(true)) + t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(~to=schema), Some(true)) }) test("Fails to parse JS undefined", t => { - let schema = S.null(S.bool) - - t->U.assertThrows( - () => %raw(`undefined`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`undefined`)}), - operation: Parse, - path: S.Path.empty, - }, + let schema = S.nullAsOption(S.bool) + + t->U.assertThrowsMessage( + () => %raw(`undefined`)->S.parseOrThrow(~to=schema), + `Expected boolean | null, received undefined`, ) }) test("Fails to parse object with missing field that marked as null", t => { - let fieldSchema = S.null(S.string) + let fieldSchema = S.nullAsOption(S.string) let schema = S.object(s => s.field("nullableField", fieldSchema)) - t->U.assertThrows( - () => %raw(`{}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: fieldSchema->S.castToUnknown, received: %raw(`undefined`)}), - operation: Parse, - path: S.Path.fromArray(["nullableField"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{}`)->S.parseOrThrow(~to=schema), + `Failed at ["nullableField"]: Expected string | null, received undefined`, ) }) test("Fails to parse JS null when schema doesn't allow optional data", t => { let schema = S.bool - t->U.assertThrows( - () => %raw(`null`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`null`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`null`)->S.parseOrThrow(~to=schema), + `Expected boolean, received null`, ) }) test("Successfully parses null and serializes it back for deprecated nullable schema", t => { - let schema = S.null(S.bool)->S.meta({description: "Deprecated", deprecated: true}) + let schema = S.nullAsOption(S.bool)->S.meta({description: "Deprecated", deprecated: true}) t->Assert.deepEqual( - %raw(`null`)->S.parseOrThrow(schema)->S.reverseConvertOrThrow(schema), + %raw(`null`)->S.parseOrThrow(~to=schema)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`), ) }) test("Serializes Some(None) to null for null nested in option", t => { - let schema = S.option(S.null(S.bool)) + let schema = S.option(S.nullAsOption(S.bool)) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), Some(None)) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), Some(None)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`null`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode( ~schema, @@ -145,17 +136,17 @@ test("Serializes Some(None) to null for null nested in option", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=null}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=null}}else if(!(typeof i==="boolean"||i===void 0)){e[0](i)}return i}`, ) }) test("Serializes Some(None) to null for null nested in null", t => { - let schema = S.null(S.null(S.bool)) + let schema = S.nullAsOption(S.nullAsOption(S.bool)) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), None) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), None) - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`null`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`null`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) t->U.assertCompiledCode( ~schema, @@ -165,7 +156,7 @@ test("Serializes Some(None) to null for null nested in null", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i===void 0){i=null}else if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=null}return i}`, + `i=>{if(i===void 0){i=null}else if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=null}}else if(!(typeof i==="boolean")){e[0](i)}return i}`, ) }) @@ -175,28 +166,27 @@ module OuterRecord = { type t = {k?: option} let schema = S.schema((s): t => { - k: ?s.matches(S.option(S.null(S.int))), + k: ?s.matches(S.option(S.nullAsOption(S.int))), }) } type t = {record?: option} let schema = S.schema(s => { - record: ?s.matches(S.option(S.null(Inner.schema))), + record: ?s.matches(S.option(S.nullAsOption(Inner.schema))), }) test("Record schema with optional nullable field", t => { let record = {record: None} t->Assert.deepEqual(record, %raw(`{ record: { BS_PRIVATE_NESTED_SOME_NONE: 0 } }`)) - t->Assert.deepEqual(record->S.reverseConvertOrThrow(schema), %raw(`{ record: null }`)) - t->Assert.deepEqual(record->S.reverseConvertToJsonStringOrThrow(schema), `{"record":null}`) + t->Assert.deepEqual(record->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{ record: null }`)) + t->Assert.deepEqual(record->S.decodeOrThrow(~from=schema, ~to=S.jsonString), `{"record":null}`) - Js.log(schema->S.reverse) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{let v0=i["record"];if(typeof v0==="object"&&v0){if(v0["BS_PRIVATE_NESTED_SOME_NONE"]===0){v0=null}else{try{let v1=v0["k"];if(typeof v1==="object"&&v1&&v1["BS_PRIVATE_NESTED_SOME_NONE"]===0){v1=null}v0={"k":v1,}}catch(e1){}}}return {"record":v0,}}`, + `i=>{let v0=i["record"];if(typeof v0==="object"&&v0&&!Array.isArray(v0)){if(v0["BS_PRIVATE_NESTED_SOME_NONE"]===0){v0=null}else{try{let v1=v0["k"];if(typeof v1==="object"&&v1&&!Array.isArray(v1)){if(v1["BS_PRIVATE_NESTED_SOME_NONE"]===0){v1=null}}else if(!(typeof v1==="number"&&!Number.isNaN(v1)&&(v1<=2147483647&&v1>=-2147483648&&v1%1===0)||v1===void 0)){e[0](v1)}v0={"k":v1,}}catch(e1){e[1](v0,e1)}}}else if(!(v0===void 0)){e[2](v0)}return {"record":v0,}}`, ) }) } diff --git a/packages/sury/tests/S_nullableAsOption_test.res b/packages/sury/tests/S_nullableAsOption_test.res index e58ef1a54..97392b442 100644 --- a/packages/sury/tests/S_nullableAsOption_test.res +++ b/packages/sury/tests/S_nullableAsOption_test.res @@ -3,16 +3,12 @@ open Ava test("Correctly parses", t => { let schema = S.nullableAsOption(S.bool) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), Some(true)) - t->U.assertThrows( - () => %raw(`"foo"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"foo"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), Some(true)) + t->U.assertThrowsMessage( + () => %raw(`"foo"`)->S.parseOrThrow(~to=schema), + `Expected boolean | undefined | null, received "foo"`, ) t->U.assertCompiledCode( @@ -25,9 +21,9 @@ test("Correctly parses", t => { test("Correctly parses transformed", t => { let schema = S.nullableAsOption(S.bool->S.to(S.string)) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), Some("true")) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), Some("true")) t->U.assertCompiledCode( ~schema, @@ -39,30 +35,34 @@ test("Correctly parses transformed", t => { test("Correctly reverse convert", t => { let schema = S.nullableAsOption(S.bool) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(Some(true)->S.reverseConvertOrThrow(schema), %raw(`true`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(Some(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(!(typeof i==="boolean"||i===void 0)){e[0](i)}return i}`, + ) }) test("Correctly reverse convert transformed", t => { let schema = S.nullableAsOption(S.bool->S.to(S.string)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(Some("true")->S.reverseConvertOrThrow(schema), %raw(`true`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(Some("true")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="string"){let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}return i}`, + `i=>{if(typeof i==="string"){let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}else if(!(i===void 0)){e[1](i)}return i}`, ) }) test("Correctly parses with default", t => { let schema = S.nullableAsOption(S.bool)->S.Option.getOr(false) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), false) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), false) - t->Assert.deepEqual(%raw(`false`)->S.parseOrThrow(schema), false) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), false) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), false) + t->Assert.deepEqual(%raw(`false`)->S.parseOrThrow(~to=schema), false) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), true) }) diff --git a/packages/sury/tests/S_nullable_test.res b/packages/sury/tests/S_nullable_test.res index fd77b3dc4..f01b4fdc0 100644 --- a/packages/sury/tests/S_nullable_test.res +++ b/packages/sury/tests/S_nullable_test.res @@ -3,16 +3,12 @@ open Ava test("Correctly parses", t => { let schema = S.nullable(S.bool) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), Null) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), Undefined) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), Value(true)) - t->U.assertThrows( - () => %raw(`"foo"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"foo"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), Null) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), Undefined) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), Value(true)) + t->U.assertThrowsMessage( + () => %raw(`"foo"`)->S.parseOrThrow(~to=schema), + `Expected boolean | undefined | null, received "foo"`, ) t->U.assertCompiledCode( @@ -25,9 +21,9 @@ test("Correctly parses", t => { test("Correctly parses transformed", t => { let schema = S.nullable(S.bool->S.to(S.string)) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), Null) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), Undefined) - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), Value("true")) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), Null) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), Undefined) + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), Value("true")) t->U.assertCompiledCode( ~schema, @@ -39,23 +35,27 @@ test("Correctly parses transformed", t => { test("Correctly reverse convert", t => { let schema = S.nullable(S.bool) - t->Assert.deepEqual(Nullable.Null->S.reverseConvertOrThrow(schema), %raw(`null`)) - t->Assert.deepEqual(Nullable.Undefined->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(Nullable.Value(true)->S.reverseConvertOrThrow(schema), %raw(`true`)) + t->Assert.deepEqual(Nullable.Null->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(Nullable.Undefined->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(Nullable.Value(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(!(typeof i==="boolean"||i===void 0||i===null)){e[0](i)}return i}`, + ) }) test("Correctly reverse convert transformed", t => { let schema = S.nullable(S.bool->S.to(S.string)) - t->Assert.deepEqual(Nullable.Null->S.reverseConvertOrThrow(schema), %raw(`null`)) - t->Assert.deepEqual(Nullable.Undefined->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(Nullable.Value("true")->S.reverseConvertOrThrow(schema), %raw(`true`)) + t->Assert.deepEqual(Nullable.Null->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(Nullable.Undefined->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(Nullable.Value("true")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="string"){let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}return i}`, + `i=>{if(typeof i==="string"){let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}else if(!(i===void 0||i===null)){e[1](i)}return i}`, ) }) diff --git a/packages/sury/tests/S_object_discriminant_test.res b/packages/sury/tests/S_object_discriminant_test.res index 7fe717aef..63b04a45f 100644 --- a/packages/sury/tests/S_object_discriminant_test.res +++ b/packages/sury/tests/S_object_discriminant_test.res @@ -127,7 +127,7 @@ module Positive = { { "discriminant": testData.discriminantData, "field": "bar", - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), {"field": "bar"}, ) }, @@ -146,7 +146,7 @@ module Positive = { ) t->Assert.deepEqual( - {"field": "bar"}->S.reverseConvertOrThrow(schema), + {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), { "discriminant": testData.discriminantData, "field": "bar", @@ -163,6 +163,7 @@ module Negative = { discriminantSchema: S.t, discriminantData: unknown, testNamePostfix: string, + missingInputExpression: string, path: S.Path.t, } @@ -171,6 +172,7 @@ module Negative = { ~discriminantData: 'any, ~description as maybeDescription=?, ~path=S.Path.empty, + ~missingInputExpression=discriminantSchema->S.toExpression, ) => { discriminantSchema: discriminantSchema->Obj.magic, discriminantData: discriminantData->Obj.magic, @@ -178,6 +180,7 @@ module Negative = { | Some(description) => ` ${description}` | None => "" }, + missingInputExpression, path, } } @@ -188,7 +191,7 @@ module Negative = { TestData.make(~discriminantSchema=S.float, ~discriminantData=123.), TestData.make(~discriminantSchema=S.bool, ~discriminantData=true), TestData.make(~discriminantSchema=S.option(S.literal(true)), ~discriminantData=None), - TestData.make(~discriminantSchema=S.null(S.literal(true)), ~discriminantData=%raw(`null`)), + TestData.make(~discriminantSchema=S.nullAsOption(S.literal(true)), ~discriminantData=%raw(`null`)), TestData.make(~discriminantSchema=S.unknown, ~discriminantData="anything"), TestData.make(~discriminantSchema=S.array(S.literal(true)), ~discriminantData=[true, true]), TestData.make( @@ -198,6 +201,7 @@ module Negative = { TestData.make( ~discriminantSchema=S.tuple2(S.literal(true), S.bool), ~discriminantData=(true, false), + ~missingInputExpression="boolean", ~path=S.Path.fromLocation("1"), ), TestData.make(~discriminantSchema=S.union([S.bool, S.literal(false)]), ~discriminantData=true), @@ -222,7 +226,7 @@ module Negative = { { "discriminant": testData.discriminantData, "field": "bar", - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), {"field": "bar"}, ) }, @@ -240,15 +244,9 @@ module Negative = { }, ) - t->U.assertThrows( - () => {"field": "bar"}->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema for ["discriminant"]${testData.path->S.Path.toString} isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Missing input for ${testData.missingInputExpression} at ["discriminant"]${testData.path->S.Path.toString}`, ) }, ) @@ -270,7 +268,7 @@ module NestedNegative = { { "discriminant": {"field": true}, "field": "bar", - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), {"field": "bar"}, ) }, @@ -286,15 +284,9 @@ module NestedNegative = { } }) - t->U.assertThrows( - () => {"field": "bar"}->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema for ["discriminant"]["nestedField"] isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Missing input for boolean at ["discriminant"]["nestedField"]`, ) }, ) @@ -308,18 +300,12 @@ test(`Fails to parse object with invalid data passed to discriminant field`, t = } }) - t->U.assertThrows( - () => - { - "discriminant": false, - "field": "bar", - }->S.parseOrThrow(schema), + t->U.assertThrowsMessage(() => { - code: InvalidType({expected: S.string->S.castToUnknown, received: Obj.magic(false)}), - operation: Parse, - path: S.Path.fromArray(["discriminant"]), - }, - ) + "discriminant": false, + "field": "bar", + }->S.parseOrThrow(~to=schema) + , `Failed at ["discriminant"]: Expected string, received false`) }) test(`Parses discriminant fields before registered fields`, t => { @@ -330,18 +316,12 @@ test(`Parses discriminant fields before registered fields`, t => { } }) - t->U.assertThrows( - () => - { - "discriminant": false, - "field": false, - }->S.parseOrThrow(schema), + t->U.assertThrowsMessage(() => { - code: InvalidType({expected: S.string->S.castToUnknown, received: Obj.magic(false)}), - operation: Parse, - path: S.Path.fromArray(["discriminant"]), - }, - ) + "discriminant": false, + "field": false, + }->S.parseOrThrow(~to=schema) + , `Failed at ["discriminant"]: Expected string, received false`) }) test(`Fails to serialize object with discriminant "Never"`, t => { @@ -352,19 +332,13 @@ test(`Fails to serialize object with discriminant "Never"`, t => { } }) - t->U.assertThrows( - () => {"field": "bar"}->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema for ["discriminant"] isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Missing input for never at ["discriminant"]`, ) }) -test(`Reverse parse validates literal fields before coming to other object fields`, t => { +test(`Reverse parse doesn't validates literal fields before coming to other object fields`, t => { let schema = S.object(s => { { "normal": s.field("field", S.string), @@ -373,7 +347,7 @@ test(`Reverse parse validates literal fields before coming to other object field }) t->U.assertThrowsMessage( - () => {"constant": false, "normal": false}->S.parseOrThrow(schema->S.reverse), - `Failed parsing: Expected { normal: string; constant: true; }, received { constant: false; normal: false; }`, + () => {"constant": false, "normal": false}->S.parseOrThrow(~to=schema->S.reverse), + `Failed at ["normal"]: Expected string, received false`, ) }) diff --git a/packages/sury/tests/S_object_escaping_test.res b/packages/sury/tests/S_object_escaping_test.res index 229bf61ad..1b6fae812 100644 --- a/packages/sury/tests/S_object_escaping_test.res +++ b/packages/sury/tests/S_object_escaping_test.res @@ -7,7 +7,7 @@ test("Successfully parses object with quotes in a field name", t => { } ) - t->Assert.deepEqual(%raw(`{"\"\'\`": "bar"}`)->S.parseOrThrow(schema), {"field": "bar"}) + t->Assert.deepEqual(%raw(`{"\"\'\`": "bar"}`)->S.parseOrThrow(~to=schema), {"field": "bar"}) }) test("Successfully parses object with new line in a field name", t => { @@ -17,7 +17,7 @@ test("Successfully parses object with new line in a field name", t => { } ) - t->Assert.deepEqual(%raw(`{"\n": "bar"}`)->S.parseOrThrow(schema), {"field": "bar"}) + t->Assert.deepEqual(%raw(`{"\n": "bar"}`)->S.parseOrThrow(~to=schema), {"field": "bar"}) }) test("Successfully serializing object with quotes in a field name", t => { @@ -27,7 +27,7 @@ test("Successfully serializing object with quotes in a field name", t => { } ) - t->Assert.deepEqual({"field": "bar"}->S.reverseConvertOrThrow(schema), %raw(`{"\"\'\`": "bar"}`)) + t->Assert.deepEqual({"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"\"\'\`": "bar"}`)) }) test("Successfully parses object transformed to object with quotes in a field name", t => { @@ -37,7 +37,7 @@ test("Successfully parses object transformed to object with quotes in a field na } ) - t->Assert.deepEqual(%raw(`{"field": "bar"}`)->S.parseOrThrow(schema), {"\"\'\`": "bar"}) + t->Assert.deepEqual(%raw(`{"field": "bar"}`)->S.parseOrThrow(~to=schema), {"\"\'\`": "bar"}) }) test("Successfully serializes object transformed to object with quotes in a field name", t => { @@ -47,7 +47,7 @@ test("Successfully serializes object transformed to object with quotes in a fiel } ) - t->Assert.deepEqual({"\"\'\`": "bar"}->S.reverseConvertOrThrow(schema), %raw(`{"field": "bar"}`)) + t->Assert.deepEqual({"\"\'\`": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": "bar"}`)) }) test("Successfully parses object with discriminant which has quotes as the field name", t => { @@ -62,7 +62,7 @@ test("Successfully parses object with discriminant which has quotes as the field %raw(`{ "\"\'\`": null, "field": "bar", - }`)->S.parseOrThrow(schema), + }`)->S.parseOrThrow(~to=schema), {"field": "bar"}, ) }) @@ -76,7 +76,7 @@ test("Successfully serializes object with discriminant which has quotes as the f }) t->Assert.deepEqual( - {"field": "bar"}->S.reverseConvertOrThrow(schema), + {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{ "\"\'\`": null, "field": "bar", @@ -96,7 +96,7 @@ test("Successfully parses object with discriminant which has quotes as the liter %raw(`{ "kind": "\"\'\`", "field": "bar", - }`)->S.parseOrThrow(schema), + }`)->S.parseOrThrow(~to=schema), {"field": "bar"}, ) }) @@ -112,7 +112,7 @@ test( }) t->Assert.deepEqual( - {"field": "bar"}->S.reverseConvertOrThrow(schema), + {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{ "kind": "\"\'\`", "field": "bar", @@ -132,7 +132,7 @@ test( ) t->Assert.deepEqual( - %raw(`{"field": "bar"}`)->S.parseOrThrow(schema), + %raw(`{"field": "bar"}`)->S.parseOrThrow(~to=schema), { "\"\'\`": "hardcoded", "field": "bar", @@ -155,7 +155,7 @@ test( { "\"\'\`": "hardcoded", "field": "bar", - }->S.reverseConvertOrThrow(schema), + }->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": "bar"}`), ) }, @@ -172,7 +172,7 @@ test( ) t->Assert.deepEqual( - %raw(`{"field": "bar"}`)->S.parseOrThrow(schema), + %raw(`{"field": "bar"}`)->S.parseOrThrow(~to=schema), { "hardcoded": "\"\'\`", "field": "bar", @@ -195,7 +195,7 @@ test( { "hardcoded": "\"\'\`", "field": "bar", - }->S.reverseConvertOrThrow(schema), + }->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": "bar"}`), ) }, @@ -204,34 +204,26 @@ test( test("Has proper error path when fails to parse object with quotes in a field name", t => { let schema = S.object(s => { - "field": s.field("\"\'\`", S.string->S.refine(s => _ => s.fail("User error"))), + "field": s.field("\"\'\`", S.string->S.refine(_ => false, ~error="User error")), } ) - t->U.assertThrows( - () => %raw(`{"\"\'\`": "bar"}`)->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["\"\'\`"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{"\"\'": "bar"}`)->S.parseOrThrow(~to=schema), + `Failed at ["\\"\'\`"]: Expected string, received undefined`, ) }) test("Has proper error path when fails to serialize object with quotes in a field name", t => { let schema = S.object(s => Dict.fromArray([ - ("\"\'\`", s.field("field", S.string->S.refine(s => _ => s.fail("User error")))), + ("\"\'\`", s.field("field", S.string->S.refine(_ => false, ~error="User error"))), ]) ) - t->U.assertThrows( - () => Dict.fromArray([("\"\'\`", "bar")])->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["\"\'\`"]), - }, + t->U.assertThrowsMessage( + () => Dict.fromArray([("\"'", "bar")])->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Failed at ["\\"'\`"]: User error`, ) }) @@ -242,12 +234,8 @@ test("Field name in a format of a path is handled properly", t => { } ) - t->U.assertThrows( - () => %raw(`{"bar": "foo"}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`undefined`)}), - operation: Parse, - path: S.Path.fromArray([`["abc"]["cde"]`]), - }, + t->U.assertThrowsMessage( + () => %raw(`{"bar": "foo"}`)->S.parseOrThrow(~to=schema), + `Failed at ["[\\"abc\\"][\\"cde\\"]"]: Expected string, received undefined`, ) }) diff --git a/packages/sury/tests/S_object_flatten_test.res b/packages/sury/tests/S_object_flatten_test.res index 68deb464f..3ee41f94d 100644 --- a/packages/sury/tests/S_object_flatten_test.res +++ b/packages/sury/tests/S_object_flatten_test.res @@ -8,19 +8,11 @@ test("Has correct tagged type", t => { } ) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - { - "bar": s.field("bar", S.string), - "foo": s.field("foo", S.string), - } - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"bar":v0,"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"bar":v0,"foo":v1,}}`, ) }) @@ -43,7 +35,7 @@ test("Can flatten S.schema", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"baz":{"bar":v0,},"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"baz":{"bar":v0,},"foo":v1,}}`, ) t->U.assertCompiledCode( ~schema, @@ -61,19 +53,11 @@ test("Can flatten & destructure S.schema", t => { } }) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - { - "bar": s.field("bar", S.string), - "foo": s.field("foo", S.string), - } - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"bar":v0,"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"bar":v0,"foo":v1,}}`, ) t->U.assertCompiledCode( ~schema, @@ -97,19 +81,11 @@ test("Can flatten strict object", t => { }, S.Strip, ) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - { - "bar": s.field("bar", S.string), - "foo": s.field("foo", S.string), - } - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"bar":v0,"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"bar":v0,"foo":v1,}}`, ) }) @@ -121,19 +97,11 @@ test("Flatten inside of a strict object", t => { } )->S.strict - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - { - "bar": s.field("bar", S.string), - "foo": s.field("foo", S.string), - } - )->S.strict, - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}let v0=i["bar"],v1=i["foo"],v2;if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}for(v2 in i){if(v2!=="bar"&&v2!=="foo"){e[3](v2)}}return {"bar":v0,"foo":v1,}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[3](i);let v0=i["bar"],v1=i["foo"],v2;typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);for(v2 in i){if(v2!=="bar"&&v2!=="foo"){e[2](v2)}}return {"bar":v0,"foo":v1,}}`, ) }) @@ -168,7 +136,7 @@ test("Flatten schema with duplicated field of the same type (flatten last)", t = t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}return {"foo":v0,"bar":v0,}}`, + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);return {"foo":v0,"bar":v0,}}`, ) // FIXME: Should validate that the fields are equal and choose the right one depending on the order t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"foo":i["bar"],}}`) @@ -181,7 +149,7 @@ test("Flatten schema with duplicated field of different type", t => { s => { "bar": s.flatten(S.object(s => s.field("foo", S.string))), - "foo": s.field("foo", S.string->S.email), + "foo": {S.enableEmail(); s.field("foo", S.email)}, }, ) }, @@ -199,19 +167,11 @@ test("Can flatten renamed object schema", t => { } ) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - { - "bar": s.field("bar", S.string), - "foo": s.field("foo", S.string), - } - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"bar":v0,"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"bar":v0,"foo":v1,}}`, ) t->Assert.is(schema->S.toExpression, `{ bar: string; foo: string; }`) }) @@ -227,7 +187,7 @@ test("Can flatten transformed object schema", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"],v1=i["foo"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"bar":e[3](v0),"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[4](i);let v0=i["bar"],v1=i["foo"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);let v2;try{v2=e[2](v0)}catch(x){e[3](x)}return {"bar":v2,"foo":v1,}}`, ) }) @@ -257,7 +217,7 @@ test("Successfully serializes simple object with flatten", t => { ) t->Assert.deepEqual( - {"foo": "foo", "bar": "bar"}->S.reverseConvertOrThrow(schema), + {"foo": "foo", "bar": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"foo": "foo", "bar": "bar"}`), ) t->U.assertCompiledCode( @@ -293,11 +253,12 @@ test("Can destructure flattened schema", t => { t->U.assertCompiledCode( ~op=#Parse, ~schema=entitySchema, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["name"],v1=i["age"],v2=i["id"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="number"||v1>2147483647||v1<-2147483648||v1%1!==0){e[2](v1)}if(typeof v2!=="string"){e[3](v2)}return {"id":v2,"name":v0,"age":v1,}}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["name"],v1=i["age"],v2=i["id"];typeof v0==="string"||e[0](v0);typeof v1==="number"&&v1<=2147483647&&v1>=-2147483648&&v1%1===0||e[1](v1);typeof v2==="string"||e[2](v2);return {"id":v2,"name":v0,"age":v1,}}`, ) + S.enableJson() t->Assert.deepEqual( - {id: "1", name: "Dmitry", age: 23}->S.reverseConvertToJsonOrThrow(entitySchema), + {id: "1", name: "Dmitry", age: 23}->S.decodeOrThrow(~from=entitySchema, ~to=S.json), %raw(`{id: "1", name: "Dmitry", age: 23}`), ) t->U.assertCompiledCode( @@ -305,4 +266,9 @@ test("Can destructure flattened schema", t => { ~schema=entitySchema, `i=>{return {"name":i["name"],"age":i["age"],"id":i["id"],}}`, ) + t->U.assertCompiledCode( + ~op=#ReverseConvertToJson, + ~schema=entitySchema, + `i=>{return {"name":i["name"],"age":i["age"],"id":i["id"],}}`, + ) }) diff --git a/packages/sury/tests/S_object_nested_test.res b/packages/sury/tests/S_object_nested_test.res index 7a8b7c6ed..32f66d512 100644 --- a/packages/sury/tests/S_object_nested_test.res +++ b/packages/sury/tests/S_object_nested_test.res @@ -8,28 +8,28 @@ test("Object with a single nested field", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"];if(typeof v1!=="string"){e[1](v1)}return v1}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["nested"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["foo"];typeof v1==="string"||e[0](v1);return v1}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":i,},}}`) }) -test("Object with a single nested field with S.null", t => { - let schema = S.object(s => s.nested("nested").field("foo", S.null(S.string))) +test("Object with a single nested field with S.nullAsOption", t => { + let schema = S.object(s => s.nested("nested").field("foo", S.nullAsOption(S.string))) t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"];let v1=v0["foo"];if(v1===null){v1=void 0}else if(!(typeof v1==="string")){e[1](v1)}return v1}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["nested"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["foo"];if(v1===null){v1=void 0}else if(!(typeof v1==="string")){e[0](v1)}return v1}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i===void 0){i=null}return {"nested":{"foo":i,},}}`, + `i=>{if(i===void 0){i=null}else if(!(typeof i==="string")){e[0](i)}return {"nested":{"foo":i,},}}`, ) t->Assert.deepEqual( - Some("bar")->S.reverseConvertOrThrow(schema), + Some("bar")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"nested":{"foo":"bar"}}`), ) }) @@ -60,10 +60,14 @@ test("Object with a single nested field with S.transform", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"];if(typeof v1!=="number"||Number.isNaN(v1)){e[1](v1)}return e[2](v1)}`, + `i=>{typeof i==="object"&&i||e[4](i);let v0=i["nested"];typeof v0==="object"&&v0||e[3](v0);let v2=v0["foo"];typeof v2==="number"&&!Number.isNaN(v2)||e[2](v2);let v1;try{v1=e[0](v0["foo"])}catch(x){e[1](x)}return v1}`, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":e[0](i),},}}`) - t->Assert.deepEqual("123.4"->S.reverseConvertOrThrow(schema), %raw(`{"nested":{"foo":123.4}}`)) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="number"&&!Number.isNaN(v0)||e[2](v0);return {"nested":{"foo":v0,},}}`, + ) + t->Assert.deepEqual("123.4"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"nested":{"foo":123.4}}`)) }) test("Object with a nested tag and optional field", t => { @@ -80,7 +84,7 @@ test("Object with a nested tag and optional field", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]||i["nested"]["tag"]!=="value"){e[0](i)}let v2=i["bar"];let v0=i["nested"];let v1=v0["foo"];if(!(typeof v1==="string"||v1===void 0)){e[1](v1)}if(typeof v2!=="string"){e[2](v2)}return {"foo":v1===void 0?"":v1,"bar":v2,}}`, + `i=>{typeof i==="object"&&i||e[4](i);let v0=i["nested"],v3=i["bar"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["tag"],v2=v0["foo"];v1==="value"||e[0](v1);if(!(typeof v2==="string"||v2===void 0)){e[1](v2)}typeof v3==="string"||e[3](v3);return {"foo":v2===void 0?"":v2,"bar":v3,}}`, ) t->U.assertCompiledCode( ~schema, @@ -103,7 +107,7 @@ test("Object with a two nested field using the same ctx", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"],v2=v0["bar"];if(typeof v1!=="string"){e[1](v1)}if(typeof v2!=="string"){e[2](v2)}return {"foo":v1,"bar":v2,}}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["nested"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["foo"],v2=v0["bar"];typeof v1==="string"||e[0](v1);typeof v2==="string"||e[1](v2);return {"foo":v1,"bar":v2,}}`, ) t->U.assertCompiledCode( ~schema, @@ -120,7 +124,7 @@ test("Object with a single nested nested field", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]||typeof i["nested"]["deeply"]!=="object"||!i["nested"]["deeply"]){e[0](i)}let v0=i["nested"];let v1=v0["deeply"],v2=v1["foo"];if(typeof v2!=="string"){e[1](v2)}return v2}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["nested"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["deeply"];typeof v1==="object"&&v1||e[1](v1);let v2=v1["foo"];typeof v2==="string"||e[0](v2);return v2}`, ) t->U.assertCompiledCode( ~schema, @@ -142,7 +146,7 @@ test("Object with a two nested field calling s.nested twice", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"],v2=v0["bar"];if(typeof v1!=="string"){e[1](v1)}if(typeof v2!=="string"){e[2](v2)}return {"foo":v1,"bar":v2,}}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["nested"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["foo"],v2=v0["bar"];typeof v1==="string"||e[0](v1);typeof v2==="string"||e[1](v2);return {"foo":v1,"bar":v2,}}`, ) t->U.assertCompiledCode( ~schema, @@ -163,25 +167,12 @@ test("Object with a flattened nested field", t => { ) ) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - s.field( - "nested", - S.schema( - s => - { - "foo": s.matches(S.string), - }, - ), - ) - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"];if(typeof v1!=="string"){e[1](v1)}return {"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["nested"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["foo"];typeof v1==="string"||e[0](v1);return {"foo":v1,}}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":i["foo"],},}}`) }) @@ -198,25 +189,12 @@ test("Object with a strict flattened nested field", t => { ) ) - t->U.unsafeAssertEqualSchemas( - schema, - S.object(s => - s.field( - "nested", - S.schema( - s => - { - "foo": s.matches(S.string), - }, - ), - ) - ), - ) + t->U.assertReverseReversesBack(schema) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"];if(typeof v1!=="string"){e[1](v1)}return {"foo":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["nested"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["foo"];typeof v1==="string"||e[0](v1);return {"foo":v1,}}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":i["foo"],},}}`) }) @@ -235,7 +213,7 @@ test("S.schema object with a deep strict applied to the nested field parent", t t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["nested"]!=="object"||!i["nested"]||Array.isArray(i["nested"])){e[0](i)}let v3;let v0=i["nested"],v1=v0["foo"],v2;if(typeof v1!=="string"){e[1](v1)}for(v2 in v0){if(v2!=="foo"){e[2](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return i}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[4](i);let v0=i["nested"],v3;typeof v0==="object"&&v0&&!Array.isArray(v0)||e[2](v0);let v1=v0["foo"],v2;typeof v1==="string"||e[0](v1);for(v2 in v0){if(v2!=="foo"){e[1](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return i}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{let v0=i["nested"];return i}`) }) @@ -245,7 +223,7 @@ test("Nested tags on reverse convert", t => { s.nested("nested").tag("tag", "value") }) - t->Assert.deepEqual(()->S.reverseConvertOrThrow(schema), %raw(`{"nested":{"tag":"value"}}`)) + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"nested":{"tag":"value"}}`)) }) test("Nested preprocessed tags on reverse convert", t => { @@ -271,31 +249,31 @@ test("Nested preprocessed tags on reverse convert", t => { t->U.assertCompiledCode( ~op=#ReverseConvert, ~schema, - `i=>{if(i!==void 0){e[0](i)}return {"nested":{"tag":e[1]("value"),"intTag":e[2]("1"),},}}`, + `i=>{i===void 0||e[6](i);let v0;try{v0=e[0]("value")}catch(x){e[1](x)}typeof v0==="string"||e[2](v0);let v1;try{v1=e[3]("1")}catch(x){e[4](x)}typeof v1==="string"||e[5](v1);return {"nested":{"tag":v0,"intTag":v1,},}}`, ) t->U.assertCompiledCode( ~op=#Parse, ~schema, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["tag"],v3=v0["intTag"];if(typeof v1!=="string"){e[1](v1)}let v2=e[2](v1);if(typeof v2!=="string"){e[3](v2)}if(v2!=="value"){e[4](v2)}if(typeof v3!=="string"){e[5](v3)}let v4=e[6](v3);if(typeof v4!=="string"){e[7](v4)}v4==="1"||e[8](v4);return void 0}`, + `i=>{typeof i==="object"&&i||e[11](i);let v0=i["nested"];typeof v0==="object"&&v0||e[10](v0);let v2=v0["tag"],v4=v0["intTag"];typeof v2==="string"||e[4](v2);let v1;try{v1=e[0](v0["tag"])}catch(x){e[1](x)}typeof v1==="string"||e[3](v1);v1==="value"||e[2](v1);typeof v4==="string"||e[9](v4);let v3;try{v3=e[5](v0["intTag"])}catch(x){e[6](x)}typeof v3==="string"||e[8](v3);v3==="1"||e[7](v3);return void 0}`, ) t->Assert.deepEqual( - ()->S.reverseConvertOrThrow(schema), + ()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"nested":{"tag":"_value", "intTag":"_1"}}`), ) t->Assert.deepEqual( - %raw(`{"nested":{"tag":"_value", "intTag":"_1"}}`)->S.parseOrThrow(schema), + %raw(`{"nested":{"tag":"_value", "intTag":"_1"}}`)->S.parseOrThrow(~to=schema), (), ) t->U.assertThrowsMessage( - () => %raw(`{"nested":{"tag":"_foo", "intTag":"_1"}}`)->S.parseOrThrow(schema), - `Failed parsing at ["nested"]["tag"]: Expected "value", received "foo"`, + () => %raw(`{"nested":{"tag":"_foo", "intTag":"_1"}}`)->S.parseOrThrow(~to=schema), + `Failed at ["nested"]["tag"]: Expected "value", received "foo"`, ) t->U.assertThrowsMessage( - () => %raw(`{"nested":{"tag":"_value", "intTag":"_2"}}`)->S.parseOrThrow(schema), - `Failed parsing at ["nested"]["intTag"]: Expected 1, received "2"`, + () => %raw(`{"nested":{"tag":"_value", "intTag":"_2"}}`)->S.parseOrThrow(~to=schema), + `Failed at ["nested"]["intTag"]: Expected "1", received "2"`, ) }) @@ -304,7 +282,7 @@ test("S.schema object with a deep strict applied to the nested field parent + re S.schema(s => { "nested": { - "foo": s.matches(S.null(S.string)), + "foo": s.matches(S.nullAsOption(S.string)), }, } ) @@ -316,12 +294,12 @@ test("S.schema object with a deep strict applied to the nested field parent + re t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["nested"]!=="object"||!i["nested"]||Array.isArray(i["nested"])){e[0](i)}let v3;let v0=i["nested"],v2;let v1=v0["foo"];if(v1===void 0){v1=null}else if(!(typeof v1==="string")){e[1](v1)}for(v2 in v0){if(v2!=="foo"){e[2](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return {"nested":{"foo":v1,},}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[4](i);let v0=i["nested"],v3;typeof v0==="object"&&v0&&!Array.isArray(v0)||e[2](v0);let v1=v0["foo"],v2;if(v1===void 0){v1=null}else if(!(typeof v1==="string")){e[0](v1)}for(v2 in v0){if(v2!=="foo"){e[1](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return {"nested":{"foo":v1,},}}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{let v0=i["nested"];let v1=v0["foo"];if(v1===null){v1=void 0}return {"nested":{"foo":v1,},}}`, + `i=>{let v0=i["nested"];let v1=v0["foo"];if(v1===null){v1=void 0}else if(!(typeof v1==="string")){e[0](v1)}return {"nested":{"foo":v1,},}}`, ) }) @@ -333,7 +311,7 @@ test("Object with a deep strict applied to the nested field parent", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["nested"]!=="object"||!i["nested"]||Array.isArray(i["nested"])){e[0](i)}let v3;let v0=i["nested"],v1=v0["foo"],v2;if(typeof v1!=="string"){e[1](v1)}for(v2 in v0){if(v2!=="foo"){e[2](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return v1}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[4](i);let v0=i["nested"],v3;typeof v0==="object"&&v0&&!Array.isArray(v0)||e[2](v0);let v1=v0["foo"],v2;typeof v1==="string"||e[0](v1);for(v2 in v0){if(v2!=="foo"){e[1](v2)}}for(v3 in i){if(v3!=="nested"){e[3](v3)}}return v1}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":i,},}}`) }) @@ -352,7 +330,7 @@ test("Object with a deep strict applied to the nested field parent + reverse", t // FIXME: Test for deepStrict applying to flattened nested fields // Test deepStrict for reversed schema // Test strict & deepStrict for S.shape - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}let v0,v1=i["foo"];for(v0 in i){if(v0!=="foo"){e[1](v0)}}if(typeof v1!=="string"){e[2](v1)}return {"nested":{"foo":v1,},}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[2](i);let v0=i["foo"],v1;typeof v0==="string"||e[0](v0);for(v1 in i){if(v1!=="foo"){e[1](v1)}}return {"nested":{"foo":v0,},}}`, ) t->U.assertCompiledCode( ~schema, @@ -381,7 +359,7 @@ test("Object with nested field together with flatten", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"],v2=v0["bar"];if(typeof v1!=="string"){e[1](v1)}if(typeof v2!=="string"){e[2](v2)}return {"flattened":{"foo":v1,},"field":v2,}}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["nested"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["foo"],v2=v0["bar"];typeof v1==="string"||e[0](v1);typeof v2==="string"||e[1](v2);return {"flattened":{"foo":v1,},"field":v2,}}`, ) t->U.assertCompiledCode( ~schema, @@ -480,37 +458,24 @@ test("s.nested.flattened doesn't work with S.string", t => { ) }) -test("s.nested.flattened doesn't work with S.schema->S.shape to self", t => { - t->Assert.throws( - () => { - let _schema = S.object( - s => { - s.nested("nested").flatten( - S.schema( - s => - { - "foo": s.matches(S.string), - }, - )->S.shape(v => v), - ) - }, - ) +test("s.nested.flattened does work with S.schema->S.shape to self", t => { + let schema = S.object(s => { + s.nested("nested").flatten( + S.schema( + s => + { + "foo": s.matches(S.string), + }, + )->S.shape(v => v), + ) + }) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#Parse, - // `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"];if(typeof v1!=="string"){e[1](v1)}return {"foo":v1,}}`, - // ) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#ReverseConvert, - // `i=>{return {"nested":{"foo":i["foo"],},}}`, - // ) - }, - ~expectations={ - message: `[Sury] Unsupported nested flatten for transformed object schema { foo: string; }`, - }, + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["nested"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["foo"];typeof v1==="string"||e[0](v1);return {"foo":v1,}}`, ) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{"foo":i["foo"],},}}`) }) test("s.nested.flatten conflicts with s.nested.field", t => { diff --git a/packages/sury/tests/S_object_spread_test.res b/packages/sury/tests/S_object_spread_test.res index 9e16a2b2c..476375eab 100644 --- a/packages/sury/tests/S_object_spread_test.res +++ b/packages/sury/tests/S_object_spread_test.res @@ -22,7 +22,7 @@ test("Successfully parses manually created schema using type spread", t => { }) t->Assert.deepEqual( - %raw(`{a: 1, b: 2, c: 3.3, d: true}`)->S.parseOrThrow(zSchema), + %raw(`{a: 1, b: 2, c: 3.3, d: true}`)->S.parseOrThrow(~to=zSchema), {a: 1, b: 2, c: 3.3, d: true}, ) }) diff --git a/packages/sury/tests/S_object_test.res b/packages/sury/tests/S_object_test.res index 4a93f1b6b..85b4ce6e5 100644 --- a/packages/sury/tests/S_object_test.res +++ b/packages/sury/tests/S_object_test.res @@ -1,5 +1,7 @@ open Ava +S.enableJson() + @live type options = {fast?: bool, mode?: int} @@ -10,7 +12,7 @@ test("Successfully parses object with inlinable string field", t => { } ) - t->Assert.deepEqual(%raw(`{field: "bar"}`)->S.parseOrThrow(schema), {"field": "bar"}) + t->Assert.deepEqual(%raw(`{field: "bar"}`)->S.parseOrThrow(~to=schema), {"field": "bar"}) }) test("Fails to parse object with inlinable string field", t => { @@ -20,13 +22,9 @@ test("Fails to parse object with inlinable string field", t => { } ) - t->U.assertThrows( - () => %raw(`{field: 123}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`123`)}), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: 123}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected string, received 123`, ) }) @@ -35,17 +33,13 @@ test( t => { let schema = S.object(s => { - "field": s.field("field", S.array(S.string->S.refine(s => _ => s.fail("User error")))), + "field": s.field("field", S.array(S.string->S.refine(_ => false, ~error="User error"))), } ) - t->U.assertThrows( - () => %raw(`{field: ["foo"]}`)->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["field", "0"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: ["foo"]}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]["0"]: User error`, ) }, ) @@ -57,7 +51,7 @@ test("Successfully parses object with inlinable bool field", t => { } ) - t->Assert.deepEqual(%raw(`{field: true}`)->S.parseOrThrow(schema), {"field": true}) + t->Assert.deepEqual(%raw(`{field: true}`)->S.parseOrThrow(~to=schema), {"field": true}) }) test("Fails to parse object with inlinable bool field", t => { @@ -67,13 +61,9 @@ test("Fails to parse object with inlinable bool field", t => { } ) - t->U.assertThrows( - () => %raw(`{field: 123}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.bool->S.castToUnknown, received: %raw(`123`)}), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: 123}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected boolean, received 123`, ) }) @@ -85,7 +75,7 @@ test("Successfully parses object with unknown field (Noop operation)", t => { ) t->Assert.deepEqual( - %raw(`{field: new Date("2015-12-12")}`)->S.parseOrThrow(schema), + %raw(`{field: new Date("2015-12-12")}`)->S.parseOrThrow(~to=schema), %raw(`{field: new Date("2015-12-12")}`), ) }) @@ -98,7 +88,7 @@ test("Successfully serializes object with unknown field (Noop operation)", t => ) t->Assert.deepEqual( - %raw(`{field: new Date("2015-12-12")}`)->S.reverseConvertOrThrow(schema), + %raw(`{field: new Date("2015-12-12")}`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{field: new Date("2015-12-12")}`), ) }) @@ -110,13 +100,9 @@ test("Fails to parse object with inlinable never field", t => { } ) - t->U.assertThrows( - () => %raw(`{field: true}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.never->S.castToUnknown, received: %raw(`true`)}), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: true}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected never, received true`, ) }) @@ -127,7 +113,7 @@ test("Successfully parses object with inlinable float field", t => { } ) - t->Assert.deepEqual(%raw(`{field: 123}`)->S.parseOrThrow(schema), {"field": 123.}) + t->Assert.deepEqual(%raw(`{field: 123}`)->S.parseOrThrow(~to=schema), {"field": 123.}) }) test("Fails to parse object with inlinable float field", t => { @@ -137,13 +123,9 @@ test("Fails to parse object with inlinable float field", t => { } ) - t->U.assertThrows( - () => %raw(`{field: true}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.float->S.castToUnknown, received: %raw(`true`)}), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: true}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected number, received true`, ) }) @@ -154,7 +136,7 @@ test("Successfully parses object with inlinable int field", t => { } ) - t->Assert.deepEqual(%raw(`{field: 123}`)->S.parseOrThrow(schema), {"field": 123}) + t->Assert.deepEqual(%raw(`{field: 123}`)->S.parseOrThrow(~to=schema), {"field": 123}) }) test("Fails to parse object with inlinable int field", t => { @@ -164,13 +146,9 @@ test("Fails to parse object with inlinable int field", t => { } ) - t->U.assertThrows( - () => %raw(`{field: true}`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.int->S.castToUnknown, received: %raw(`true`)}), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => %raw(`{field: true}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected int32, received true`, ) }) @@ -181,7 +159,7 @@ test("Successfully parses object with not inlinable empty object field", t => { } ) - t->Assert.deepEqual(%raw(`{field: {}}`)->S.parseOrThrow(schema), {"field": ()}) + t->Assert.deepEqual(%raw(`{field: {}}`)->S.parseOrThrow(~to=schema), {"field": ()}) }) test("Fails to parse object with not inlinable empty object field", t => { @@ -193,8 +171,8 @@ test("Fails to parse object with not inlinable empty object field", t => { ) t->U.assertThrowsMessage( - () => %raw(`{field: true}`)->S.parseOrThrow(schema), - `Failed parsing: Expected { field: {}; }, received { field: true; }`, + () => %raw(`{field: true}`)->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected {}, received true`, ) }) @@ -205,13 +183,9 @@ test("Fails to parse object when provided invalid data", t => { } ) - t->U.assertThrows( - () => %raw(`12`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`12`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`12`)->S.parseOrThrow(~to=schema), + `Expected { field: string; }, received 12`, ) }) @@ -222,7 +196,7 @@ test("Successfully serializes object with single field", t => { } ) - t->Assert.deepEqual({"field": "bar"}->S.reverseConvertOrThrow(schema), %raw(`{field: "bar"}`)) + t->Assert.deepEqual({"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{field: "bar"}`)) }) test("Successfully parses object with multiple fields", t => { @@ -234,7 +208,7 @@ test("Successfully parses object with multiple fields", t => { ) t->Assert.deepEqual( - %raw(`{boo: "bar", zoo: "jee"}`)->S.parseOrThrow(schema), + %raw(`{boo: "bar", zoo: "jee"}`)->S.parseOrThrow(~to=schema), {"boo": "bar", "zoo": "jee"}, ) }) @@ -248,7 +222,7 @@ test("Successfully serializes object with multiple fields", t => { ) t->Assert.deepEqual( - {"boo": "bar", "zoo": "jee"}->S.reverseConvertOrThrow(schema), + {"boo": "bar", "zoo": "jee"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: "bar", zoo: "jee"}`), ) }) @@ -263,7 +237,7 @@ test("Successfully parses object with transformed field", t => { } ) - t->Assert.deepEqual(%raw(`{string: "bar"}`)->S.parseOrThrow(schema), {"string": "barfield"}) + t->Assert.deepEqual(%raw(`{string: "bar"}`)->S.parseOrThrow(~to=schema), {"string": "barfield"}) }) test("Fails to parse object when transformed field has throws error", t => { @@ -273,13 +247,9 @@ test("Fails to parse object when transformed field has throws error", t => { } ) - t->U.assertThrows( - () => {"field": "bar"}->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => {"field": "bar"}->S.parseOrThrow(~to=schema), + `Failed at ["field"]: User error`, ) }) @@ -293,13 +263,9 @@ test("Shows transformed object field name in error path when fails to parse", t } ) - t->U.assertThrows( - () => {"originalFieldName": "bar"}->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["originalFieldName"]), - }, + t->U.assertThrowsMessage( + () => {"originalFieldName": "bar"}->S.parseOrThrow(~to=schema), + `Failed at ["originalFieldName"]: User error`, ) }) @@ -314,7 +280,7 @@ test("Successfully serializes object with transformed field", t => { ) t->Assert.deepEqual( - {"string": "bar"}->S.reverseConvertOrThrow(schema), + {"string": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"string": "barfield"}`), ) }) @@ -329,13 +295,9 @@ test("Fails to serializes object when transformed field has throws error", t => } ) - t->U.assertThrows( - () => {"field": "bar"}->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["field"]), - }, + t->U.assertThrowsMessage( + () => {"field": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Failed at ["field"]: User error`, ) }) @@ -349,13 +311,9 @@ test("Shows transformed object field name in error path when fails to serializes } ) - t->U.assertThrows( - () => {"transformedFieldName": "bar"}->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["transformedFieldName"]), - }, + t->U.assertThrowsMessage( + () => {"transformedFieldName": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Failed at ["transformedFieldName"]: User error`, ) }) @@ -371,19 +329,13 @@ test("Shows transformed to nested object field name in error path when fails to } ) - t->U.assertThrows( - () => - { - "v1": { - "transformedFieldName": "bar", - }, - }->S.reverseConvertOrThrow(schema), + t->U.assertThrowsMessage(() => { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["v1", "transformedFieldName"]), - }, - ) + "v1": { + "transformedFieldName": "bar", + }, + }->S.decodeOrThrow(~from=schema, ~to=S.unknown) + , `Failed at ["v1"]["transformedFieldName"]: User error`) }) test("Successfully parses object with optional fields", t => { @@ -395,7 +347,7 @@ test("Successfully parses object with optional fields", t => { ) t->Assert.deepEqual( - %raw(`{boo: "bar"}`)->S.parseOrThrow(schema), + %raw(`{boo: "bar"}`)->S.parseOrThrow(~to=schema), {"boo": Some("bar"), "zoo": None}, ) }) @@ -409,7 +361,7 @@ test("Successfully serializes object with optional fields", t => { ) t->Assert.deepEqual( - {"boo": Some("bar"), "zoo": None}->S.reverseConvertOrThrow(schema), + {"boo": Some("bar"), "zoo": None}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: "bar", zoo: undefined}`), ) }) @@ -423,7 +375,7 @@ test("Successfully parses object with optional fields with default", t => { ) t->Assert.deepEqual( - %raw(`{boo: "bar"}`)->S.parseOrThrow(schema), + %raw(`{boo: "bar"}`)->S.parseOrThrow(~to=schema), {"boo": "bar", "zoo": "default zoo"}, ) }) @@ -437,7 +389,7 @@ test("Successfully serializes object with optional fields with default", t => { ) t->Assert.deepEqual( - {"boo": "bar", "zoo": "baz"}->S.reverseConvertOrThrow(schema), + {"boo": "bar", "zoo": "baz"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: "bar", zoo: "baz"}`), ) }) @@ -453,7 +405,7 @@ test( }) t->Assert.deepEqual( - %raw(`{mode: 1}`)->S.parseOrThrow(optionsSchema), + %raw(`{mode: 1}`)->S.parseOrThrow(~to=optionsSchema), { fast: %raw(`undefined`), mode: 1, @@ -471,7 +423,7 @@ test("Successfully serializes object with optional fields using (?)", t => { }) t->Assert.deepEqual( - {mode: 1}->S.reverseConvertOrThrow(optionsSchema), + {mode: 1}->S.decodeOrThrow(~from=optionsSchema, ~to=S.unknown), %raw(`{mode: 1, fast: undefined}`), ) }) @@ -486,7 +438,7 @@ test("Successfully parses object with mapped field names", t => { ) t->Assert.deepEqual( - %raw(`{"Name":"Dmitry","Email":"dzakh.dev@gmail.com","Age":21}`)->S.parseOrThrow(schema), + %raw(`{"Name":"Dmitry","Email":"dzakh.dev@gmail.com","Age":21}`)->S.parseOrThrow(~to=schema), {"name": "Dmitry", "email": "dzakh.dev@gmail.com", "age": 21}, ) }) @@ -501,7 +453,7 @@ test("Successfully serializes object with mapped field", t => { ) t->Assert.deepEqual( - {"name": "Dmitry", "email": "dzakh.dev@gmail.com", "age": 21}->S.reverseConvertOrThrow(schema), + {"name": "Dmitry", "email": "dzakh.dev@gmail.com", "age": 21}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"Name":"Dmitry","Email":"dzakh.dev@gmail.com","Age":21}`), ) }) @@ -509,13 +461,13 @@ test("Successfully serializes object with mapped field", t => { test("Successfully parses object transformed to tuple", t => { let schema = S.object(s => (s.field("boo", S.int), s.field("zoo", S.int))) - t->Assert.deepEqual(%raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(schema), (1, 2)) + t->Assert.deepEqual(%raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(~to=schema), (1, 2)) }) test("Successfully serializes object transformed to tuple", t => { let schema = S.object(s => (s.field("boo", S.int), s.field("zoo", S.int))) - t->Assert.deepEqual((1, 2)->S.reverseConvertOrThrow(schema), %raw(`{boo: 1, zoo: 2}`)) + t->Assert.deepEqual((1, 2)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: 1, zoo: 2}`)) }) test("Successfully parses object transformed to nested object", t => { @@ -529,7 +481,7 @@ test("Successfully parses object transformed to nested object", t => { ) t->Assert.deepEqual( - %raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(schema), + %raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(~to=schema), {"v1": {"boo": 1, "zoo": 2}}, ) }) @@ -545,7 +497,7 @@ test("Successfully serializes object transformed to nested object", t => { ) t->Assert.deepEqual( - {"v1": {"boo": 1, "zoo": 2}}->S.reverseConvertOrThrow(schema), + {"v1": {"boo": 1, "zoo": 2}}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: 1, zoo: 2}`), ) }) @@ -557,7 +509,7 @@ test("Successfully parses object transformed to nested tuple", t => { } ) - t->Assert.deepEqual(%raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(schema), {"v1": (1, 2)}) + t->Assert.deepEqual(%raw(`{boo: 1, zoo: 2}`)->S.parseOrThrow(~to=schema), {"v1": (1, 2)}) }) test("Successfully serializes object transformed to nested tuple", t => { @@ -567,19 +519,19 @@ test("Successfully serializes object transformed to nested tuple", t => { } ) - t->Assert.deepEqual({"v1": (1, 2)}->S.reverseConvertOrThrow(schema), %raw(`{boo: 1, zoo: 2}`)) + t->Assert.deepEqual({"v1": (1, 2)}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{boo: 1, zoo: 2}`)) }) test("Successfully parses object with only one field returned from transformer", t => { let schema = S.object(s => s.field("field", S.bool)) - t->Assert.deepEqual(%raw(`{"field": true}`)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(%raw(`{"field": true}`)->S.parseOrThrow(~to=schema), true) }) test("Successfully serializes object with only one field returned from transformer", t => { let schema = S.object(s => s.field("field", S.bool)) - t->Assert.deepEqual(true->S.reverseConvertOrThrow(schema), %raw(`{"field": true}`)) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": true}`)) }) test("Successfully parses object transformed to the one with hardcoded fields", t => { @@ -591,7 +543,7 @@ test("Successfully parses object transformed to the one with hardcoded fields", ) t->Assert.deepEqual( - %raw(`{"field": true}`)->S.parseOrThrow(schema), + %raw(`{"field": true}`)->S.parseOrThrow(~to=schema), { "hardcoded": false, "field": true, @@ -611,7 +563,7 @@ test("Successfully serializes object transformed to the one with hardcoded field { "hardcoded": false, "field": true, - }->S.reverseConvertOrThrow(schema), + }->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": true}`), ) }) @@ -619,13 +571,13 @@ test("Successfully serializes object transformed to the one with hardcoded field test("Successfully parses object transformed to variant", t => { let schema = S.object(s => #VARIANT(s.field("field", S.bool))) - t->Assert.deepEqual(%raw(`{"field": true}`)->S.parseOrThrow(schema), #VARIANT(true)) + t->Assert.deepEqual(%raw(`{"field": true}`)->S.parseOrThrow(~to=schema), #VARIANT(true)) }) test("Successfully serializes object transformed to variant", t => { let schema = S.object(s => #VARIANT(s.field("field", S.bool))) - t->Assert.deepEqual(#VARIANT(true)->S.reverseConvertOrThrow(schema), %raw(`{"field": true}`)) + t->Assert.deepEqual(#VARIANT(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": true}`)) }) test("Parse reversed schema with nested objects and tuples has type validation", t => { @@ -643,7 +595,7 @@ test("Parse reversed schema with nested objects and tuples has type validation", t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, - `i=>{if(typeof i!=="object"||!i||i["foo"]!==1||typeof i["obj"]!=="object"||!i["obj"]||i["obj"]["foo"]!==2||!Array.isArray(i["tuple"])||i["tuple"].length!==2||i["tuple"]["0"]!==3){e[0](i)}let v0=i["obj"],v1=v0["bar"],v2=i["tuple"],v3=v2["1"];if(typeof v1!=="string"){e[1](v1)}if(typeof v3!=="boolean"){e[2](v3)}return {"bar":v1,"baz":v3,}}`, + `i=>{typeof i==="object"&&i||e[7](i);let v0=i["foo"],v1=i["obj"],v4=i["tuple"];v0===1||e[0](v0);typeof v1==="object"&&v1||e[3](v1);let v2=v1["foo"],v3=v1["bar"];v2===2||e[1](v2);typeof v3==="string"||e[2](v3);Array.isArray(v4)&&v4.length===2||e[6](v4);let v5=v4["0"],v6=v4["1"];v5===3||e[4](v5);typeof v6==="boolean"||e[5](v6);return {"bar":v3,"baz":v6,}}`, ) }) @@ -694,12 +646,12 @@ module BenchmarkWithSObject = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(~to=schema), makeTestObject()) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"];if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"];if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, + `i=>{typeof i==="object"&&i||e[10](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"];typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6||e[9](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"];typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, ) S.global({}) }) @@ -710,12 +662,12 @@ module BenchmarkWithSObject = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(schema), ()) + t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(~to=schema), ()) t->U.assertCompiledCode( ~schema, ~op=#Assert, - `i=>{if(typeof i!=="object"||!i||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"];if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"];if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}return void 0}`, + `i=>{typeof i==="object"&&i||e[10](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"];typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6||e[9](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"];typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);return void 0}`, ) S.global({}) }) @@ -727,12 +679,12 @@ module BenchmarkWithSObject = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(~to=schema), makeTestObject()) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]||Array.isArray(i["deeplyNested"])){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v11;if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[10](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[12](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"],v11;typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6&&!Array.isArray(v6)||e[10](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[9](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, ) S.global({}) }) @@ -744,12 +696,12 @@ module BenchmarkWithSObject = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(schema), ()) + t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(~to=schema), ()) t->U.assertCompiledCode( ~schema, ~op=#Assert, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]||Array.isArray(i["deeplyNested"])){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v11;if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[10](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return void 0}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[12](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"],v11;typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6&&!Array.isArray(v6)||e[10](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[9](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return void 0}`, ) S.global({}) }) @@ -760,7 +712,7 @@ module BenchmarkWithSObject = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.reverseConvertOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.decodeOrThrow(~from=schema, ~to=S.unknown), makeTestObject()) t->U.assertCompiledCode( ~schema, @@ -816,10 +768,10 @@ module Benchmark = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"];if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"];if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, + `i=>{typeof i==="object"&&i||e[10](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"];typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6||e[9](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"];typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);return {"number":v0,"negNumber":v1,"maxNumber":v2,"string":v3,"longString":v4,"boolean":v5,"deeplyNested":{"foo":v7,"num":v8,"bool":v9,},}}`, ) - t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(~to=schema), makeTestObject()) S.global({}) }) @@ -830,12 +782,12 @@ module Benchmark = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(schema), ()) + t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(~to=schema), ()) t->U.assertCompiledCode( ~schema, ~op=#Assert, - `i=>{if(typeof i!=="object"||!i||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"];if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"];if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}return void 0}`, + `i=>{typeof i==="object"&&i||e[10](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"];typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6||e[9](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"];typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);return void 0}`, ) S.global({}) }) @@ -847,12 +799,12 @@ module Benchmark = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.parseOrThrow(~to=schema), makeTestObject()) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]||Array.isArray(i["deeplyNested"])){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v11;if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[10](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return i}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[12](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"],v11;typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6&&!Array.isArray(v6)||e[10](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[9](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return i}`, ) S.global({}) }) @@ -864,12 +816,12 @@ module Benchmark = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(schema), ()) + t->Assert.deepEqual(makeTestObject()->S.assertOrThrow(~to=schema), ()) t->U.assertCompiledCode( ~schema, ~op=#Assert, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||typeof i["deeplyNested"]!=="object"||!i["deeplyNested"]||Array.isArray(i["deeplyNested"])){e[0](i)}let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v11;if(typeof v0!=="number"){e[1](v0)}if(typeof v1!=="number"){e[2](v1)}if(typeof v2!=="number"){e[3](v2)}if(typeof v3!=="string"){e[4](v3)}if(typeof v4!=="string"){e[5](v4)}if(typeof v5!=="boolean"){e[6](v5)}let v6=i["deeplyNested"],v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;if(typeof v7!=="string"){e[7](v7)}if(typeof v8!=="number"){e[8](v8)}if(typeof v9!=="boolean"){e[9](v9)}for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[10](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return void 0}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[12](i);let v0=i["number"],v1=i["negNumber"],v2=i["maxNumber"],v3=i["string"],v4=i["longString"],v5=i["boolean"],v6=i["deeplyNested"],v11;typeof v0==="number"||e[0](v0);typeof v1==="number"||e[1](v1);typeof v2==="number"||e[2](v2);typeof v3==="string"||e[3](v3);typeof v4==="string"||e[4](v4);typeof v5==="boolean"||e[5](v5);typeof v6==="object"&&v6&&!Array.isArray(v6)||e[10](v6);let v7=v6["foo"],v8=v6["num"],v9=v6["bool"],v10;typeof v7==="string"||e[6](v7);typeof v8==="number"||e[7](v8);typeof v9==="boolean"||e[8](v9);for(v10 in v6){if(v10!=="foo"&&v10!=="num"&&v10!=="bool"){e[9](v10)}}for(v11 in i){if(v11!=="number"&&v11!=="negNumber"&&v11!=="maxNumber"&&v11!=="string"&&v11!=="longString"&&v11!=="boolean"&&v11!=="deeplyNested"){e[11](v11)}}return void 0}`, ) S.global({}) }) @@ -880,7 +832,7 @@ module Benchmark = { }) let schema = makeSchema() - t->Assert.deepEqual(makeTestObject()->S.reverseConvertOrThrow(schema), makeTestObject()) + t->Assert.deepEqual(makeTestObject()->S.decodeOrThrow(~from=schema, ~to=S.unknown), makeTestObject()) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{let v0=i["deeplyNested"];return i}`) S.global({}) @@ -898,7 +850,7 @@ test("Successfully parses object and serializes it back to the initial data", t } ) - t->Assert.deepEqual(any->S.parseOrThrow(schema)->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema)->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Allows to create object schema with unused fields", t => { @@ -910,7 +862,7 @@ test("Allows to create object schema with unused fields", t => { }) t->Assert.deepEqual( - %raw(`{"field": "foo", "unused": "bar"}`)->S.parseOrThrow(schema), + %raw(`{"field": "foo", "unused": "bar"}`)->S.parseOrThrow(~to=schema), {"field": "foo"}, ) }) @@ -941,7 +893,7 @@ test("Successfully parses object schema with single field registered multiple ti } }) t->Assert.deepEqual( - %raw(`{"field": "foo"}`)->S.parseOrThrow(schema), + %raw(`{"field": "foo"}`)->S.parseOrThrow(~to=schema), {"field1": "foo", "field2": "foo"}, ) }) @@ -964,11 +916,11 @@ test("Reverse convert of object schema with single field registered multiple tim ) t->Assert.deepEqual( - {"field1": "foo", "field2": "foo", "field3": "foo"}->S.reverseConvertOrThrow(schema), + {"field1": "foo", "field2": "foo", "field3": "foo"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"field": "foo"}`), ) // t->U.assertThrows( - // () => {"field1": "foo", "field2": "foo", "field3": "foz"}->S.reverseConvertOrThrow(schema), + // () => {"field1": "foo", "field2": "foo", "field3": "foz"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), // { // code: InvalidOperation({ // description: `Another source has conflicting data for the field ["field"]`, @@ -997,17 +949,17 @@ test("Can destructure fields of simple nested objects", t => { } }) t->Assert.deepEqual( - %raw(`{"nested": {"foo": "foo", "bar": "bar"}}`)->S.parseOrThrow(schema), + %raw(`{"nested": {"foo": "foo", "bar": "bar"}}`)->S.parseOrThrow(~to=schema), {"baz": "bar", "foz": "foo"}, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]){e[0](i)}let v0=i["nested"],v1=v0["foo"],v2=v0["bar"];if(typeof v1!=="string"){e[1](v1)}if(typeof v2!=="string"){e[2](v2)}return {"baz":v2,"foz":v1,}}`, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["nested"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["foo"],v2=v0["bar"];typeof v1==="string"||e[0](v1);typeof v2==="string"||e[1](v2);return {"baz":v2,"foz":v1,}}`, ) t->Assert.deepEqual( - {"baz": "bar", "foz": "foo"}->S.reverseConvertToJsonOrThrow(schema), + {"baz": "bar", "foz": "foo"}->S.decodeOrThrow(~from=schema, ~to=S.json), %raw(`{"nested": {"foo": "foo", "bar": "bar"}}`), ) t->U.assertCompiledCode( @@ -1026,52 +978,37 @@ test("Object schema parsing checks order", t => { })->S.strict // Type check should be the first - t->U.assertThrows( - () => %raw(`"foo"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"foo"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"foo"`)->S.parseOrThrow(~to=schema), + `Expected { tag: "value"; key: string; }, received "foo"`, ) // Tag check should be the second - t->U.assertThrows( + t->U.assertThrowsMessage( () => - %raw(`{tag: "wrong", key: 123, unknownKey: "value", unknownKey2: "value"}`)->S.parseOrThrow( + %raw(`{tag: "wrong", key: 123, unknownKey: "value", unknownKey2: "value"}`)->S.parseOrThrow(~to= schema, ), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`{tag: "wrong", key: 123, unknownKey: "value", unknownKey2: "value"}`), - }), - operation: Parse, - path: S.Path.empty, - }, + `Failed at ["tag"]: Expected "value", received "wrong"`, ) // Field check should be the third - t->U.assertThrows( + t->U.assertThrowsMessage( () => - %raw(`{tag: "value", key: 123, unknownKey: "value", unknownKey2: "value"}`)->S.parseOrThrow( + %raw(`{tag: "value", key: 123, unknownKey: "value", unknownKey2: "value"}`)->S.parseOrThrow(~to= schema, ), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`123`)}), - operation: Parse, - path: S.Path.fromLocation("key"), - }, + `Failed at ["key"]: Expected string, received 123`, ) // Unknown keys check should be the last - t->U.assertThrows( + t->U.assertThrowsMessage( () => - %raw(`{tag: "value", key: "value", unknownKey: "value2", unknownKey2: "value2"}`)->S.parseOrThrow( + %raw(`{tag: "value", key: "value", unknownKey: "value2", unknownKey2: "value2"}`)->S.parseOrThrow(~to= schema, ), - {code: ExcessField("unknownKey"), operation: Parse, path: S.Path.empty}, + `Unrecognized key "unknownKey"`, ) // Parses valid t->Assert.deepEqual( - %raw(`{tag: "value", key: "value"}`)->S.parseOrThrow(schema), + %raw(`{tag: "value", key: "value"}`)->S.parseOrThrow(~to=schema), { "key": "value", }, @@ -1090,7 +1027,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"],v1=i["bar"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}return {"foo":v0,"bar":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["foo"],v1=i["bar"];typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);return {"foo":v0,"bar":v1,}}`, ) t->U.assertCompiledCode( ~schema, @@ -1106,10 +1043,11 @@ module Compiled = { "bar": s.field( "bar", S.object( - s => { - {"baz": s.field("baz", S.string)} - }, - )->S.refine(_ => _ => ()), + s => + { + "baz": s.field("baz", S.string), + }, + )->S.refine(_ => true), ), } ) @@ -1117,12 +1055,12 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||i["foo"]!==12||typeof i["bar"]!=="object"||!i["bar"]){e[0](i)}let v0=i["bar"],v1=v0["baz"];if(typeof v1!=="string"){e[1](v1)}let v2={"baz":v1,};e[2](v2);return {"foo":12,"bar":v2,}}`, + `i=>{if(typeof i!=="object"||!i){e[4](i)}let v0=i["foo"],v1=i["bar"];if(v0!==12){e[0](v0)}if(typeof v1!=="object"||!v1){e[3](v1)}let v2=v1["baz"],v3={"baz":v2,};if(typeof v2!=="string"){e[1](v2)}if(!e[2](v3)){e[5]()}return {"foo":v0,"bar":v3,}}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{let v0=i["bar"];e[0](v0);return {"foo":12,"bar":{"baz":v0["baz"],},}}`, + `i=>{let v0=i["bar"];if(!e[0](v0)){e[1]()}return i}`, ) }) @@ -1137,7 +1075,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["bar"];if(typeof v0!=="boolean"){e[2](v0)}return Promise.all([e[1](i["foo"]),]).then(a=>({"foo":a[0],"bar":v0,}))}`, + `i=>{typeof i==="object"&&i||e[3](i);let v1=i["bar"];let v0;try{v0=e[0](i["foo"]).catch(x=>e[1](x))}catch(x){e[1](x)}typeof v1==="boolean"||e[2](v1);return Promise.all([v0,]).then(([v0,])=>{return {"foo":v0,"bar":v1,}})}`, ) }) @@ -1149,7 +1087,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="object"||!i){e[0](i)}return e[1](i["foo"])}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0;try{v0=e[0](i["foo"]).catch(x=>e[1](x))}catch(x){e[1](x)}return v0}`, ) }) @@ -1164,7 +1102,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)){e[0](i)}let v0=i["foo"],v1=i["bar"],v2;if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}for(v2 in i){if(v2!=="foo"&&v2!=="bar"){e[3](v2)}}return {"foo":v0,"bar":v1,}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[3](i);let v0=i["foo"],v1=i["bar"],v2;typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);for(v2 in i){if(v2!=="foo"&&v2!=="bar"){e[2](v2)}}return {"foo":v0,"bar":v1,}}`, ) }) @@ -1193,7 +1131,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["nested"]!=="object"||!i["nested"]||Array.isArray(i["nested"])){e[0](i)}let v0,v1=i["nested"];for(v0 in v1){if(true){e[1](v0)}}return {"nested":void 0,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v1=i["nested"];typeof v1==="object"&&v1&&!Array.isArray(v1)||e[1](v1);let v0;for(v0 in v1){if(true){e[0](v0)}}return {"nested":void 0,}}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"nested":{},}}`) }) @@ -1213,7 +1151,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||i["tag"]!==0){e[0](i)}let v0=i["FOO"],v1=i["BAR"],v2;if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}for(v2 in i){if(v2!=="tag"&&v2!=="FOO"&&v2!=="BAR"){e[3](v2)}}return {"foo":v0,"bar":v1,"zoo":1,}}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[4](i);let v0=i["tag"],v1=i["FOO"],v2=i["BAR"],v3;v0===0||e[0](v0);typeof v1==="string"||e[1](v1);typeof v2==="boolean"||e[2](v2);for(v3 in i){if(v3!=="tag"&&v3!=="FOO"&&v3!=="BAR"){e[3](v3)}}return {"foo":v1,"bar":v2,"zoo":1,}}`, ) }, ) @@ -1261,10 +1199,10 @@ test( "android": {"current": "1.2", "minimum": "1.1"}, } - let value = appVersions->S.parseOrThrow(appVersionsSchema) + let value = appVersions->S.parseOrThrow(~to=appVersionsSchema) t->Assert.deepEqual(value, appVersions) - let data = appVersions->S.reverseConvertToJsonOrThrow(appVersionsSchema) + let data = appVersions->S.decodeOrThrow(~from=appVersionsSchema, ~to=S.json) t->Assert.deepEqual(data, appVersions->Obj.magic) }, ) @@ -1281,7 +1219,7 @@ test("Compiles to async serialize operation with the sync object schema", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvertAsync, - `i=>{if(i!==void 0){e[0](i)}return Promise.resolve({})}`, + `i=>{i===void 0||e[0](i);return Promise.resolve({})}`, ) }) @@ -1310,17 +1248,6 @@ test("Reverse tagged object to primitive schema", t => { s.tag("kind", "test") s.field("field", S.bool) }) - t->U.assertEqualSchemas( - schema->S.reverse, - S.bool - ->S.shape(bool => - { - "kind": "test", - "field": bool, - } - ) - ->S.castToUnknown, - ) t->U.assertReverseReversesBack(schema) t->U.assertReverseParsesBack(schema, true) @@ -1339,6 +1266,19 @@ test("Reverse object with discriminant which is an object transformed to literal ) s.field("field", S.bool) }) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["kind"],v2=i["field"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["nestedKind"];v1==="test"||e[0](v1);typeof v2==="boolean"||e[2](v2);return v2}`, + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{return {"kind":{"nestedKind":"test",},"field":i,}}`, + ) + t->U.assertReverseReversesBack(schema) t->U.assertReverseParsesBack(schema, true) }) @@ -1352,25 +1292,6 @@ test("Reverse with output of nested object/tuple schema", t => { }, } }) - t->U.assertEqualSchemas( - schema->S.reverse, - S.schema(s => { - { - "nested": { - "field": (s.matches(S.bool), true), - }, - } - }) - ->S.shape(data => - { - "kind": "test", - "raw_field": { - let (v, _) = data["nested"]["field"] - v - }, - } - ) - ->S.castToUnknown, - ) + t->U.assertReverseReversesBack(schema) t->U.assertReverseParsesBack(schema, {"nested": {"field": (true, true)}}) }) diff --git a/packages/sury/tests/S_object_withoutDeclaredFields_test.res b/packages/sury/tests/S_object_withoutDeclaredFields_test.res index 3f8481d62..467aba4cc 100644 --- a/packages/sury/tests/S_object_withoutDeclaredFields_test.res +++ b/packages/sury/tests/S_object_withoutDeclaredFields_test.res @@ -3,46 +3,46 @@ open Ava test("Successfully parses empty object", t => { let schema = S.object(_ => ()) - t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(~to=schema), ()) }) test("Successfully parses object with excess keys", t => { let schema = S.object(_ => ()) - t->Assert.deepEqual(%raw(`{field:"bar"}`)->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(%raw(`{field:"bar"}`)->S.parseOrThrow(~to=schema), ()) }) test("Successfully parses empty object when UnknownKeys are strict", t => { let schema = S.object(_ => ())->S.strict - t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(~to=schema), ()) }) test("Fails to parse object with excess keys when UnknownKeys are strict", t => { let schema = S.object(_ => ())->S.strict - t->U.assertThrows( - () => %raw(`{field:"bar"}`)->S.parseOrThrow(schema), - {code: ExcessField("field"), operation: Parse, path: S.Path.empty}, + t->U.assertThrowsMessage( + () => %raw(`{field:"bar"}`)->S.parseOrThrow(~to=schema), + `Unrecognized key "field"`, ) }) test("Successfully parses object with excess keys and returns transformed value", t => { - let transformedValue = {"bas": true} - let schema = S.object(_ => transformedValue) + // FIXME: S.object mutatate `let transformedValue = {"bas": true}` + let schema = S.object(_ => {"bas": true}) - t->Assert.deepEqual(%raw(`{field:"bar"}`)->S.parseOrThrow(schema), transformedValue) + t->Assert.deepEqual(%raw(`{field:"bar"}`)->S.parseOrThrow(~to=schema), {"bas": true}) }) test("Successfully serializes transformed value to empty object", t => { let transformedValue = {"bas": true} let schema = S.object(_ => transformedValue) - t->Assert.deepEqual(transformedValue->S.reverseConvertOrThrow(schema), %raw("{}")) + t->Assert.deepEqual(transformedValue->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw("{}")) }) test("Allows to pass array to object value", t => { let schema = S.object(_ => ()) - t->Assert.deepEqual(%raw(`[]`)->S.parseOrThrow(schema), ()) + t->Assert.deepEqual(%raw(`[]`)->S.parseOrThrow(~to=schema), ()) }) diff --git a/packages/sury/tests/S_option_test.res b/packages/sury/tests/S_option_test.res index bc84ca49e..d802a0d95 100644 --- a/packages/sury/tests/S_option_test.res +++ b/packages/sury/tests/S_option_test.res @@ -9,26 +9,22 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected string | undefined, received 123.45`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { @@ -41,21 +37,25 @@ module Common = { ) }) - // Undefined check should be first + // Undefined check should be first ? test("Compiled async parse code snapshot", t => { let schema = S.option(S.unknown->S.transform(_ => {asyncParser: i => Promise.resolve(i)})) t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{try{i=e[0](i)}catch(e0){if(!(i===void 0)){e[1](i,e0)}}return Promise.resolve(i)}`, + `i=>{try{let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}catch(e0){if(!(i===void 0)){e[2](i,e0)}}return Promise.resolve(i)}`, ) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(!(typeof i==="string"||i===void 0)){e[0](i)}return i}`, + ) }) test("Reverse to self", t => { @@ -71,7 +71,7 @@ module Common = { } test("Classify schema", t => { - let schema = S.option(S.null(S.string)) + let schema = S.option(S.nullAsOption(S.string)) t->U.assertEqualSchemas( schema->S.castToUnknown, @@ -95,43 +95,35 @@ test("Classify schema", t => { test("Successfully parses primitive", t => { let schema = S.option(S.bool) - t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(schema), Some(true)) + t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(~to=schema), Some(true)) }) test("Fails to parse JS null", t => { let schema = S.option(S.bool) - t->U.assertThrows( - () => %raw(`null`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`null`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`null`)->S.parseOrThrow(~to=schema), + `Expected boolean | undefined, received null`, ) }) test("Fails to parse JS undefined when schema doesn't allow optional data", t => { let schema = S.bool - t->U.assertThrows( - () => %raw(`undefined`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`undefined`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`undefined`)->S.parseOrThrow(~to=schema), + `Expected boolean, received undefined`, ) }) test("Serializes Some(None) to undefined for option nested in null", t => { - let schema = S.null(S.option(S.bool)) + let schema = S.nullAsOption(S.option(S.bool)) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), Some(None)) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), None) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), Some(None)) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), None) - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`null`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) t->U.assertCompiledCode( ~schema, @@ -141,32 +133,32 @@ test("Serializes Some(None) to undefined for option nested in null", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i===void 0){i=null}else if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}return i}`, + `i=>{if(i===void 0){i=null}else if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}}else if(!(typeof i==="boolean")){e[0](i)}return i}`, ) }) test("Applies valFromOption for Some()", t => { let schema = S.option(S.literal()) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(Some()->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(Some()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(!(i===void 0)){e[0](i)}return i}`) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}}else if(!(i===void 0)){e[0](i)}return i}`, ) }) test("Nested option support", t => { let schema = S.option(S.option(S.bool)) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(Some(Some(true))->S.reverseConvertOrThrow(schema), %raw(`true`)) - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(Some(Some(true))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode( ~schema, @@ -176,18 +168,18 @@ test("Nested option support", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}}else if(!(typeof i==="boolean"||i===void 0)){e[0](i)}return i}`, ) }) test("Triple nested option support", t => { let schema = S.option(S.option(S.option(S.bool))) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(Some(Some(Some(true)))->S.reverseConvertOrThrow(schema), %raw(`true`)) - t->Assert.deepEqual(Some(Some(None))->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(Some(None)->S.reverseConvertOrThrow(schema), %raw(`undefined`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(Some(Some(Some(true)))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`true`)) + t->Assert.deepEqual(Some(Some(None))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(Some(None)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode( ~schema, @@ -197,7 +189,7 @@ test("Triple nested option support", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===1){i=void 0}else if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===1){i=void 0}else if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i=void 0}else{e[0](i)}}else if(!(typeof i==="boolean"||i===void 0)){e[1](i)}return i}`, ) }) @@ -206,20 +198,20 @@ test( t => { let schema = S.option(S.object(_ => ())) - t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(schema), Some()) - t->Assert.deepEqual(Some()->S.reverseConvertOrThrow(schema), %raw(`{}`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(%raw(`undefined`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`{}`)->S.parseOrThrow(~to=schema), Some()) + t->Assert.deepEqual(Some()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{}`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){i={BS_PRIVATE_NESTED_SOME_NONE:0}}else if(!(i===void 0)){e[0](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){i={BS_PRIVATE_NESTED_SOME_NONE:0}}else if(!(i===void 0)){e[0](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i&&i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i={}}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["BS_PRIVATE_NESTED_SOME_NONE"]===0){i={}}}else if(!(i===void 0)){e[0](i)}return i}`, ) }, ) @@ -228,21 +220,25 @@ test("Doesn't apply valFromOption for non-undefined literals in option", t => { let schema: S.t>> = S.option(S.literal(%raw(`null`))) // Note: It'll fail without a type annotation, but we can't do anything here - t->Assert.deepEqual(Some(%raw(`null`))->S.reverseConvertOrThrow(schema), %raw(`null`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(Some(%raw(`null`))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(!(i===null||i===void 0)){e[0](i)}return i}`, + ) }) test("Option with unknown", t => { let schema = S.option(S.unknown) t->Assert.deepEqual( - Some(%raw(`undefined`))->S.reverseConvertOrThrow(schema), + Some(%raw(`undefined`))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{BS_PRIVATE_NESTED_SOME_NONE: 0}`), ) - t->Assert.deepEqual(Some(%raw(`"foo"`))->S.reverseConvertOrThrow(schema), %raw(`"foo"`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(Some(%raw(`"foo"`))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"foo"`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCodeIsNoop(~schema, ~op=#Parse) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) @@ -251,12 +247,12 @@ test("Option with unknown", t => { test("Option with transformed unknown", t => { let schema = S.option(S.unknown->S.shape(v => {"field": v})) - t->Assert.deepEqual(Some(%raw(`undefined`))->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(Some(%raw(`undefined`))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->Assert.deepEqual( - Some({"field": %raw(`"foo"`)})->S.reverseConvertOrThrow(schema), + Some({"field": %raw(`"foo"`)})->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"foo"`), ) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) t->U.assertCompiledCode( ~schema, @@ -266,6 +262,6 @@ test("Option with transformed unknown", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i){i=i["field"]}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){i=i["field"]}else if(!(i===void 0)){e[0](i)}return i}`, ) }) diff --git a/packages/sury/tests/S_parseAsync_test.res b/packages/sury/tests/S_parseAsync_test.res index c1ee87f2d..dc34182ef 100644 --- a/packages/sury/tests/S_parseAsync_test.res +++ b/packages/sury/tests/S_parseAsync_test.res @@ -3,7 +3,7 @@ open Ava let validAsyncRefine = S.transform(_, _ => { asyncParser: value => value->Promise.resolve, }) -let invalidSyncRefine = S.refine(_, s => _ => s.fail("Sync user error")) +let invalidSyncRefine = S.refine(_, _ => false, ~error="Sync user error") let unresolvedPromise = Promise.make((_, _) => ()) let makeInvalidPromise = (s: S.s<'a>) => Promise.resolve()->Promise.then(() => s.fail("Async user error")) @@ -28,7 +28,7 @@ let invalidAsyncRefine = S.transform(_, s => { // %raw(`123`)->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`123`)}), +// code: InvalidType({expected: schema->S.castToUnknown, value: %raw(`123`)}), // operation: ParseAsync, // path: S.Path.empty, // }), @@ -254,7 +254,7 @@ let invalidAsyncRefine = S.transform(_, s => { // }->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: invalidSchema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: invalidSchema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.fromArray(["k2"]), // }), @@ -323,7 +323,7 @@ let invalidAsyncRefine = S.transform(_, s => { // "k1": 1, // "k2": 2, // } -// ->S.parseAsyncOrThrow(schema) +// ->S.parseAsyncOrThrow(~to=schema) // ->ignore // t->Assert.deepEqual(actionCounter.contents, 2) @@ -386,7 +386,7 @@ let invalidAsyncRefine = S.transform(_, s => { // %raw(`[1, true, 3]`)->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: invalidSchema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: invalidSchema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.fromArray(["1"]), // }), @@ -433,7 +433,7 @@ let invalidAsyncRefine = S.transform(_, s => { // }), // ) -// [1, 2]->S.parseAsyncOrThrow(schema)->ignore +// [1, 2]->S.parseAsyncOrThrow(~to=schema)->ignore // t->Assert.deepEqual(actionCounter.contents, 2) // }) @@ -480,9 +480,18 @@ module Union = { test("[Union] Passes with Parse operation. Async item should fail", t => { let schema = S.union([S.literal(2)->validAsyncRefine, S.literal(2), S.literal(3)]) - t->U.assertThrowsMessage(() => { - 2->S.parseOrThrow(schema) - }, "Failed parsing: Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead") + t->Assert.deepEqual( + 2->S.parseOrThrow(~to=schema), + 2, + ~message="I'm not sure whether this is correct logic, but it's what we have now", + ) + t->U.assertThrowsMessage( + () => { + 4->S.parseOrThrow(~to=schema) + }, + "Expected 2 | 2 | 3, received 4 +- Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead", + ) }) // Failing.asyncTest( @@ -497,17 +506,17 @@ module Union = { // { // code: InvalidUnion([ // U.error({ - // code: InvalidType({expected: S.literal(1.), received: input})->S.castToUnknown, + // code: InvalidType({expected: S.literal(1.), value: input})->S.castToUnknown, // path: S.Path.empty, // operation: ParseAsync, // }), // U.error({ - // code: InvalidType({expected: S.literal(2.), received: input})->S.castToUnknown, + // code: InvalidType({expected: S.literal(2.), value: input})->S.castToUnknown, // path: S.Path.empty, // operation: ParseAsync, // }), // U.error({ - // code: InvalidType({expected: S.literal(3.), received: input})->S.castToUnknown, + // code: InvalidType({expected: S.literal(3.), value: input})->S.castToUnknown, // path: S.Path.empty, // operation: ParseAsync, // }), @@ -538,7 +547,7 @@ module Union = { }), ]) - 2->S.parseAsyncOrThrow(schema)->ignore + 2->S.parseAsyncOrThrow(~to=schema)->ignore t->Assert.deepEqual(actionCounter.contents, 1) }) @@ -563,7 +572,7 @@ module Union = { // %raw(`[1, 2, true]`)->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: invalidSchema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: invalidSchema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.fromArray(["2"]), // }), @@ -584,7 +593,7 @@ module Union = { // }), // ) -// [1, 2]->S.parseAsyncOrThrow(schema)->ignore +// [1, 2]->S.parseAsyncOrThrow(~to=schema)->ignore // t->Assert.deepEqual(actionCounter.contents, 2) // }) @@ -642,7 +651,7 @@ module Union = { // {"k1": 1, "k2": 2, "k3": true}->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: invalidSchema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: invalidSchema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.fromArray(["k3"]), // }), @@ -663,7 +672,7 @@ module Union = { // }), // ) -// {"k1": 1, "k2": 2}->S.parseAsyncOrThrow(schema)->ignore +// {"k1": 1, "k2": 2}->S.parseAsyncOrThrow(~to=schema)->ignore // t->Assert.deepEqual(actionCounter.contents, 2) // }) @@ -704,7 +713,7 @@ module Union = { // module Null = { // asyncTest("[Null] Successfully parses", t => { -// let schema = S.null(S.int->validAsyncRefine) +// let schema = S.nullAsOption(S.int->validAsyncRefine) // Promise.all([ // (1->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { @@ -719,7 +728,7 @@ module Union = { // }) // asyncTest("[Null] Fails to parse with invalid async refine", t => { -// let schema = S.null(S.int->invalidAsyncRefine) +// let schema = S.nullAsOption(S.int->invalidAsyncRefine) // (1->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { // t->Assert.deepEqual( @@ -737,13 +746,13 @@ module Union = { // }) // test("[Null] Returns sync error when fails to parse sync part of async item", t => { -// let schema = S.null(S.int->validAsyncRefine) +// let schema = S.nullAsOption(S.int->validAsyncRefine) // t->Assert.deepEqual( // true->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: schema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.empty, // }), @@ -794,7 +803,7 @@ module Union = { // true->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: schema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.empty, // }), @@ -846,7 +855,7 @@ module Union = { // true->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: schema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.empty, // }), @@ -898,7 +907,7 @@ module Union = { // "true"->S.parseAnyAsyncInStepsWith(schema), // Error( // U.error({ -// code: InvalidType({expected: invalidSchema->S.castToUnknown, received: %raw(`true`)}), +// code: InvalidType({expected: invalidSchema->S.castToUnknown, value: %raw(`true`)}), // operation: ParseAsync, // path: S.Path.empty, // }), diff --git a/packages/sury/tests/S_parseJsonStringWith_test.res b/packages/sury/tests/S_parseJsonStringWith_test.res index b79ffff93..b91845f96 100644 --- a/packages/sury/tests/S_parseJsonStringWith_test.res +++ b/packages/sury/tests/S_parseJsonStringWith_test.res @@ -1,47 +1,44 @@ open Ava +S.enableJsonString() + test("Successfully parses", t => { let schema = S.bool - t->Assert.deepEqual("true"->S.parseJsonStringOrThrow(schema), true) + t->Assert.deepEqual("true"->S.decodeOrThrow(~from=S.jsonString, ~to=schema), true) }) test("Successfully parses unknown", t => { let schema = S.unknown - t->Assert.deepEqual("true"->S.parseJsonStringOrThrow(schema), true->Obj.magic) + t->Assert.deepEqual( + "true"->S.decodeOrThrow(~from=S.jsonString, ~to=schema), + "true"->Obj.magic, + ~message="S.unknown should keep json schema as a value", + ) + + t->Assert.deepEqual( + "tru"->S.decodeOrThrow(~from=S.jsonString, ~to=schema), + "tru"->Obj.magic, + ~message="It also doesn't validate the value being a json string, because it expects input to already be a valid json string", + ) }) test("Fails to parse JSON", t => { let schema = S.bool - switch "123,"->S.parseJsonStringOrThrow(schema) { - | _ => t->Assert.fail("Must return Error") - | exception S.Error({code, flag, path}) => { - t->Assert.deepEqual(flag, S.Flag.typeValidation) - t->Assert.deepEqual(path, S.Path.empty) - switch code { - // Different errors for different Node.js versions - | OperationFailed("Unexpected token , in JSON at position 3") - | OperationFailed("Unexpected non-whitespace character after JSON at position 3") - | OperationFailed( - "Unexpected non-whitespace character after JSON at position 3 (line 1 column 4)", - ) => () - | _ => t->Assert.fail("Code must be OperationFailed") - } - } - } + U.assertThrowsMessage( + t, + () => "123,"->S.decodeOrThrow(~from=S.jsonString, ~to=schema), + `Expected JSON string, received "123,"`, + ) }) test("Fails to parse", t => { let schema = S.bool - t->U.assertThrows( - () => "123"->S.parseJsonStringOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: Obj.magic(123)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "123"->S.decodeOrThrow(~from=S.jsonString, ~to=schema), + `Expected boolean, received 123`, ) }) diff --git a/packages/sury/tests/S_parseWith_test.res b/packages/sury/tests/S_parseWith_test.res index 679dc581c..c055eb5fd 100644 --- a/packages/sury/tests/S_parseWith_test.res +++ b/packages/sury/tests/S_parseWith_test.res @@ -3,25 +3,21 @@ open Ava test("Successfully parses", t => { let schema = S.bool - t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(schema), true) + t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(~to=schema), true) }) test("Successfully parses unknown", t => { let schema = S.unknown - t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(schema), true->Obj.magic) + t->Assert.deepEqual(JSON.Encode.bool(true)->S.parseOrThrow(~to=schema), true->Obj.magic) }) test("Fails to parse", t => { let schema = S.bool - t->U.assertThrows( - () => %raw("123")->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw("123")}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw("123")->S.parseOrThrow(~to=schema), + `Expected boolean, received 123`, ) }) @@ -29,6 +25,6 @@ test("Fails to parse with unwraped result", t => { let schema = S.bool t->Assert.throws(() => { - %raw("123")->S.parseOrThrow(schema) - }, ~expectations={message: "Failed parsing: Expected boolean, received 123"}) + %raw("123")->S.parseOrThrow(~to=schema) + }, ~expectations={message: "Expected boolean, received 123"}) }) diff --git a/packages/sury/tests/S_recursive_test.res b/packages/sury/tests/S_recursive_test.res index 3596e63bc..14525b895 100644 --- a/packages/sury/tests/S_recursive_test.res +++ b/packages/sury/tests/S_recursive_test.res @@ -22,7 +22,7 @@ test("Successfully parses recursive object", t => { {"Id": "2", "Children": []}, {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], - }->S.parseOrThrow(nodeSchema), + }->S.parseOrThrow(~to=nodeSchema), { id: "1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], @@ -44,11 +44,11 @@ test("Fails to parses recursive object when provided invalid type", t => { switch { "Id": "1", "Children": ["invalid"], - }->S.parseOrThrow(nodeSchema) { + }->S.parseOrThrow(~to=nodeSchema) { | _ => "Shouldn't pass" - | exception S.Error({message}) => message + | exception S.Exn({message}) => message }, - `Failed parsing at ["Children"]["0"]: Expected Node, received "invalid"`, + `Failed at ["Children"]["0"]: Expected Node, received "invalid"`, ) }) @@ -65,8 +65,8 @@ asyncTest("Successfully parses recursive object using S.parseAsyncOrThrow", t => t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#ParseAsync, - `i=>{let v0=e[0](i);return Promise.resolve(v0)} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{let v0;v0=e[0](i);return Promise.resolve(v0)} +Node: i=>{typeof i==="object"&&i||e[3](i);let v0=i["Id"],v1=i["Children"];typeof v0==="string"||e[0](v0);Array.isArray(v1)||e[2](v1);let v5=new Array(v1.length);for(let v2=0;v2Node--1"](v1[v2]);v5[v2]=v3}catch(v4){v4.path="[\\"Children\\"]"+'["'+v2+'"]'+v4.path;throw v4}}return {"id":v0,"children":v5,}}`, ) %raw(`{ @@ -76,7 +76,7 @@ Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], }`) - ->S.parseAsyncOrThrow(nodeSchema) + ->S.parseAsyncOrThrow(~to=nodeSchema) ->Promise.thenResolve(result => { t->Assert.deepEqual( result, @@ -98,18 +98,22 @@ test("Successfully serializes recursive object", t => { ) }) + let reversedDefs = (nodeSchema->S.reverse->S.untag).defs->Option.getUnsafe + let nodeSeq = (reversedDefs->Dict.getUnsafe("Node")->S.untag).seq->Float.toString + let recKey = `${nodeSeq}-${nodeSeq}--0` t->U.assertCompiledCode( ~schema=nodeSchema->S.reverse, ~op=#Convert, - `i=>{let v0=e[0](i);return v0} -Node: i=>{let v0=i["children"],v5=new Array(v0.length);for(let v1=0;v1{let v0;v0=e[0](i);return v0} +Node: i=>{let v0=i["children"];let v4=new Array(v0.length);for(let v1=0;v1Assert.deepEqual( { id: "1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], - }->S.reverseConvertOrThrow(nodeSchema), + }->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown), %raw(`{ "Id": "1", "Children": [ @@ -126,35 +130,22 @@ test("Fails to parse nested recursive object", t => { s => { id: s.field( "Id", - S.string->S.refine( - s => - id => { - if id === "4" { - s.fail("Invalid id") - } - }, - ), + S.string->S.refine(id => id !== "4", ~error="Invalid id"), ), children: s.field("Children", S.array(nodeSchema)), }, ) }) - t->U.assertThrows( - () => - { - "Id": "1", - "Children": [ - {"Id": "2", "Children": []}, - {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, - ], - }->S.parseOrThrow(nodeSchema), + t->U.assertThrowsMessage(() => { - code: OperationFailed("Invalid id"), - operation: Parse, - path: S.Path.fromArray(["Children", "1", "Children", "0", "Id"]), - }, - ) + "Id": "1", + "Children": [ + {"Id": "2", "Children": []}, + {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, + ], + }->S.parseOrThrow(~to=nodeSchema) + , `Failed at ["Children"]["1"]["Children"]["0"]["Id"]: Invalid id`) }) test("Fails to parse nested recursive object inside of another object", t => { @@ -168,14 +159,7 @@ test("Fails to parse nested recursive object inside of another object", t => { s => { id: s.field( "Id", - S.string->S.refine( - s => - id => { - if id === "4" { - s.fail("Invalid id") - } - }, - ), + S.string->S.refine(id => id !== "4", ~error="Invalid id"), ), children: s.field("Children", S.array(nodeSchema)), }, @@ -185,23 +169,17 @@ test("Fails to parse nested recursive object inside of another object", t => { ) ) - t->U.assertThrows( - () => - { - "recursive": { - "Id": "1", - "Children": [ - {"Id": "2", "Children": []}, - {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, - ], - }, - }->S.parseOrThrow(schema), + t->U.assertThrowsMessage(() => { - code: OperationFailed("Invalid id"), - operation: Parse, - path: S.Path.fromArray(["recursive", "Children", "1", "Children", "0", "Id"]), - }, - ) + "recursive": { + "Id": "1", + "Children": [ + {"Id": "2", "Children": []}, + {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, + ], + }, + }->S.parseOrThrow(~to=schema) + , `Failed at ["recursive"]["Children"]["1"]["Children"]["0"]["Id"]: Invalid id`) }) test("Parses multiple nested recursive object inside of another object", t => { @@ -241,7 +219,7 @@ test("Parses multiple nested recursive object inside of another object", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v1;try{v1=e[1](i["recursive1"])}catch(v0){if(v0&&v0.s===s){v0.path="[\\"recursive1\\"]"+v0.path}throw v0}let v3;try{v3=e[2](i["recursive2"])}catch(v2){if(v2&&v2.s===s){v2.path="[\\"recursive2\\"]"+v2.path}throw v2}return {"recursive1":v1,"recursive2":v3,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0;try{v0=e[0](i["recursive1"]);}catch(v1){v1.path="[\\"recursive1\\"]"+v1.path;throw v1}let v2;try{v2=e[1](i["recursive2"]);}catch(v3){v3.path="[\\"recursive2\\"]"+v3.path;throw v3}return {"recursive1":v0,"recursive2":v2,}}`, ) t->Assert.deepEqual( @@ -260,7 +238,7 @@ test("Parses multiple nested recursive object inside of another object", t => { {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], }, - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), { "recursive1": { id: "1", @@ -280,31 +258,20 @@ test("Fails to serialise nested recursive object", t => { s => { id: s.field( "Id", - S.string->S.refine( - s => - id => { - if id === "4" { - s.fail("Invalid id") - } - }, - ), + S.string->S.refine(id => id !== "4", ~error="Invalid id"), ), children: s.field("Children", S.array(nodeSchema)), }, ) }) - t->U.assertThrows( + t->U.assertThrowsMessage( () => { id: "1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], - }->S.reverseConvertOrThrow(nodeSchema), - { - code: OperationFailed("Invalid id"), - operation: ReverseConvert, - path: S.Path.fromArray(["children", "1", "children", "0", "id"]), - }, + }->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown), + `Failed at ["children"]["1"]["children"]["0"]["id"]: Invalid id`, ) }) @@ -328,8 +295,8 @@ test( t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#Parse, - `i=>{let v0=e[0](i);return v0} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{let v0;v0=e[0](i);return v0} +Node: i=>{typeof i==="object"&&i||e[5](i);let v0=i["Id"],v1=i["Children"];typeof v0==="string"||e[0](v0);Array.isArray(v1)||e[2](v1);let v5=new Array(v1.length);for(let v2=0;v2Node--0"](v1[v2]);v5[v2]=v3}catch(v4){v4.path="[\\"Children\\"]"+'["'+v2+'"]'+v4.path;throw v4}}let v6;try{v6=e[3]({"id":v0,"children":v5,})}catch(x){e[4](x)}return v6}`, ) t->Assert.deepEqual( { @@ -338,7 +305,7 @@ Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let {"Id": "2", "Children": []}, {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], - }->S.parseOrThrow(nodeSchema), + }->S.parseOrThrow(~to=nodeSchema), { id: "node_1", children: [ @@ -351,7 +318,9 @@ Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#ReverseConvert, - `i=>{let v0=e[0](i);return v0}`, + ~embedded=[("Node", 0)], + `i=>{let v0;v0=e[0](i);return v0} +Node: i=>{let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="object"&&v0||e[5](v0);let v1=v0["id"],v2=v0["children"];typeof v1==="string"||e[2](v1);Array.isArray(v2)||e[4](v2);let v6=new Array(v2.length);for(let v3=0;v3Assert.deepEqual( { @@ -360,7 +329,7 @@ Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let {id: "node_2", children: []}, {id: "node_3", children: [{id: "node_4", children: []}]}, ], - }->S.reverseConvertOrThrow(nodeSchema), + }->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown), { "Id": "1", "Children": [ @@ -399,7 +368,7 @@ test("Recursively transforms nested objects when added transform to the placehol {"Id": "2", "Children": []}, {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], - }->S.parseOrThrow(nodeSchema), + }->S.parseOrThrow(~to=nodeSchema), { id: "1", children: [ @@ -415,7 +384,7 @@ test("Recursively transforms nested objects when added transform to the placehol {id: "child_2", children: []}, {id: "child_3", children: [{id: "child_4", children: []}]}, ], - }->S.reverseConvertOrThrow(nodeSchema), + }->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown), { "Id": "1", "Children": [ @@ -439,6 +408,7 @@ test("Shallowly transforms object when added transform to the S.recursive result serializer: node => {...node, id: node.id->String.slice(~start=7)}, }) + // FIXME: There's a double run of array decoder t->Assert.deepEqual( { "Id": "1", @@ -446,7 +416,7 @@ test("Shallowly transforms object when added transform to the S.recursive result {"Id": "2", "Children": []}, {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], - }->S.parseOrThrow(nodeSchema), + }->S.parseOrThrow(~to=nodeSchema), { id: "parent_1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], @@ -456,7 +426,7 @@ test("Shallowly transforms object when added transform to the S.recursive result { id: "parent_1", children: [{id: "2", children: []}, {id: "3", children: [{id: "4", children: []}]}], - }->S.reverseConvertOrThrow(nodeSchema), + }->S.decodeOrThrow(~from=nodeSchema, ~to=S.unknown), { "Id": "1", "Children": [ @@ -469,13 +439,18 @@ test("Shallowly transforms object when added transform to the S.recursive result t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#Parse, - `i=>{let v0=e[0](i);return e[1](v0)} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{let v0;v0=e[0](i);let v1;try{v1=e[1](v0)}catch(x){e[2](x)}return v1} +Node: i=>{typeof i==="object"&&i||e[3](i);let v0=i["Id"],v1=i["Children"];typeof v0==="string"||e[0](v0);Array.isArray(v1)||e[2](v1);let v5=new Array(v1.length);for(let v2=0;v2Node--0"](v1[v2]);v5[v2]=v3}catch(v4){v4.path="[\\"Children\\"]"+'["'+v2+'"]'+v4.path;throw v4}}return {"id":v0,"children":v5,}}`, ) + let reversedDefs = + ((nodeSchema->S.reverse->S.untag).to->Option.getUnsafe->S.untag).defs->Option.getUnsafe + let recKey = `${(S.unknown->S.untag).seq->Float.toString}-${(reversedDefs->Dict.getUnsafe("Node")->S.untag).seq->Float.toString}--0` t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#ReverseConvert, - `i=>{let v0=e[1](e[0](i));return v0}`, + ~embedded=[("Node", 2)], + `i=>{let v0;try{v0=e[0](i)}catch(x){e[1](x)}let v1;v1=e[2](v0);return v1} +Node: i=>{typeof i==="object"&&i||e[3](i);let v0=i["id"],v1=i["children"];typeof v0==="string"||e[0](v0);Array.isArray(v1)||e[2](v1);let v5=new Array(v1.length);for(let v2=0;v2 t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#ParseAsync, - `i=>{let v0=e[0](i);return v0} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+'["'+v2+'"]'+v3.path}throw v3})}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+'["'+v2+'"]'+v3.path}throw v3}v6[v2]=v5}return Promise.all([e[2](v0),Promise.all(v6),]).then(a=>({"id":a[0],"children":a[1],}))}`, + `i=>{let v0;v0=e[0](i);return v0} +Node: i=>{typeof i==="object"&&i||e[5](i);let v1=i["Id"],v2=i["Children"];typeof v1==="string"||e[2](v1);let v0;try{v0=e[0](i["Id"]).catch(x=>e[1](x))}catch(x){e[1](x)}Array.isArray(v2)||e[4](v2);let v6=new Array(v2.length);for(let v3=0;v3Node--1"](v2[v3]);v6[v3]=v4.catch(v5=>{v5.path="[\\"Children\\"]"+'["'+v3+'"]'+v5.path;throw v5})}catch(v5){v5.path="[\\"Children\\"]"+'["'+v3+'"]'+v5.path;throw v5}}let v7=Promise.all(v6);return Promise.all([v0,v7,]).then(([v0,v7,])=>{return {"id":v0,"children":v7,}})}`, ) %raw(`{ @@ -503,7 +478,7 @@ Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], }`) - ->S.parseAsyncOrThrow(nodeSchema) + ->S.parseAsyncOrThrow(~to=nodeSchema) ->Promise.thenResolve(result => { t->Assert.deepEqual( result, @@ -545,7 +520,7 @@ test("Parses recursive object with async fields in parallel", t => { {"Id": "3", "Children": [{"Id": "4", "Children": []}]}, ], }`) - ->S.parseAsyncOrThrow(nodeSchema) + ->S.parseAsyncOrThrow(~to=nodeSchema) ->ignore t->Assert.deepEqual(actionCounter.contents, 4) @@ -553,8 +528,8 @@ test("Parses recursive object with async fields in parallel", t => { t->U.assertCompiledCode( ~schema=nodeSchema, ~op=#ParseAsync, - `i=>{let v0=e[0](i);return v0} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+'["'+v2+'"]'+v3.path}throw v3})}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+'["'+v2+'"]'+v3.path}throw v3}v6[v2]=v5}return Promise.all([e[2](v0),Promise.all(v6),]).then(a=>({"id":a[0],"children":a[1],}))}`, + `i=>{let v0;v0=e[0](i);return v0} +Node: i=>{typeof i==="object"&&i||e[5](i);let v1=i["Id"],v2=i["Children"];typeof v1==="string"||e[2](v1);let v0;try{v0=e[0](i["Id"]).catch(x=>e[1](x))}catch(x){e[1](x)}Array.isArray(v2)||e[4](v2);let v6=new Array(v2.length);for(let v3=0;v3Node--1"](v2[v3]);v6[v3]=v4.catch(v5=>{v5.path="[\\"Children\\"]"+'["'+v3+'"]'+v5.path;throw v5})}catch(v5){v5.path="[\\"Children\\"]"+'["'+v3+'"]'+v5.path;throw v5}}let v7=Promise.all(v6);return Promise.all([v0,v7,]).then(([v0,v7,])=>{return {"id":v0,"children":v7,}})}`, ) }) @@ -571,7 +546,7 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0=e[0](i);return v0} -Node: i=>{if(typeof i!=="object"||!i||!Array.isArray(i["Children"])){e[0](i)}let v0=i["Id"];if(typeof v0!=="string"){e[1](v0)}let v1=i["Children"],v6=new Array(v1.length);for(let v2=0;v2{let v0;v0=e[0](i);return v0} +Node: i=>{typeof i==="object"&&i||e[3](i);let v0=i["Id"],v1=i["Children"];typeof v0==="string"||e[0](v0);Array.isArray(v1)||e[2](v1);let v5=new Array(v1.length);for(let v2=0;v2Node--0"](v1[v2]);v5[v2]=v3}catch(v4){v4.path="[\\"Children\\"]"+'["'+v2+'"]'+v4.path;throw v4}}return {"id":v0,"children":v5,}}`, ) }) diff --git a/packages/sury/tests/S_refine_test.res b/packages/sury/tests/S_refine_test.res index b9f42c60e..870ef6340 100644 --- a/packages/sury/tests/S_refine_test.res +++ b/packages/sury/tests/S_refine_test.res @@ -1,59 +1,37 @@ open Ava test("Successfully refines on parsing", t => { - let schema = S.int->S.refine(s => - value => - if value < 0 { - s.fail("Should be positive") - } - ) + let schema = S.int->S.refine(value => value >= 0, ~error="Should be positive") - t->Assert.deepEqual(%raw(`12`)->S.parseOrThrow(schema), 12) - t->U.assertThrows( - () => %raw(`-12`)->S.parseOrThrow(schema), - { - code: OperationFailed("Should be positive"), - operation: Parse, - path: S.Path.empty, - }, - ) + t->Assert.deepEqual(%raw(`12`)->S.parseOrThrow(~to=schema), 12) + t->U.assertThrowsMessage(() => %raw(`-12`)->S.parseOrThrow(~to=schema), `Should be positive`) +}) + +test("Fails with default error message", t => { + let schema = S.int->S.refine(value => value >= 0) + + t->Assert.deepEqual(%raw(`12`)->S.parseOrThrow(~to=schema), 12) + t->U.assertThrowsMessage(() => %raw(`-12`)->S.parseOrThrow(~to=schema), `Refinement failed`) }) test("Fails with custom path", t => { - let schema = S.int->S.refine(s => - value => - if value < 0 { - s.fail(~path=S.Path.fromArray(["data", "myInt"]), "Should be positive") - } + let schema = S.int->S.refine( + value => value >= 0, + ~error="Should be positive", + ~path=["confirm"], ) - t->U.assertThrows( - () => %raw(`-12`)->S.parseOrThrow(schema), - { - code: OperationFailed("Should be positive"), - operation: Parse, - path: S.Path.fromArray(["data", "myInt"]), - }, + t->U.assertThrowsMessage( + () => %raw(`-12`)->S.parseOrThrow(~to=schema), + `Failed at ["confirm"]: Should be positive`, ) }) test("Successfully refines on serializing", t => { - let schema = S.int->S.refine(s => - value => - if value < 0 { - s.fail("Should be positive") - } - ) + let schema = S.int->S.refine(value => value >= 0, ~error="Should be positive") - t->Assert.deepEqual(12->S.reverseConvertOrThrow(schema), %raw("12")) - t->U.assertThrows( - () => -12->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Should be positive"), - operation: ReverseConvert, - path: S.Path.empty, - }, - ) + t->Assert.deepEqual(12->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw("12")) + t->U.assertThrowsMessage(() => -12->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Should be positive`) }) test("Successfully parses simple object with empty refine", t => { @@ -62,13 +40,13 @@ test("Successfully parses simple object with empty refine", t => { "foo": s.field("foo", S.string), "bar": s.field("bar", S.bool), } - )->S.refine(_ => _ => ()) + )->S.refine(_ => true) t->Assert.deepEqual( %raw(`{ "foo": "string", "bar": true, - }`)->S.parseOrThrow(schema), + }`)->S.parseOrThrow(~to=schema), { "foo": "string", "bar": true, @@ -82,49 +60,48 @@ test("Compiled parse code snapshot for simple object with refine", t => { "foo": s.field("foo", S.string), "bar": s.field("bar", S.bool), } - )->S.refine(s => _ => s.fail("foo")) + )->S.refine(_ => false, ~error="foo") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"],v1=i["bar"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}let v2={"foo":v0,"bar":v1,};e[3](v2);return v2}`, + `i=>{if(typeof i!=="object"||!i){e[3](i)}let v0=i["foo"],v1=i["bar"],v2={"foo":v0,"bar":v1,};if(typeof v0!=="string"){e[0](v0)}if(typeof v1!=="boolean"){e[1](v1)}if(!e[2](v2)){e[4]()}return v2}`, ) }) test("Reverse schema to the original schema", t => { - let schema = S.int->S.refine(s => - value => - if value < 0 { - s.fail("Should be positive") - } - ) + let schema = S.int->S.refine(value => value >= 0, ~error="Should be positive") t->Assert.not(schema->S.reverse, schema->S.castToUnknown) t->U.assertEqualSchemas(schema->S.reverse, S.int->S.castToUnknown) }) test("Succesfully uses reversed schema for parsing back to initial value", t => { - let schema = S.int->S.refine(s => - value => - if value < 0 { - s.fail("Should be positive") - } - ) + let schema = S.int->S.refine(value => value >= 0, ~error="Should be positive") t->U.assertReverseParsesBack(schema, 12) }) // https://github.com/DZakh/rescript-schema/issues/79 module Issue79 = { test("Successfully parses", t => { - let schema = S.object(s => s.field("myField", S.nullable(S.string)))->S.refine(_ => _ => ()) - let jsonString = `{"myField": "test"}` + let schema = S.object(s => s.field("myField", S.nullable(S.string)))->S.refine(_ => true) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["myField"];if(!(typeof v0==="string"||v0===void 0||v0===null)){e[1](v0)}e[2](v0);return v0}`, + `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["myField"];if(!(typeof v0==="string"||v0===void 0||v0===null)){e[1](v0)}if(!e[2](v0)){e[3]()}return v0}`, ) - t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{let v0=i["myField"];e[0](v0);return v0}`) + t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{let v0=i["myField"];if(!e[0](v0)){e[1]()}return v0}`) - t->Assert.deepEqual(jsonString->S.parseJsonStringOrThrow(schema), Value("test")) + t->Assert.deepEqual(%raw(`{"myField": "test"}`)->S.parseOrThrow(~to=schema), Value("test")) }) } + +test("Chaining refinements", t => { + let schema = S.int + ->S.refine(value => value > 0, ~error="Must be positive") + ->S.refine(value => mod(value, 2) === 0, ~error="Must be even") + + t->Assert.deepEqual(%raw(`4`)->S.parseOrThrow(~to=schema), 4) + t->U.assertThrowsMessage(() => %raw(`-2`)->S.parseOrThrow(~to=schema), `Must be positive`) + t->U.assertThrowsMessage(() => %raw(`3`)->S.parseOrThrow(~to=schema), `Must be even`) +}) diff --git a/packages/sury/tests/S_reverseConvertToJsonStringOrThrow_test.res b/packages/sury/tests/S_reverseConvertToJsonStringOrThrow_test.res index d207bfa4a..a30112bf8 100644 --- a/packages/sury/tests/S_reverseConvertToJsonStringOrThrow_test.res +++ b/packages/sury/tests/S_reverseConvertToJsonStringOrThrow_test.res @@ -1,9 +1,11 @@ open Ava +S.enableJsonString() + test("Successfully parses", t => { let schema = S.bool - t->Assert.deepEqual(true->S.reverseConvertToJsonStringOrThrow(schema), "true") + t->Assert.deepEqual(true->S.decodeOrThrow(~from=schema, ~to=S.jsonString), "true") }) test("Successfully parses object", t => { @@ -18,7 +20,7 @@ test("Successfully parses object", t => { { "id": "0", "isDeleted": true, - }->S.reverseConvertToJsonStringOrThrow(schema), + }->S.decodeOrThrow(~from=schema, ~to=S.jsonString), `{"id":"0","isDeleted":true}`, ) }) @@ -35,7 +37,7 @@ test("Successfully parses object with space", t => { { "id": "0", "isDeleted": true, - }->S.reverseConvertToJsonStringOrThrow(~space=2, schema), + }->S.decodeOrThrow(~from=schema, ~to=S.jsonStringWithSpace(2)), `{ "id": "0", "isDeleted": true @@ -43,11 +45,13 @@ test("Successfully parses object with space", t => { ) }) -test("Fails to serialize Unknown schema", t => { +test("unknown <-> json string expects unknown to be a json string", t => { let schema = S.unknown t->U.assertThrowsMessage( - () => Obj.magic(123)->S.reverseConvertToJsonStringOrThrow(schema), - `Failed converting to JSON: unknown is not valid JSON`, + () => Obj.magic(123)->S.decodeOrThrow(~from=S.unknown, ~to=S.jsonString), + "Expected JSON string, received 123", ) + t->Assert.deepEqual(Obj.magic("123")->S.decodeOrThrow(~from=S.unknown, ~to=S.jsonString), "123") + t->U.assertCompiledCode(~schema, ~op=#ReverseConvertToJson, `i=>{e[0](i);return i}`) }) diff --git a/packages/sury/tests/S_reverseConvertToJsonWith_test.res b/packages/sury/tests/S_reverseConvertToJsonWith_test.res index 4e45af898..7b8ad4893 100644 --- a/packages/sury/tests/S_reverseConvertToJsonWith_test.res +++ b/packages/sury/tests/S_reverseConvertToJsonWith_test.res @@ -3,28 +3,29 @@ open Ava S.enableJson() test("Successfully reverse converts jsonable schemas", t => { - t->Assert.deepEqual(true->S.reverseConvertToJsonOrThrow(S.bool), true->JSON.Encode.bool) - t->Assert.deepEqual(true->S.reverseConvertToJsonOrThrow(S.literal(true)), true->JSON.Encode.bool) - t->Assert.deepEqual("abc"->S.reverseConvertToJsonOrThrow(S.string), "abc"->JSON.Encode.string) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=S.bool, ~to=S.json), true->JSON.Encode.bool) + t->Assert.deepEqual(true->S.decodeOrThrow(~from=S.literal(true), ~to=S.json), true->JSON.Encode.bool) + t->Assert.deepEqual("abc"->S.decodeOrThrow(~from=S.string, ~to=S.json), "abc"->JSON.Encode.string) t->Assert.deepEqual( - "abc"->S.reverseConvertToJsonOrThrow(S.literal("abc")), + "abc"->S.decodeOrThrow(~from=S.literal("abc"), ~to=S.json), "abc"->JSON.Encode.string, ) - t->Assert.deepEqual(123->S.reverseConvertToJsonOrThrow(S.int), 123.->JSON.Encode.float) - t->Assert.deepEqual(123->S.reverseConvertToJsonOrThrow(S.literal(123)), 123.->JSON.Encode.float) - t->Assert.deepEqual(123.->S.reverseConvertToJsonOrThrow(S.float), 123.->JSON.Encode.float) - t->Assert.deepEqual(123.->S.reverseConvertToJsonOrThrow(S.literal(123.)), 123.->JSON.Encode.float) + t->Assert.deepEqual(123->S.decodeOrThrow(~from=S.int, ~to=S.json), 123.->JSON.Encode.float) + t->Assert.deepEqual(123->S.decodeOrThrow(~from=S.literal(123), ~to=S.json), 123.->JSON.Encode.float) + t->Assert.deepEqual(123.->S.decodeOrThrow(~from=S.float, ~to=S.json), 123.->JSON.Encode.float) + t->Assert.deepEqual(123.->S.decodeOrThrow(~from=S.literal(123.), ~to=S.json), 123.->JSON.Encode.float) t->Assert.deepEqual( - (true, "foo", 123)->S.reverseConvertToJsonOrThrow(S.literal((true, "foo", 123))), + (true, "foo", 123)->S.decodeOrThrow(~from=S.literal((true, "foo", 123)), ~to=S.json), JSON.Encode.array([JSON.Encode.bool(true), JSON.Encode.string("foo"), JSON.Encode.float(123.)]), ) t->Assert.deepEqual( - {"foo": true}->S.reverseConvertToJsonOrThrow(S.literal({"foo": true})), + {"foo": true}->S.decodeOrThrow(~from=S.literal({"foo": true}), ~to=S.json), JSON.Encode.object(Dict.fromArray([("foo", JSON.Encode.bool(true))])), ) t->Assert.deepEqual( - {"foo": (true, "foo", 123)}->S.reverseConvertToJsonOrThrow( - S.literal({"foo": (true, "foo", 123)}), + {"foo": (true, "foo", 123)}->S.decodeOrThrow( + ~from=S.literal({"foo": (true, "foo", 123)}), + ~to=S.json, ), JSON.Encode.object( Dict.fromArray([ @@ -39,39 +40,38 @@ test("Successfully reverse converts jsonable schemas", t => { ]), ), ) - t->Assert.deepEqual(None->S.reverseConvertToJsonOrThrow(S.null(S.bool)), JSON.Encode.null) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=S.nullAsOption(S.bool), ~to=S.json), JSON.Encode.null) t->Assert.deepEqual( - JSON.Encode.null->S.reverseConvertToJsonOrThrow(S.literal(JSON.Encode.null)), + JSON.Encode.null->S.decodeOrThrow(~from=S.literal(JSON.Encode.null), ~to=S.json), JSON.Encode.null, ) - t->Assert.deepEqual([]->S.reverseConvertToJsonOrThrow(S.array(S.bool)), JSON.Encode.array([])) + t->Assert.deepEqual([]->S.decodeOrThrow(~from=S.array(S.bool), ~to=S.json), JSON.Encode.array([])) t->Assert.deepEqual( - Dict.make()->S.reverseConvertToJsonOrThrow(S.dict(S.bool)), + Dict.make()->S.decodeOrThrow(~from=S.dict(S.bool), ~to=S.json), JSON.Encode.object(Dict.make()), ) t->Assert.deepEqual( - true->S.reverseConvertToJsonOrThrow(S.object(s => s.field("foo", S.bool))), + true->S.decodeOrThrow(~from=S.object(s => s.field("foo", S.bool)), ~to=S.json), JSON.Encode.object(Dict.fromArray([("foo", JSON.Encode.bool(true))])), ) t->Assert.deepEqual( - true->S.reverseConvertToJsonOrThrow(S.tuple1(S.bool)), + true->S.decodeOrThrow(~from=S.tuple1(S.bool), ~to=S.json), JSON.Encode.array([JSON.Encode.bool(true)]), ) t->Assert.deepEqual( - "foo"->S.reverseConvertToJsonOrThrow(S.union([S.literal("foo"), S.literal("bar")])), + "foo"->S.decodeOrThrow(~from=S.union([S.literal("foo"), S.literal("bar")]), ~to=S.json), JSON.Encode.string("foo"), ) }) -test("Fails to reverse convert Option schema", t => { +test("Encodes option schema to JSON", t => { let schema = S.option(S.bool) - t->U.assertThrows( - () => None->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Encode.null) + t->Assert.deepEqual(Some(true)->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Encode.bool(true)) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvertToJson, + `i=>{if(i===void 0){i=null}else if(!(typeof i==="boolean")){e[0](i)}return i}`, ) }) @@ -82,9 +82,8 @@ test("Allows to convert to JSON with option as an object field", t => { } ) t->Assert.deepEqual( - {"foo": None}->S.reverseConvertToJsonOrThrow(schema), - %raw(`{"foo":undefined}`), - ~message="Shouldn't have undefined value here. Needs to be fixed in future versions", + {"foo": None}->S.decodeOrThrow(~from=schema, ~to=S.json), + %raw(`{}`), ) }) @@ -95,9 +94,8 @@ test("Allows to convert to JSON with optional S.json as an object field", t => { } ) t->Assert.deepEqual( - {"foo": None}->S.reverseConvertToJsonOrThrow(schema), - %raw(`{"foo":undefined}`), - ~message="Shouldn't have undefined value here. Needs to be fixed in future versions", + {"foo": None}->S.decodeOrThrow(~from=schema, ~to=S.json), + %raw(`{}`), ) }) @@ -105,172 +103,152 @@ test("Doesn't allow to convert to JSON array with optional items", t => { let schema = S.array(S.option(S.bool)) t->U.assertThrowsMessage( - () => [None]->S.reverseConvertToJsonOrThrow(schema), - "Failed converting to JSON: (boolean | undefined)[] is not valid JSON", + () => [None]->S.decodeOrThrow(~from=schema, ~to=S.json), + "Failed at []: Can't decode boolean | undefined to JSON. Use S.to to define a custom decoder", ) }) -test("Doesn't allow to convert to JSON tuple with optional items", t => { +test("Doesn't allow to encode tuple with optional item to JSON", t => { let schema = S.tuple1(S.option(S.bool)) t->U.assertThrowsMessage( - () => None->S.reverseConvertToJsonOrThrow(schema), - `Failed converting to JSON at ["0"]: [boolean | undefined] is not valid JSON`, + () => None->S.decodeOrThrow(~from=schema, ~to=S.json), + `Can't decode boolean | undefined to JSON. Use S.to to define a custom decoder`, ) }) test("Allows to convert to JSON with option as dict field", t => { let schema = S.dict(S.option(S.bool)) - t->Assert.deepEqual( - Dict.fromArray([("foo", None)])->S.reverseConvertToJsonOrThrow(schema), - %raw(`{foo:undefined}`), - ~message="Shouldn't have undefined value here. Needs to be fixed in future versions", + t->U.assertThrowsMessage( + () => dict{"foo": None}->S.decodeOrThrow(~from=schema, ~to=S.json), + `Failed at []: Can't decode boolean | undefined to JSON. Use S.to to define a custom decoder`, ) }) -test("Fails to reverse convert Undefined literal", t => { +test("Encodes undefined to JSON as null", t => { let schema = S.literal() - t->U.assertThrows( - () => ()->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, - ) + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Null) }) -test("Fails to reverse convert Function literal", t => { +test("Fails to encode Function to JSON", t => { let fn = () => () let schema = S.literal(fn) - t->U.assertThrows( - () => fn->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => fn->S.decodeOrThrow(~from=schema, ~to=S.json), + `Can't decode Function to JSON. Use S.to to define a custom decoder`, ) }) -test("Fails to reverse convert Object literal", t => { +test("Fails to encode Error literal to JSON", t => { let error = %raw(`new Error("foo")`) let schema = S.literal(error) + t->U.assertThrowsMessage( - () => error->S.reverseConvertToJsonOrThrow(schema), - `Failed converting to JSON: [object Error] is not valid JSON`, + () => error->S.decodeOrThrow(~from=schema, ~to=S.json), + `Can't decode [object Error] to JSON. Use S.to to define a custom decoder`, ) - t->Assert.is(error->S.reverseConvertOrThrow(schema), error) + t->Assert.is(error->S.decodeOrThrow(~from=schema, ~to=S.unknown), error) t->U.assertThrowsMessage( - () => %raw(`new Error("foo")`)->S.reverseConvertOrThrow(schema), - `Failed converting: Expected [object Error], received [object Error]`, + () => %raw(`new Error("foo")`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected [object Error], received [object Error]`, ) }) -test("Fails to reverse convert Symbol literal", t => { +test("Fails to encode Symbol to JSON", t => { let symbol = %raw(`Symbol()`) let schema = S.literal(symbol) - t->U.assertThrows( - () => symbol->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, - ) -}) - -test("Fails to reverse convert BigInt literal", t => { - let bigint = %raw(`1234n`) - let schema = S.literal(bigint) - t->U.assertThrows( - () => bigint->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => symbol->S.decodeOrThrow(~from=schema, ~to=S.json), + `Can't decode Symbol() to JSON. Use S.to to define a custom decoder`, ) }) -test("Fails to reverse convert Dict literal with invalid field", t => { +test("Encodes object literal with bigint to JSON", t => { let dict = %raw(`{"foo": 123n}`) let schema = S.literal(dict) - t->U.assertThrowsMessage( - () => dict->S.reverseConvertToJsonOrThrow(schema), - `Failed converting to JSON: { foo: 123n; } is not valid JSON`, + t->Assert.deepEqual( + dict->S.decodeOrThrow(~from=schema, ~to=S.json), + JSON.Object(dict{"foo": JSON.String("123")}), ) }) -test("Fails to reverse convert NaN literal", t => { +test("Encodes NaN to JSON", t => { let schema = S.literal(%raw(`NaN`)) - t->U.assertThrows( - () => ()->S.reverseConvertToJsonOrThrow(schema), - { - code: InvalidJsonSchema(schema->S.castToUnknown), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, + t->Assert.deepEqual(%raw(`NaN`)->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Null) + t->U.assertThrowsMessage( + () => ()->S.decodeOrThrow(~from=schema, ~to=S.json), + `Expected NaN, received undefined`, ) }) -test("Fails to reverse convert Unknown schema", t => { - t->U.assertThrows( - () => Obj.magic(123)->S.reverseConvertToJsonOrThrow(S.unknown), - {code: InvalidJsonSchema(S.unknown), operation: ReverseConvertToJson, path: S.Path.empty}, +test("Fails to encode Never to JSON", t => { + t->U.assertThrowsMessage( + () => Obj.magic(123)->S.decodeOrThrow(~from=S.never, ~to=S.json), + `Expected never, received 123`, ) }) -test("Fails to reverse convert Never schema", t => { - t->U.assertThrows( - () => Obj.magic(123)->S.reverseConvertToJsonOrThrow(S.never), - { - code: InvalidType({expected: S.never->S.castToUnknown, received: Obj.magic(123)}), - operation: ReverseConvertToJson, - path: S.Path.empty, - }, +test("Encodes object with unknown schema to JSON", t => { + t->Assert.deepEqual( + Obj.magic(true)->S.decodeOrThrow(~from=S.object(s => s.field("foo", S.unknown)), ~to=S.json), + JSON.Object(dict{"foo": JSON.Boolean(true)}), ) -}) - -test("Fails to reverse convert object with invalid nested schema", t => { t->U.assertThrowsMessage( - () => Obj.magic(true)->S.reverseConvertToJsonOrThrow(S.object(s => s.field("foo", S.unknown))), - `Failed converting to JSON: { foo: unknown; } is not valid JSON`, + () => Obj.magic(123n)->S.decodeOrThrow(~from=S.object(s => s.field("foo", S.unknown)), ~to=S.json), + `Expected JSON, received 123n`, ) }) -test("Fails to reverse convert tuple with invalid nested schema", t => { +test("Encodes tuple with unknown item to JSON", t => { + t->Assert.deepEqual( + Obj.magic(true)->S.decodeOrThrow(~from=S.tuple1(S.unknown), ~to=S.json), + JSON.Array([JSON.Boolean(true)]), + ) t->U.assertThrowsMessage( - () => Obj.magic(true)->S.reverseConvertToJsonOrThrow(S.tuple1(S.unknown)), - `Failed converting to JSON at ["0"]: [unknown] is not valid JSON`, + () => Obj.magic(123n)->S.decodeOrThrow(~from=S.tuple1(S.unknown), ~to=S.json), + `Expected JSON, received 123n`, ) }) -test("Doesn't serialize union to JSON when at least one item is not JSON-able", t => { +test("Encodes a union to JSON when at least one item is not JSON-able", t => { let schema = S.union([S.string, S.unknown->(U.magic: S.t => S.t)]) + t->Assert.deepEqual("foo"->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Encode.string("foo")) + t->Assert.deepEqual(%raw(`true`)->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Encode.bool(true)) t->U.assertThrowsMessage( - () => "foo"->S.reverseConvertToJsonOrThrow(schema), - "Failed converting to JSON: string | unknown is not valid JSON", + () => %raw(`123n`)->S.decodeOrThrow(~from=schema, ~to=S.json), + `Expected string | unknown, received 123n +- Expected string, received 123n +- Expected JSON, received 123n`, ) - // Not related to the test, just check that it doesn't crash while we are at it - t->Assert.deepEqual("foo"->S.reverseConvertOrThrow(schema), %raw(`"foo"`)) - t->Assert.deepEqual(%raw(`123`)->S.reverseConvertOrThrow(schema), %raw(`123`)) t->U.assertCompiledCode( ~schema, - ~op=#ReverseConvert, - `i=>{try{if(typeof i!=="string"){e[0](i)}}catch(e0){}return i}`, + ~op=#ReverseConvertToJson, + `i=>{try{typeof i==="string"||e[0](i);}catch(e1){try{e[1](i);}catch(e2){e[2](i,e1,e2)}}return i}`, ) }) -test("Fails to reverse convert union with invalid json schemas", t => { +test("Encodes a union of NaN and unknown to JSON", t => { let schema = S.union([S.literal(%raw(`NaN`)), S.unknown->(U.magic: S.t => S.t)]) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvertToJson, + `i=>{try{Number.isNaN(i)||e[0](i);i=null}catch(e1){try{e[1](i);}catch(e2){e[2](i,e1,e2)}}return i}`, + ) + + t->Assert.deepEqual(%raw(`NaN`)->S.decodeOrThrow(~from=schema, ~to=S.json), JSON.Null) + t->Assert.deepEqual( + %raw(`"bar"`)->S.decodeOrThrow(~from=schema, ~to=S.json), + JSON.Encode.string("bar"), + ) t->U.assertThrowsMessage( - () => %raw(`NaN`)->S.reverseConvertToJsonOrThrow(schema), - "Failed converting to JSON: NaN | unknown is not valid JSON", + () => %raw(`123n`)->S.decodeOrThrow(~from=schema, ~to=S.json), + `Expected NaN | unknown, received 123n +- Expected NaN, received 123n +- Expected JSON, received 123n`, ) }) @@ -376,11 +354,17 @@ module SerializesDeepRecursive = { t->U.assertCompiledCode( ~schema=bodySchema, ~op=#ReverseConvert, - `i=>{let v1;try{v1=e[0](i["condition"])}catch(v0){if(v0&&v0.s===s){v0.path="[\\"condition\\"]"+v0.path}throw v0}return {"condition":v1,}}`, + `i=>{let v0;try{v0=e[0](i["condition"]);}catch(v1){v1.path="[\\"condition\\"]"+v1.path;throw v1}return {"condition":v0,}}`, + ) + // Note: Can be optimized to not recursively validate JSON values a second time + t->U.assertCompiledCode( + ~schema=bodySchema, + ~op=#ReverseConvertToJson, + `i=>{let v0;try{v0=e[0](i["condition"]);}catch(v1){v1.path="[\\"condition\\"]"+v1.path;throw v1}try{e[1](v0);}catch(v2){v2.path="[\\"condition\\"]"+v2.path;throw v2}return {"condition":v0,}}`, ) t->Assert.deepEqual( - {condition: condition}->S.reverseConvertToJsonOrThrow(bodySchema), + {condition: condition}->S.decodeOrThrow(~from=bodySchema, ~to=S.json), { "condition": conditionJSON, }->U.magic, diff --git a/packages/sury/tests/S_schema_test.res b/packages/sury/tests/S_schema_test.res index dff9e68d7..e464f5944 100644 --- a/packages/sury/tests/S_schema_test.res +++ b/packages/sury/tests/S_schema_test.res @@ -42,13 +42,13 @@ test("Object with embeded transformed schema", t => { let schema = S.schema(s => { "foo": "bar", - "zoo": s.matches(S.null(S.int)), + "zoo": s.matches(S.nullAsOption(S.int)), } ) let objectSchema = S.object(s => { "foo": s.field("foo", S.literal("bar")), - "zoo": s.field("zoo", S.null(S.int)), + "zoo": s.field("zoo", S.nullAsOption(S.int)), } ) // t->U.assertEqualSchemas(schema, objectSchema) @@ -58,11 +58,11 @@ test("Object with embeded transformed schema", t => { ) t->Assert.is( schema->U.getCompiledCodeString(~op=#ReverseConvert), - `i=>{let v0=i["zoo"];if(v0===void 0){v0=null}return {"foo":"bar","zoo":v0,}}`, + `i=>{let v0=i["zoo"];if(v0===void 0){v0=null}else if(!(typeof v0==="number"&&!Number.isNaN(v0)&&(v0<=2147483647&&v0>=-2147483648&&v0%1===0))){e[0](v0)}return {"foo":"bar","zoo":v0,}}`, ) t->Assert.is( objectSchema->U.getCompiledCodeString(~op=#ReverseConvert), - `i=>{let v0=i["zoo"];if(v0===void 0){v0=null}return {"foo":"bar","zoo":v0,}}`, + `i=>{let v0=i["zoo"];if(v0===void 0){v0=null}else if(!(typeof v0==="number"&&!Number.isNaN(v0)&&(v0<=2147483647&&v0>=-2147483648&&v0%1===0))){e[0](v0)}return {"foo":"bar","zoo":v0,}}`, ) }) @@ -80,7 +80,7 @@ test("Strict object with embeded returns input without object recreation", t => t->Assert.is( schema->U.getCompiledCodeString(~op=#Parse), - `i=>{if(typeof i!=="object"||!i||Array.isArray(i)||i["foo"]!=="bar"){e[0](i)}let v0=i["zoo"],v1;if(typeof v0!=="number"||v0>2147483647||v0<-2147483648||v0%1!==0){e[1](v0)}for(v1 in i){if(v1!=="foo"&&v1!=="zoo"){e[2](v1)}}return i}`, + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[3](i);let v0=i["foo"],v1=i["zoo"],v2;v0==="bar"||e[0](v0);typeof v1==="number"&&v1<=2147483647&&v1>=-2147483648&&v1%1===0||e[1](v1);for(v2 in i){if(v2!=="foo"&&v2!=="zoo"){e[2](v2)}}return i}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) }) @@ -97,11 +97,11 @@ test("Tuple with embeded schema", t => { // S.schema does return i without tuple recreation t->Assert.is( schema->U.getCompiledCodeString(~op=#Parse), - `i=>{if(!Array.isArray(i)||i.length!==3||i["1"]!==void 0||i["2"]!=="bar"){e[0](i)}let v0=i["0"];if(typeof v0!=="string"){e[1](v0)}return i}`, + `i=>{Array.isArray(i)&&i.length===3||e[3](i);let v0=i["0"],v1=i["1"],v2=i["2"];typeof v0==="string"||e[0](v0);v1===void 0||e[1](v1);v2==="bar"||e[2](v2);return i}`, ) t->Assert.is( tupleSchema->U.getCompiledCodeString(~op=#Parse), - `i=>{if(!Array.isArray(i)||i.length!==3||i["1"]!==void 0||i["2"]!=="bar"){e[0](i)}let v0=i["0"];if(typeof v0!=="string"){e[1](v0)}return [v0,void 0,"bar",]}`, + `i=>{Array.isArray(i)&&i.length===3||e[3](i);let v0=i["0"],v1=i["1"],v2=i["2"];typeof v0==="string"||e[0](v0);v1===void 0||e[1](v1);v2==="bar"||e[2](v2);return [v0,v1,v2,]}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) t->Assert.is( @@ -111,25 +111,24 @@ test("Tuple with embeded schema", t => { }) test("Tuple with embeded transformed schema", t => { - let schema = S.schema(s => (s.matches(S.null(S.string)), (), "bar")) + let schema = S.schema(s => (s.matches(S.nullAsOption(S.string)), (), "bar")) let tupleSchema = S.tuple(s => ( - s.item(0, S.null(S.string)), + s.item(0, S.nullAsOption(S.string)), s.item(1, S.literal()), s.item(2, S.literal("bar")), )) - // t->U.assertEqualSchemas(schema, tupleSchema) t->Assert.is( schema->U.getCompiledCodeString(~op=#Parse), tupleSchema->U.getCompiledCodeString(~op=#Parse), ) t->Assert.is( schema->U.getCompiledCodeString(~op=#ReverseConvert), - `i=>{let v0=i["0"];if(v0===void 0){v0=null}return [v0,void 0,"bar",]}`, + `i=>{let v0=i["0"];if(v0===void 0){v0=null}else if(!(typeof v0==="string")){e[0](v0)}return [v0,void 0,"bar",]}`, ) t->Assert.is( tupleSchema->U.getCompiledCodeString(~op=#ReverseConvert), - `i=>{let v0=i["0"];if(v0===void 0){v0=null}return [v0,void 0,"bar",]}`, + `i=>{let v0=i["0"];if(v0===void 0){v0=null}else if(!(typeof v0==="string")){e[0](v0)}return [v0,void 0,"bar",]}`, ) }) @@ -156,7 +155,6 @@ test("Nested object with embeded schema", t => { ), } ) - // t->U.assertEqualSchemas(schema, objectSchema) t->Assert.is( schema->U.getCompiledCodeString(~op=#Parse), @@ -203,18 +201,11 @@ test( } ) - t->Assert.deepEqual(%raw(`["foo", true]`)->S.parseOrThrow(schema), {"0": "foo", "1": true}) + t->Assert.deepEqual(%raw(`["foo", true]`)->S.parseOrThrow(~to=schema), {"0": "foo", "1": true}) - t->U.assertThrows( - () => %raw(`["foo", true]`)->S.parseOrThrow(schema->S.strict), - { - code: InvalidType({ - expected: schema->S.strict->S.castToUnknown, - received: %raw(`["foo", true]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["foo", true]`)->S.parseOrThrow(~to=schema->S.strict), + `Expected { 0: string; 1: boolean; }, received ["foo", true]`, ) }, ) @@ -224,24 +215,17 @@ test( t => { let schema = S.schema(s => (s.matches(S.string), s.matches(S.bool)))->S.strict - t->Assert.deepEqual(%raw(`["foo", true]`)->S.parseOrThrow(schema), ("foo", true)) + t->Assert.deepEqual(%raw(`["foo", true]`)->S.parseOrThrow(~to=schema), ("foo", true)) - t->U.assertThrows( - () => %raw(`["foo", true, 1]`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.strict->S.castToUnknown, - received: %raw(`["foo", true, 1]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["foo", true, 1]`)->S.parseOrThrow(~to=schema), + `Expected [string, boolean], received ["foo", true, 1]`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2){e[0](i)}let v0=i["0"],v1=i["1"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}return i}`, + `i=>{Array.isArray(i)&&i.length===2||e[2](i);let v0=i["0"],v1=i["1"];typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);return i}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#Convert) }, @@ -255,14 +239,14 @@ test("Object schema with empty object field", t => { ) t->U.assertThrowsMessage( - () => %raw(`{"foo": "bar"}`)->S.parseOrThrow(schema), - `Failed parsing: Expected { foo: {}; }, received { foo: "bar"; }`, + () => %raw(`{"foo": "bar"}`)->S.parseOrThrow(~to=schema), + `Failed at ["foo"]: Expected {}, received "bar"`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["foo"]!=="object"||!i["foo"]){e[0](i)}return {"foo":{},}}`, + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];typeof v0==="object"&&v0||e[0](v0);return {"foo":{},}}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) }) @@ -275,14 +259,14 @@ test("Object schema with nested object field containing only literal", t => { ) t->U.assertThrowsMessage( - () => %raw(`{"foo": {"bar": "bap"}}`)->S.parseOrThrow(schema), - `Failed parsing: Expected { foo: { bar: "baz"; }; }, received { foo: { bar: "bap"; }; }`, + () => %raw(`{"foo": {"bar": "bap"}}`)->S.parseOrThrow(~to=schema), + `Failed at ["foo"]["bar"]: Expected "baz", received "bap"`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["foo"]!=="object"||!i["foo"]||i["foo"]["bar"]!=="baz"){e[0](i)}return {"foo":{"bar":"baz",},}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["foo"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["bar"];v1==="baz"||e[0](v1);return {"foo":{"bar":v1,},}}`, ) t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) }) @@ -296,13 +280,13 @@ test("https://github.com/DZakh/sury/issues/131", t => { let json = (%raw(`{"weird": true}`): JSON.t) t->U.assertThrowsMessage( - () => json->S.parseOrThrow(testSchema), - `Failed parsing: Expected { foobar: (string | undefined)[]; }, received { weird: true; }`, + () => json->S.parseOrThrow(~to=testSchema), + `Failed at ["foobar"]: Expected (string | undefined)[], received undefined`, ) t->U.assertCompiledCode( ~schema=testSchema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||!Array.isArray(i["foobar"])){e[0](i)}let v0=i["foobar"],v5=new Array(v0.length);for(let v1=0;v1{typeof i==="object"&&i||e[2](i);let v0=i["foobar"];Array.isArray(v0)||e[1](v0);for(let v1=0;v1Assert.deepEqual(nan->S.parseOrThrow(S.float), nan) - t->Assert.deepEqual(await nan->S.parseAsyncOrThrow(S.float), nan) + t->Assert.deepEqual(nan->S.parseOrThrow(~to=S.float), nan) + t->Assert.deepEqual(await nan->S.parseAsyncOrThrow(~to=S.float), nan) S.global({}) - t->U.assertThrows( - () => nan->S.parseOrThrow(S.float), - { - code: S.InvalidType({ - expected: S.float->S.castToUnknown, - received: nan, - }), - operation: Parse, - path: S.Path.empty, - }, - ) - await t->U.assertThrowsAsync( - () => nan->S.parseAsyncOrThrow(S.float), - { - code: S.InvalidType({ - expected: S.float->S.castToUnknown, - received: nan, - }), - operation: ParseAsync, - path: S.Path.empty, - }, + t->U.assertThrowsMessage(() => nan->S.parseOrThrow(~to=S.float), `Expected number, received NaN`) + await t->U.asyncAssertThrowsMessage( + () => nan->S.parseAsyncOrThrow(~to=S.float), + `Expected number, received NaN`, ) t->Assert.throws( () => { - nan->S.assertOrThrow(S.float) + nan->S.assertOrThrow(~to=S.float) }, ~expectations={ - message: "Failed asserting: Expected number, received NaN", + message: "Expected number, received NaN", }, ) }) diff --git a/packages/sury/tests/S_shape_test.res b/packages/sury/tests/S_shape_test.res index b4a906e3f..45a9efa53 100644 --- a/packages/sury/tests/S_shape_test.res +++ b/packages/sury/tests/S_shape_test.res @@ -5,73 +5,57 @@ S.enableJson() test("Parses with wrapping the value in variant", t => { let schema = S.string->S.shape(s => Ok(s)) - t->Assert.deepEqual("Hello world!"->S.parseOrThrow(schema), Ok("Hello world!")) + t->Assert.deepEqual("Hello world!"->S.parseOrThrow(~to=schema), Ok("Hello world!")) }) asyncTest("Parses with wrapping async schema in variant", async t => { let schema = S.string->S.transform(_ => {asyncParser: async i => i})->S.shape(s => Ok(s)) - t->Assert.deepEqual(await "Hello world!"->S.parseAsyncOrThrow(schema), Ok("Hello world!")) + t->Assert.deepEqual(await "Hello world!"->S.parseAsyncOrThrow(~to=schema), Ok("Hello world!")) t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="string"){e[0](i)}return Promise.all([e[1](i),]).then(a=>({"TAG":"Ok","_0":a[0],}))}`, + `i=>{typeof i==="string"||e[2](i);let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}return v0.then(v0=>{return {"TAG":"Ok","_0":v0,}})}`, ) }) test("Fails to parse wrapped schema", t => { let schema = S.string->S.shape(s => Ok(s)) - t->U.assertThrows( - () => 123->S.parseOrThrow(schema), - { - code: InvalidType({received: 123->Obj.magic, expected: schema->S.castToUnknown}), - operation: Parse, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Expected string, received 123`) }) test("Serializes with unwrapping the value from variant", t => { let schema = S.string->S.shape(s => Ok(s)) - t->Assert.deepEqual(Ok("Hello world!")->S.reverseConvertOrThrow(schema), %raw(`"Hello world!"`)) + t->Assert.deepEqual(Ok("Hello world!")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"Hello world!"`)) }) test("Fails to serialize when can't unwrap the value from variant", t => { let schema = S.string->S.shape(s => Ok(s)) t->Assert.deepEqual( - Error("Hello world!")->S.reverseConvertOrThrow(schema), + Error("Hello world!")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"Hello world!"`), ~message=`Convert operation doesn't perform exhaustiveness check`, ) t->U.assertThrowsMessage( - () => Error("Hello world!")->S.parseOrThrow(schema->S.reverse), - `Failed parsing: Expected { TAG: "Ok"; _0: string; }, received { TAG: "Error"; _0: "Hello world!"; }`, + () => Error("Hello world!")->S.parseOrThrow(~to=schema->S.reverse), + `Failed at ["TAG"]: Expected "Ok", received "Error"`, ) }) test("Successfully parses when the value is not used as the variant payload", t => { let schema = S.string->S.shape(_ => #foo) - t->Assert.deepEqual("Hello world!"->S.parseOrThrow(schema), #foo) + t->Assert.deepEqual("Hello world!"->S.parseOrThrow(~to=schema), #foo) }) test("Fails to serialize when the value is not used as the variant payload", t => { let schema = S.string->S.shape(_ => #foo) - t->U.assertThrows( - () => #foo->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => #foo->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Missing input for string`) }) test( @@ -79,18 +63,18 @@ test( t => { let schema = S.literal((true, 12))->S.shape(_ => #foo) - t->Assert.deepEqual(#foo->S.reverseConvertOrThrow(schema), %raw(`[true, 12]`)) + t->Assert.deepEqual(#foo->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[true, 12]`)) }, ) test("Successfully parses when tuple is destructured", t => { let schema = S.literal((true, 12))->S.shape(((_, twelve)) => twelve) - t->Assert.deepEqual(%raw(`[true, 12]`)->S.parseOrThrow(schema), %raw(`12`)) + t->Assert.deepEqual(%raw(`[true, 12]`)->S.parseOrThrow(~to=schema), %raw(`12`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2||i["0"]!==true||i["1"]!==12){e[0](i)}return 12}`, + `i=>{Array.isArray(i)&&i.length===2||e[2](i);let v0=i["0"],v1=i["1"];v0===true||e[0](v0);v1===12||e[1](v1);return v1}`, ) }) @@ -106,13 +90,13 @@ test( t->Assert.deepEqual( { "foo": "bar", - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), %raw(`"bar"`), ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}return v0}`, + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);return v0}`, ) }, ) @@ -131,13 +115,13 @@ test( t->Assert.deepEqual( { "foo": {"bar": "jazz"}, - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), %raw(`"jazz"`), ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i||typeof i["foo"]!=="object"||!i["foo"]){e[0](i)}let v0=i["foo"],v1=v0["bar"];if(typeof v1!=="string"){e[1](v1)}return v1}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["foo"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["bar"];typeof v1==="string"||e[0](v1);return v1}`, ) }, ) @@ -193,12 +177,12 @@ test( t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}let v1=e[2]({"foo":v0,});if(typeof v1!=="object"||!v1){e[3](v1)}let v2=v1["faz"];if(typeof v2!=="string"){e[4](v2)}return v2}`, + `i=>{typeof i==="object"&&i||e[5](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);let v1;try{v1=e[1]({"foo":v0,})}catch(x){e[2](x)}typeof v1==="object"&&v1||e[4](v1);let v2=v1["faz"];typeof v2==="string"||e[3](v2);return v2}`, ) t->Assert.deepEqual( { "foo": "bar", - }->S.parseOrThrow(schema), + }->S.parseOrThrow(~to=schema), %raw(`"bar"`), ) }, @@ -207,14 +191,9 @@ test( test("Reverse convert of tagged tuple with destructured literal", t => { let schema = S.tuple2(S.literal(true), S.literal(12))->S.shape(((_, twelve)) => twelve) - t->U.assertEqualSchemas( - schema->S.reverse, - S.literal(12)->S.shape(i1 => (true, i1))->S.castToUnknown, - ) - - t->Assert.deepEqual(12->S.reverseConvertOrThrow(schema), %raw(`[true, 12]`)) + t->Assert.deepEqual(12->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[true, 12]`)) - let code = `i=>{if(i!==12){e[0](i)}return [true,i,]}` + let code = `i=>{i===12||e[0](i);return [true,i,]}` t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, code) t->U.assertCompiledCode(~schema, ~op=#ReverseParse, code) }) @@ -226,27 +205,20 @@ test("Reverse convert of tagged tuple with destructured bool", t => { literal, )) - t->U.assertEqualSchemas( - schema->S.reverse, - S.tuple2(S.bool, S.literal("foo")) - ->S.shape(((item, literal)) => (true, literal, item)) - ->S.castToUnknown, - ) - - t->Assert.deepEqual((false, "foo")->S.reverseConvertOrThrow(schema), %raw(`[true, "foo",false]`)) + t->Assert.deepEqual((false, "foo")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[true, "foo",false]`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return [true,"foo",i["0"],]}`) t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, - `i=>{if(!Array.isArray(i)||i.length!==2||i["1"]!=="foo"){e[0](i)}let v0=i["0"];if(typeof v0!=="boolean"){e[1](v0)}return [true,"foo",v0,]}`, + `i=>{Array.isArray(i)&&i.length===2||e[2](i);let v0=i["0"],v1=i["1"];typeof v0==="boolean"||e[0](v0);v1==="foo"||e[1](v1);return [true,v1,v0,]}`, ) }) test("Successfully parses when value registered multiple times", t => { let schema = S.string->S.shape(s => #Foo(s, s)) - t->Assert.deepEqual(%raw(`"abc"`)->S.parseOrThrow(schema), #Foo("abc", "abc")) + t->Assert.deepEqual(%raw(`"abc"`)->S.parseOrThrow(~to=schema), #Foo("abc", "abc")) }) test("Reverse convert with value registered multiple times", t => { @@ -259,9 +231,9 @@ test("Reverse convert with value registered multiple times", t => { `i=>{let v0=i["VAL"];return v0["1"]}`, ) - t->Assert.deepEqual(#Foo("abc", "abc")->S.reverseConvertOrThrow(schema), %raw(`"abc"`)) + t->Assert.deepEqual(#Foo("abc", "abc")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"abc"`)) // t->U.assertThrows( - // () => #Foo("abc", "abcd")->S.reverseConvertOrThrow(schema), + // () => #Foo("abc", "abcd")->S.decodeOrThrow(~from=schema, ~to=S.unknown), // { // code: InvalidOperation({ // description: `Another source has conflicting data`, @@ -281,7 +253,7 @@ test("Can destructure object value passed to S.shape", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"],v1=i["bar"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"foo":v0,"bar":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["foo"],v1=i["bar"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"foo":v0,"bar":v1,}}`, ) t->U.assertCompiledCode( ~schema, @@ -296,7 +268,7 @@ test("Compiled code snapshot of variant applied to object", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}return {"TAG":"Ok","_0":v0,}}`, + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);return {"TAG":"Ok","_0":v0,}}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"foo":i["_0"],}}`) @@ -305,7 +277,7 @@ test("Compiled code snapshot of variant applied to object", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}let v1;(v1=v0==="true")||v0==="false"||e[2](v0);return {"TAG":"Ok","_0":v1,}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v1=i["foo"];typeof v1==="string"||e[1](v1);let v0;(v0=v1==="true")||v1==="false"||e[0](v1);return {"TAG":"Ok","_0":v0,}}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"foo":""+i["_0"],}}`) }) @@ -316,14 +288,14 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}return {"TAG":"Ok","_0":i,}}`, + `i=>{typeof i==="string"||e[0](i);return {"TAG":"Ok","_0":i,}}`, ) }) test("Compiled parse code snapshot without transform", t => { let schema = S.string->S.shape(s => s) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="string"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="string"||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { @@ -343,12 +315,12 @@ test( t => { let schema = S.literal((true, 12))->S.shape(_ => #foo) - t->Assert.deepEqual(#foo->S.reverseConvertOrThrow(schema), %raw(`[true,12]`)) + t->Assert.deepEqual(#foo->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[true,12]`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!=="foo"){e[0](i)}return [true,12,]}`, + `i=>{i==="foo"||e[0](i);return [true,12,]}`, ) }, ) @@ -366,12 +338,12 @@ test("Works with variant schema used multiple times as a child schema", t => { t->U.assertCompiledCode( ~schema=appVersionsSchema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["ios"],v1=i["android"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="string"){e[2](v1)}return {"ios":{"current":v0,"minimum":"1.0",},"android":{"current":v1,"minimum":"1.0",},}}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["ios"],v1=i["android"];typeof v0==="string"||e[0](v0);typeof v1==="string"||e[1](v1);return {"ios":{"current":i["ios"],"minimum":"1.0",},"android":{"current":i["android"],"minimum":"1.0",},}}`, ) t->U.assertCompiledCode( ~schema=appVersionsSchema, ~op=#ReverseConvert, - `i=>{let v0=i["ios"];let v1=i["android"];return {"ios":v0["current"],"android":v1["current"],}}`, + `i=>{let v0=i["ios"],v1=i["android"];return {"ios":v0["current"],"android":v1["current"],}}`, ) let rawAppVersions = { @@ -383,10 +355,10 @@ test("Works with variant schema used multiple times as a child schema", t => { "android": {"current": "1.2", "minimum": "1.0"}, } - t->Assert.deepEqual(rawAppVersions->S.parseOrThrow(appVersionsSchema), appVersions) + t->Assert.deepEqual(rawAppVersions->S.parseOrThrow(~to=appVersionsSchema), appVersions) t->Assert.deepEqual( - appVersions->S.reverseConvertToJsonOrThrow(appVersionsSchema), + appVersions->S.decodeOrThrow(~from=appVersionsSchema, ~to=S.json), rawAppVersions->Obj.magic, ) }) @@ -415,35 +387,10 @@ test("Succesfully uses reversed variant schema to self for parsing back to initi test("Reverse convert tuple turned to Ok", t => { let schema = S.tuple2(S.string, S.bool)->S.shape(t => Ok(t)) - t->Assert.deepEqual(Ok(("foo", true))->S.reverseConvertOrThrow(schema), %raw(`["foo", true]`)) + t->Assert.deepEqual(Ok(("foo", true))->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["foo", true]`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{let v0=i["_0"];return v0}`) }) -test("Reverse with output of nested object/tuple schema", t => { - let schema = S.bool->S.shape(v => { - { - "nested": { - "field": (v, true), - }, - } - }) - t->U.assertEqualSchemas( - schema->S.reverse, - S.schema(s => { - { - "nested": { - "field": (s.matches(S.bool), true), - }, - } - }) - ->S.shape(v => { - let (b, _) = v["nested"]["field"] - b - }) - ->S.castToUnknown, - ) -}) - test( "Succesfully parses reversed schema with output of nested object/tuple and parses it back to initial value", t => { @@ -464,14 +411,14 @@ test("S.json shaped to literal should keep validation", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0=e[0](i);return "foo"} -JSON: i=>{if(Array.isArray(i)){let v4=new Array(i.length);for(let v0=0;v0{e[0](i);return "foo"} +JSON: i=>{if(Array.isArray(i)){for(let v0=0;v0JSON--0"](i[v0]);}catch(v1){v1.path='["'+v0+'"]'+v1.path;throw v1}}}else if(typeof i==="object"&&i&&!Array.isArray(i)){for(let v2 in i){try{e[1]["unknown->JSON--0"](i[v2]);}catch(v3){v3.path='["'+v2+'"]'+v3.path;throw v3}}}else if(!(typeof i==="string"||typeof i==="boolean"||typeof i==="number"&&!Number.isNaN(i)||i===null)){e[2](i)}return i}`, ) - t->Assert.deepEqual("foo"->S.parseOrThrow(schema), "foo") + t->Assert.deepEqual("foo"->S.parseOrThrow(~to=schema), "foo") t->U.assertThrowsMessage( - () => %raw(`undefined`)->S.parseOrThrow(schema), - "Failed parsing: Expected JSON, received undefined", + () => %raw(`undefined`)->S.parseOrThrow(~to=schema), + "Expected JSON, received undefined", ) - t->Assert.deepEqual("bar"->S.parseOrThrow(schema), "foo") + t->Assert.deepEqual("bar"->S.parseOrThrow(~to=schema), "foo") }) diff --git a/packages/sury/tests/S_string_test.res b/packages/sury/tests/S_string_test.res index c2a2c9e55..467c441b0 100644 --- a/packages/sury/tests/S_string_test.res +++ b/packages/sury/tests/S_string_test.res @@ -9,32 +9,28 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected string, received true`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="string"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="string"||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { diff --git a/packages/sury/tests/S_test.ts b/packages/sury/tests/S_test.ts index cf234e8b3..4a1911bf1 100644 --- a/packages/sury/tests/S_test.ts +++ b/packages/sury/tests/S_test.ts @@ -3,34 +3,52 @@ import { expectType, TypeEqual } from "ts-expect"; import * as S from "../src/S.js"; +// FIXME: S.max should be applied to output +// From https://x.com/dzakh_dev/status/1963982551208309222 +// const PixelSchema = S.pattern(/^\d{1,3}px$/) +// .with(S.to, S.number, parseInt) +// .with(S.max, 100) +// .with(S.meta, { +// description: "A pixel value between 0 and 100", +// }); + // FIXME: Move the test to e2e // import { stringSchema } from "../genType/GenType.gen.js"; +// FIXME: This is fails +// S.parser( +// S.union([ +// "bar", +// "bas", +// S.string.with(S.to, S.schema("unknown").with(S.noValidation, true)), +// ]) +// ) + type SchemaEqual< Schema extends S.Schema, Output, - Input = Output + Input = Output, > = TypeEqual, Output> & TypeEqual, Input>; // Can use genType schema // expectType>(true); test("JSON string demo", (t) => { - // t.throws(() => S.parseOrThrow("123", S.jsonString), { + // t.throws(() => S.parser("123", S.jsonString), { // name: "Error", // message: // "[Sury] Schema S.jsonString is not enabled. To start using it, add S.enableJsonString() at the project root.", // }); - t.deepEqual(S.parseOrThrow("123", S.jsonString), "123"); - // i=>{if(typeof i!=="string"){e[0](i)}try{JSON.parse(i)}catch(t){e[1](i)}return i} + t.deepEqual(S.parser(S.jsonString)("123"), "123"); + // i=>{if(typeof i!=="string"){e[1](i)}try{JSON.parse(i)}catch(t){e[0](i)}return i} const schemaWithTo = S.jsonString.with(S.to, S.number); - t.deepEqual(S.parseOrThrow("123", schemaWithTo), 123); - // i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[1](i)}if(typeof v0!=="number"||Number.isNaN(v0)){e[2](v0)}return v0} + t.deepEqual(S.parser(schemaWithTo)("123"), 123); + // i=>{if(typeof i!=="string"){e[2](i)}let v0;try{v0=JSON.parse(i)}catch(t){e[0](i)}if(typeof v0!=="number"||Number.isNaN(v0)){e[1](v0)}return v0} const schemaWithTo2 = S.number.with(S.to, S.jsonString); - t.deepEqual(S.convertOrThrow(123, schemaWithTo2), "123"); + t.deepEqual(S.decoder(schemaWithTo2)(123), "123"); // i=>{return ""+i} }); @@ -39,7 +57,7 @@ S.enableJsonString(); test("Successfully parses string", (t) => { const schema = S.string; - const value = S.parseOrThrow("123", schema); + const value = S.parser(schema)("123"); t.deepEqual(value, "123"); @@ -49,7 +67,7 @@ test("Successfully parses string", (t) => { test("Successfully parses string with built-in refinement", (t) => { const schema = S.string.with(S.length, 5); - const result = S.safe(() => S.parseOrThrow("123", schema)); + const result = S.safe(() => S.parser(schema)("123")); expectType>>(true); @@ -57,10 +75,7 @@ test("Successfully parses string with built-in refinement", (t) => { t.fail("Should fail"); return; } - t.is( - result.error.message, - "Failed parsing: String must be exactly 5 characters long" - ); + t.is(result.error.message, "String must be exactly 5 characters long"); expectType>(true); expectType< @@ -76,20 +91,20 @@ test("Successfully parses string with built-in refinement", (t) => { test("Successfully parses string with built-in refinement and custom message", (t) => { const schema = S.string.with(S.length, 5, "Postcode must have 5 symbols"); - const result = S.safe(() => S.parseOrThrow("123", schema)); + const result = S.safe(() => S.parser(schema)("123")); if (result.success) { t.fail("Should fail"); return; } - t.is(result.error.message, "Failed parsing: Postcode must have 5 symbols"); + t.is(result.error.message, "Postcode must have 5 symbols"); expectType>(true); }); test("Successfully parses string with built-in transform", (t) => { const schema = S.trim(S.string); - const value = S.parseOrThrow(" 123", schema); + const value = S.parser(schema)(" 123"); t.deepEqual(value, "123"); @@ -97,9 +112,9 @@ test("Successfully parses string with built-in transform", (t) => { expectType>(true); }); -test("Successfully parses string with built-in datetime transform", (t) => { - const schema = S.datetime(S.string); - const value = S.parseOrThrow("2020-01-01T00:00:00Z", schema); +test("Successfully parses string to Date via S.to(S.date)", (t) => { + const schema = S.to(S.string, S.date); + const value = S.parser(schema)("2020-01-01T00:00:00Z"); t.deepEqual(value, new Date("2020-01-01T00:00:00Z")); @@ -107,9 +122,29 @@ test("Successfully parses string with built-in datetime transform", (t) => { expectType>(true); }); +test("Successfully parses string to Date with S.to", (t) => { + const schema = S.string.with(S.to, S.date); + const value = S.parser(schema)("2024-01-01T00:00:00.000Z"); + + t.deepEqual(value, new Date("2024-01-01T00:00:00.000Z")); + + expectType>(true); + expectType>(true); +}); + +test("Successfully converts Date to string with S.to", (t) => { + const schema = S.date.with(S.to, S.string); + const value = S.decoder(schema)(new Date("2024-01-01T00:00:00.000Z")); + + t.is(value, "2024-01-01T00:00:00.000Z"); + + expectType>(true); + expectType>(true); +}); + test("Successfully parses int", (t) => { const schema = S.int32; - const value = S.parseOrThrow(123, schema); + const value = S.parser(schema)(123); t.deepEqual(value, 123); @@ -119,7 +154,7 @@ test("Successfully parses int", (t) => { test("Successfully parses float", (t) => { const schema = S.number; - const value = S.parseOrThrow(123.4, schema); + const value = S.parser(schema)(123.4); t.deepEqual(value, 123.4); @@ -129,7 +164,7 @@ test("Successfully parses float", (t) => { test("Successfully parses BigInt", (t) => { const schema = S.bigint; - const value = S.parseOrThrow(123n, schema); + const value = S.parser(schema)(123n); t.deepEqual(value, 123n); @@ -140,7 +175,7 @@ test("Successfully parses BigInt", (t) => { test("Successfully parses symbol", (t) => { const schema = S.symbol; const data = Symbol("foo"); - const value = S.parseOrThrow(data, schema); + const value = S.parser(schema)(data); t.deepEqual(value, data); t.notDeepEqual(value, Symbol("foo")); // Because this is how symbols work @@ -161,7 +196,7 @@ test("Function literal schema", (t) => { } t.is(schema.const, fn); - const value = S.parseOrThrow(fn, schema); + const value = S.parser(schema)(fn); t.deepEqual(value, fn); t.notDeepEqual(value, function () {}); @@ -172,15 +207,15 @@ test("Fails to parse float when NaN is provided", (t) => { t.throws( () => { - const value = S.parseOrThrow(NaN, schema); + const value = S.parser(schema)(NaN); expectType>(true); expectType>(true); }, { name: "SuryError", - message: "Failed parsing: Expected number, received NaN", - } + message: "Expected number, received NaN", + }, ); }); @@ -189,7 +224,7 @@ test("Successfully parses float when NaN is provided and NaN check disabled in g disableNanNumberValidation: true, }); const schema = S.number; - const value = S.parseOrThrow(NaN, schema); + const value = S.parser(schema)(NaN); S.global({}); t.deepEqual(value, NaN); @@ -200,7 +235,7 @@ test("Successfully parses float when NaN is provided and NaN check disabled in g test("Successfully parses bool", (t) => { const schema = S.boolean; - const value = S.parseOrThrow(true, schema); + const value = S.parser(schema)(true); t.deepEqual(value, true); @@ -210,7 +245,7 @@ test("Successfully parses bool", (t) => { test("Successfully parses unknown", (t) => { const schema = S.unknown; - const value = S.parseOrThrow(true, schema); + const value = S.parser(schema)(true); t.deepEqual(value, true); @@ -220,7 +255,7 @@ test("Successfully parses unknown", (t) => { test("Successfully parses any", (t) => { const schema = S.any; - const value = S.parseOrThrow(true, schema); + const value = S.parser(schema)(true); t.deepEqual(value, true); @@ -230,7 +265,7 @@ test("Successfully parses any", (t) => { test("Successfully parses json", (t) => { const schema = S.json; - const value = S.parseOrThrow(true, schema); + const value = S.parser(schema)(true); t.deepEqual(value, true); @@ -241,17 +276,17 @@ test("Successfully parses json", (t) => { test("Successfully parses invalid json without validation", (t) => { const schema = S.json.with(S.noValidation, true); - const value = S.parseOrThrow(undefined, schema); - t.deepEqual( - S.parseOrThrow(undefined, schema), - undefined, - "This is wrong but it's intentional" - ); + let fn = S.parser(schema); + + const value = fn(undefined); + t.deepEqual(value, undefined, "This is wrong but it's intentional"); + + t.deepEqual(fn.name, `noopOperation`); t.deepEqual( - S.parseOrThrow([undefined], schema), + fn([undefined]), [undefined], - "Nested should theoretically fail, but currently it doesn't" + "Nested fields shouldn't be validated as well", ); expectType>(true); @@ -260,7 +295,7 @@ test("Successfully parses invalid json without validation", (t) => { test("Successfully parses undefined", (t) => { const schema = S.schema(undefined); - const value = S.parseOrThrow(undefined, schema); + const value = S.parser(schema)(undefined); t.deepEqual(value, undefined); @@ -270,7 +305,7 @@ test("Successfully parses undefined", (t) => { test("Successfully parses void", (t) => { const schema = S.void; - const value = S.parseOrThrow(undefined, schema); + const value = S.parser(schema)(undefined); t.deepEqual(value, undefined); @@ -283,22 +318,22 @@ test("Fails to parse never", (t) => { t.throws( () => { - const value = S.parseOrThrow(true, schema); + const value = S.parser(schema)(true); expectType>(true); expectType>(true); }, { name: "SuryError", - message: "Failed parsing: Expected never, received true", - } + message: "Expected never, received true", + }, ); }); test("Can get a reason from an error", (t) => { const schema = S.never; - const result = S.safe(() => S.parseOrThrow(true, schema)); + const result = S.safe(() => S.parser(schema)(true)); if (result.success) { t.fail("Should fail"); @@ -309,7 +344,7 @@ test("Can get a reason from an error", (t) => { test("Successfully parses array", (t) => { const schema = S.array(S.string); - const value = S.parseOrThrow(["foo"], schema); + const value = S.parser(schema)(["foo"]); t.deepEqual(value, ["foo"]); @@ -317,16 +352,23 @@ test("Successfully parses array", (t) => { expectType>(true); }); +test("Transforms array of bigint to array of string", (t) => { + const fn = S.decoder(S.array(S.bigint), S.array(S.string)); + + t.deepEqual( + fn.toString(), + `i=>{let v2=new Array(i.length);for(let v1=0;v1 { const schema = S.array(S.string).with(S.min, 1).with(S.max, 2); - const value = S.parseOrThrow(["foo"], schema); + const value = S.parser(schema)(["foo"]); t.deepEqual(value, ["foo"]); - const result = S.safe(() => S.parseOrThrow([], schema)); - t.deepEqual( - result.error?.message, - "Failed parsing: Array must be 1 or more items long" - ); + const result = S.safe(() => S.parser(schema)([])); + t.deepEqual(result.error?.message, "Array must be 1 or more items long"); expectType>(true); expectType>(true); @@ -334,7 +376,7 @@ test("Successfully parses array with min and max refinements", (t) => { test("Successfully parses record", (t) => { const schema = S.record(S.string); - const value = S.parseOrThrow({ foo: "bar" }, schema); + const value = S.parser(schema)({ foo: "bar" }); t.deepEqual(value, { foo: "bar" }); @@ -344,7 +386,7 @@ test("Successfully parses record", (t) => { test("Successfully parses JSON string", (t) => { const schema = S.jsonString.with(S.to, S.boolean); - const value = S.parseOrThrow(`true`, schema); + const value = S.parser(schema)(`true`); t.deepEqual(value, true); t.deepEqual(schema.type === "string" && schema.format === "json", true); @@ -360,36 +402,59 @@ test("Parse JSON string, extract a field, and serialize it back to JSON string", S.schema({ type: "info", value: S.number, - }).with(S.shape, (msg) => msg.value) + }).with(S.shape, (msg) => msg.value), ) .with(S.to, S.jsonString); - t.deepEqual(S.parseOrThrow(`{"type": "info", "value": 123}`, schema), "123"); - t.throws(() => S.parseOrThrow(`{"type": "info", "value": "123"}`, schema), { + t.deepEqual(S.parser(schema)(`{"type": "info", "value": 123}`), "123"); + t.throws(() => S.parser(schema)(`{"type": "info", "value": "123"}`), { name: "SuryError", - message: `Failed parsing at ["value"]: Expected number, received "123"`, + message: `Failed at ["value"]: Expected number, received "123"`, }); - t.deepEqual( - S.reverseConvertOrThrow("123", schema), - `{"type":"info","value":123}` - ); + t.deepEqual(S.encoder(schema)("123"), `{"type":"info","value":123}`); expectType>(true); }); +test("Parse JSON string to object with bigint and back", (t) => { + S.enableUint8Array(); + + const messageSchema = S.schema({ + type: "info", + value: S.bigint, + }); + + const decode = S.decoder(S.jsonString, messageSchema); + const encode = S.decoder( + messageSchema, + // Cast to string to disable json string encoder + S.jsonString.with(S.to, S.string, (string) => string), + S.uint8Array, + ); + + t.deepEqual(decode(`{"type": "info", "value": "123"}`), { + type: "info", + value: 123n, + }); + t.deepEqual( + encode({ type: "info", value: 123n }), + new Uint8Array([ + 123, 34, 116, 121, 112, 101, 34, 58, 34, 105, 110, 102, 111, 34, 44, 34, + 118, 97, 108, 117, 101, 34, 58, 34, 49, 50, 51, 34, 125, + ]), + ); +}); + test("Successfully serialized JSON object", (t) => { const objectSchema = S.schema({ foo: [1, S.number] }); const schema = S.jsonString.with(S.to, objectSchema); const schemaWithSpace = S.jsonStringWithSpace(2).with(S.to, objectSchema); - const value = S.convertOrThrow({ foo: [1, 2] }, S.reverse(schema)); + const value = S.encoder(schema)({ foo: [1, 2] }); t.deepEqual(value, '{"foo":[1,2]}'); - const valueWithSpace = S.reverseConvertOrThrow( - { foo: [1, 2] }, - schemaWithSpace - ); + const valueWithSpace = S.encoder(schemaWithSpace)({ foo: [1, 2] }); t.deepEqual(valueWithSpace, '{\n "foo": [\n 1,\n 2\n ]\n}'); expectType< @@ -413,8 +478,8 @@ test("Successfully serialized JSON object", (t) => { test("Successfully parses optional string", (t) => { const schema = S.optional(S.string); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(undefined, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(undefined); t.deepEqual(value1, "foo"); t.deepEqual(value2, undefined); @@ -430,8 +495,8 @@ test("Optional enum", (t) => { const statuses = S.union(["Win", "Draw", "Loss"]); const schema = S.optional(statuses); - t.deepEqual(S.parseOrThrow("Win", schema), "Win"); - t.deepEqual(S.parseOrThrow(undefined, schema), undefined); + t.deepEqual(S.parser(schema)("Win"), "Win"); + t.deepEqual(S.parser(schema)(undefined), undefined); expectType< TypeEqual< @@ -454,8 +519,8 @@ test("Optional enum", (t) => { test("Successfully parses schema wrapped in optional multiple times", (t) => { const schema = S.optional(S.optional(S.optional(S.string))); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(undefined, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(undefined); t.deepEqual(value1, "foo"); t.deepEqual(value2, undefined); @@ -469,8 +534,8 @@ test("Successfully parses schema wrapped in optional multiple times", (t) => { test("Successfully parses nullable string", (t) => { const schema = S.nullable(S.string); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(null, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(null); t.deepEqual(value1, "foo"); t.deepEqual(value2, undefined); @@ -483,34 +548,40 @@ test("Successfully parses nullable string", (t) => { test("Successfully parses nullable of array with default", (t) => { const schema = S.nullable(S.array(S.string), []); - const value1 = S.parseOrThrow(["foo"], schema); - const value2 = S.parseOrThrow(null, schema); + const value1 = S.parser(schema)(["foo"]); + const value2 = S.parser(schema)(null); t.deepEqual(value1, ["foo"]); t.deepEqual(value2, []); expectType, typeof schema>>( - true + true, ); expectType>(true); }); test("Successfully parses nullable string with default", (t) => { const schema = S.nullable(S.string, "bar"); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(null, schema); + + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(null); t.deepEqual(value1, "foo"); t.deepEqual(value2, "bar"); + t.throws(() => S.parser(schema)(undefined), { + name: "SuryError", + message: "Expected string | null, received undefined", + }); + expectType, typeof schema>>(true); expectType>(true); }); test("Successfully parses nullable string with dynamic default", (t) => { const schema = S.nullable(S.string, () => "bar"); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(null, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(null); t.deepEqual(value1, "foo"); t.deepEqual(value2, "bar"); @@ -521,9 +592,9 @@ test("Successfully parses nullable string with dynamic default", (t) => { test("Successfully parses nullish string", (t) => { const schema = S.nullish(S.string); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(undefined, schema); - const value3 = S.parseOrThrow(null, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(undefined); + const value3 = S.parser(schema)(null); t.deepEqual(value1, "foo"); t.deepEqual(value2, undefined); @@ -541,8 +612,8 @@ test("Successfully parses nullish string", (t) => { test("Successfully parses schema wrapped in nullable multiple times", (t) => { const nullable = S.nullable(S.string); const schema = S.nullable(S.nullable(nullable)); - const value1 = S.parseOrThrow("foo", schema); - const value2 = S.parseOrThrow(null, schema); + const value1 = S.parser(schema)("foo"); + const value2 = S.parser(schema)(null); // TODO: Test that it should flatten nested nullable schemas @@ -561,12 +632,12 @@ test("Fails to parse with invalid data", (t) => { t.throws( () => { - S.parseOrThrow(123, schema); + S.parser(schema)(123); }, { name: "SuryError", - message: "Failed parsing: Expected string, received 123", - } + message: "Expected string, received 123", + }, ); }); @@ -585,6 +656,8 @@ test("Test JSON Schema of int32", (t) => { t.deepEqual(S.toJSONSchema(schema), { type: "integer", + minimum: -2147483648, + maximum: 2147483647, }); }); @@ -601,12 +674,14 @@ test("Test extended JSON Schema", (t) => { $ref: "Foo", readOnly: true, type: "integer", + minimum: -2147483648, + maximum: 2147483647, }); }); test("Successfully reverse converts with valid value", (t) => { const schema = S.string; - const result = S.reverseConvertOrThrow("123", schema); + const result = S.encoder(schema)("123"); t.deepEqual(result, "123"); @@ -615,7 +690,7 @@ test("Successfully reverse converts with valid value", (t) => { test("Successfully reverse converts to Json with valid value", (t) => { const schema = S.string; - const result = S.reverseConvertToJsonOrThrow("123", schema); + const result = S.encoder(schema, S.json)("123"); t.deepEqual(result, "123"); @@ -623,8 +698,7 @@ test("Successfully reverse converts to Json with valid value", (t) => { }); test("Successfully reverse converts to Json string with valid value", (t) => { - const schema = S.int32; - const result = S.reverseConvertToJsonStringOrThrow(123, schema); + const result = S.encoder(S.int32, S.jsonString)(123); t.deepEqual(result, `123`); @@ -636,54 +710,75 @@ test("Fails to serialize never", (t) => { t.throws( () => { - // @ts-ignore - S.convertOrThrow("123", S.reverse(schema)); + S.encoder(schema)("123" as never); }, { name: "SuryError", - message: `Failed converting: Expected never, received "123"`, - } + message: `Expected never, received "123"`, + }, ); }); test("Successfully parses with transform to another type", (t) => { - const schema = S.string.with(S.transform, (string) => Number(string)); - const value = S.parseOrThrow("123", schema); + const schema = S.string.with(S.to, S.number, (string) => Number(string)); + const value = S.parser(schema)("123"); t.deepEqual(value, 123); expectType>(true); }); +test("Handles errors during custom encoding", (t) => { + const schema = S.string.with(S.to, S.number, undefined, (number) => { + if (number < 100) { + throw new Error("Number is too small"); + } + return number.toString(); + }); + + const output = S.parser(schema)("80"); + t.deepEqual(output, 80); + + t.throws( + () => { + S.encoder(schema)(output); + }, + { + name: "SuryError", + message: "Number is too small", + }, + ); +}); + test("Fails to parse with transform with user error", (t) => { - const schema = S.string.with(S.transform, (string, s) => { + const schema = S.string.with(S.to, S.number, (string) => { const number = Number(string); if (Number.isNaN(number)) { - s.fail("Invalid number"); + throw new Error("Invalid number"); } return number; }); - const value = S.parseOrThrow("123", schema); + const value = S.parser(schema)("123"); t.deepEqual(value, 123); expectType>(true); t.throws( () => { - S.parseOrThrow("asdf", schema); + S.parser(schema)("asdf"); }, { name: "SuryError", - message: "Failed parsing: Invalid number", - } + message: "Invalid number", + }, ); }); test("Successfully converts reversed schema with transform to another type", (t) => { - const schema = S.string.with(S.transform, undefined, (number) => { + const schema = S.string.with(S.to, S.number, undefined, (number) => { expectType>(true); return number.toString(); }); - const result = S.convertOrThrow(123, S.reverse(schema)); + const result = S.encoder(schema)(123); t.deepEqual(result, "123"); @@ -693,8 +788,9 @@ test("Successfully converts reversed schema with transform to another type", (t) test("Successfully parses with refine", (t) => { const schema = S.string.with(S.refine, (string) => { expectType>(true); + return true; }); - const value = S.parseOrThrow("123", schema); + const value = S.parser(schema)("123"); t.deepEqual(value, "123"); @@ -704,8 +800,9 @@ test("Successfully parses with refine", (t) => { test("Successfully reverse converts with refine", (t) => { const schema = S.string.with(S.refine, (string) => { expectType>(true); + return true; }); - const result = S.convertOrThrow("123", S.reverse(schema)); + const result = S.encoder(schema)("123"); t.deepEqual(result, "123"); @@ -713,26 +810,43 @@ test("Successfully reverse converts with refine", (t) => { }); test("Fails to parses with refine raising an error", (t) => { - const schema = S.string.with(S.refine, (_, s) => { - s.fail("User error"); + const schema = S.string.with(S.refine, () => false, { + error: "User error", }); t.throws( () => { - S.parseOrThrow("123", schema); + S.parser(schema)("123"); }, { name: "SuryError", - message: "Failed parsing: User error", - } + message: "User error", + }, + ); +}); + +test("Fails to parse with refine with path option", (t) => { + const schema = S.string.with(S.refine, () => false, { + error: "User error", + path: ["data", "field"], + }); + + t.throws( + () => { + S.parser(schema)("123"); + }, + { + name: "SuryError", + message: `Failed at ["data"]["field"]: User error`, + }, ); }); test("Successfully parses async schema", async (t) => { - const schema = S.string.with(S.asyncParserRefine, async (string) => { + const schema = S.string.with(S.asyncDecoderAssert, async (string) => { expectType>(true); }); - const value = await S.safeAsync(() => S.parseAsyncOrThrow("123", schema)); + const value = await S.safeAsync(() => S.asyncParser(schema)("123")); t.deepEqual(value, { success: true, value: "123" }); @@ -740,20 +854,32 @@ test("Successfully parses async schema", async (t) => { }); test("Fails to parses async schema", async (t) => { - const schema = S.string.with(S.asyncParserRefine, async (_, s) => { - return Promise.resolve().then(() => { - s.fail("User error"); - }); + const schema = S.string.with(S.asyncDecoderAssert, async () => { + throw new Error("User error"); }); - const result = await S.safeAsync(() => S.parseAsyncOrThrow("123", schema)); + const result = await S.safeAsync(() => S.asyncParser(schema)("123")); if (result.success) { t.fail("Should fail"); return; } - t.is(result.error.message, "Failed async parsing: User error"); + t.is(result.error.message, "User error"); t.true(result.error instanceof S.Error); + + expectType< + TypeEqual< + typeof result.error.code, + | "invalid_input" + | "invalid_operation" + | "unsupported_decode" + | "invalid_conversion" + | "unrecognized_keys" + | "custom" + > + >(true); + + t.is(result.error.code, "invalid_conversion"); }); test("Successfully parses object by provided shape", (t) => { @@ -761,13 +887,10 @@ test("Successfully parses object by provided shape", (t) => { foo: S.string, bar: S.boolean, }); - const value = S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + bar: true, + }); t.deepEqual(value, { foo: "bar", @@ -804,14 +927,11 @@ test("Successfully parses object with quoted keys", (t) => { [`'`]: S.string, ["`"]: S.string, }); - const value = S.parseOrThrow( - { - '"': '"', - "'": "'", - "`": "`", - }, - schema - ); + const value = S.parser(schema)({ + '"': '"', + "'": "'", + "`": "`", + }); t.deepEqual(value, { '"': '"', @@ -836,13 +956,10 @@ test("Successfully parses tagged object", (t) => { tag: "block" as const, bar: S.boolean, }); - const value = S.parseOrThrow( - { - tag: "block", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + tag: "block", + bar: true, + }); t.deepEqual(value, { tag: "block", @@ -878,11 +995,11 @@ test("Successfully parses and reverse convert object with optional field", (t) = bar: S.optional(S.boolean), baz: S.boolean, }); - const value = S.parseOrThrow({ baz: true }, schema); + const value = S.parser(schema)({ baz: true }); t.deepEqual(value, { bar: undefined, baz: true }); - const reversed = S.convertOrThrow({ baz: true }, S.reverse(schema)); - t.deepEqual(reversed, { bar: undefined, baz: true }); + const reversed = S.encoder(schema)({ baz: true }); + t.deepEqual(reversed, { baz: true }); expectType< SchemaEqual< @@ -904,13 +1021,10 @@ test("Successfully parses object with field names transform", (t) => { foo: s.field("Foo", S.string), bar: s.field("Bar", S.boolean), })); - const value = S.parseOrThrow( - { - Foo: "bar", - Bar: true, - }, - schema - ); + const value = S.parser(schema)({ + Foo: "bar", + Bar: true, + }); t.deepEqual(value, { foo: "bar", @@ -949,17 +1063,14 @@ test("Successfully parses advanced object with all features", (t) => { }; }); - const value = S.parseOrThrow( - { - nested: { - field: 123, - }, - type: 0, - id: "id", - Foo: "bar", + const value = S.parser(schema)({ + nested: { + field: 123, }, - schema - ); + type: 0, + id: "id", + Foo: "bar", + }); t.deepEqual(value, { nested: 123, @@ -986,16 +1097,13 @@ test("Successfully parses advanced object with all features", (t) => { test("Successfully parses object with transformed field", (t) => { const schema = S.schema({ - foo: S.string.with(S.transform, (string) => Number(string)), + foo: S.string.with(S.to, S.number, (string) => Number(string)), bar: S.boolean, }); - const value = S.parseOrThrow( - { - foo: "123", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "123", + bar: true, + }); t.deepEqual(value, { foo: 123, @@ -1033,13 +1141,10 @@ test("Fails to parse strict object with exccess fields", (t) => { t.throws( () => { - const value = S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + bar: true, + }); expectType< TypeEqual< typeof schema, @@ -1064,8 +1169,8 @@ test("Fails to parse strict object with exccess fields", (t) => { }, { name: "SuryError", - message: `Failed parsing: Unrecognized key "bar"`, - } + message: `Unrecognized key "bar"`, + }, ); }); @@ -1078,15 +1183,12 @@ test("Fails to parse deep strict object with exccess fields", (t) => { t.throws( () => { - const value = S.parseOrThrow( - { - foo: { - a: "bar", - b: true, - }, + const value = S.parser(schema)({ + foo: { + a: "bar", + b: true, }, - schema - ); + }); expectType< SchemaEqual< typeof schema, @@ -1100,8 +1202,8 @@ test("Fails to parse deep strict object with exccess fields", (t) => { }, { name: "SuryError", - message: `Failed parsing at ["foo"]: Unrecognized key "b"`, - } + message: `Failed at ["foo"]: Unrecognized key "b"`, + }, ); }); @@ -1117,13 +1219,10 @@ test("Fails to parse strict object with exccess fields which created using globa t.throws( () => { - const value = S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + bar: true, + }); expectType< TypeEqual< typeof schema, @@ -1148,8 +1247,8 @@ test("Fails to parse strict object with exccess fields which created using globa }, { name: "SuryError", - message: `Failed parsing: Unrecognized key "bar"`, - } + message: `Unrecognized key "bar"`, + }, ); }); @@ -1158,17 +1257,14 @@ test("Resets object strict mode with strip method", (t) => { S.strict( S.schema({ foo: S.string, - }) - ) + }), + ), ); - const value = S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + bar: true, + }); t.deepEqual(value, { foo: "bar" }); @@ -1201,12 +1297,12 @@ test("Successfully parses intersected objects", (t) => { }), S.schema({ baz: S.string, - }) + }), ); t.deepEqual( - S.compile(schema, "Input", "Output", "Sync", true).toString(), - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"],v1=i["bar"],v2=i["baz"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}if(typeof v2!=="string"){e[3](v2)}return {"foo":v0,"bar":v1,"baz":v2,}}` + S.parser(schema).toString(), + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["foo"],v1=i["bar"],v2=i["baz"];typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);typeof v2==="string"||e[2](v2);return {"foo":v0,"bar":v1,"baz":v2,}}`, ); expectType< @@ -1222,13 +1318,10 @@ test("Successfully parses intersected objects", (t) => { >(true); const result = S.safe(() => - S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ) + S.parser(schema)({ + foo: "bar", + bar: true, + }), ); if (result.success) { t.fail("Should fail"); @@ -1236,17 +1329,14 @@ test("Successfully parses intersected objects", (t) => { } t.is( result.error.message, - `Failed parsing at ["baz"]: Expected string, received undefined` + `Failed at ["baz"]: Expected string, received undefined`, ); - const value = S.parseOrThrow( - { - foo: "bar", - baz: "baz", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + baz: "baz", + bar: true, + }); t.deepEqual(value, { foo: "bar", baz: "baz", @@ -1266,14 +1356,14 @@ test("Fails to parse intersected objects with transform", (t) => { })), S.schema({ baz: S.string, - }) + }), ); }, { name: "Error", // TODO: Can theoretically support this case message: `[Sury] The merge supports only structured object schemas without transformations`, - } + }, ); // expectType< @@ -1288,7 +1378,7 @@ test("Fails to parse intersected objects with transform", (t) => { // >(true); // const result = S.safe(() => - // S.parseOrThrow( + // S.parser( // { // foo: "bar", // bar: true, @@ -1302,10 +1392,10 @@ test("Fails to parse intersected objects with transform", (t) => { // } // t.is( // result.error.message, - // `Failed parsing at ["baz"]: Expected string, received undefined` + // `Failed at ["baz"]: Expected string, received undefined` // ); - // const value = S.parseOrThrow( + // const value = S.parser( // { // foo: "bar", // baz: "baz", @@ -1327,22 +1417,23 @@ test("Successfully serializes S.merge", (t) => { }), S.schema({ baz: S.string, - }) + }), ); t.deepEqual( - S.compile(schema, "Output", "Input", "Sync", true).toString(), - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"],v1=i["bar"],v2=i["baz"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}if(typeof v2!=="string"){e[3](v2)}return i}` + S.parser(S.reverse(schema)).toString(), + `i=>{typeof i==="object"&&i||e[3](i);let v0=i["foo"],v1=i["bar"],v2=i["baz"];typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);typeof v2==="string"||e[2](v2);return {"foo":v0,"bar":v1,"baz":v2,}}`, ); - - const value = S.reverseConvertOrThrow( - { - foo: "bar", - baz: "baz", - bar: true, - }, - schema + t.deepEqual( + S.encoder(schema).toString().startsWith("function noopOperation(i) {"), + true, ); + + const value = S.encoder(schema)({ + foo: "bar", + baz: "baz", + bar: true, + }); expectType>>(true); t.deepEqual(value, { @@ -1363,17 +1454,14 @@ test("Merge overwrites the left fields by schema from the right", (t) => { S.schema({ type: "foo" as const, fooCount: S.number, - }) + }), ); - const value = S.parseOrThrow( - { - type: "foo", - name: "foo", - fooCount: 123, - }, - fooSchema - ); + const value = S.parser(fooSchema)({ + type: "foo", + name: "foo", + fooCount: 123, + }); expectType< SchemaEqual< @@ -1395,18 +1483,15 @@ test("Merge overwrites the left fields by schema from the right", (t) => { t.throws( () => - S.parseOrThrow( - { - type: "bar", - name: "foo", - fooCount: 123, - }, - fooSchema - ), + S.parser(fooSchema)({ + type: "bar", + name: "foo", + fooCount: 123, + }), { name: "SuryError", - message: `Failed parsing: Expected { type: "foo"; name: string; fooCount: number; }, received { type: "bar"; name: "foo"; fooCount: 123; }`, - } + message: `Failed at ["type"]: Expected "foo", received "bar"`, + }, ); }); @@ -1418,7 +1503,7 @@ test("Name of merge schema", (t) => { }), S.schema({ baz: S.string, - }) + }), ); t.is(S.toExpression(schema), `{ foo: string; bar: boolean; baz: string; }`); @@ -1429,13 +1514,10 @@ test("Successfully parses object using S.schema", (t) => { foo: S.string, bar: S.boolean, }); - const value = S.parseOrThrow( - { - foo: "bar", - bar: true, - }, - schema - ); + const value = S.parser(schema)({ + foo: "bar", + bar: true, + }); t.deepEqual(value, { foo: "bar", @@ -1464,7 +1546,7 @@ test("Successfully parses object using S.schema", (t) => { test("Successfully parses tuple using S.schema", (t) => { const schema = S.schema([S.string, S.boolean] as const); - const value = S.parseOrThrow(["bar", true], schema); + const value = S.parser(schema)(["bar", true]); t.deepEqual(value, ["bar", true]); @@ -1474,7 +1556,7 @@ test("Successfully parses tuple using S.schema", (t) => { test("Successfully parses primitive schema passed to S.schema", (t) => { const schema = S.schema(S.string); - const value = S.parseOrThrow("bar", schema); + const value = S.parser(schema)("bar"); t.deepEqual(value, "bar"); @@ -1485,7 +1567,7 @@ test("Successfully parses primitive schema passed to S.schema", (t) => { test("Successfully parses literal using S.schema with as cost", (t) => { const schema = S.schema("foo" as const); - const value = S.parseOrThrow("foo", schema); + const value = S.parser(schema)("foo"); t.deepEqual(value, "foo"); @@ -1499,12 +1581,9 @@ test("Successfully parses nested object using S.schema", (t) => { bar: S.number, }, }); - const value = S.parseOrThrow( - { - foo: { bar: 123 }, - }, - schema - ); + const value = S.parser(schema)({ + foo: { bar: 123 }, + }); t.deepEqual(value, { foo: { bar: 123 }, @@ -1538,13 +1617,10 @@ test("S.schema example", (t) => { radius: S.number, }); - const value = S.parseOrThrow( - { - kind: "circle", - radius: 123, - }, - circleSchema - ); + const value = S.parser(circleSchema)({ + kind: "circle", + radius: 123, + }); t.deepEqual(value, { kind: "circle", @@ -1561,7 +1637,7 @@ test("S.name", (t) => { test("Successfully parses and returns result", (t) => { const schema = S.string; - const value = S.safe(() => S.parseOrThrow("123", schema)); + const value = S.safe(() => S.parser(schema)("123")); t.deepEqual(value, { success: true, value: "123" }); @@ -1592,7 +1668,7 @@ test("Successfully parses and returns result", (t) => { test("Successfully reverse converts and returns result", (t) => { const schema = S.string; - const value = S.safe(() => S.convertOrThrow("123", S.reverse(schema))); + const value = S.safe(() => S.encoder(schema)("123")); t.deepEqual(value, { success: true, value: "123" }); @@ -1622,7 +1698,7 @@ test("Successfully reverse converts and returns result", (t) => { test("Successfully parses union", (t) => { const schema = S.union([S.string, S.number]); - const value = S.safe(() => S.parseOrThrow("123", schema)); + const value = S.safe(() => S.parser(schema)("123")); t.deepEqual(value, { success: true, value: "123" }); @@ -1631,7 +1707,7 @@ test("Successfully parses union", (t) => { test("Successfully parses union of literals", (t) => { const schema = S.union(["foo", 123, true]); - const value = S.safe(() => S.parseOrThrow("foo", schema)); + const value = S.safe(() => S.parser(schema)("foo")); t.deepEqual(value, { success: true, value: "foo" }); @@ -1654,13 +1730,10 @@ test("Shape union", (t) => { y: S.number, }, ]); - const value = S.parseOrThrow( - { - kind: "circle", - radius: 123, - }, - shapeSchema - ); + const value = S.parser(shapeSchema)({ + kind: "circle", + radius: 123, + }); t.deepEqual(value, { kind: "circle", @@ -1704,10 +1777,10 @@ test("Shape union", (t) => { test("Successfully parses union with transformed items", (t) => { const schema = S.union([ - S.string.with(S.transform, (string) => Number(string)), + S.string.with(S.to, S.number, (string) => Number(string)), S.number, ]); - const value = S.safe(() => S.parseOrThrow("123", schema)); + const value = S.safe(() => S.parser(schema)("123")); t.deepEqual(value, { success: true, value: 123 }); @@ -1717,7 +1790,7 @@ test("Successfully parses union with transformed items", (t) => { test("String literal", (t) => { const schema = S.schema("tuna"); - t.deepEqual(S.parseOrThrow("tuna", schema), "tuna"); + t.deepEqual(S.parser(schema)("tuna"), "tuna"); expectType>(true); }); @@ -1730,11 +1803,12 @@ test("Nested string literal", (t) => { }); t.deepEqual( - S.parseOrThrow( - { nested: "tuna", withoutAsConst: "tuna", inSchema: "tuna" }, - schema - ), - { nested: "tuna", withoutAsConst: "tuna", inSchema: "tuna" } + S.parser(schema)({ + nested: "tuna", + withoutAsConst: "tuna", + inSchema: "tuna", + }), + { nested: "tuna", withoutAsConst: "tuna", inSchema: "tuna" }, ); expectType< @@ -1748,7 +1822,7 @@ test("Nested string literal", (t) => { test("Boolean literal", (t) => { const schema = S.schema(true); - t.deepEqual(S.parseOrThrow(true, schema), true); + t.deepEqual(S.parser(schema)(true), true); expectType>(true); }); @@ -1756,7 +1830,7 @@ test("Boolean literal", (t) => { test("Number literal", (t) => { const schema = S.schema(123); - t.deepEqual(S.parseOrThrow(123, schema), 123); + t.deepEqual(S.parser(schema)(123), 123); expectType>(true); }); @@ -1764,7 +1838,7 @@ test("Number literal", (t) => { test("Undefined literal", (t) => { const schema = S.schema(undefined); - t.deepEqual(S.parseOrThrow(undefined, schema), undefined); + t.deepEqual(S.parser(schema)(undefined), undefined); expectType>(true); }); @@ -1772,7 +1846,7 @@ test("Undefined literal", (t) => { test("Null literal", (t) => { const schema = S.schema(null); - t.deepEqual(S.parseOrThrow(null, schema), null); + t.deepEqual(S.parser(schema)(null), null); expectType>(true); }); @@ -1781,7 +1855,7 @@ test("Symbol literal", (t) => { let symbol = Symbol(); const schema = S.schema(symbol); - t.deepEqual(S.parseOrThrow(symbol, schema), symbol); + t.deepEqual(S.parser(schema)(symbol), symbol); expectType>(true); }); @@ -1789,7 +1863,7 @@ test("Symbol literal", (t) => { test("BigInt literal", (t) => { const schema = S.schema(123n); - t.deepEqual(S.parseOrThrow(123n, schema), 123n); + t.deepEqual(S.parser(schema)(123n), 123n); expectType>(true); }); @@ -1797,7 +1871,7 @@ test("BigInt literal", (t) => { test("NaN literal", (t) => { const schema = S.schema(NaN); - t.deepEqual(S.parseOrThrow(NaN, schema), NaN); + t.deepEqual(S.parser(schema)(NaN), NaN); expectType>(true); }); @@ -1805,10 +1879,7 @@ test("NaN literal", (t) => { test("Tuple literal", (t) => { const cliArgsSchema = S.schema(["help", "lint"] as const); - t.deepEqual(S.parseOrThrow(["help", "lint"], cliArgsSchema), [ - "help", - "lint", - ]); + t.deepEqual(S.parser(cliArgsSchema)(["help", "lint"]), ["help", "lint"]); expectType< TypeEqual< @@ -1819,7 +1890,7 @@ test("Tuple literal", (t) => { }); test("Correctly infers type", (t) => { - const schema = S.string.with(S.transform, Number); + const schema = S.string.with(S.to, S.number, Number); expectType>(true); expectType, string>>(true); expectType, number>>(true); @@ -1829,7 +1900,7 @@ test("Correctly infers type", (t) => { test("Successfully parses undefined using the default value", (t) => { const schema = S.string.with(S.optional, "foo"); - const value = S.parseOrThrow(undefined, schema); + const value = S.parser(schema)(undefined); t.deepEqual(value, "foo"); t.deepEqual(schema.default, "foo"); @@ -1843,7 +1914,7 @@ test("Successfully parses undefined using the default value for transformed sche // const schema = S.boolean.with(S.optional, false).with(S.to, S.string); const schema = S.boolean.with(S.to, S.string).with(S.optional, "false"); - const value = S.parseOrThrow(undefined, schema); + const value = S.parser(schema)(undefined); t.deepEqual(value, "false"); t.deepEqual(schema.default, false); @@ -1855,15 +1926,17 @@ test("Successfully parses undefined using the default value for transformed sche test("Successfully parses undefined using the default value from callback", (t) => { const schema = S.string.with(S.optional, () => "foo"); - const value = S.parseOrThrow(undefined, schema); + const value = S.parser(schema)(undefined); t.deepEqual(value, "foo"); t.deepEqual( schema.default, undefined, - "Currently doesn't work with callback default" + "Currently doesn't work with callback default", ); + //FIXME: This is broken + // @ts-expect-error expectType>(true); }); @@ -1893,7 +1966,7 @@ test("Creates schema with description and title", (t) => { t.deepEqual(undocumentedStringSchema.description, undefined); t.deepEqual( documentedStringSchema.description, - "A useful bit of text, if you know what to do with it." + "A useful bit of text, if you know what to do with it.", ); t.deepEqual(undocumentedStringSchema.title, undefined); t.deepEqual(documentedStringSchema.title, "My schema"); @@ -1926,9 +1999,9 @@ test("Creates schema with deprecation", (t) => { }); test("Tuple with single element", (t) => { - const schema = S.schema([S.string.with(S.transform, (s) => Number(s))]); + const schema = S.schema([S.string.with(S.to, S.number, (s) => Number(s))]); - t.deepEqual(S.parseOrThrow(["123"], schema), [123]); + t.deepEqual(S.parser(schema)(["123"]), [123]); expectType>(true); }); @@ -1936,7 +2009,7 @@ test("Tuple with single element", (t) => { test("Tuple with multiple elements", (t) => { const schema = S.schema([S.string, S.number, true]); - t.deepEqual(S.parseOrThrow(["123", 123, true], schema), ["123", 123, true]); + t.deepEqual(S.parser(schema)(["123", 123, true]), ["123", 123, true]); expectType>(true); }); @@ -1958,7 +2031,7 @@ test("Tuple types", (t) => { [ { foo: string; - } + }, ] > >(true); @@ -1971,7 +2044,7 @@ test("Tuple types", (t) => { const tuple2LiteralAndSchema = S.schema(["foo", S.boolean]); expectType>( - true + true, ); const tuple2LiteralAsCosntAndSchema = S.schema(["foo" as const, S.boolean]); @@ -2037,50 +2110,85 @@ test("Env schema: Reggression version", (t) => { }; t.deepEqual( - S.compile(env(S.boolean), "Input", "Output", "Sync", true).toString(), - `i=>{if(typeof i==="string"){if(i==="t"){i=true}else if(i==="1"){i=true}else if(i==="f"){i=false}else if(i==="0"){i=false}else{try{let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}catch(e4){e[1](i,e4)}}}else{e[2](i)}return i}` + S.parser(env(S.boolean)).toString(), + `i=>{if(typeof i==="string"){if(i==="t"){i=true}else if(i==="1"){i=true}else if(i==="f"){i=false}else if(i==="0"){i=false}else{try{let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}catch(e4){e[1](i,e4)}}}else{e[2](i)}return i}`, ); - t.deepEqual(S.parseOrThrow("t", env(S.boolean)), true); - t.deepEqual(S.parseOrThrow("true", env(S.boolean)), true); + t.deepEqual(S.parser(env(S.boolean))("t"), true); + t.deepEqual(S.parser(env(S.boolean))("true"), true); }); -test("Unnest schema", (t) => { - const schema = S.unnest( - S.schema({ - id: S.string, - name: S.nullable(S.string), - deleted: S.boolean, - }) +test("CompactColumns schema", (t) => { + const schema = S.to( + S.compactColumns(S.unknown), + S.array( + S.schema({ + id: S.string, + name: S.nullable(S.string), + deleted: S.boolean, + }) + ) ); - const value = S.reverseConvertOrThrow( - [ - { id: "0", name: "Hello", deleted: false }, - { id: "1", name: undefined, deleted: true }, - ], - schema - ); + // Test parsing columnar data to row objects + // S.nullable converts null to undefined on parsing + const parse = S.parser(schema); + const parsed = parse([ + ["0", "1"], + ["Hello", null], + [false, true], + ] as unknown[][]); + t.deepEqual(parsed, [ + { id: "0", name: "Hello", deleted: false }, + { id: "1", name: undefined, deleted: true }, + ]); - let expected: typeof value = [ + // Test encoding row objects back to columnar data + // S.nullable converts undefined back to null on encoding + const encode = S.encoder(schema); + const encoded = encode([ + { id: "0", name: "Hello", deleted: false }, + { id: "1", name: undefined, deleted: true }, + ] as any); + t.deepEqual(encoded, [ ["0", "1"], ["Hello", null], [false, true], - ]; + ]); +}); - t.deepEqual(value, expected); +test("CompactColumns with json and bigint", (t) => { + const schema = S.to( + S.compactColumns(S.json), + S.array( + S.schema({ + id: S.string, + amount: S.bigint, + }) + ) + ); - expectType< - SchemaEqual< - typeof schema, - { - id: string; - name?: string | undefined; - deleted: boolean; - }[], - (string[] | boolean[] | (string | null)[])[] - > - >(true); + // Test parsing - json strings are converted to bigint via BigInt() + const parse = S.parser(schema); + const parsed = parse([ + ["0", "1"], + ["12345678901234567890", "98765432109876543210"], + ]); + t.deepEqual(parsed, [ + { id: "0", amount: 12345678901234567890n }, + { id: "1", amount: 98765432109876543210n }, + ]); + + // Test encoding - bigint values are converted back to strings for json + const encode = S.encoder(schema); + const encoded = encode([ + { id: "0", amount: 12345678901234567890n }, + { id: "1", amount: 98765432109876543210n }, + ]); + t.deepEqual(encoded, [ + ["0", "1"], + ["12345678901234567890", "98765432109876543210"], + ]); }); test("Set schema", (t) => { @@ -2092,27 +2200,34 @@ test("Set schema", (t) => { t.is(schema.class, Set); } - const parser = S.compile(schema, "Any", "Output", "Sync", true); + const parser = S.parser(schema); expectType Set>>(true); - t.is(parser.toString(), "i=>{if(!(i instanceof e[0])){e[1](i)}return i}"); + t.is(parser.toString(), "i=>{i instanceof e[0]||e[1](i);return i}"); const data = new Set(["foo", "bar"]); t.is(parser(data), data); t.throws(() => parser(123), { name: "SuryError", - message: "Failed parsing: Expected Set, received 123", + message: "Expected Set, received 123", }); }); test("Full Set schema", (t) => { const mySet = (itemSchema: S.Schema): S.Schema> => - S.instance(Set) - .with(S.transform, (input) => { + S.instance(Set) + .with(S.to, S.instance(Set), (input) => { const output = new Set(); - input.forEach((item) => { - output.add(S.parseOrThrow(item, itemSchema)); + input.forEach((item, index) => { + try { + output.add(S.parser(itemSchema)(item)); + } catch (e) { + if (e instanceof S.Error) { + throw new Error(`At item ${index} - ${e.reason}`); + } + throw e; + } }); return output; }) @@ -2125,17 +2240,17 @@ test("Full Set schema", (t) => { expectType, unknown>>(true); t.deepEqual( - S.parseOrThrow(new Set([1, 2, 3]), numberSetSchema), - new Set([1, 2, 3]) + S.parser(numberSetSchema)(new Set([1, 2, 3])), + new Set([1, 2, 3]), ); - t.throws(() => S.parseOrThrow([1, 2, "3"], numberSetSchema), { + t.throws(() => S.parser(numberSetSchema)([1, 2, "3"]), { name: "SuryError", - message: `Failed parsing: Expected Set, received [1, 2, "3"]`, + message: `Expected Set, received [1, 2, "3"]`, }); - t.throws(() => S.parseOrThrow(new Set([1, 2, "3"]), numberSetSchema), { + t.throws(() => S.parser(numberSetSchema)(new Set([1, 2, "3"])), { name: "SuryError", - message: `Failed parsing: Expected number, received "3"`, + message: `At item 3 - Expected number, received "3"`, }); }); @@ -2147,16 +2262,16 @@ test("Coerce string to number", (t) => { expectType>(true); expectType | undefined>>(true); - t.deepEqual(S.parseOrThrow("123", schema), 123); - t.deepEqual(S.parseOrThrow("123.4", schema), 123.4); - t.deepEqual(S.reverseConvertOrThrow(123, schema), "123"); + t.deepEqual(S.parser(schema)("123"), 123); + t.deepEqual(S.parser(schema)("123.4"), 123.4); + t.deepEqual(S.encoder(schema)(123), "123"); }); test("Shape string to object", (t) => { const schema = S.shape(S.string, (string) => ({ foo: string })); - t.deepEqual(S.parseOrThrow("bar", schema), { foo: "bar" }); - t.deepEqual(S.reverseConvertOrThrow({ foo: "bar" }, schema), "bar"); + t.deepEqual(S.parser(schema)("bar"), { foo: "bar" }); + t.deepEqual(S.encoder(schema)({ foo: "bar" }), "bar"); }); test("Tuple with transform to object", (t) => { @@ -2168,7 +2283,7 @@ test("Tuple with transform to object", (t) => { }; }); - t.deepEqual(S.parseOrThrow(["point", 1, -4], pointSchema), { x: 1, y: -4 }); + t.deepEqual(S.parser(pointSchema)(["point", 1, -4]), { x: 1, y: -4 }); expectType< SchemaEqual< @@ -2187,12 +2302,12 @@ test("Assert throws with invalid data", (t) => { t.throws( () => { - S.assertOrThrow(123, schema); + S.assert(schema, 123); }, { name: "SuryError", - message: "Failed asserting: Expected string, received 123", - } + message: "Expected string, received 123", + }, ); }); @@ -2201,7 +2316,7 @@ test("Assert passes with valid data", (t) => { const data: unknown = "abc"; expectType>(true); - S.assertOrThrow(data, schema); + S.assert(schema, data); expectType>(true); t.pass(); }); @@ -2214,8 +2329,8 @@ test("Schema of object with empty prototype", (t) => { const data = { foo: "bar", }; - t.deepEqual(S.parseOrThrow(data, schema), data); - t.deepEqual(S.reverseConvertOrThrow(data, schema), data); + t.deepEqual(S.parser(schema)(data), data); + t.deepEqual(S.encoder(schema)(data), data); }); test("Successfully parses recursive object", (t) => { @@ -2228,29 +2343,26 @@ test("Successfully parses recursive object", (t) => { S.schema({ id: S.string, children: S.array(nodeSchema), - }) + }), ); expectType>(true); t.deepEqual( - S.parseOrThrow( - { - id: "1", - children: [ - { id: "2", children: [] }, - { id: "3", children: [{ id: "4", children: [] }] }, - ], - }, - nodeSchema - ), + S.parser(nodeSchema)({ + id: "1", + children: [ + { id: "2", children: [] }, + { id: "3", children: [{ id: "4", children: [] }] }, + ], + }), { id: "1", children: [ { id: "2", children: [] }, { id: "3", children: [{ id: "4", children: [] }] }, ], - } + }, ); }); @@ -2279,42 +2391,40 @@ test("Mutually recursive objects", (t) => { })); const userSchema = S.recursive("User", (userSchema) => - makeUserSchema(S.recursive("Post", (_) => makePostSchema(userSchema))) + makeUserSchema( + S.recursive("Post", (_) => makePostSchema(userSchema)), + ), ); const postSchema = S.recursive("Post", (postSchema) => - makePostSchema(S.recursive("User", (_) => makeUserSchema(postSchema))) + makePostSchema( + S.recursive("User", (_) => makeUserSchema(postSchema)), + ), ); expectType>(true); expectType>(true); t.deepEqual( - S.parseOrThrow( - { - email: "test@test.com", - posts: [ - { Title: "Hello", Author: { email: "test@test.com", posts: [] } }, - ], - }, - userSchema - ), + S.parser(userSchema)({ + email: "test@test.com", + posts: [ + { Title: "Hello", Author: { email: "test@test.com", posts: [] } }, + ], + }), { email: "test@test.com", posts: [ { title: "Hello", author: { email: "test@test.com", posts: [] } }, ], - } + }, ); t.deepEqual( - S.parseOrThrow( - { - Title: "Hello", - Author: { email: "test@test.com", posts: [] }, - }, - postSchema - ), - { title: "Hello", author: { email: "test@test.com", posts: [] } } + S.parser(postSchema)({ + Title: "Hello", + Author: { email: "test@test.com", posts: [] }, + }), + { title: "Hello", author: { email: "test@test.com", posts: [] } }, ); }); @@ -2331,29 +2441,26 @@ test("Recursive object with S.shape", (t) => { }).with(S.shape, (input) => ({ id: input.ID, children: input.CHILDREN, - })) + })), ); expectType>(true); t.deepEqual( - S.parseOrThrow( - { - ID: "1", - CHILDREN: [ - { ID: "2", CHILDREN: [] }, - { ID: "3", CHILDREN: [{ ID: "4", CHILDREN: [] }] }, - ], - }, - nodeSchema - ), + S.parser(nodeSchema)({ + ID: "1", + CHILDREN: [ + { ID: "2", CHILDREN: [] }, + { ID: "3", CHILDREN: [{ ID: "4", CHILDREN: [] }] }, + ], + }), { id: "1", children: [ { id: "2", children: [] }, { id: "3", children: [{ id: "4", children: [] }] }, ], - } + }, ); }); @@ -2363,21 +2470,22 @@ test("Recursive with self as transform target", (t) => { t.throws( () => { let nodeSchema = S.recursive("Node", (self) => - S.string.with(S.to, S.array(self)) + S.string.with(S.to, S.array(self)), ); expectType>(true); - t.deepEqual(S.parseOrThrow(`["[]","[]"]`, nodeSchema), [[], []]); + t.deepEqual(S.parser(nodeSchema)(`["[]","[]"]`), [[], []]); }, { - message: - "Failed parsing: Unsupported transformation from string to Node[]", - } + message: "Can't decode Node to Node[]. Use S.to to define a custom decoder", + }, ); }); test("Port schema", (t) => { - const portSchema = S.int32.with(S.port); + S.enablePort(); + + const portSchema = S.port; if (portSchema.type === "number") { t.deepEqual(portSchema.format, "port"); } else { @@ -2386,61 +2494,63 @@ test("Port schema", (t) => { expectType>(true); - const portSchemaFromNumber = S.number.with(S.port); - if (portSchemaFromNumber.type === "number") { - t.deepEqual(portSchemaFromNumber.format, "port"); + t.throws( + () => { + S.parser(portSchema)(10.2); + }, + { + name: "SuryError", + message: "Expected port, received 10.2", + }, + "Should prevent non-integer numbers", + ); + + const portCoercedFromString = S.string.with(S.to, S.port); + expectType>(true); + if (portCoercedFromString.type === "string") { + t.deepEqual( + portCoercedFromString.format, + undefined, + "Shouldn't add port format to the string input type", + ); + } else { + t.fail("portCoercedFromString should be a string"); + } + + if (S.reverse(portCoercedFromString).type === "number") { + t.deepEqual(S.parser(portCoercedFromString)("10"), 10); t.throws( () => { - S.parseOrThrow(10.2, portSchemaFromNumber); + S.parser(portCoercedFromString)(10.2); }, { name: "SuryError", - message: "Failed parsing: Expected port, received 10.2", + message: "Expected string, received 10.2", }, - "Should prevent non-integer numbers" + "Should prevent non-string values", ); - } else { - t.fail("portSchemaFromNumber should be a number"); - } - - const portCoercedFromString = S.string.with(S.to, S.number).with(S.port); - expectType>(true); - - t.deepEqual( - portCoercedFromString.type, - "string", - "Schema metadata should be of the input type" - ); - // FIXME: - // t.deepEqual( - // (portCoercedFromString as any).format, - // "port", - // "Shouldn't add port format to the string input type" - // ); - - if (S.reverse(portCoercedFromString).type === "number") { - t.deepEqual(S.parseOrThrow("10", portCoercedFromString), 10); t.throws( () => { - S.parseOrThrow(10.2, portCoercedFromString); + S.parser(portCoercedFromString)("10.2"); }, { name: "SuryError", - message: "Failed parsing: Expected port, received 10.2", + message: "Expected port, received 10.2", }, - "Should prevent non-integer numbers" + "Should prevent non-integer numbers", ); - t.deepEqual(S.reverseConvertOrThrow(10, portCoercedFromString), "10"); + t.deepEqual(S.encoder(portCoercedFromString)(10), "10"); } else { t.fail("portCoercedFromString should be a number"); } }); test("Example", (t) => { + S.enableEmail(); // Create login schema with email and password const loginSchema = S.schema({ - email: S.string.with(S.email), + email: S.email, password: S.string.with(S.min, 8), }); @@ -2449,20 +2559,17 @@ test("Example", (t) => { t.throws( () => { - // Throws the S.Error(`Failed parsing at ["email"]: Invalid email address`) - S.parseOrThrow({ email: "", password: "" }, loginSchema); + // Throws the S.Error(`Failed at ["email"]: Expected email, received ""`) + S.parser(loginSchema)({ email: "", password: "" }); }, - { message: `Failed parsing at ["email"]: Invalid email address` } + { message: `Failed at ["email"]: Expected email, received ""` }, ); // Returns data as { email: string; password: string } - const result = S.parseOrThrow( - { - email: "jane@example.com", - password: "12345678", - }, - loginSchema - ); + const result = S.parser(loginSchema)({ + email: "jane@example.com", + password: "12345678", + }); t.deepEqual(result, { email: "jane@example.com", @@ -2479,18 +2586,86 @@ test("Example", (t) => { expectType>(true); }); -test("parseJsonOrThrow", async (t) => { +test("Decode from json", async (t) => { + t.deepEqual(S.decoder(S.json, S.array(S.bigint))(["123"]), [123n]); + t.deepEqual(S.decoder(S.array(S.bigint), S.json)([123n]), ["123"]); + const schema = S.string.with(S.nullable); - t.deepEqual(S.parseJsonOrThrow("hello", schema), "hello"); - t.deepEqual(S.parseJsonOrThrow(null, schema), undefined); + t.deepEqual(S.decoder(S.json, schema)("hello"), "hello"); + t.deepEqual(S.decoder(S.json, schema)(null), undefined); + + // Date fields should be encoded to ISO string when decoding to JSON + const dateSchema = S.schema({ field: S.date }); + const dateToJson = S.decoder(dateSchema, S.json); + t.deepEqual(dateToJson({ field: new Date("2024-01-01T00:00:00.000Z") }), { + field: "2024-01-01T00:00:00.000Z", + }); + t.deepEqual( + dateToJson.toString(), + `i=>{return {"field":i["field"].toISOString(),}}`, + ); + + // Date fields should work through the full jsonString pipeline + const dateToJsonString = S.decoder(dateSchema, S.jsonString); + t.deepEqual( + dateToJsonString({ field: new Date("2024-01-01T00:00:00.000Z") }), + `{"field":"2024-01-01T00:00:00.000Z"}`, + ); + + // JSON to Date: decode ISO string from JSON back to Date + const jsonToDate = S.decoder(S.json, dateSchema); + t.deepEqual(jsonToDate({ field: "2024-01-01T00:00:00.000Z" }), { + field: new Date("2024-01-01T00:00:00.000Z"), + }); + t.deepEqual( + jsonToDate.toString(), + `i=>{typeof i==="object"&&i&&!Array.isArray(i)||e[2](i);let v1=i["field"];typeof v1==="string"||e[1](v1);let v0=new Date(i["field"]);!Number.isNaN(v0.getTime())||e[0](v0);return {"field":v0,}}`, + ); + + // JSON string to Date: full round-trip through jsonString + const jsonStringToDate = S.decoder(S.jsonString, dateSchema); + t.deepEqual( + jsonStringToDate(`{"field":"2024-01-01T00:00:00.000Z"}`), + { field: new Date("2024-01-01T00:00:00.000Z") }, + ); }); -test("parseJsonStringOrThrow", async (t) => { +test("Decode from json string", async (t) => { const schema = S.nullable(S.string); - t.deepEqual(S.parseJsonStringOrThrow(`"hello"`, schema), "hello"); - t.deepEqual(S.parseJsonStringOrThrow("null", schema), undefined); + t.deepEqual(S.decoder(S.jsonString, schema)(`"hello"`), "hello"); + t.deepEqual(S.decoder(S.jsonString, schema)("null"), undefined); +}); + +test("Decode from json string, convert to number", async (t) => { + const fn = S.decoder(S.jsonString, S.string, S.number); + + expectType number>>(true); + + t.deepEqual(fn(`"123"`), 123); +}); + +test("Decode from json string to array of bigints", async (t) => { + const fn = S.decoder(S.jsonString, S.array(S.bigint)); + + expectType bigint[]>>(true); + + t.deepEqual(fn(`["123"]`), [123n]); +}); + +test("Parse to literal with no validation to emulate assert", async (t) => { + const fn = S.parser( + S.schema({ foo: S.string }), + S.schema(true).with(S.noValidation, true), + ); + + expectType true>>(true); + t.deepEqual(fn({ foo: "bar" }), true); + t.deepEqual( + fn.toString(), + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);return true}`, + ); }); test("ArkType pattern matching", async (t) => { @@ -2502,14 +2677,14 @@ test("ArkType pattern matching", async (t) => { S.boolean, null, S.record(self), - ]) + ]), ); - t.deepEqual(S.parseOrThrow(`foo`, schema), "foo"); - t.deepEqual(S.parseOrThrow(5n, schema), "5"); - t.deepEqual(S.parseOrThrow({ nested: 5n }, schema), { nested: "5" }); - t.deepEqual(S.reverseConvertOrThrow("5", schema), 5n); - t.deepEqual(S.reverseConvertOrThrow("foo", schema), "foo"); + t.deepEqual(S.parser(schema)(`foo`), "foo"); + t.deepEqual(S.parser(schema)(5n), "5"); + t.deepEqual(S.parser(schema)({ nested: 5n }), { nested: "5" }); + t.deepEqual(S.encoder(schema)("5"), 5n); + t.deepEqual(S.encoder(schema)("foo"), "foo"); }); test("Example of transformed schema", (t) => { @@ -2561,7 +2736,6 @@ test("Example of transformed schema", (t) => { // 4. Or via JSON Schema t.deepEqual(S.toJSONSchema(userSchema), { type: "object", - additionalProperties: true, properties: { USER_ID: { type: "string", @@ -2582,19 +2756,19 @@ test("Example of transformed schema", (t) => { const fromJsonSchema = S.fromJSONSchema(S.toJSONSchema(userSchema)); t.deepEqual( - S.parseOrThrow({ USER_ID: "0", USER_NAME: "Dmitry" }, fromJsonSchema), + S.parser(fromJsonSchema)({ USER_ID: "0", USER_NAME: "Dmitry" }), { USER_ID: "0", USER_NAME: "Dmitry", }, - "Parsing works, but doesn't keep transformations" + "Parsing works, but doesn't keep transformations", ); if (fromJsonSchema.type === "object") { t.is(fromJsonSchema.additionalItems, "strip"); - t.deepEqual( - fromJsonSchema.items.map((i) => i.location), - ["USER_ID", "USER_NAME"] - ); + t.deepEqual(Object.keys(fromJsonSchema.properties), [ + "USER_ID", + "USER_NAME", + ]); } else { t.fail("fromJsonSchema should be an object"); } @@ -2604,7 +2778,7 @@ test("Brand", (t) => { const schema = S.string.with(S.brand, "Foo"); type Foo = S.Infer; expectType, string>>(true); - const result = S.parseOrThrow("hello", schema); + const result = S.parser(schema)("hello"); expectType>(result); t.deepEqual(result, "hello"); t.deepEqual(schema.name, "Foo", "Should also set the brand id as the name"); @@ -2619,8 +2793,9 @@ test("fromJSONSchema", (t) => { format: "email", }); expectType>(true); - const result = S.safe(() => S.assertOrThrow("example.com", emailSchema)); - t.is(result.error?.message, "Failed asserting: Invalid email address"); + const result = S.safe(() => S.assert(emailSchema, "example.com")); + + t.is(result.error?.message, `Expected email, received "example.com"`); }); test("Compile types", async (t) => { @@ -2629,66 +2804,67 @@ test("Compile types", async (t) => { S.schema(null).with(S.to, S.schema(undefined)), ]); - const fn1 = S.compile(schema, "Input", "Output", "Sync"); + const fn1 = S.decoder(schema); expectType< TypeEqual string | undefined> >(true); t.deepEqual(fn1("hello"), "hello"); t.deepEqual(fn1(null), undefined); - const fn2 = S.compile(schema, "Output", "Input", "Sync", false); + const fn2 = S.encoder(schema); expectType< TypeEqual string | null> >(true); t.deepEqual(fn2("hello"), "hello"); t.deepEqual(fn2(undefined), null); - const fn3 = S.compile(schema, "Any", "Output", "Sync", true); + const fn3 = S.parser(schema); expectType string | undefined>>( - true + true, ); t.deepEqual(fn3("hello"), "hello"); t.deepEqual(fn3(null), undefined); - const fn4 = S.compile(schema, "Json", "Output", "Sync"); + const fn4 = S.decoder(S.json, schema); expectType string | undefined>>( - true + true, ); t.deepEqual(fn4("hello"), "hello"); t.deepEqual(fn4(null), undefined); - const fn5 = S.compile(schema, "JsonString", "Output", "Sync"); + const fn5 = S.decoder(S.jsonString, schema); expectType string | undefined>>( - true + true, ); t.deepEqual(fn5(`"hello"`), "hello"); t.deepEqual(fn5("null"), undefined); - const fn6 = S.compile(schema, "Output", "Json", "Sync"); + const fn6 = S.encoder(schema, S.json); expectType S.JSON>>( - true + true, ); t.deepEqual(fn6("hello"), "hello"); t.deepEqual(fn6(undefined), null); - const fn7 = S.compile(schema, "Output", "JsonString", "Sync"); + const fn7 = S.encoder(schema, S.jsonString); expectType string>>( - true + true, ); t.deepEqual(fn7("hello"), `"hello"`); t.deepEqual(fn7(undefined), "null"); - const fn8 = S.compile(schema, "Output", "Assert", "Sync", true); - expectType void>>(true); - t.deepEqual(fn8("hello"), undefined); - t.deepEqual(fn8(undefined), undefined); + // FIXME: + // const fn8 = S.compile(schema, "Output", "Assert", "Sync", true); + // expectType void>>(true); + // t.deepEqual(fn8("hello"), undefined); + // t.deepEqual(fn8(undefined), undefined); - const fn9 = S.compile(schema, "Output", "JsonString", "Async"); - expectType< - TypeEqual Promise> - >(true); - t.deepEqual(await fn9("hello"), `"hello"`); - t.deepEqual(await fn9(undefined), "null"); + // const fn9 = S.compile(schema, "Output", "JsonString", "Async"); + // expectType< + // TypeEqual Promise> + // >(true); + // t.deepEqual(await fn9("hello"), `"hello"`); + // t.deepEqual(await fn9(undefined), "null"); t.pass(); }); @@ -2696,10 +2872,11 @@ test("Compile types", async (t) => { test("Preprocess nested fields", (t) => { const stripPrefix = ( schema: S.Schema, - prefix: string + prefix: string, ): S.Schema => - S.transform( + S.to( schema, + S.string, (v) => { if (v.startsWith(prefix)) { return v.slice(1); @@ -2707,8 +2884,8 @@ test("Preprocess nested fields", (t) => { throw new Error(`String must start with ${prefix}`); } }, - (v) => prefix + v - ).with(S.to, S.string); + (v) => prefix + v, + ); const schema = S.schema({ nested: { @@ -2717,7 +2894,14 @@ test("Preprocess nested fields", (t) => { }, }).with(S.shape, (_) => undefined); - const value = S.reverseConvertOrThrow(undefined, schema); + const fn = S.encoder(schema); + + t.deepEqual( + fn.toString(), + `i=>{i===void 0||e[4](i);let v0;try{v0=e[0]("foo")}catch(x){e[1](x)}let v1;try{v1=e[2]("1")}catch(x){e[3](x)}return {"nested":{"tag":v0,"numberTag":v1,},}}`, + ); + + const value = fn(undefined); t.deepEqual(value, { nested: { numberTag: "~1", @@ -2736,36 +2920,36 @@ test("Union of object keys", (t) => { const schema = S.union(Object.keys(allCurrencies)); expectType>(true); - t.deepEqual(S.parseOrThrow("USD", schema), "USD"); - t.throws(() => S.parseOrThrow("GBP", schema), { + t.deepEqual(S.parser(schema)("USD"), "USD"); + t.throws(() => S.parser(schema)("GBP"), { name: "SuryError", - message: `Failed parsing: Expected "USD" | "BGP" | "EUR", received "GBP"`, + message: `Expected "USD" | "BGP" | "EUR", received "GBP"`, }); const schema2 = S.union( - Object.keys(allCurrencies) as (keyof typeof allCurrencies)[] + Object.keys(allCurrencies) as (keyof typeof allCurrencies)[], ); expectType< SchemaEqual >(true); - t.deepEqual(S.parseOrThrow("USD", schema), "USD"); - t.throws(() => S.parseOrThrow("GBP", schema), { + t.deepEqual(S.parser(schema)("USD"), "USD"); + t.throws(() => S.parser(schema)("GBP"), { name: "SuryError", - message: `Failed parsing: Expected "USD" | "BGP" | "EUR", received "GBP"`, + message: `Expected "USD" | "BGP" | "EUR", received "GBP"`, }); const schema3 = S.union( (Object.keys(allCurrencies) as (keyof typeof allCurrencies)[]).map( - (literal) => S.schema(literal) - ) + (literal) => S.schema(literal), + ), ); expectType< SchemaEqual >(true); - t.deepEqual(S.parseOrThrow("USD", schema), "USD"); - t.throws(() => S.parseOrThrow("GBP", schema), { + t.deepEqual(S.parser(schema)("USD"), "USD"); + t.throws(() => S.parser(schema)("GBP"), { name: "SuryError", - message: `Failed parsing: Expected "USD" | "BGP" | "EUR", received "GBP"`, + message: `Expected "USD" | "BGP" | "EUR", received "GBP"`, }); }); @@ -2775,12 +2959,12 @@ test("Union of dynamic enum as const", (t) => { const schema = S.union(test); expectType>( - true + true, ); - t.deepEqual(S.parseOrThrow("a", schema), "a"); - t.throws(() => S.parseOrThrow("d", schema), { + t.deepEqual(S.parser(schema)("a"), "a"); + t.throws(() => S.parser(schema)("d"), { name: "SuryError", - message: `Failed parsing: Expected "a" | "b" | "c", received "d"`, + message: `Expected "a" | "b" | "c", received "d"`, }); }); @@ -2788,20 +2972,51 @@ test("Overwrite error message", (t) => { const schema = S.string.with(S.min, 3, "Invalid string"); const fieldSchema = (schema: S.Schema): S.Schema => { - return S.unknown.with(S.transform, (v) => { + return S.any.with(S.to, schema, (v) => { try { - return S.parseOrThrow(v, schema); + S.assert(schema, v); + return v; } catch (e) { if (e instanceof S.Error) { throw new Error(e.reason); } throw e; } - }) satisfies S.Schema as S.Schema; + }); }; - t.throws(() => S.parseOrThrow("hi", fieldSchema(schema)), { - name: "Error", - message: "Invalid string", - }); + // Doesn't work starting from 11.0.0-alpha.4 + // The error is always wrapped in SuryError + t.throws( + () => S.parser(S.schema({ foo: fieldSchema(schema) }))({ foo: "hi" }), + { + name: "SuryError", + message: `Failed at ["foo"]: Invalid string`, + }, + ); +}); + +test("Uint8Array", (t) => { + S.enableUint8Array(); + + let data = new Uint8Array([1, 2, 3]); + + t.deepEqual(S.parser(S.uint8Array)(data), data); + t.deepEqual( + S.parser(S.uint8Array).toString(), + `i=>{i instanceof e[0]||e[1](i);return i}`, + ); + + t.deepEqual( + S.decoder(S.string, S.uint8Array, S.jsonString)("data"), + `"data"`, + ); + t.deepEqual( + S.decoder(S.string, S.uint8Array, S.jsonString).toString(), + `i=>{return JSON.stringify(e[1].decode(e[0].encode(i)))}`, + ); + t.deepEqual( + S.decoder(S.unknown, S.uint8Array, S.jsonString).toString(), + `i=>{i instanceof e[1]||e[2](i);return JSON.stringify(e[0].decode(i))}`, + ); }); diff --git a/packages/sury/tests/S_toExpression_test.res b/packages/sury/tests/S_toExpression_test.res index 9b426df5d..b5ccdf7b7 100644 --- a/packages/sury/tests/S_toExpression_test.res +++ b/packages/sury/tests/S_toExpression_test.res @@ -24,28 +24,39 @@ test("Expression of Array schema", t => { t->Assert.deepEqual(S.array(S.string)->S.toExpression, "string[]") }) -test("Expression of Unnest schema", t => { +test("Expression of compactColumns schema without S.to", t => { + t->Assert.deepEqual(S.compactColumns(S.unknown)->S.toExpression, "unknown[][]") + t->Assert.deepEqual(S.compactColumns(S.string)->S.toExpression, "string[][]") + t->Assert.deepEqual(S.compactColumns(S.int)->S.toExpression, "int32[][]") +}) + +test("Expression of compactColumns schema", t => { t->Assert.deepEqual( - S.unnest( + S.compactColumns(S.unknown) + ->S.to( S.schema(s => { "foo": s.matches(S.string), "bar": s.matches(S.int), } ), - )->S.toExpression, + ) + ->S.toExpression, "[string[], int32[]]", ) }) -test("Expression of reversed Unnest schema", t => { +test("Expression of reversed compactColumns schema", t => { t->Assert.deepEqual( - S.unnest( - S.schema(s => - { - "foo": s.matches(S.string), - "bar": s.matches(S.int), - } + S.compactColumns(S.unknown) + ->S.to( + S.array( + S.schema(s => + { + "foo": s.matches(S.string), + "bar": s.matches(S.int), + } + ), ), ) ->S.reverse @@ -74,7 +85,7 @@ test("Expression of Option schema with name", t => { }) test("Expression of Null schema", t => { - t->Assert.deepEqual(S.null(S.string)->S.toExpression, "string | null") + t->Assert.deepEqual(S.nullAsOption(S.string)->S.toExpression, "string | null") }) test("Expression of Union schema", t => { @@ -123,10 +134,10 @@ test("Expression of renamed schema", t => { t->Assert.deepEqual(renamedSchema->S.toExpression, "Ethers.BigInt") // Uses new name when failing t->U.assertThrowsMessage( - () => "smth"->S.parseOrThrow(renamedSchema), - `Failed parsing: Expected Ethers.BigInt, received "smth"`, + () => "smth"->S.parseOrThrow(~to=renamedSchema), + `Expected Ethers.BigInt, received "smth"`, ) - let schema = S.null(S.never)->S.meta({name: "Ethers.BigInt"}) + let schema = S.nullAsOption(S.never)->S.meta({name: "Ethers.BigInt"}) t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, @@ -135,14 +146,14 @@ test("Expression of renamed schema", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{try{e[0](i);}catch(e0){if(i===void 0){i=null}}return i}`, + `i=>{try{e[0](i);}catch(e0){if(i===void 0){i=null}else{e[1](i,e0)}}return i}`, ) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`null`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) // TODO: Can be improved. No need to duplicate Expected/received error t->U.assertThrowsMessage( - () => %raw(`"smth"`)->S.parseOrThrow(schema->S.reverse), - `Failed parsing: Expected Ethers.BigInt, received "smth" -- Expected never, received "smth"`, + () => %raw(`"smth"`)->S.parseOrThrow(~to=schema->S.reverse), + `Expected Ethers.BigInt, received "smth" + - Expected never, received "smth"`, ) }) @@ -163,16 +174,16 @@ test("Expression of recursive schema", t => { t->Assert.deepEqual(renamedRoot->S.toExpression, `NodeRoot`) t->U.assertThrowsMessage( - () => %raw(`null`)->S.parseOrThrow(nodeSchema), - `Failed parsing: Expected Node, received null`, + () => %raw(`null`)->S.parseOrThrow(~to=nodeSchema), + `Expected Node, received null`, ) t->U.assertThrowsMessage( - () => %raw(`null`)->S.parseOrThrow(S.tuple1(nodeSchema)), - `Failed parsing: Expected [Node], received null`, + () => %raw(`null`)->S.parseOrThrow(~to=S.tuple1(nodeSchema)), + `Expected [Node], received null`, ) t->U.assertThrowsMessage( - () => %raw(`null`)->S.parseOrThrow(S.tuple1(renamedRoot)), - `Failed parsing: Expected [NodeRoot], received null`, + () => %raw(`null`)->S.parseOrThrow(~to=S.tuple1(renamedRoot)), + `Expected [NodeRoot], received null`, ) t->U.assertThrowsMessage( ~message=`It shouldn't rename node schema ref name`, @@ -180,8 +191,8 @@ test("Expression of recursive schema", t => { %raw(`{ Id: "0", Children: [null] - }`)->S.parseOrThrow(renamedRoot), - `Failed parsing at ["Children"]["0"]: Expected Node, received null`, + }`)->S.parseOrThrow(~to=renamedRoot), + `Failed at ["Children"]["0"]: Expected Node, received null`, ) }) @@ -198,11 +209,11 @@ test("Expression of deeply renamed recursive schema", t => { t->Assert.deepEqual(nodeSchema->S.toExpression, `MyNode`) t->U.assertThrowsMessage( - () => %raw(`null`)->S.parseOrThrow(nodeSchema), - `Failed parsing: Expected MyNode, received null`, + () => %raw(`null`)->S.parseOrThrow(~to=nodeSchema), + `Expected MyNode, received null`, ) t->U.assertThrowsMessage( - () => %raw(`{Id: "0"}`)->S.parseOrThrow(nodeSchema), - `Failed parsing: Expected MyNode, received { Id: "0"; }`, + () => %raw(`{Id: "0"}`)->S.parseOrThrow(~to=nodeSchema), + `Failed at ["Children"]: Expected MyNode[], received undefined`, ) }) diff --git a/packages/sury/tests/S_toJSONSchema_test.res b/packages/sury/tests/S_toJSONSchema_test.res index e561858d4..0f22d4a7f 100644 --- a/packages/sury/tests/S_toJSONSchema_test.res +++ b/packages/sury/tests/S_toJSONSchema_test.res @@ -11,42 +11,95 @@ test("JSONSchema of string schema", t => { }) test("JSONSchema of int schema", t => { - t->Assert.deepEqual(S.int->S.toJSONSchema, %raw(`{"type": "integer"}`)) + t->Assert.deepEqual(S.int->S.toJSONSchema, %raw(`{"type": "integer", "minimum": -2147483648, "maximum": 2147483647}`)) }) test("JSONSchema of float schema", t => { t->Assert.deepEqual(S.float->S.toJSONSchema, %raw(`{"type": "number"}`)) }) +test("JSONSchema of S.json transformed to object with bigint and array of optional items", t => { + let nonJsonableSchema = S.schema(s => + { + "id": s.matches(S.bigint), + "data": s.matches(S.unknown), + "items": s.matches(S.array(S.option(S.float->S.floatMax(1.)))), + } + ) + // TODO: Should coerce nonJsonableSchema to jsonable JSON Schema + t->Assert.deepEqual(S.json->S.to(nonJsonableSchema)->S.toJSONSchema, %raw(`{}`)) +}) + test("JSONSchema of email schema", t => { + S.enableEmail() t->Assert.deepEqual( - S.string->S.email->S.toJSONSchema, + S.email->S.toJSONSchema, %raw(`{"type": "string", "format": "email"}`), ) }) test("JSONSchema of url schema", t => { + S.enableUrl() t->Assert.deepEqual( - S.string->S.url->S.toJSONSchema, + S.url->S.toJSONSchema, %raw(`{"type": "string", "format": "uri"}`), ~message="The format should be uri for url schema", ) }) -test("JSONSchema of datetime schema", t => { +test("JSONSchema of S.string->S.to(S.date)", t => { + t->Assert.deepEqual( + S.string->S.to(S.date)->S.toJSONSchema, + %raw(`{"type": "string", "format": "date-time"}`), + ) +}) + +test("JSONSchema of S.string->S.to(S.date) with description", t => { + t->Assert.deepEqual( + S.string->S.to(S.date)->S.meta({description: "A date"})->S.toJSONSchema, + %raw(`{"type": "string", "format": "date-time", "description": "A date"}`), + ) +}) + +test("JSONSchema of S.string with description converted to S.date", t => { t->Assert.deepEqual( - S.string->S.datetime->S.toJSONSchema, + S.string->S.meta({description: "A date"})->S.to(S.date)->S.toJSONSchema, + %raw(`{"type": "string", "format": "date-time", "description": "A date"}`), + ) +}) + +test("JSONSchema of S.isoDateTime", t => { + S.enableIsoDateTime() + t->Assert.deepEqual( + S.isoDateTime->S.toJSONSchema, %raw(`{"type": "string", "format": "date-time"}`), ) }) +test("JSONSchema of object with transformed field preserves field metadata", t => { + t->Assert.deepEqual( + S.object(s => + s.field("birthDate", S.string->S.meta({description: "Birth date"})->S.to(S.date)) + )->S.toJSONSchema, + %raw(`{ + "type": "object", + "properties": { + "birthDate": {"type": "string", "format": "date-time", "description": "Birth date"} + }, + "required": ["birthDate"] + }`), + ) +}) + test("JSONSchema of cuid schema", t => { - t->Assert.deepEqual(S.string->S.cuid->S.toJSONSchema, %raw(`{"type": "string"}`)) + S.enableCuid() + t->Assert.deepEqual(S.cuid->S.toJSONSchema, %raw(`{"type": "string"}`)) }) test("JSONSchema of uuid schema", t => { + S.enableUuid() t->Assert.deepEqual( - S.string->S.uuid->S.toJSONSchema, + S.uuid->S.toJSONSchema, %raw(`{"type": "string", "format": "uuid"}`), ) }) @@ -54,14 +107,7 @@ test("JSONSchema of uuid schema", t => { test("JSONSchema of pattern schema", t => { t->Assert.deepEqual( S.string->S.pattern(/abc/g)->S.toJSONSchema, - %raw(`{"type": "string","pattern": "/abc/g"}`), - ) -}) - -test("JSONSchema of string schema uses the last refinement for format", t => { - t->Assert.deepEqual( - S.string->S.email->S.datetime->S.toJSONSchema, - %raw(`{"type": "string", "format": "date-time"}`), + %raw(`{"type": "string","pattern": "abc"}`), ) }) @@ -94,16 +140,17 @@ test("JSONSchema of string with both min and max", t => { }) test("JSONSchema of int with min", t => { - t->Assert.deepEqual(S.int->S.min(1)->S.toJSONSchema, %raw(`{"type": "integer", "minimum": 1}`)) + t->Assert.deepEqual(S.int->S.min(1)->S.toJSONSchema, %raw(`{"type": "integer", "minimum": 1, "maximum": 2147483647}`)) }) test("JSONSchema of int with max", t => { - t->Assert.deepEqual(S.int->S.max(1)->S.toJSONSchema, %raw(`{"type": "integer", "maximum": 1}`)) + t->Assert.deepEqual(S.int->S.max(1)->S.toJSONSchema, %raw(`{"type": "integer", "minimum": -2147483648, "maximum": 1}`)) }) test("JSONSchema of port", t => { + S.enablePort() t->Assert.deepEqual( - S.int->S.port->S.toJSONSchema, + S.port->S.toJSONSchema, %raw(`{ "type": "integer", "minimum": 0, @@ -128,7 +175,7 @@ test("JSONSchema of float with max", t => { test("JSONSchema of nullable float", t => { t->Assert.deepEqual( - S.null(S.float)->S.toJSONSchema, + S.nullAsOption(S.float)->S.toJSONSchema, %raw(`{"anyOf": [{"type": "number"}, {"type": "null"}]}`), ) }) @@ -157,7 +204,6 @@ test("JSONSchema of object literal", t => { S.literal({"received": true})->S.toJSONSchema, %raw(`{ "type": "object", - "additionalProperties": true, "properties": { "received": { "type": "boolean", @@ -180,14 +226,14 @@ test("JSONSchema of null", t => { test("JSONSchema of undefined", t => { t->U.assertThrowsMessage( () => S.literal(%raw(`undefined`))->S.toJSONSchema, - `Failed converting to JSON: undefined is not valid JSON`, + `Expected JSON, received undefined`, ) }) test("JSONSchema of NaN", t => { t->U.assertThrowsMessage( () => S.literal(%raw(`NaN`))->S.toJSONSchema, - `Failed converting to JSON: NaN is not valid JSON`, + `Expected JSON, received NaN`, ) }) @@ -213,7 +259,6 @@ test("JSONSchema of object of literals schema", t => { )->S.toJSONSchema, %raw(`{ "type": "object", - "additionalProperties": true, "properties": { "foo": { "type": "string", @@ -322,7 +367,7 @@ test("JSONSchema of dict with optional fields", t => { test("JSONSchema of dict with optional invalid field", t => { t->U.assertThrowsMessage( () => S.dict(S.option(S.bigint))->S.toJSONSchema, - `Failed converting to JSON: { [key: string]: bigint | undefined; } is not valid JSON`, + `Failed at []: Expected JSON, received bigint | undefined`, ) }) @@ -333,7 +378,6 @@ test("JSONSchema of object with single string field", t => { "type": "object", "properties": {"field": {"type": "string"}}, "required": ["field"], - "additionalProperties": true, }`), ) }) @@ -356,7 +400,6 @@ test("JSONSchema of object with optional field", t => { %raw(`{ "type": "object", "properties": {"field": {"type": "string"}}, - "additionalProperties": true, }`), ) }) @@ -374,7 +417,6 @@ test("JSONSchema of object with deprecated field", t => { "description": "Use another field" }}, "required": ["field"], - "additionalProperties": true, }`), ) }) @@ -412,11 +454,9 @@ test("JSONSchema of nested object", t => { "type": "object", "properties": {"Field": {"type": "string"}}, "required": ["Field"], - "additionalProperties": true, }, }, "required": ["objectWithOneStringField"], - "additionalProperties": true, }`), ) }) @@ -436,7 +476,6 @@ test("JSONSchema of object with one optional and one normal field", t => { "optionalField": {"type": "string"}, }, "required": ["field"], - "additionalProperties": true, }`), ) }) @@ -444,7 +483,7 @@ test("JSONSchema of object with one optional and one normal field", t => { test("JSONSchema of optional root schema", t => { t->U.assertThrowsMessage( () => S.option(S.string)->S.toJSONSchema, - "Failed converting to JSON: string | undefined is not valid JSON", + "Expected JSON, received string | undefined", ) }) @@ -458,7 +497,6 @@ test("JSONSchema of object with S.option(S.option(_)) field", t => { "type": "string", }, }, - "additionalProperties": true, }`), ) }) @@ -466,18 +504,7 @@ test("JSONSchema of object with S.option(S.option(_)) field", t => { test("JSONSchema of reversed object with S.option(S.option(_)) field", t => { t->U.assertThrowsMessage( () => S.object(s => s.field("field", S.option(S.option(S.string))))->S.reverse->S.toJSONSchema, - // FIXME: Should work. Investigate why this test fails - // %raw(`{ - // "type": "object", - // "properties": { - // "field": { - // "type": "string", - // }, - // }, - // "additionalProperties": true, - // }`), - // (), - `Failed converting to JSON: string | undefined | { BS_PRIVATE_NESTED_SOME_NONE: 0; } is not valid JSON`, + `Expected JSON, received string | undefined | { BS_PRIVATE_NESTED_SOME_NONE: 0; }`, ) }) @@ -507,7 +534,6 @@ test( %raw(`{ "type": "object", "properties": {"field": {"type": "boolean"}}, // No 'default: true' here, but that's fine - "additionalProperties": true, }`), ) }, @@ -543,13 +569,12 @@ test("Transformed schema schema uses default with correct type", t => { %raw(`{ "type": "object", "properties": {"field": {"default": true, "type": "boolean"}}, - "additionalProperties": true, }`), ) }) test("Currently Option.getOrWith is not reflected on JSON schema", t => { - let schema = S.null(S.bool)->S.Option.getOrWith(() => true) + let schema = S.nullAsOption(S.bool)->S.Option.getOrWith(() => true) t->Assert.deepEqual( schema->S.toJSONSchema, @@ -587,7 +612,7 @@ test("Primitive schema with an example", t => { }) test("Transformed schema with an example", t => { - let schema = S.null(S.bool)->S.meta({examples: [%raw(`null`)]}) + let schema = S.nullAsOption(S.bool)->S.meta({examples: [None]}) t->Assert.deepEqual( schema->S.toJSONSchema, @@ -638,16 +663,12 @@ test("Additional raw schema works with optional fields", t => { "properties": { "optionalField": {"nullable": true, "type": "string"}, }, - "additionalProperties": true, }`), ) }) test("JSONSchema of unknown schema", t => { - t->U.assertThrowsMessage( - () => S.unknown->S.toJSONSchema, - `Failed converting to JSON: unknown is not valid JSON`, - ) + t->U.assertThrowsMessage(() => S.unknown->S.toJSONSchema, `Expected JSON, received unknown`) }) test("JSON schema doesn't affect final schema", t => { @@ -671,7 +692,6 @@ test("JSONSchema of recursive schema", t => { %raw(`{ $defs: { Node: { - additionalProperties: true, properties: { Children: { items: { $ref: "#/$defs/Node" }, type: "array" }, Id: { type: "string" }, @@ -710,7 +730,6 @@ test("JSONSchema of nested recursive schema", t => { %raw(`{ type: 'object', properties: { node: { '$ref': '#/$defs/Node' } }, - additionalProperties: true, required: [ 'node' ], '$defs': { Node: { @@ -719,7 +738,6 @@ test("JSONSchema of nested recursive schema", t => { Children: { items: { $ref: "#/$defs/Node" }, type: "array" }, Id: { type: "string" }, }, - additionalProperties: true, required: [ 'Id', 'Children' ] } } @@ -728,53 +746,46 @@ test("JSONSchema of nested recursive schema", t => { }) test("JSONSchema of recursive schema with non-jsonable field", t => { - t->Assert.throws( - () => { - let schema = S.recursive( - "Node", - nodeSchema => { - S.object( - s => - { - "id": s.field("Id", S.bigint), - "children": s.field("Children", S.array(nodeSchema)), - }, - ) - }, - ) - schema->S.toJSONSchema - }, - // FIXME: This doesn't have the most readable message - // Because isJsonable check doesn't work properly with recursive schemas - ~expectations={ - message: "[Sury] Unexpected schema type", - }, - ) + t->U.assertThrowsMessage(() => { + let schema = S.recursive( + "Node", + nodeSchema => { + S.object( + s => + { + "id": s.field("Id", S.bigint), + "children": s.field("Children", S.array(nodeSchema)), + }, + ) + }, + ) + schema->S.toJSONSchema + }, `Failed at ["Id"]: Expected JSON, received bigint`) }) test("Fails to create schema for schemas with optional items", t => { t->U.assertThrowsMessage( () => S.array(S.option(S.string))->S.toJSONSchema, - "Failed converting to JSON: (string | undefined)[] is not valid JSON", + "Failed at []: Expected JSON, received string | undefined", ) t->U.assertThrowsMessage( - () => S.union([S.option(S.string), S.null(S.string)])->S.toJSONSchema, - "Failed converting to JSON: string | undefined | null is not valid JSON", + () => S.union([S.option(S.string), S.nullAsOption(S.string)])->S.toJSONSchema, + "Expected JSON, received string | undefined | null", ) t->U.assertThrowsMessage( () => S.tuple1(S.option(S.string))->S.toJSONSchema, - `Failed converting to JSON at ["0"]: [string | undefined] is not valid JSON`, + `Failed at ["0"]: Expected JSON, received string | undefined`, ) t->U.assertThrowsMessage( () => S.tuple1(S.array(S.option(S.string)))->S.toJSONSchema, - `Failed converting to JSON at ["0"]: [(string | undefined)[]] is not valid JSON`, + `Failed at ["0"][]: Expected JSON, received string | undefined`, ) }) test("JSONSchema error of nested object has path", t => { t->U.assertThrowsMessage( () => S.object(s => s.nested("nested").field("field", S.bigint))->S.toJSONSchema, - `Failed converting to JSON: { nested: { field: bigint; }; } is not valid JSON`, + `Failed at ["nested"]["field"]: Expected JSON, received bigint`, ) }) @@ -825,11 +836,12 @@ module Example = { }, Age: { type: "integer", + minimum: -2147483648, + maximum: 2147483647, deprecated: true, description: "Use rating instead", }, }, - additionalProperties: true, required: ["Id", "Title", "Rating"], }`), ) diff --git a/packages/sury/tests/S_to_test.res b/packages/sury/tests/S_to_test.res index 6d0efe989..0f5d14a05 100644 --- a/packages/sury/tests/S_to_test.res +++ b/packages/sury/tests/S_to_test.res @@ -10,25 +10,15 @@ test("Coerce from string to string", t => { test("Coerce from string to bool", t => { let schema = S.string->S.to(S.bool) - t->Assert.deepEqual("false"->S.parseOrThrow(schema), false) - t->Assert.deepEqual("true"->S.parseOrThrow(schema), true) - t->U.assertThrows( - () => "tru"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"tru"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(false->S.reverseConvertOrThrow(schema), %raw(`"false"`)) + t->Assert.deepEqual("false"->S.parseOrThrow(~to=schema), false) + t->Assert.deepEqual("true"->S.parseOrThrow(~to=schema), true) + t->U.assertThrowsMessage(() => "tru"->S.parseOrThrow(~to=schema), `Expected boolean, received "tru"`) + t->Assert.deepEqual(false->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"false"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;(v0=i==="true")||i==="false"||e[1](i);return v0}`, + `i=>{typeof i==="string"||e[1](i);let v0;(v0=i==="true")||i==="false"||e[0](i);return v0}`, ) t->U.assertCompiledCode( ~schema, @@ -41,22 +31,15 @@ test("Coerce from string to bool", t => { test("Coerce from bool to string", t => { let schema = S.bool->S.to(S.string) - t->Assert.deepEqual(false->S.parseOrThrow(schema), "false") - t->Assert.deepEqual(true->S.parseOrThrow(schema), "true") - t->U.assertThrows( - () => "tru"->S.reverseConvertOrThrow(schema), - { - code: InvalidType({ - expected: S.bool->S.castToUnknown, - received: %raw(`"tru"`), - }), - path: S.Path.empty, - operation: ReverseConvert, - }, + t->Assert.deepEqual(false->S.parseOrThrow(~to=schema), "false") + t->Assert.deepEqual(true->S.parseOrThrow(~to=schema), "true") + t->U.assertThrowsMessage( + () => "tru"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected boolean, received "tru"`, ) - t->Assert.deepEqual("false"->S.reverseConvertOrThrow(schema), %raw(`false`)) + t->Assert.deepEqual("false"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`false`)) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(typeof i!=="boolean"){e[0](i)}return ""+i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{typeof i==="boolean"||e[0](i);return ""+i}`) t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{return \"\"+i}`) t->U.assertCompiledCode( ~schema, @@ -68,108 +51,90 @@ test("Coerce from bool to string", t => { test("Coerce from string to bool literal", t => { let schema = S.string->S.to(S.literal(false)) - t->Assert.deepEqual("false"->S.parseOrThrow(schema), false) - t->U.assertThrows( - () => "true"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(false)->S.castToUnknown, - received: %raw(`"true"`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual("false"->S.parseOrThrow(~to=schema), false) + t->U.assertThrowsMessage( + () => "true"->S.parseOrThrow(~to=schema), + `Expected "false", received "true"`, ) - t->Assert.deepEqual(false->S.reverseConvertOrThrow(schema), %raw(`"false"`)) + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Expected string, received 123`) + t->Assert.deepEqual(false->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"false"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}i==="false"||e[1](i);return false}`, + `i=>{typeof i==="string"||e[1](i);i==="false"||e[0](i);return false}`, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==false){e[0](i)}return "false"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===false||e[0](i);return "false"}`) +}) + +test("S.string->S.refine->S.to(S.literal) reports type error before refinement error", t => { + let schema = + S.string + ->S.refine(v => v->String.length > 0, ~error="non-empty") + ->S.to(S.literal(false)) + + t->Assert.deepEqual("false"->S.parseOrThrow(~to=schema), false) + t->U.assertThrowsMessage(() => ""->S.parseOrThrow(~to=schema), "non-empty") + t->U.assertThrowsMessage( + () => "true"->S.parseOrThrow(~to=schema), + `Expected "false", received "true"`, + ) + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `Expected string, received 123`) }) test("Coerce from string to null literal", t => { let schema = S.string->S.to(S.literal(%raw(`null`))) - t->Assert.deepEqual("null"->S.parseOrThrow(schema), %raw(`null`)) - t->U.assertThrows( - () => "true"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`null`))->S.castToUnknown, - received: %raw(`"true"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(%raw(`null`)->S.reverseConvertOrThrow(schema), %raw(`"null"`)) + t->Assert.deepEqual("null"->S.parseOrThrow(~to=schema), %raw(`null`)) + t->U.assertThrowsMessage(() => "true"->S.parseOrThrow(~to=schema), `Expected "null", received "true"`) + t->Assert.deepEqual(%raw(`null`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"null"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}i==="null"||e[1](i);return null}`, + `i=>{typeof i==="string"||e[1](i);i==="null"||e[0](i);return null}`, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==null){e[0](i)}return "null"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===null||e[0](i);return "null"}`) }) test("Coerce from string to undefined literal", t => { let schema = S.string->S.to(S.literal(%raw(`undefined`))) - t->Assert.deepEqual("undefined"->S.parseOrThrow(schema), %raw(`undefined`)) - t->U.assertThrows( - () => "true"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`undefined`))->S.castToUnknown, - received: %raw(`"true"`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual("undefined"->S.parseOrThrow(~to=schema), %raw(`undefined`)) + t->U.assertThrowsMessage( + () => "true"->S.parseOrThrow(~to=schema), + `Expected "undefined", received "true"`, ) - t->Assert.deepEqual(%raw(`undefined`)->S.reverseConvertOrThrow(schema), %raw(`"undefined"`)) + t->Assert.deepEqual(%raw(`undefined`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"undefined"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}i==="undefined"||e[1](i);return void 0}`, + `i=>{typeof i==="string"||e[1](i);i==="undefined"||e[0](i);return void 0}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!==void 0){e[0](i)}return "undefined"}`, + `i=>{i===void 0||e[0](i);return "undefined"}`, ) }) test("Coerce from string to NaN literal", t => { let schema = S.string->S.to(S.literal(%raw(`NaN`))) - t->Assert.deepEqual("NaN"->S.parseOrThrow(schema), %raw(`NaN`)) - t->U.assertThrows( - () => "true"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(%raw(`NaN`))->S.castToUnknown, - received: %raw(`"true"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(%raw(`NaN`)->S.reverseConvertOrThrow(schema), %raw(`"NaN"`)) + t->Assert.deepEqual("NaN"->S.parseOrThrow(~to=schema), %raw(`NaN`)) + t->U.assertThrowsMessage(() => "true"->S.parseOrThrow(~to=schema), `Expected "NaN", received "true"`) + t->Assert.deepEqual(%raw(`NaN`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"NaN"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}i==="NaN"||e[1](i);return NaN}`, + `i=>{typeof i==="string"||e[1](i);i==="NaN"||e[0](i);return NaN}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(!Number.isNaN(i)){e[0](i)}return "NaN"}`, + `i=>{Number.isNaN(i)||e[0](i);return "NaN"}`, ) }) @@ -177,50 +142,36 @@ test("Coerce from string to string literal", t => { let quotedString = `"'\`` let schema = S.string->S.to(S.literal(quotedString)) - t->Assert.deepEqual(quotedString->S.parseOrThrow(schema), quotedString) - t->U.assertThrows( - () => "bar"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(quotedString)->S.castToUnknown, - received: %raw(`"bar"`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual(quotedString->S.parseOrThrow(~to=schema), quotedString) + t->U.assertThrowsMessage( + () => "bar"->S.parseOrThrow(~to=schema), + `Expected "${quotedString}", received "bar"`, ) - t->Assert.deepEqual(quotedString->S.reverseConvertOrThrow(schema), %raw(`quotedString`)) - t->U.assertThrows( - () => "bar"->S.reverseConvertOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(quotedString)->S.castToUnknown, - received: %raw(`"bar"`), - }), - path: S.Path.empty, - operation: ReverseConvert, - }, + t->Assert.deepEqual(quotedString->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`quotedString`)) + t->U.assertThrowsMessage( + () => "bar"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected "${quotedString}", received "bar"`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}if(i!=="\\"\'\`"){e[1](i)}return i}`, + `i=>{typeof i==="string"||e[1](i);i==="\\"\'\`"||e[0](i);return i}`, ) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!=="\\"\'\`"){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i==="\\"\'\`"||e[0](i);return i}`) }) test("Coerce from object shaped as string to float", t => { let schema = S.object(s => s.field("foo", S.string))->S.to(S.float) - t->Assert.deepEqual({"foo": "123"}->S.parseOrThrow(schema), 123.) + t->Assert.deepEqual({"foo": "123"}->S.parseOrThrow(~to=schema), 123.) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}let v1=+v0;Number.isNaN(v1)&&e[2](v0);return v1}`, + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["foo"];typeof v0==="string"||e[0](v0);let v1=+v0;!Number.isNaN(v1)||e[1](v0);return v1}`, ) - t->Assert.deepEqual(123.->S.reverseConvertOrThrow(schema), %raw(`{"foo": "123"}`)) + t->Assert.deepEqual(123.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"foo": "123"}`)) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return {"foo":""+i,}}`) }) @@ -229,60 +180,44 @@ test("Coerce to literal can be used as tag and automatically embeded on reverse let _ = s.field("tag", S.string->S.to(S.literal(true))) }) - t->Assert.deepEqual(()->S.reverseConvertOrThrow(schema), %raw(`{"tag": "true"}`)) + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"tag": "true"}`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(i!==void 0){e[0](i)}return {"tag":"true",}}`, + `i=>{i===void 0||e[0](i);return {"tag":"true",}}`, ) - t->Assert.deepEqual({"tag": "true"}->S.parseOrThrow(schema), ()) - t->U.assertThrows( - () => {"tag": "false"}->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(true)->S.castToUnknown, - received: %raw(`"false"`), - }), - path: S.Path.fromLocation("tag"), - operation: Parse, - }, + t->Assert.deepEqual({"tag": "true"}->S.parseOrThrow(~to=schema), ()) + t->U.assertThrowsMessage( + () => {"tag": "false"}->S.parseOrThrow(~to=schema), + `Failed at ["tag"]: Expected "true", received "false"`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["tag"];if(typeof v0!=="string"){e[1](v0)}v0==="true"||e[2](v0);return void 0}`, + // FIXME: Test that it'll work with S.refine on S.string + `i=>{typeof i==="object"&&i||e[2](i);let v0=i["tag"];typeof v0==="string"||e[1](v0);v0==="true"||e[0](v0);return void 0}`, ) }) test("Coerce from string to float", t => { let schema = S.string->S.to(S.float) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), 10.) - t->Assert.deepEqual("10.2"->S.parseOrThrow(schema), 10.2) - t->U.assertThrows( - () => "tru"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.float->S.castToUnknown, - received: %raw(`"tru"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(10.->S.reverseConvertOrThrow(schema), %raw(`"10"`)) - t->Assert.deepEqual(10.2->S.reverseConvertOrThrow(schema), %raw(`"10.2"`)) + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), 10.) + t->Assert.deepEqual("10.2"->S.parseOrThrow(~to=schema), 10.2) + t->U.assertThrowsMessage(() => "tru"->S.parseOrThrow(~to=schema), `Expected number, received "tru"`) + t->Assert.deepEqual(10.->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) + t->Assert.deepEqual(10.2->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10.2"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0=+i;Number.isNaN(v0)&&e[1](i);return v0}`, + `i=>{typeof i==="string"||e[1](i);let v0=+i;!Number.isNaN(v0)||e[0](i);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - `i=>{let v0=+i;Number.isNaN(v0)&&e[0](i);return v0}`, + `i=>{let v0=+i;!Number.isNaN(v0)||e[0](i);return v0}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) }) @@ -290,67 +225,48 @@ test("Coerce from string to float", t => { test("Coerce from string to int32", t => { let schema = S.string->S.to(S.int) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), 10) - t->U.assertThrows( - () => "2147483648"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.int->S.castToUnknown, - received: %raw(`"2147483648"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->U.assertThrows( - () => "10.2"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.int->S.castToUnknown, - received: %raw(`"10.2"`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), 10) + t->U.assertThrowsMessage( + () => "2147483648"->S.parseOrThrow(~to=schema), + `Expected int32, received "2147483648"`, ) - t->Assert.deepEqual(10->S.reverseConvertOrThrow(schema), %raw(`"10"`)) + t->U.assertThrowsMessage(() => "10.2"->S.parseOrThrow(~to=schema), `Expected int32, received "10.2"`) + t->Assert.deepEqual(10->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0=+i;(v0>2147483647||v0<-2147483648||v0%1!==0)&&e[1](i);return v0}`, + `i=>{typeof i==="string"||e[1](i);let v0=+i;v0<=2147483647&&v0>=-2147483648&&v0%1===0||e[0](i);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - `i=>{let v0=+i;(v0>2147483647||v0<-2147483648||v0%1!==0)&&e[0](i);return v0}`, + `i=>{let v0=+i;v0<=2147483647&&v0>=-2147483648&&v0%1===0||e[0](i);return v0}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) }) test("Coerce from string to port", t => { - let schema = S.string->S.to(S.int->S.port) + S.enablePort() + let schema = S.string->S.to(S.port) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), 10) + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), 10) t->U.assertThrowsMessage( - () => "2147483648"->S.parseOrThrow(schema), - `Failed parsing: Expected port, received 2147483648`, + () => "2147483648"->S.parseOrThrow(~to=schema), + `Expected port, received 2147483648`, ) - t->U.assertThrowsMessage( - () => "10.2"->S.parseOrThrow(schema), - `Failed parsing: Expected port, received 10.2`, - ) - t->Assert.deepEqual(10->S.reverseConvertOrThrow(schema), %raw(`"10"`)) + t->U.assertThrowsMessage(() => "10.2"->S.parseOrThrow(~to=schema), `Expected port, received 10.2`) + t->Assert.deepEqual(10->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0=+i;(Number.isNaN(v0))&&e[1](i);v0>0&&v0<65536&&v0%1===0||e[2](v0);return v0}`, + `i=>{typeof i==="string"||e[2](i);let v0=+i;!Number.isNaN(v0)||e[1](i);v0>0&&v0<65536&&v0%1===0||e[0](v0);return v0}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - `i=>{let v0=+i;(Number.isNaN(v0))&&e[0](i);v0>0&&v0<65536&&v0%1===0||e[1](v0);return v0}`, + `i=>{let v0=+i;!Number.isNaN(v0)||e[1](i);v0>0&&v0<65536&&v0%1===0||e[0](v0);return v0}`, ) t->U.assertCompiledCode( ~schema, @@ -362,57 +278,37 @@ test("Coerce from string to port", t => { test("Coerce from true to bool", t => { let schema = S.literal(true)->S.to(S.bool) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==true){e[0](i)}return i}`) - t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{if(i!==true){e[0](i)}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===true||e[0](i);return i}`) + t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{i===true||e[0](i);return i}`) }) test("Coerce from string to bigint literal", t => { let schema = S.string->S.to(S.literal(10n)) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), 10n) - t->U.assertThrows( - () => "11"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.literal(10n)->S.castToUnknown, - received: %raw(`"11"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(10n->S.reverseConvertOrThrow(schema), %raw(`"10"`)) + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), 10n) + t->U.assertThrowsMessage(() => "11"->S.parseOrThrow(~to=schema), `Expected "10", received "11"`) + t->Assert.deepEqual(10n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}i==="10"||e[1](i);return 10n}`, + `i=>{typeof i==="string"||e[1](i);i==="10"||e[0](i);return 10n}`, ) t->U.assertCompiledCode(~schema, ~op=#Convert, `i=>{i==="10"||e[0](i);return 10n}`) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==10n){e[0](i)}return "10"}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===10n||e[0](i);return "10"}`) }) test("Coerce from string to bigint", t => { let schema = S.string->S.to(S.bigint) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), 10n) - t->U.assertThrows( - () => "10.2"->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.bigint->S.castToUnknown, - received: %raw(`"10.2"`), - }), - path: S.Path.empty, - operation: Parse, - }, - ) - t->Assert.deepEqual(10n->S.reverseConvertOrThrow(schema), %raw(`"10"`)) + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), 10n) + t->U.assertThrowsMessage(() => "10.2"->S.parseOrThrow(~to=schema), `Expected bigint, received "10.2"`) + t->Assert.deepEqual(10n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=BigInt(i)}catch(_){e[1](i)}return v0}`, + `i=>{typeof i==="string"||e[1](i);let v0;try{v0=BigInt(i)}catch(_){e[0](i)}return v0}`, ) t->U.assertCompiledCode( ~schema, @@ -425,23 +321,25 @@ test("Coerce from string to bigint", t => { test("Coerce string after a transform", t => { let schema = S.string->S.transform(_ => {parser: v => v, serializer: v => v})->S.to(S.bool) - // t->U.assertThrowsMessage( - // () => "true"->S.parseOrThrow(schema), - // `Failed parsing: Expected boolean, received "true"`, - // ) + t->U.assertThrowsMessage( + () => "true"->S.parseOrThrow(~to=schema), + `Expected boolean, received "true"`, + ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0=e[1](i);if(typeof v0!=="boolean"){e[2](v0)}return v0}`, + `i=>{typeof i==="string"||e[3](i);let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="boolean"||e[2](v0);return v0}`, ) - // FIXME: This is not correct. Should be fixed after S.transform is removed by S.to - // t->Assert.deepEqual(true->S.parseOrThrow(S.reverse(schema)), %raw(`true`)) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#ReverseParse, - // `i=>{if(typeof i!=="boolean"){e[1](i)}return e[0](i)}`, - // ) + t->U.assertThrowsMessage( + () => true->S.parseOrThrow(~to=S.reverse(schema)), + `Expected string, received true`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseParse, + `i=>{typeof i==="boolean"||e[3](i);let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="string"||e[2](v0);return v0}`, + ) }) @unboxed @@ -458,15 +356,15 @@ test("Coerce string to unboxed union (each item separately)", t => { ]), ) - t->Assert.deepEqual("10"->S.parseOrThrow(schema), Number(10.)) - t->Assert.deepEqual("true"->S.parseOrThrow(schema), Boolean(true)) + t->Assert.deepEqual("10"->S.parseOrThrow(~to=schema), Number(10.)) + t->Assert.deepEqual("true"->S.parseOrThrow(~to=schema), Boolean(true)) t->Assert.throws( () => { - "t"->S.parseOrThrow(schema) + "t"->S.parseOrThrow(~to=schema) }, ~expectations={ - message: `Failed parsing: Expected number | boolean, received "t" + message: `Expected number | boolean, received "t" - Expected number, received "t" - Expected boolean, received "t"`, }, @@ -475,40 +373,46 @@ test("Coerce string to unboxed union (each item separately)", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}try{let v0=+i;Number.isNaN(v0)&&e[1](i);i=v0}catch(e0){try{let v1;(v1=i==="true")||i==="false"||e[2](i);i=v1}catch(e1){e[3](i,e0,e1)}}return i}`, + `i=>{typeof i==="string"||e[3](i);try{let v0=+i;!Number.isNaN(v0)||e[1](i);i=v0}catch(e1){try{let v1;(v1=i==="true")||i==="false"||e[0](i);i=v1}catch(e2){e[2](i,e1,e2)}}return i}`, ) - t->Assert.deepEqual(Number(10.)->S.reverseConvertOrThrow(schema), %raw(`"10"`)) - t->Assert.deepEqual(Boolean(true)->S.reverseConvertOrThrow(schema), %raw(`"true"`)) + t->Assert.deepEqual(Number(10.)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"10"`)) + t->Assert.deepEqual(Boolean(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) - // // TODO: Can be improved + // TODO: Can be improved t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="number"&&!Number.isNaN(i)){i=""+i}else if(typeof i==="boolean"){i=""+i}return i}`, - ) -}) - -// test("Coerce string to JSON schema", t => { -// let schema = S.string->S.to( -// S.recursive(self => { -// S.union([ -// S.schema(_ => Json.Null), -// S.schema(s => Json.Number(s.matches(S.float))), -// S.schema(s => Json.Boolean(s.matches(S.bool))), -// S.schema(s => Json.String(s.matches(S.string))), -// S.schema(s => Json.Object(s.matches(S.dict(self)))), -// S.schema(s => Json.Array(s.matches(S.array(self)))), -// ]) -// }), -// ) + `i=>{if(typeof i==="number"&&!Number.isNaN(i)){i=""+i}else if(typeof i==="boolean"){i=""+i}else{e[0](i)}return i}`, + ) +}) -// t->U.assertCompiledCode( -// ~schema, -// ~op=#ReverseConvert, -// ``, -// ) -// }) +test("Coerce string to custom JSON schema", t => { + let schema = S.string->S.to( + S.recursive("CustomJSON", self => { + S.union([ + S.schema(_ => JSON.Null), + S.schema(s => JSON.Number(s.matches(S.float))), + S.schema(s => JSON.Boolean(s.matches(S.bool))), + S.schema(s => JSON.String(s.matches(S.string))), + S.schema(s => JSON.Object(s.matches(S.dict(self)))), + S.schema(s => JSON.Array(s.matches(S.array(self)))), + ]) + }), + ) + + t->U.assertThrowsMessage( + () => S.decodeOrThrow(JSON.Boolean(true), ~from=schema, ~to=S.unknown), + `Expected string, received true`, + ~message="I don't know what we expect here, but currently it works this way", + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v0=e[0](i);if(typeof v0!=="string"){e[1](v0)}return v0}`, + ) +}) test("Keeps description of the schema we are coercing to (not working)", t => { // Fix it later if it's needed @@ -526,53 +430,41 @@ test("Keeps description of the schema we are coercing to (not working)", t => { test("Coerce from unit to null literal", t => { let schema = S.unit->S.to(S.literal(%raw(`null`))) - t->Assert.deepEqual(()->S.parseOrThrow(schema), %raw(`null`)) - t->U.assertThrows( - () => %raw(`null`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: S.unit->S.castToUnknown, - received: %raw(`null`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual(()->S.parseOrThrow(~to=schema), %raw(`null`)) + t->U.assertThrowsMessage( + () => %raw(`null`)->S.parseOrThrow(~to=schema), + // FIXME: It fails because we overwrite expected name with string version + `Expected undefined, received null`, ) - t->Assert.deepEqual(%raw(`null`)->S.reverseConvertOrThrow(schema), %raw(`undefined`)) + t->Assert.deepEqual(%raw(`null`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`undefined`)) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(i!==void 0){e[0](i)}return null}`) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==null){e[0](i)}return void 0}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{i===void 0||e[0](i);return null}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===null||e[0](i);return void 0}`) }) test("Coerce from string to optional bool", t => { let schema = S.string->S.to(S.option(S.bool)) - t->Assert.deepEqual("undefined"->S.parseOrThrow(schema), None) - t->Assert.deepEqual("true"->S.parseOrThrow(schema), Some(true)) - t->U.assertThrows( - () => %raw(`null`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`null`), - }), - path: S.Path.empty, - operation: Parse, - }, + t->Assert.deepEqual("undefined"->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual("true"->S.parseOrThrow(~to=schema), Some(true)) + + t->U.assertThrowsMessage( + () => %raw(`null`)->S.parseOrThrow(~to=schema), + `Expected string, received null`, ) - t->Assert.deepEqual(Some(true)->S.reverseConvertOrThrow(schema), %raw(`"true"`)) - t->Assert.deepEqual(None->S.reverseConvertOrThrow(schema), %raw(`"undefined"`)) + t->Assert.deepEqual(Some(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"true"`)) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"undefined"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}try{let v0;(v0=i==="true")||i==="false"||e[1](i);i=v0}catch(e0){try{i==="undefined"||e[2](i);i=void 0}catch(e1){e[3](i,e0,e1)}}return i}`, + `i=>{typeof i==="string"||e[2](i);try{let v0;(v0=i==="true")||i==="false"||e[0](i);i=v0}catch(e0){if(i==="undefined"){i=void 0}else{e[1](i,e0)}}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="boolean"){i=""+i}else if(i===void 0){i="undefined"}return i}`, + `i=>{if(typeof i==="boolean"){i=""+i}else if(i===void 0){i="undefined"}else{e[0](i)}return i}`, ) }) @@ -584,54 +476,211 @@ test("Coerce from object to string", t => { )->S.to(S.string) t->U.assertThrowsMessage(() => { - %raw(`{"foo": "bar"}`)->S.parseOrThrow(schema) - }, `Failed parsing: Unsupported transformation from { foo: string; } to string`) + %raw(`{"foo": "bar"}`)->S.parseOrThrow(~to=schema) + }, `Can't decode { foo: string; } to string. Use S.to to define a custom decoder`) t->U.assertThrowsMessage(() => { - %raw(`{"foo": "bar"}`)->S.reverseConvertOrThrow(schema) - }, `Failed converting: Unsupported transformation from string to { foo: string; }`) + %raw(`{"foo": "bar"}`)->S.decodeOrThrow(~from=schema, ~to=S.unknown) + }, `Can't decode string to { foo: string; }. Use S.to to define a custom decoder`) }) test("Coerce from string to JSON and then to bigint", t => { let schema = S.string->S.to(S.json)->S.to(S.bigint) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), %raw(`123n`)) - t->Assert.deepEqual(123n->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), %raw(`123n`)) + t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=BigInt(i)}catch(_){e[1](i)}return v0}`, + `i=>{typeof i==="string"||e[1](i);let v0;try{v0=BigInt(i)}catch(_){e[0](i)}return v0}`, ) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, - `i=>{if(typeof i!=="bigint"){e[0](i)}return ""+i}`, + `i=>{typeof i==="bigint"||e[0](i);return ""+i}`, ) }) -// Only.test("Coerce from JSON to bigint", t => { -// let schema = S.json->S.to(S.bigint) +test("Coerce from JSON to bigint", t => { + let schema = S.json->S.to(S.bigint) -// t->Assert.deepEqual("123"->S.parseOrThrow(schema), %raw(`123n`)) -// t->Assert.deepEqual(123->S.parseOrThrow(schema), %raw(`123n`)) -// t->U.assertThrowsMessage(() => { -// true->S.parseOrThrow(schema) -// }, "foo") + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), %raw(`123n`)) + t->U.assertThrowsMessage(() => { + 123->S.parseOrThrow(~to=schema) + }, "Expected string, received 123") + t->U.assertThrowsMessage(() => { + true->S.parseOrThrow(~to=schema) + }, "Expected string, received true") -// t->Assert.deepEqual(123n->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) -// t->U.assertCompiledCode( -// ~schema, -// ~op=#Parse, -// `i=>{if(typeof i!=="string"){e[0](i)}let v0;try{v0=BigInt(i)}catch(_){e[1](i)}return v0}`, + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + ~embedded=[], + `i=>{typeof i==="string"||e[1](i);let v0;try{v0=BigInt(i)}catch(_){e[0](i)}return v0}`, + ) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, ~embedded=[], `i=>{return ""+i}`) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseParse, + ~embedded=[], + `i=>{typeof i==="bigint"||e[0](i);return ""+i}`, + ) +}) + +test("Coerce from JSON to unit", t => { + let schema = S.json->S.to(S.unit) + + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), ()) + t->U.assertThrowsMessage(() => { + %raw(`undefined`)->S.parseOrThrow(~to=schema) + }, "Expected null, received undefined") + t->Assert.deepEqual(()->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + ~embedded=[], + `i=>{i===null||e[0](i);return void 0}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + ~embedded=[], + `i=>{i===void 0||e[0](i);return null}`, + ) +}) + +test("Coerce from JSON to NaN", t => { + let schema = S.json->S.to(S.literal(%raw(`NaN`))) + + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), %raw(`NaN`)) + t->U.assertThrowsMessage(() => { + %raw(`undefined`)->S.parseOrThrow(~to=schema) + }, "Expected null, received undefined") + t->Assert.deepEqual(%raw(`NaN`)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + + t->U.assertCompiledCode(~schema, ~op=#Parse, ~embedded=[], `i=>{i===null||e[0](i);return NaN}`) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + ~embedded=[], + `i=>{Number.isNaN(i)||e[0](i);return null}`, + ) +}) + +test("Coerce from JSON to optional bigint", t => { + let schema = S.json->S.to(S.option(S.bigint)) + + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(%raw(`"123"`)->S.parseOrThrow(~to=schema), Some(123n)) + t->U.assertThrowsMessage(() => { + %raw(`123`)->S.parseOrThrow(~to=schema) + }, `Expected bigint | undefined, received 123`) + t->Assert.deepEqual(None->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`null`)) + t->Assert.deepEqual(Some(123n)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) + + t->U.assertCompiledCode( + ~schema, + ~embedded=[], + ~op=#Parse, + `i=>{if(typeof i==="string"){let v0;try{v0=BigInt(i)}catch(_){e[0](i)}i=v0}else if(i===null){i=void 0}else{e[1](i)}return i}`, + ) + t->U.assertCompiledCode( + ~schema, + ~embedded=[], + ~op=#ReverseConvert, + `i=>{if(typeof i==="bigint"){i=""+i}else if(i===void 0){i=null}else{e[0](i)}return i}`, + ) +}) + +test("Coerce from JSON to array of bigint", t => { + let schema = S.json->S.to(S.array(S.bigint)) + + t->Assert.deepEqual(%raw(`["123"]`)->S.parseOrThrow(~to=schema), [123n]) + t->U.assertThrowsMessage(() => { + %raw(`[123]`)->S.parseOrThrow(~to=schema) + }, `Failed at ["0"]: Expected string, received 123`) + t->Assert.deepEqual([123n]->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["123"]`)) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + ~embedded=[], + `i=>{Array.isArray(i)||e[2](i);let v4=new Array(i.length);for(let v0=0;v0U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + ~embedded=[], + `i=>{let v2=new Array(i.length);for(let v1=0;v1 { + let schema = S.json->S.to(S.schema(s => (s.matches(S.string), s.matches(S.bigint)))) + + t->Assert.deepEqual(%raw(`["foo", "123"]`)->S.parseOrThrow(~to=schema), ("foo", 123n)) + t->U.assertThrowsMessage(() => { + %raw(`["foo"]`)->S.parseOrThrow(~to=schema) + }, `Expected [string, bigint], received ["foo"]`) + t->Assert.deepEqual(("foo", 123n)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["foo", "123"]`)) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + ~embedded=[], + `i=>{Array.isArray(i)||e[4](i);i.length===2||e[3](i);let v0=i["0"],v2=i["1"];typeof v0==="string"||e[0](v0);typeof v2==="string"||e[2](v2);let v1;try{v1=BigInt(v2)}catch(_){e[1](v2)}return [v0,v1,]}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + ~embedded=[], + `i=>{return [i["0"],""+i["1"],]}`, + ) +}) + +// test("Coerce from JSON to object with optional field", t => { +// let schema = S.json->S.to( +// S.schema(s => +// { +// "id": s.matches(S.bigint), +// "isDeleted": s.matches(S.option(S.string)), +// } +// ), // ) -// t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) + +// // t->Assert.deepEqual( +// // { +// // "id": "123", +// // }->S.parseOrThrow(~to=schema), +// // { +// // "id": 123n, +// // "isDeleted": None, +// // }, +// // ) +// // t->U.assertThrowsMessage(() => { +// // 123->S.parseOrThrow(~to=schema) +// // }, "Expected string, received 123") +// // t->U.assertThrowsMessage(() => { +// // true->S.parseOrThrow(~to=schema) +// // }, "Expected string, received true") + +// // t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) + // t->U.assertCompiledCode( // ~schema, -// ~op=#ReverseParse, -// `i=>{if(typeof i!=="bigint"){e[0](i)}return ""+i}`, +// ~op=#Parse, +// `i=>{if(typeof i!=="string"){e[1](i)}let v0;try{v0=BigInt(i)}catch(_){e[0](i)}return v0}`, // ) +// // t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return ""+i}`) +// // t->U.assertCompiledCode( +// // ~schema, +// // ~op=#ReverseParse, +// // `i=>{typeof i==="bigint"||e[0](i);return ""+i}`, +// // ) // }) test("Coerce from union to bigint", t => { @@ -640,22 +689,26 @@ test("Coerce from union to bigint", t => { S.bigint, ) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), %raw(`123n`)) - t->Assert.deepEqual(123->S.parseOrThrow(schema), %raw(`123n`)) - t->U.assertThrowsMessage(() => { - true->S.parseOrThrow(schema) - }, "Failed parsing: Unsupported transformation from boolean to bigint") + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), %raw(`123n`)) + t->Assert.deepEqual(123->S.parseOrThrow(~to=schema), %raw(`123n`)) + t->U.assertThrowsMessage( + () => { + true->S.parseOrThrow(~to=schema) + }, + `Expected string | number | boolean, received true +- Can't decode boolean to bigint. Use S.to to define a custom decoder`, + ) t->U.assertThrowsMessage(() => { - 123n->S.parseOrThrow(schema) - }, "Failed parsing: Expected string | number | boolean, received 123n") + 123n->S.parseOrThrow(~to=schema) + }, "Expected string | number | boolean, received 123n") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){let v0;try{v0=BigInt(i)}catch(_){e[0](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){i=BigInt(i)}else if(typeof i==="boolean"){throw e[1]}else{e[2](i)}return i}`, + `i=>{if(typeof i==="string"){let v0;try{v0=BigInt(i)}catch(_){e[0](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){i=BigInt(i)}else if(typeof i==="boolean"){e[2](i,e[1])}else{e[3](i)}return i}`, ) - t->Assert.deepEqual(123n->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual(123n->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) // TODO: Can be improved t->U.assertCompiledCode( @@ -666,73 +719,70 @@ test("Coerce from union to bigint", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, - `i=>{if(typeof i!=="bigint"){e[0](i)}try{i=""+i}catch(e0){try{throw e[1]}catch(e1){try{throw e[2]}catch(e2){e[3](i,e0,e1,e2)}}}return i}`, + `i=>{typeof i==="bigint"||e[3](i);try{i=""+i}catch(e0){try{throw e[0]}catch(e1){try{throw e[1]}catch(e2){e[2](i,e0,e1,e2)}}}return i}`, ) }) test("Coerce from union to bigint with refinement on union", t => { let schema = S.union([S.string->S.castToUnknown, S.float->S.castToUnknown, S.bool->S.castToUnknown]) - ->S.refine(s => - v => - if typeof(v) === #bigint { - s.fail("Unsupported bigint") - } - ) + ->S.refine(v => typeof(v) !== #bigint, ~error="Unsupported bigint") ->S.to(S.bigint) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){e[0](i);let v0;try{v0=BigInt(i)}catch(_){e[1](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){e[2](i);i=BigInt(i)}else if(typeof i==="boolean"){throw e[4]}else{e[5](i)}return i}`, + `i=>{if(typeof i==="string"){if(!e[0](i)){e[1]()}let v0;try{v0=BigInt(i)}catch(_){e[2](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){if(!e[0](i)){e[1]()}i=BigInt(i)}else if(typeof i==="boolean"){throw e[4]}else{e[5](i)}return i}`, ) }) test("Coerce from union to bigint with refinement on union (with an item transformed to)", t => { let schema = - S.union([S.string->S.castToUnknown, S.float->S.to(S.string)->S.castToUnknown, S.bool->S.castToUnknown]) - ->S.refine(s => - v => - if typeof(v) === #bigint { - s.fail("Unsupported bigint") - } - ) + S.union([ + S.string->S.castToUnknown, + S.float->S.to(S.string)->S.castToUnknown, + S.bool->S.castToUnknown, + ]) + ->S.refine(v => typeof(v) !== #bigint, ~error="Unsupported bigint") ->S.to(S.bigint) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){e[0](i);let v0;try{v0=BigInt(i)}catch(_){e[1](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){let v1=""+i;e[2](v1);let v2;try{v2=BigInt(v1)}catch(_){e[3](v1)}i=v2}else if(typeof i==="boolean"){throw e[5]}else{e[6](i)}return i}`, - ~message="Should apply refinement after the item transformation" + `i=>{if(typeof i==="string"){if(!e[0](i)){e[1]()}let v0;try{v0=BigInt(i)}catch(_){e[2](i)}i=v0}else if(typeof i==="number"&&!Number.isNaN(i)){let v1=""+i;if(!e[0](v1)){e[1]()}let v2;try{v2=BigInt(v1)}catch(_){e[3](v1)}i=v2}else if(typeof i==="boolean"){throw e[5]}else{e[6](i)}return i}`, + ~message="Should apply refinement after the item transformation", ) }) - test("Coerce from union to bigint and then to string", t => { let schema = S.union([S.string->S.castToUnknown, S.float->S.castToUnknown, S.bool->S.castToUnknown]) ->S.to(S.bigint) ->S.to(S.string) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), %raw(`"123"`)) - t->Assert.deepEqual(123->S.parseOrThrow(schema), %raw(`"123"`)) - t->U.assertThrowsMessage(() => { - true->S.parseOrThrow(schema) - }, "Failed parsing: Unsupported transformation from boolean to bigint") + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), %raw(`"123"`)) + t->Assert.deepEqual(123->S.parseOrThrow(~to=schema), %raw(`"123"`)) + t->U.assertThrowsMessage( + () => { + true->S.parseOrThrow(~to=schema) + }, + `Expected string | number | boolean, received true +- Can't decode boolean to bigint. Use S.to to define a custom decoder`, + ) t->U.assertThrowsMessage(() => { - 123n->S.parseOrThrow(schema) - }, "Failed parsing: Expected string | number | boolean, received 123n") + 123n->S.parseOrThrow(~to=schema) + }, "Expected string | number | boolean, received 123n") t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){let v0;try{v0=BigInt(i)}catch(_){e[0](i)}i=""+v0}else if(typeof i==="number"&&!Number.isNaN(i)){i=""+BigInt(i)}else if(typeof i==="boolean"){throw e[1]}else{e[2](i)}return i}`, + `i=>{if(typeof i==="string"){let v0;try{v0=BigInt(i)}catch(_){e[0](i)}i=""+v0}else if(typeof i==="number"&&!Number.isNaN(i)){i=""+BigInt(i)}else if(typeof i==="boolean"){e[2](i,e[1])}else{e[3](i)}return i}`, ) - t->Assert.deepEqual("123"->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual("123"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) t->U.assertThrowsMessage(() => { - "abc"->S.reverseConvertOrThrow(schema) - }, `Failed parsing: Expected bigint, received "abc"`) + "abc"->S.decodeOrThrow(~from=schema, ~to=S.unknown) + }, `Expected bigint, received "abc"`) // TODO: Can be improved t->U.assertCompiledCode( @@ -748,11 +798,11 @@ test("Coerce from union to wider union should keep the original value type", t = S.union([S.string->S.castToUnknown, S.float->S.castToUnknown, S.bool->S.castToUnknown]), ) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), %raw(`"123"`)) - t->Assert.deepEqual(123->S.parseOrThrow(schema), %raw(`123`)) + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), %raw(`"123"`)) + t->Assert.deepEqual(123->S.parseOrThrow(~to=schema), %raw(`123`)) t->U.assertThrowsMessage(() => { - true->S.parseOrThrow(schema) - }, "Failed parsing: Expected string | number, received true") + true->S.parseOrThrow(~to=schema) + }, "Expected string | number, received true") t->U.assertCompiledCode( ~schema, @@ -761,19 +811,19 @@ test("Coerce from union to wider union should keep the original value type", t = ) }) -test("Fails to transform union to union to string (no reason for this, just not supported)", t => { +test("Fails to transform union to union to string", t => { let schema = S.union([S.string->S.castToUnknown, S.float->S.castToUnknown]) ->S.to(S.union([S.string->S.castToUnknown, S.float->S.castToUnknown, S.bool->S.castToUnknown])) ->S.to(S.string) t->U.assertThrowsMessage(() => { - true->S.parseOrThrow(schema) - }, "Failed parsing: Unsupported transformation from string | number to string") + true->S.parseOrThrow(~to=schema) + }, "Expected string | number, received true") }) test( - "Coerce from union to wider union fails if the order of items is different (no reason for this, just not supported)", + "Transform from union to wider union with different items order (applies decoder to both one at a time)", t => { let schema = S.union([S.string->S.castToUnknown, S.float->S.castToUnknown])->S.to( @@ -781,7 +831,13 @@ test( ) t->U.assertThrowsMessage(() => { - true->S.parseOrThrow(schema) - }, "Failed parsing: Unsupported transformation from string | number to number | string | boolean") + true->S.parseOrThrow(~to=schema) + }, "Expected string | number, received true") + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + // TODO: Can be optimized to remove the second check + `i=>{if(!(typeof i==="string"||typeof i==="number"&&!Number.isNaN(i))){e[0](i)}if(!(typeof i==="number"&&!Number.isNaN(i)||typeof i==="string"||typeof i==="boolean")){e[1](i)}return i}`, + ) }, ) diff --git a/packages/sury/tests/S_transform_test.res b/packages/sury/tests/S_transform_test.res index 8f07e5afb..355d6e6fe 100644 --- a/packages/sury/tests/S_transform_test.res +++ b/packages/sury/tests/S_transform_test.res @@ -1,15 +1,17 @@ open Ava +S.enableJson() + test("Parses unknown primitive with transformation to the same type", t => { let schema = S.string->S.transform(_ => {parser: value => value->String.trim}) - t->Assert.deepEqual(" Hello world!"->S.parseOrThrow(schema), "Hello world!") + t->Assert.deepEqual(" Hello world!"->S.parseOrThrow(~to=schema), "Hello world!") }) test("Parses unknown primitive with transformation to another type", t => { let schema = S.int->S.transform(_ => {parser: value => value->Int.toFloat}) - t->Assert.deepEqual(123->S.parseOrThrow(schema), 123.) + t->Assert.deepEqual(123->S.parseOrThrow(~to=schema), 123.) }) asyncTest( @@ -19,30 +21,23 @@ asyncTest( asyncParser: value => Promise.resolve()->Promise.thenResolve(() => value->Int.toFloat), }) - t->Assert.deepEqual(await 123->S.parseAsyncOrThrow(schema), 123.) + t->Assert.deepEqual(await 123->S.parseAsyncOrThrow(~to=schema), 123.) }, ) test("Fails to parse primitive with transform when parser isn't provided", t => { let schema = S.string->S.transform(_ => {serializer: value => value}) - t->U.assertThrows( - () => "Hello world!"->S.parseOrThrow(schema), - { - code: InvalidOperation({description: "The S.transform parser is missing"}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "Hello world!"->S.parseOrThrow(~to=schema), + `The S.transform parser is missing`, ) }) test("Fails to parse when user throws error in a Transformed Primitive parser", t => { let schema = S.string->S.transform(s => {parser: _ => s.fail("User error")}) - t->U.assertThrows( - () => "Hello world!"->S.parseOrThrow(schema), - {code: OperationFailed("User error"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "Hello world!"->S.parseOrThrow(~to=schema), `User error`) }) test("Uses the path from S.Error.throw called in the transform parser", t => { @@ -50,22 +45,19 @@ test("Uses the path from S.Error.throw called in the transform parser", t => { S.string->S.transform(_ => { parser: _ => U.throwError( - U.error({ - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["a", "b"]), - }), + S.Error.make( + Custom({ + reason: "User error", + path: S.Path.fromArray(["a", "b"]), + }), + ), ), }), ) - t->U.assertThrows( - () => ["Hello world!"]->S.parseOrThrow(schema), - { - code: OperationFailed("User error"), - operation: Parse, - path: S.Path.fromArray(["0", "a", "b"]), - }, + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + `Failed at ["0"]["a"]["b"]: User error`, ) }) @@ -74,93 +66,99 @@ test("Uses the path from S.Error.throw called in the transform serializer", t => S.string->S.transform(_ => { serializer: _ => U.throwError( - U.error({ - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["a", "b"]), - }), + S.Error.make( + Custom({ + reason: "User error", + path: S.Path.fromArray(["a", "b"]), + }), + ), ), }), ) - t->U.assertThrows( - () => ["Hello world!"]->S.reverseConvertToJsonOrThrow(schema), - { - code: OperationFailed("User error"), - operation: ReverseConvert, - path: S.Path.fromArray(["0", "a", "b"]), - }, + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.decodeOrThrow(~from=schema, ~to=S.json), + `Failed at ["0"]["a"]["b"]: User error`, ) }) -test("Transform parser passes through non rescript-schema errors", t => { - let schema = S.array( - S.string->S.transform(_ => {parser: _ => JsError.throwWithMessage("Application crashed")}), - ) +test("All errors thrown in operation context are caught and wrapped in SuryError", t => { + let jsError = JsError.make("Application crashed") + let schema = S.array(S.string->S.transform(_ => {parser: _ => JsError.throw(jsError)})) - t->Assert.throws( - () => {["Hello world!"]->S.parseOrThrow(schema)}, - ~expectations={ - message: "Application crashed", - }, + t->U.assertThrowsMessage( + () => {["Hello world!"]->S.parseOrThrow(~to=schema)}, + `Failed at ["0"]: Application crashed`, ) + switch ["Hello world!"]->S.parseOrThrow(~to=schema) { + | _ => t->Assert.fail("Didn't throw") + | exception S.Exn(error) => + switch error->S.Error.classify { + | InvalidConversion({cause}) => t->Assert.is(cause->Obj.magic, jsError) + | _ => t->Assert.fail("Thrown another exception") + } + } }) -test("Transform parser passes through other rescript exceptions", t => { +test("Operation context catches ReScript exceptions as they are", t => { let schema = S.array(S.string->S.transform(_ => {parser: _ => U.throwTestException()})) - t->U.assertThrowsTestException(() => {["Hello world!"]->S.parseOrThrow(schema)}) + t->U.assertThrowsMessage( + () => {["Hello world!"]->S.parseOrThrow(~to=schema)}, + `Failed at ["0"]: { RE_EXN_ID: "U.Test"; Error: [object Error]; }`, + ) }) test("Transform definition passes through non rescript-schema errors", t => { let schema = S.array(S.string->S.transform(_ => JsError.throwWithMessage("Application crashed"))) t->Assert.throws( - () => {["Hello world!"]->S.parseOrThrow(schema)}, + () => {["Hello world!"]->S.parseOrThrow(~to=schema)}, ~expectations={ message: "Application crashed", }, ) }) -test("Transform definition passes through other rescript exceptions", t => { +test("Rescript exceptions caught in transform", t => { let schema = S.array(S.string->S.transform(_ => U.throwTestException())) + t->U.assertThrowsTestException( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + ~message="When exn thrown outside of the operation context, it's not wrapped in SuryError", + ) - t->U.assertThrowsTestException(() => {["Hello world!"]->S.parseOrThrow(schema)}) + let schema = S.array(S.string->S.transform(_ => {parser: _ => U.throwTestException()})) + t->U.assertThrowsMessage( + () => ["Hello world!"]->S.parseOrThrow(~to=schema), + `Failed at ["0"]: { RE_EXN_ID: "U.Test"; Error: [object Error]; }`, + ) }) test("Successfully serializes primitive with transformation to the same type", t => { let schema = S.string->S.transform(_ => {serializer: value => value->String.trim}) - t->Assert.deepEqual(" Hello world!"->S.reverseConvertOrThrow(schema), %raw(`"Hello world!"`)) + t->Assert.deepEqual(" Hello world!"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"Hello world!"`)) }) test("Successfully serializes primitive with transformation to another type", t => { let schema = S.float->S.transform(_ => {serializer: value => value->Int.toFloat}) - t->Assert.deepEqual(123->S.reverseConvertOrThrow(schema), %raw(`123`)) + t->Assert.deepEqual(123->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`123`)) }) test("Transformed Primitive serializing fails when serializer isn't provided", t => { let schema = S.string->S.transform(_ => {parser: value => value}) - t->U.assertThrows( - () => "Hello world!"->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({description: "The S.transform serializer is missing"}), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "Hello world!"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `The S.transform serializer is missing`, ) }) test("Fails to serialize when user throws error in a Transformed Primitive serializer", t => { let schema = S.string->S.transform(s => {serializer: _ => s.fail("User error")}) - t->U.assertThrows( - () => "Hello world!"->S.reverseConvertOrThrow(schema), - {code: OperationFailed("User error"), operation: ReverseConvert, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => "Hello world!"->S.decodeOrThrow(~from=schema, ~to=S.unknown), `User error`) }) test("Transform operations applyed in the right order when parsing", t => { @@ -169,10 +167,7 @@ test("Transform operations applyed in the right order when parsing", t => { ->S.transform(s => {parser: _ => s.fail("First transform")}) ->S.transform(s => {parser: _ => s.fail("Second transform")}) - t->U.assertThrows( - () => 123->S.parseOrThrow(schema), - {code: OperationFailed("First transform"), operation: Parse, path: S.Path.empty}, - ) + t->U.assertThrowsMessage(() => 123->S.parseOrThrow(~to=schema), `First transform`) }) test("Transform operations applyed in the right order when serializing", t => { @@ -181,14 +176,7 @@ test("Transform operations applyed in the right order when serializing", t => { ->S.transform(s => {serializer: _ => s.fail("First transform")}) ->S.transform(s => {serializer: _ => s.fail("Second transform")}) - t->U.assertThrows( - () => 123->S.reverseConvertOrThrow(schema), - { - code: OperationFailed("Second transform"), - operation: ReverseConvert, - path: S.Path.empty, - }, - ) + t->U.assertThrowsMessage(() => 123->S.decodeOrThrow(~from=schema, ~to=S.unknown), `Second transform`) }) test( @@ -201,51 +189,45 @@ test( serializer: value => value->Int.fromFloat, }) - t->Assert.deepEqual(any->S.parseOrThrow(schema)->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema)->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }, ) test("Fails to parse schema with transform having both parser and asyncParser", t => { let schema = S.string->S.transform(_ => {parser: _ => (), asyncParser: _ => Promise.resolve()}) - t->U.assertThrows( - () => "foo"->S.parseOrThrow(schema), - { - code: InvalidOperation({ - description: "The S.transform doesn\'t allow parser and asyncParser at the same time. Remove parser in favor of asyncParser", - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "foo"->S.parseOrThrow(~to=schema), + `The S.transform doesn't allow parser and asyncParser at the same time. Remove parser in favor of asyncParser`, ) }) test("Fails to parse async using parseOrThrow", t => { let schema = S.string->S.transform(_ => {asyncParser: value => Promise.resolve(value)}) - t->U.assertThrows( - () => %raw(`"Hello world!"`)->S.parseOrThrow(schema), - {code: UnexpectedAsync, operation: Parse, path: S.Path.empty}, + t->U.assertThrowsMessage( + () => %raw(`"Hello world!"`)->S.parseOrThrow(~to=schema), + `Encountered unexpected async transform or refine. Use parseAsyncOrThrow operation instead`, ) }) test("Successfully parses with empty transform", t => { let schema = S.string->S.transform(_ => {}) - t->Assert.deepEqual(%raw(`"Hello world!"`)->S.parseOrThrow(schema), "Hello world!") + t->Assert.deepEqual(%raw(`"Hello world!"`)->S.parseOrThrow(~to=schema), "Hello world!") }) test("Successfully serializes with empty transform", t => { let schema = S.string->S.transform(_ => {}) - t->Assert.deepEqual("Hello world!"->S.reverseConvertOrThrow(schema), %raw(`"Hello world!"`)) + t->Assert.deepEqual("Hello world!"->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"Hello world!"`)) }) asyncTest("Successfully parses async using parseAsyncOrThrow", t => { let schema = S.string->S.transform(_ => {asyncParser: value => Promise.resolve(value)}) %raw(`"Hello world!"`) - ->S.parseAsyncOrThrow(schema) + ->S.parseAsyncOrThrow(~to=schema) ->Promise.thenResolve(result => { t->Assert.deepEqual(result, "Hello world!") }) @@ -254,9 +236,9 @@ asyncTest("Successfully parses async using parseAsyncOrThrow", t => { asyncTest("Fails to parse async with user error", t => { let schema = S.string->S.transform(s => {asyncParser: _ => s.fail("User error")}) - t->U.assertThrowsAsync( - () => %raw(`"Hello world!"`)->S.parseAsyncOrThrow(schema), - {code: OperationFailed("User error"), operation: ParseAsync, path: S.Path.empty}, + t->U.asyncAssertThrowsMessage( + () => %raw(`"Hello world!"`)->S.parseAsyncOrThrow(~to=schema), + `User error`, ) }) @@ -270,11 +252,11 @@ asyncTest("Can apply other actions after async transform", t => { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="string"){e[0](i)}return e[1](i).then(e[2]).then(e[3])}`, + `i=>{typeof i==="string"||e[6](i);let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}return v0.then(v0=>{let v1;try{v1=e[2](v0)}catch(x){e[3](x)}let v2;try{v2=e[4](v1).catch(x=>e[5](x))}catch(x){e[5](x)}return v2})}`, ) %raw(`" Hello world!"`) - ->S.parseAsyncOrThrow(schema) + ->S.parseAsyncOrThrow(~to=schema) ->Promise.thenResolve(result => { t->Assert.deepEqual(result, "Hello world!") }) @@ -289,7 +271,7 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="number"||i>2147483647||i<-2147483648||i%1!==0){e[0](i)}return e[1](i)}`, + `i=>{typeof i==="number"&&i<=2147483647&&i>=-2147483648&&i%1===0||e[2](i);let v0;try{v0=e[0](i)}catch(x){e[1](x)}return v0}`, ) }) @@ -302,7 +284,7 @@ test("Compiled async parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i!=="number"||i>2147483647||i<-2147483648||i%1!==0){e[0](i)}return e[1](i)}`, + `i=>{typeof i==="number"&&i<=2147483647&&i>=-2147483648&&i%1===0||e[2](i);let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}return v0}`, ) }) @@ -312,7 +294,32 @@ test("Compiled serialize code snapshot", t => { serializer: value => value->Int.fromFloat, }) - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return e[0](i)}`) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="number"&&v0<=2147483647&&v0>=-2147483648&&v0%1===0||e[2](v0);return v0}`, + ) +}) + +test("Compiled serialize code snapshot with two transforms", t => { + let schema = + S.string + ->S.transform(_ => { + parser: string => string->Int.fromString->Option.getOrThrow, + serializer: int => int->Int.toString, + }) + ->S.to( + S.int->S.transform(_ => { + parser: int => int->Int.toFloat, + serializer: float => float->Float.toInt, + }), + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="number"&&v0<=2147483647&&v0>=-2147483648&&v0%1===0||e[5](v0);let v1;try{v1=e[2](v0)}catch(x){e[3](x)}typeof v1==="string"||e[4](v1);return v1}`, + ) }) test("Reverse schema to the original schema", t => { diff --git a/packages/sury/tests/S_tuple1_test.res b/packages/sury/tests/S_tuple1_test.res index 4108b3a74..032ec99a2 100644 --- a/packages/sury/tests/S_tuple1_test.res +++ b/packages/sury/tests/S_tuple1_test.res @@ -10,41 +10,30 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse extra item in strict mode", t => { let schema = factory()->S.strict - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: invalidAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected [int32], received [123, true]`, ) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected [int32], received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) } diff --git a/packages/sury/tests/S_tuple2_test.res b/packages/sury/tests/S_tuple2_test.res index 9580a3726..d062a7cd2 100644 --- a/packages/sury/tests/S_tuple2_test.res +++ b/packages/sury/tests/S_tuple2_test.res @@ -10,41 +10,30 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse invalid value", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: invalidAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected [int32, boolean], received [123]`, ) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected [int32, boolean], received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) } diff --git a/packages/sury/tests/S_tuple_test.res b/packages/sury/tests/S_tuple_test.res index 7ce2dd9a0..3bcbb9308 100644 --- a/packages/sury/tests/S_tuple_test.res +++ b/packages/sury/tests/S_tuple_test.res @@ -1,5 +1,7 @@ open Ava +S.enableJson() + module Tuple0 = { let value = () let any = %raw(`[]`) @@ -10,48 +12,37 @@ module Tuple0 = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), value) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), value) }) test("Fails to parse extra value in strict mode (default for tuple)", t => { let schema = factory() - t->U.assertThrows( - () => invalidAny->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: invalidAny, - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidAny->S.parseOrThrow(~to=schema), + `Expected [], received [true]`, ) }) test("Ignores extra items in strip mode", t => { let schema = factory() - t->Assert.deepEqual(invalidAny->S.parseOrThrow(schema->S.strip), ()) + t->Assert.deepEqual(invalidAny->S.parseOrThrow(~to=schema->S.strip), ()) }) test("Fails to parse invalid type", t => { let schema = factory() - t->U.assertThrows( - () => invalidTypeAny->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: invalidTypeAny}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => invalidTypeAny->S.parseOrThrow(~to=schema), + `Expected [], received "Hello world!"`, ) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(value->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(value->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) } @@ -65,22 +56,15 @@ test("Fills holes with S.unit", t => { test("Successfully parses tuple with holes", t => { let schema = S.tuple(s => (s.item(0, S.string), s.item(2, S.int))) - t->Assert.deepEqual(%raw(`["value",, 123]`)->S.parseOrThrow(schema), ("value", 123)) + t->Assert.deepEqual(%raw(`["value",, 123]`)->S.parseOrThrow(~to=schema), ("value", 123)) }) test("Fails to parse tuple with holes", t => { let schema = S.tuple(s => (s.item(0, S.string), s.item(2, S.int))) - t->U.assertThrows( - () => %raw(`["value", "smth", 123]`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`["value", "smth", 123]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["value", "smth", 123]`)->S.parseOrThrow(~to=schema), + `Failed at ["1"]: Expected undefined, received "smth"`, ) }) @@ -88,7 +72,7 @@ test("Successfully serializes tuple with holes", t => { let schema = S.tuple(s => (s.item(0, S.string), s.item(2, S.int))) t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{return [i["0"],void 0,i["1"],]}`) - t->Assert.deepEqual(("value", 123)->S.reverseConvertOrThrow(schema), %raw(`["value",, 123]`)) + t->Assert.deepEqual(("value", 123)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["value",, 123]`)) }) test("Reverse convert of tuple schema with single item registered multiple times", t => { @@ -108,11 +92,11 @@ test("Reverse convert of tuple schema with single item registered multiple times ) t->Assert.deepEqual( - {"item1": "foo", "item2": "foo"}->S.reverseConvertOrThrow(schema), + {"item1": "foo", "item2": "foo"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["foo"]`), ) // t->U.assertThrows( - // () => {"item1": "foo", "item2": "foz"}->S.reverseConvertOrThrow(schema), + // () => {"item1": "foo", "item2": "foz"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), // { // code: InvalidOperation({ // description: `Another source has conflicting data for the field ["0"]`, @@ -129,15 +113,9 @@ test(`Fails to serialize tuple with discriminant "Never"`, t => { s.item(1, S.string) }) - t->U.assertThrows( - () => "bar"->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema for ["0"] isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => "bar"->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Missing input for never at ["0"]`, ) }) @@ -155,28 +133,22 @@ test(`Fails to serialize tuple with discriminant "Never" inside of an object (te } ) - t->U.assertThrows( - () => {"foo": "bar"}->S.reverseConvertOrThrow(schema), - { - code: InvalidOperation({ - description: `Schema for ["0"] isn\'t registered`, - }), - operation: ReverseConvert, - path: S.Path.fromLocation(`foo`), - }, + t->U.assertThrowsMessage( + () => {"foo": "bar"}->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Failed at ["foo"]: Missing input for never at ["0"]`, ) }) test("Successfully parses tuple transformed to variant", t => { let schema = S.tuple(s => #VARIANT(s.item(0, S.bool))) - t->Assert.deepEqual(%raw(`[true]`)->S.parseOrThrow(schema), #VARIANT(true)) + t->Assert.deepEqual(%raw(`[true]`)->S.parseOrThrow(~to=schema), #VARIANT(true)) }) test("Successfully serializes tuple transformed to variant", t => { let schema = S.tuple(s => #VARIANT(s.item(0, S.bool))) - t->Assert.deepEqual(#VARIANT(true)->S.reverseConvertOrThrow(schema), %raw(`[true]`)) + t->Assert.deepEqual(#VARIANT(true)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`[true]`)) }) test("Fails to serialize tuple transformed to variant", t => { @@ -184,14 +156,14 @@ test("Fails to serialize tuple transformed to variant", t => { let invalid = Error("foo") t->Assert.deepEqual( - invalid->S.reverseConvertOrThrow(schema), + invalid->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`["foo"]`), ~message=`Convert operation doesn't perform exhaustiveness check`, ) t->U.assertThrowsMessage( - () => Error("foo")->S.parseOrThrow(schema->S.reverse), - `Failed parsing: Expected { TAG: "Ok"; _0: boolean; }, received { TAG: "Error"; _0: "foo"; }`, + () => Error("foo")->S.parseOrThrow(~to=schema->S.reverse), + `Failed at ["TAG"]: Expected "Ok", received "Error"`, ) }) @@ -221,59 +193,33 @@ test("Tuple schema parsing checks order", t => { }) // Type check should be the first - t->U.assertThrows( - () => %raw(`"foo"`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`"foo"`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"foo"`)->S.parseOrThrow(~to=schema), + `Expected [string, "value"], received "foo"`, ) // Length check should be the second - t->U.assertThrows( - () => %raw(`["value"]`)->S.parseOrThrow(schema), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`["value"]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["value"]`)->S.parseOrThrow(~to=schema), + `Expected [string, "value"], received ["value"]`, ) // Length check should be the second (extra items in strict mode) - t->U.assertThrows( - () => %raw(`["value", "value", "value"]`)->S.parseOrThrow(schema->S.strict), - { - code: InvalidType({ - expected: schema->S.castToUnknown, - received: %raw(`["value", "value", "value"]`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["value", "value", "value"]`)->S.parseOrThrow(~to=schema->S.strict), + `Expected [string, "value"], received ["value", "value", "value"]`, ) // Tag check should be the third - t->U.assertThrows( - () => %raw(`["value", "wrong"]`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: schema->S.castToUnknown, received: %raw(`["value", "wrong"]`)}), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`["value", "wrong"]`)->S.parseOrThrow(~to=schema), + `Failed at ["1"]: Expected "value", received "wrong"`, ) // Field check should be the last - t->U.assertThrows( - () => %raw(`[1, "value"]`)->S.parseOrThrow(schema), - { - code: InvalidType({expected: S.string->S.castToUnknown, received: %raw(`1`)}), - operation: Parse, - path: S.Path.fromLocation("0"), - }, + t->U.assertThrowsMessage( + () => %raw(`[1, "value"]`)->S.parseOrThrow(~to=schema), + `Failed at ["0"]: Expected string, received 1`, ) // Parses valid t->Assert.deepEqual( - %raw(`["value", "value"]`)->S.parseOrThrow(schema), + %raw(`["value", "value"]`)->S.parseOrThrow(~to=schema), { "key": "value", }, @@ -283,8 +229,8 @@ test("Tuple schema parsing checks order", t => { test("Works correctly with not-modified object item", t => { let schema = S.tuple1(S.object(s => s.field("foo", S.string))) - t->Assert.deepEqual(%raw(`[{"foo": "bar"}]`)->S.parseOrThrow(schema), "bar") - t->Assert.deepEqual("bar"->S.reverseConvertToJsonOrThrow(schema), %raw(`[{"foo": "bar"}]`)) + t->Assert.deepEqual(%raw(`[{"foo": "bar"}]`)->S.parseOrThrow(~to=schema), "bar") + t->Assert.deepEqual("bar"->S.decodeOrThrow(~from=schema, ~to=S.json), %raw(`[{"foo": "bar"}]`)) }) module Compiled = { @@ -294,7 +240,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2){e[0](i)}let v0=i["0"],v1=i["1"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}return [v0,v1,]}`, + `i=>{Array.isArray(i)&&i.length===2||e[2](i);let v0=i["0"],v1=i["1"];typeof v0==="string"||e[0](v0);typeof v1==="boolean"||e[1](v1);return [v0,v1,]}`, ) }) @@ -307,7 +253,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(!Array.isArray(i)||i.length!==2){e[0](i)}let v0=i["1"];if(typeof v0!=="boolean"){e[2](v0)}return Promise.all([e[1](i["0"]),]).then(a=>([a[0],v0,]))}`, + `i=>{Array.isArray(i)&&i.length===2||e[3](i);let v1=i["1"];let v0;try{v0=e[0](i["0"]).catch(x=>e[1](x))}catch(x){e[1](x)}typeof v1==="boolean"||e[2](v1);return Promise.all([v0,]).then(([v0,])=>{return [v0,v1,]})}`, ) }) @@ -321,7 +267,7 @@ module Compiled = { let schema = S.tuple(_ => ()) // TODO: No need to do unit check ? - t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{if(i!==void 0){e[0](i)}return []}`) + t->U.assertCompiledCode(~schema, ~op=#ReverseConvert, `i=>{i===void 0||e[0](i);return []}`) }) test( @@ -339,7 +285,7 @@ module Compiled = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==3||i["0"]!==0){e[0](i)}let v0=i["1"],v1=i["2"];if(typeof v0!=="string"){e[1](v0)}if(typeof v1!=="boolean"){e[2](v1)}return {"foo":v0,"bar":v1,"zoo":1,}}`, + `i=>{Array.isArray(i)&&i.length===3||e[3](i);let v0=i["0"],v1=i["1"],v2=i["2"];v0===0||e[0](v0);typeof v1==="string"||e[1](v1);typeof v2==="boolean"||e[2](v2);return {"foo":v1,"bar":v2,"zoo":1,}}`, ) }, ) @@ -385,10 +331,10 @@ test("Works with tuple schema used multiple times as a child schema", t => { "android": {"current": "1.2", "minimum": "1.1"}, } - let value = rawAppVersions->S.parseOrThrow(appVersionsSchema) + let value = rawAppVersions->S.parseOrThrow(~to=appVersionsSchema) t->Assert.deepEqual(value, appVersions) - let data = appVersions->S.reverseConvertToJsonOrThrow(appVersionsSchema) + let data = appVersions->S.decodeOrThrow(~from=appVersionsSchema, ~to=S.json) t->Assert.deepEqual(data, rawAppVersions->Obj.magic) }) diff --git a/packages/sury/tests/S_union_test.res b/packages/sury/tests/S_union_test.res index 02d6bfadc..9687a351a 100644 --- a/packages/sury/tests/S_union_test.res +++ b/packages/sury/tests/S_union_test.res @@ -22,7 +22,7 @@ test("Successfully creates a Union schema factory with single schema and flatten test("Successfully parses polymorphic variants", t => { let schema = S.union([S.literal(#apple), S.literal(#orange)]) - t->Assert.deepEqual(%raw(`"apple"`)->S.parseOrThrow(schema), #apple) + t->Assert.deepEqual(%raw(`"apple"`)->S.parseOrThrow(~to=schema), #apple) }) test("Parses when both schemas misses parser and have the same type", t => { @@ -32,21 +32,20 @@ test("Parses when both schemas misses parser and have the same type", t => { ]) try { - let _ = %raw(`null`)->S.parseOrThrow(schema) + let _ = %raw(`null`)->S.parseOrThrow(~to=schema) t->Assert.fail("Expected to throw") } catch { - | S.Error(error) => - t->Assert.is(error.message, `Failed parsing: Expected string | string, received null`) + | S.Exn(error) => t->Assert.is(error.message, `Expected string | string, received null`) } try { - let _ = %raw(`"foo"`)->S.parseOrThrow(schema) + let _ = %raw(`"foo"`)->S.parseOrThrow(~to=schema) t->Assert.fail("Expected to throw") } catch { - | S.Error(error) => + | S.Exn(error) => t->Assert.is( error.message, - `Failed parsing: Expected string | string, received "foo" + `Expected string | string, received "foo" - The S.transform parser is missing`, ) } @@ -54,7 +53,7 @@ test("Parses when both schemas misses parser and have the same type", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){try{throw e[0]}catch(e0){try{throw e[1]}catch(e1){e[2](i,e0,e1)}}}else{e[3](i)}return i}`, + `i=>{if(typeof i==="string"){try{throw e[0]}catch(e0){e[2](i,e[1])}}else{e[3](i)}return i}`, ) }) @@ -65,21 +64,20 @@ test("Parses when both schemas misses parser and have different types", t => { ]) try { - let _ = %raw(`null`)->S.parseOrThrow(schema) + let _ = %raw(`null`)->S.parseOrThrow(~to=schema) t->Assert.fail("Expected to throw") } catch { - | S.Error(error) => - t->Assert.is(error.message, `Failed parsing: Expected "apple" | string, received null`) + | S.Exn(error) => t->Assert.is(error.message, `Expected "apple" | string, received null`) } try { - let _ = %raw(`"abc"`)->S.parseOrThrow(schema) + let _ = %raw(`"abc"`)->S.parseOrThrow(~to=schema) t->Assert.fail("Expected to throw") } catch { - | S.Error(error) => + | S.Exn(error) => t->Assert.is( error.message, - `Failed parsing: Expected "apple" | string, received "abc" + `Expected "apple" | string, received "abc" - The S.transform parser is missing`, ) } @@ -87,7 +85,7 @@ test("Parses when both schemas misses parser and have different types", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){if(i==="apple"){throw e[0]}else{try{throw e[1]}catch(e1){e[2](i,e1)}}}else{e[3](i)}return i}`, + `i=>{if(typeof i==="string"){try{throw e[0]}catch(e0){e[2](i,e[1])}}else{e[3](i)}return i}`, ) }) @@ -98,13 +96,13 @@ test("Serializes when both schemas misses serializer", t => { ]) try { - let _ = %raw(`null`)->S.reverseConvertOrThrow(schema) + let _ = %raw(`null`)->S.decodeOrThrow(~from=schema, ~to=S.unknown) t->Assert.fail("Expected to throw") } catch { - | S.Error(error) => + | S.Exn(error) => t->Assert.is( error.message, - `Failed parsing: Expected unknown | unknown, received null + `Expected unknown | unknown, received null - The S.transform serializer is missing`, ) } @@ -119,17 +117,17 @@ test("Serializes when both schemas misses serializer", t => { test("When union of json and string schemas, should parse the first one", t => { let schema = S.union([S.json->S.shape(_ => #json), S.string->S.shape(_ => #str)]) - t->Assert.deepEqual(%raw(`"string"`)->S.parseOrThrow(schema), #json) + t->Assert.deepEqual(%raw(`"string"`)->S.parseOrThrow(~to=schema), #json) t->U.assertThrowsMessage( - () => %raw(`undefined`)->S.parseOrThrow(schema), - `Failed parsing: Expected JSON | string, received undefined + () => %raw(`undefined`)->S.parseOrThrow(~to=schema), + `Expected JSON | string, received undefined - Expected JSON, received undefined`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{try{let v0=e[0](i);i="json"}catch(e0){if(typeof i==="string"){i="str"}else{e[1](i,e0)}}return i}`, + `i=>{try{e[0](i);i="json"}catch(e0){if(typeof i==="string"){i="str"}else{e[1](i,e0)}}return i}`, ) }) @@ -138,39 +136,43 @@ test("Ensures parsing order with unknown schema", t => { S.string->S.length(2), S.bool->Obj.magic, // Should be checked before unknown S.unknown->S.transform(_ => {parser: _ => "pass"}), - // TODO: Should disabled deopt at this point S.float->Obj.magic, S.bigint->Obj.magic, ]) - t->Assert.deepEqual(%raw(`"string"`)->S.parseOrThrow(schema), "pass") - t->Assert.deepEqual(%raw(`"to"`)->S.parseOrThrow(schema), "to") - t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(schema), %raw(`true`)) + t->Assert.deepEqual(%raw(`"string"`)->S.parseOrThrow(~to=schema), "pass") + t->Assert.deepEqual(%raw(`"to"`)->S.parseOrThrow(~to=schema), "to") + t->Assert.deepEqual(%raw(`true`)->S.parseOrThrow(~to=schema), %raw(`true`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{try{if(typeof i!=="string"){e[0](i)}if(i.length!==e[1]){e[2]()}}catch(e0){try{if(typeof i!=="boolean"){e[3](i)}}catch(e1){try{i=e[4](i)}catch(e2){if(!(typeof i==="number"&&!Number.isNaN(i)||typeof i==="bigint")){e[5](i,e0,e1,e2)}}}}return i}`, + `i=>{try{typeof i==="string"||e[2](i);if(i.length!==e[0]){e[1]()}}catch(e2){try{typeof i==="boolean"||e[3](i);}catch(e3){try{let v0;try{v0=e[4](i)}catch(x){e[5](x)}i=v0}catch(e4){if(!(typeof i==="number"&&!Number.isNaN(i)||typeof i==="bigint")){e[6](i,e2,e3,e4)}}}}return i}`, ) }) test("Parses when second schema misses parser", t => { let schema = S.union([S.literal(#apple), S.string->S.transform(_ => {serializer: _ => "apple"})]) - t->Assert.deepEqual("apple"->S.parseOrThrow(schema), #apple) + t->Assert.deepEqual("apple"->S.parseOrThrow(~to=schema), #apple) + t->U.assertThrowsMessage( + () => "foo"->S.parseOrThrow(~to=schema), + `Expected "apple" | string, received "foo" +- The S.transform parser is missing`, + ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){if(!(i==="apple")){try{throw e[0]}catch(e1){e[1](i,e1)}}}else{e[2](i)}return i}`, + `i=>{if(typeof i==="string"){if(!(i==="apple")){e[1](i,e[0])}}else{e[2](i)}return i}`, ) }) test("Parses with string catch all", t => { let schema = S.union([S.literal("apple"), S.string, S.literal("banana")]) - t->Assert.deepEqual("apple"->S.parseOrThrow(schema), "apple") - t->Assert.deepEqual("foo"->S.parseOrThrow(schema), "foo") + t->Assert.deepEqual("apple"->S.parseOrThrow(~to=schema), "apple") + t->Assert.deepEqual("foo"->S.parseOrThrow(~to=schema), "foo") t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(!(typeof i==="string")){e[0](i)}return i}`) }) @@ -178,12 +180,18 @@ test("Parses with string catch all", t => { test("Serializes when second struct misses serializer", t => { let schema = S.union([S.literal(#apple), S.string->S.transform(_ => {parser: _ => #apple})]) - t->Assert.deepEqual(#apple->S.reverseConvertOrThrow(schema), %raw(`"apple"`)) + t->Assert.deepEqual(#apple->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"apple"`)) + t->U.assertThrowsMessage( + () => #orange->S.decodeOrThrow(~from=schema, ~to=S.unknown), + `Expected "apple" | unknown, received "orange" +- Expected "apple", received "orange" +- The S.transform serializer is missing`, + ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{try{if(i!=="apple"){e[0](i)}}catch(e0){try{throw e[1]}catch(e1){e[2](i,e0,e1)}}return i}`, + `i=>{try{if(typeof i!=="string"||!(i==="apple")){e[0](i)}}catch(e1){try{throw e[1]}catch(e2){e[2](i,e1,e2)}}return i}`, ) }) @@ -223,7 +231,7 @@ module Advanced = { %raw(`{ "kind": "circle", "radius": 1, - }`)->S.parseOrThrow(shapeSchema), + }`)->S.parseOrThrow(~to=shapeSchema), Circle({radius: 1.}), ) }) @@ -233,7 +241,7 @@ module Advanced = { %raw(`{ "kind": "square", "x": 2, - }`)->S.parseOrThrow(shapeSchema), + }`)->S.parseOrThrow(~to=shapeSchema), Square({x: 2.}), ) }) @@ -244,7 +252,7 @@ module Advanced = { "kind": "triangle", "x": 2, "y": 3, - }`)->S.parseOrThrow(shapeSchema), + }`)->S.parseOrThrow(~to=shapeSchema), Triangle({x: 2., y: 3.}), ) }) @@ -256,16 +264,10 @@ module Advanced = { "y": 3, }`) - let error: U.errorPayload = { - code: InvalidType({ - expected: shapeSchema->S.castToUnknown, - received: shape->Obj.magic, - }), - operation: Parse, - path: S.Path.empty, - } - - t->U.assertThrows(() => shape->S.parseOrThrow(shapeSchema), error) + t->U.assertThrowsMessage( + () => shape->S.parseOrThrow(~to=shapeSchema), + `Expected { kind: "circle"; radius: number; } | { kind: "square"; x: number; } | { kind: "triangle"; x: number; y: number; }, received { kind: "oval"; x: 2; y: 3; }`, + ) }) test("Fails to parse with unknown kind when the union is an object field", t => { @@ -280,43 +282,26 @@ module Advanced = { "field": shape, } - let error: U.errorPayload = { - code: InvalidType({ - expected: shapeSchema->S.castToUnknown, - received: shape->Obj.magic, - }), - operation: Parse, - path: S.Path.fromLocation("field"), - } - t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["field"];if(typeof v0==="object"&&v0){if(v0["kind"]==="circle"){let v1=v0["radius"];if(typeof v1!=="number"||Number.isNaN(v1)){e[1](v1)}v0={"TAG":"Circle","radius":v1,}}else if(v0["kind"]==="square"){let v2=v0["x"];if(typeof v2!=="number"||Number.isNaN(v2)){e[2](v2)}v0={"TAG":"Square","x":v2,}}else if(v0["kind"]==="triangle"){let v3=v0["x"],v4=v0["y"];if(typeof v3!=="number"||Number.isNaN(v3)){e[3](v3)}if(typeof v4!=="number"||Number.isNaN(v4)){e[4](v4)}v0={"TAG":"Triangle","x":v3,"y":v4,}}else{e[5](v0)}}else{e[6](v0)}return v0}`, + `i=>{typeof i==="object"&&i||e[6](i);let v0=i["field"];if(typeof v0==="object"&&v0&&!Array.isArray(v0)){if(v0["kind"]==="circle"){let v1=v0["radius"];typeof v1==="number"&&!Number.isNaN(v1)||e[0](v1);v0={"TAG":"Circle","radius":v1,}}else if(v0["kind"]==="square"){let v2=v0["x"];typeof v2==="number"&&!Number.isNaN(v2)||e[1](v2);v0={"TAG":"Square","x":v2,}}else if(v0["kind"]==="triangle"){let v3=v0["x"],v4=v0["y"];typeof v3==="number"&&!Number.isNaN(v3)||e[2](v3);typeof v4==="number"&&!Number.isNaN(v4)||e[3](v4);v0={"TAG":"Triangle","x":v3,"y":v4,}}else{e[4](v0)}}else{e[5](v0)}return v0}`, ) - t->U.assertThrows(() => data->S.parseOrThrow(schema), error) - t->Assert.is( - (error->U.error).message, - `Failed parsing at ["field"]: Expected { kind: "circle"; radius: number; } | { kind: "square"; x: number; } | { kind: "triangle"; x: number; y: number; }, received { kind: "oval"; x: 2; y: 3; }`, + t->U.assertThrowsMessage( + () => data->S.parseOrThrow(~to=schema), + `Failed at ["field"]: Expected { kind: "circle"; radius: number; } | { kind: "square"; x: number; } | { kind: "triangle"; x: number; y: number; }, received { kind: "oval"; x: 2; y: 3; }`, ) }) test("Fails to parse with invalid data type", t => { - t->U.assertThrows( - () => %raw(`"Hello world!"`)->S.parseOrThrow(shapeSchema), - { - code: InvalidType({ - expected: shapeSchema->S.castToUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }, + t->U.assertThrowsMessage( + () => %raw(`"Hello world!"`)->S.parseOrThrow(~to=shapeSchema), + `Expected { kind: "circle"; radius: number; } | { kind: "square"; x: number; } | { kind: "triangle"; x: number; y: number; }, received "Hello world!"`, ) }) - test("Passes through not defined item on converting without type validation", t => { + test("Performs exhaustiveness check on converting without type validation", t => { let incompleteSchema = S.union([ S.object(s => { s.tag("kind", "circle") @@ -334,13 +319,15 @@ module Advanced = { let v = Triangle({x: 2., y: 3.}) - // This is not valid but expected behavior. Use parse to ensure type validation - t->Assert.is(v->S.reverseConvertOrThrow(incompleteSchema), v->Obj.magic) + t->U.assertThrowsMessage( + () => v->S.decodeOrThrow(~from=incompleteSchema, ~to=S.unknown), + `Expected { TAG: "Circle"; radius: number; } | { TAG: "Square"; x: number; }, received { TAG: "Triangle"; x: 2; y: 3; }`, + ) }) test("Successfully serializes Circle shape", t => { t->Assert.deepEqual( - Circle({radius: 1.})->S.reverseConvertOrThrow(shapeSchema), + Circle({radius: 1.})->S.decodeOrThrow(~from=shapeSchema, ~to=S.unknown), %raw(`{ "kind": "circle", "radius": 1, @@ -350,7 +337,7 @@ module Advanced = { test("Successfully serializes Square shape", t => { t->Assert.deepEqual( - Square({x: 2.})->S.reverseConvertOrThrow(shapeSchema), + Square({x: 2.})->S.decodeOrThrow(~from=shapeSchema, ~to=S.unknown), %raw(`{ "kind": "square", "x": 2, @@ -360,7 +347,7 @@ module Advanced = { test("Successfully serializes Triangle shape", t => { t->Assert.deepEqual( - Triangle({x: 2., y: 3.})->S.reverseConvertOrThrow(shapeSchema), + Triangle({x: 2., y: 3.})->S.decodeOrThrow(~from=shapeSchema, ~to=S.unknown), %raw(`{ "kind": "triangle", "x": 2, @@ -373,7 +360,7 @@ module Advanced = { t->U.assertCompiledCode( ~schema=shapeSchema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){if(i["kind"]==="circle"){let v0=i["radius"];if(typeof v0!=="number"||Number.isNaN(v0)){e[0](v0)}i={"TAG":"Circle","radius":v0,}}else if(i["kind"]==="square"){let v1=i["x"];if(typeof v1!=="number"||Number.isNaN(v1)){e[1](v1)}i={"TAG":"Square","x":v1,}}else if(i["kind"]==="triangle"){let v2=i["x"],v3=i["y"];if(typeof v2!=="number"||Number.isNaN(v2)){e[2](v2)}if(typeof v3!=="number"||Number.isNaN(v3)){e[3](v3)}i={"TAG":"Triangle","x":v2,"y":v3,}}else{e[4](i)}}else{e[5](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["kind"]==="circle"){let v0=i["radius"];typeof v0==="number"&&!Number.isNaN(v0)||e[0](v0);i={"TAG":"Circle","radius":v0,}}else if(i["kind"]==="square"){let v1=i["x"];typeof v1==="number"&&!Number.isNaN(v1)||e[1](v1);i={"TAG":"Square","x":v1,}}else if(i["kind"]==="triangle"){let v2=i["x"],v3=i["y"];typeof v2==="number"&&!Number.isNaN(v2)||e[2](v2);typeof v3==="number"&&!Number.isNaN(v3)||e[3](v3);i={"TAG":"Triangle","x":v2,"y":v3,}}else{e[4](i)}}else{e[5](i)}return i}`, ) }) @@ -381,7 +368,8 @@ module Advanced = { t->U.assertCompiledCode( ~schema=shapeSchema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i){if(i["TAG"]==="Circle"){i={"kind":"circle","radius":i["radius"],}}else if(i["TAG"]==="Square"){i={"kind":"square","x":i["x"],}}else if(i["TAG"]==="Triangle"){i={"kind":"triangle","x":i["x"],"y":i["y"],}}}return i}`, + // TODO: Can be optimized + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["TAG"]==="Circle"){let v0=i["radius"];typeof v0==="number"&&!Number.isNaN(v0)||e[0](v0);i={"kind":"circle","radius":v0,}}else if(i["TAG"]==="Square"){let v1=i["x"];typeof v1==="number"&&!Number.isNaN(v1)||e[1](v1);i={"kind":"square","x":v1,}}else if(i["TAG"]==="Triangle"){let v2=i["x"],v3=i["y"];typeof v2==="number"&&!Number.isNaN(v2)||e[2](v2);typeof v3==="number"&&!Number.isNaN(v3)||e[3](v3);i={"kind":"triangle","x":v2,"y":v3,}}else{e[4](i)}}else{e[5](i)}return i}`, ) }) } @@ -397,8 +385,8 @@ test("NaN should be checked before number even if it's later item in the union", S.literal(%raw(`NaN`))->S.shape(_ => None), ]) - t->Assert.deepEqual(%raw(`NaN`)->S.parseOrThrow(schema), None) - t->Assert.deepEqual(1.->S.parseOrThrow(schema), Some(1.)) + t->Assert.deepEqual(%raw(`NaN`)->S.parseOrThrow(~to=schema), None) + t->Assert.deepEqual(1.->S.parseOrThrow(~to=schema), Some(1.)) t->U.assertCompiledCode( ~schema, @@ -415,13 +403,13 @@ test("Array should be checked before object even if it's later item in the union let schema = S.union([S.object(s => [s.field("foo", S.string)]), S.array(S.string)]) - t->Assert.deepEqual(%raw(`["baz"]`)->S.parseOrThrow(schema), ["baz"]) - t->Assert.deepEqual(%raw(`{"foo": "bar"}`)->S.parseOrThrow(schema), ["bar"]) + t->Assert.deepEqual(%raw(`["baz"]`)->S.parseOrThrow(~to=schema), ["baz"]) + t->Assert.deepEqual(%raw(`{"foo": "bar"}`)->S.parseOrThrow(~to=schema), ["bar"]) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(Array.isArray(i)){for(let v0=0;v0{if(Array.isArray(i)){for(let v0=0;v0Obj.magic, ]) - t->Assert.deepEqual(%raw(`new Set(["baz"])`)->S.parseOrThrow(schema), %raw(`new Set(["baz"])`)) - t->Assert.deepEqual(%raw(`{"foo": "bar"}`)->S.parseOrThrow(schema), ["bar"]) + t->Assert.deepEqual(%raw(`new Set(["baz"])`)->S.parseOrThrow(~to=schema), %raw(`new Set(["baz"])`)) + t->Assert.deepEqual(%raw(`{"foo": "bar"}`)->S.parseOrThrow(~to=schema), ["bar"]) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(i instanceof e[0]){}else if(typeof i==="object"&&i){let v0=i["foo"];if(typeof v0!=="string"){e[1](v0)}i=[v0,]}else{e[2](i)}return i}`, + `i=>{if(i instanceof e[0]){}else if(typeof i==="object"&&i&&!Array.isArray(i)){let v0=i["foo"];typeof v0==="string"||e[1](v0);i=[v0,]}else{e[2](i)}return i}`, ) }) @@ -467,19 +455,20 @@ test("Successfully serializes unboxed variant", t => { let toString = S.string->S.shape(s => String(s)) let schema = S.union([toInt, toString]) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), Int(123)) - t->Assert.deepEqual(String("abc")->S.reverseConvertOrThrow(schema), %raw(`"abc"`)) - t->Assert.deepEqual(Int(123)->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), Int(123)) + t->Assert.deepEqual("abc"->S.parseOrThrow(~to=schema), String("abc")) + t->Assert.deepEqual(String("abc")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"abc"`)) + t->Assert.deepEqual(Int(123)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="string"){try{i=e[0](i)}catch(e0){e[1](i,e0)}}else{e[2](i)}return i}`, + `i=>{if(typeof i==="string"){try{let v0;try{v0=e[0](i)}catch(x){e[1](x)}i=v0}catch(e0){}}else{e[2](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{try{let v0=e[0](i);if(typeof v0!=="string"){e[1](v0)}i=v0}catch(e0){}return i}`, + `i=>{try{let v0;try{v0=e[0](i)}catch(x){e[1](x)}typeof v0==="string"||e[2](v0);i=v0}catch(e0){if(!(typeof i==="string")){e[3](i,e0)}}return i}`, ) // The same, but toString schema is the first @@ -487,15 +476,15 @@ test("Successfully serializes unboxed variant", t => { // since it's the second let schema = S.union([toString, toInt]) - t->Assert.deepEqual("123"->S.parseOrThrow(schema), String("123")) - t->Assert.deepEqual(String("abc")->S.reverseConvertOrThrow(schema), %raw(`"abc"`)) - t->Assert.deepEqual(Int(123)->S.reverseConvertOrThrow(schema), %raw(`"123"`)) + t->Assert.deepEqual("123"->S.parseOrThrow(~to=schema), String("123")) + t->Assert.deepEqual(String("abc")->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"abc"`)) + t->Assert.deepEqual(Int(123)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`"123"`)) t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{if(!(typeof i==="string")){e[0](i)}return i}`) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{try{if(typeof i!=="string"){e[0](i)}}catch(e0){try{let v0=e[1](i);if(typeof v0!=="string"){e[2](v0)}i=v0}catch(e1){e[3](i,e0,e1)}}return i}`, + `i=>{try{typeof i==="string"||e[0](i);}catch(e1){try{let v0;try{v0=e[1](i)}catch(x){e[2](x)}typeof v0==="string"||e[3](v0);i=v0}catch(e2){e[4](i,e1,e2)}}return i}`, ) }) @@ -505,15 +494,23 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(!(typeof i==="number"&&(i===0||i===1))){e[0](i)}return i}`, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)&&(i===0||i===1))){e[0](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseParse, - `i=>{if(!(typeof i==="number"&&(i===0||i===1))){e[0](i)}return i}`, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)&&(i===0||i===1))){e[0](i)}return i}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#Convert, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)&&(i===0||i===1))){e[0](i)}return i}`, + ) + t->U.assertCompiledCode( + ~schema, + ~op=#ReverseConvert, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)&&(i===0||i===1))){e[0](i)}return i}`, ) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#Convert) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) }) asyncTest("Compiled async parse code snapshot", async t => { @@ -525,19 +522,19 @@ asyncTest("Compiled async parse code snapshot", async t => { t->U.assertCompiledCode( ~schema, ~op=#ParseAsync, - `i=>{if(typeof i==="number"){if(i===0){i=e[0](i)}else if(!(i===1)){e[1](i)}}else{e[2](i)}return Promise.resolve(i)}`, + `i=>{if(typeof i==="number"&&!Number.isNaN(i)){if(i===0){let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}else if(!(i===1)){e[2](i)}}else{e[3](i)}return Promise.resolve(i)}`, ) t->U.assertCompiledCode( ~schema, ~op=#ConvertAsync, - `i=>{if(typeof i==="number"){if(i===0){i=e[0](i)}}return Promise.resolve(i)}`, + `i=>{if(typeof i==="number"&&!Number.isNaN(i)){if(i===0){let v0;try{v0=e[0](i).catch(x=>e[1](x))}catch(x){e[1](x)}i=v0}else if(!(i===1)){e[2](i)}}else{e[3](i)}return Promise.resolve(i)}`, ) - t->Assert.deepEqual(await 1->S.parseAsyncOrThrow(schema), 1) + t->Assert.deepEqual(await 1->S.parseAsyncOrThrow(~to=schema), 1) t->Assert.throws( - () => 2->S.parseAsyncOrThrow(schema), + () => 2->S.parseAsyncOrThrow(~to=schema), ~expectations={ - message: "Failed async parsing: Expected 0 | 1, received 2", + message: "Expected 0 | 1, received 2", }, ) }) @@ -547,7 +544,7 @@ test("Union with nested variant", t => { S.schema(s => { "foo": { - "tag": #Null(s.matches(S.null(S.string))), + "tag": #Null(s.matches(S.nullAsOption(S.string))), }, } ), @@ -565,15 +562,15 @@ test("Union with nested variant", t => { "foo": { "tag": #Null(None), }, - }->S.reverseConvertOrThrow(schema), + }->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"foo":{"tag":{"NAME":"Null","VAL":null}}}`), ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - // TODO: Can make it work without the second case since it doesn't do anything besides i=i - `i=>{if(typeof i==="object"&&i){if(typeof i["foo"]==="object"&&i["foo"]&&typeof i["foo"]["tag"]==="object"&&i["foo"]["tag"]&&i["foo"]["tag"]["NAME"]==="Null"){let v0=i["foo"];let v1=v0["tag"];let v2=v1["VAL"];if(v2===void 0){v2=null}i={"foo":{"tag":{"NAME":"Null","VAL":v2,},},}}else if(typeof i["foo"]==="object"&&i["foo"]&&typeof i["foo"]["tag"]==="object"&&i["foo"]["tag"]&&i["foo"]["tag"]["NAME"]==="Option"){let v3=i["foo"];let v4=v3["tag"];}}return i}`, + // TODO: Can optimize it + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){try{let v0=i["foo"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["tag"];typeof v1==="object"&&v1&&v1["NAME"]==="Null"||e[1](v1);let v2=v1["VAL"];if(v2===void 0){v2=null}else if(!(typeof v2==="string")){e[0](v2)}i={"foo":{"tag":{"NAME":v1["NAME"],"VAL":v2,},},}}catch(e0){try{let v3=i["foo"];typeof v3==="object"&&v3||e[5](v3);let v4=v3["tag"];typeof v4==="object"&&v4&&v4["NAME"]==="Option"||e[4](v4);let v5=v4["VAL"];if(!(typeof v5==="string"||v5===void 0)){e[3](v5)}i={"foo":{"tag":{"NAME":v4["NAME"],"VAL":v5,},},}}catch(e1){e[6](i,e0,e1)}}}else{e[7](i)}return i}`, ) }) @@ -587,44 +584,38 @@ test("Nested union doesn't mutate the input", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="object"||!i){e[0](i)}let v0=i["foo"];if(typeof v0==="boolean"){v0=""+v0}else if(!(typeof v0==="string")){e[1](v0)}return {"foo":v0,}}`, + // FIXME: i["foo"] shouldn't be duplicated + `i=>{typeof i==="object"&&i||e[1](i);let v0=i["foo"];if(typeof v0==="boolean"){v0=""+i["foo"]}else if(!(typeof v0==="string")){e[0](v0)}return {"foo":v0,}}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - `i=>{let v0=i["foo"];if(typeof v0==="boolean"){v0=""+i["foo"]}return {"foo":v0,}}`, + `i=>{let v0=i["foo"];if(typeof v0==="boolean"){v0=""+i["foo"]}else if(!(typeof v0==="string")){e[0](v0)}return {"foo":v0,}}`, ) }) -test("Compiled serialize code snapshot", t => { - let schema = S.union([S.literal(0), S.literal(1)]) - - t->U.assertCompiledCodeIsNoop(~schema, ~op=#Convert) - t->U.assertCompiledCodeIsNoop(~schema, ~op=#ReverseConvert) -}) - test("Compiled serialize code snapshot of objects returning literal fields", t => { let schema = S.union([ S.object(s => s.field("foo", S.literal(0))), S.object(s => s.field("bar", S.literal(1))), ]) - t->Assert.deepEqual(1->S.reverseConvertOrThrow(schema), %raw(`{"bar":1}`)) + t->Assert.deepEqual(1->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"bar":1}`)) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="number"){if(i===0){i={"foo":i,}}else if(i===1){i={"bar":i,}}}return i}`, + `i=>{if(typeof i==="number"&&!Number.isNaN(i)){if(i===0){i={"foo":i,}}else if(i===1){i={"bar":i,}}else{e[0](i)}}else{e[1](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#Convert, - `i=>{if(typeof i==="object"&&i){if(i["foo"]===0){i=0}else if(i["bar"]===1){i=1}}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["foo"]===0){i=i["foo"]}else if(i["bar"]===1){i=i["bar"]}else{e[0](i)}}else{e[1](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){if(i["foo"]===0){i=0}else if(i["bar"]===1){i=1}else{e[0](i)}}else{e[1](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["foo"]===0){i=i["foo"]}else if(i["bar"]===1){i=i["bar"]}else{e[0](i)}}else{e[1](i)}return i}`, ) }) @@ -633,7 +624,7 @@ test("Enum is a shorthand for union", t => { }) test("Reverse schema with items", t => { - let schema = S.union([S.literal(%raw(`0`)), S.null(S.bool)]) + let schema = S.union([S.literal(%raw(`0`)), S.nullAsOption(S.bool)]) t->U.assertEqualSchemas( schema->S.reverse, @@ -642,7 +633,7 @@ test("Reverse schema with items", t => { }) test("Succesfully uses reversed schema for parsing back to initial value", t => { - let schema = S.union([S.literal(%raw(`0`)), S.null(S.bool)]) + let schema = S.union([S.literal(%raw(`0`)), S.nullAsOption(S.bool)]) t->U.assertReverseParsesBack(schema, None) }) @@ -718,8 +709,8 @@ module CrazyUnion = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0=e[0](i);return v0} -Crazy: i=>{if(typeof i==="object"&&i){if(i["type"]==="A"&&Array.isArray(i["nested"])){let v0=i["nested"],v5=new Array(v0.length);for(let v1=0;v1{let v0;v0=e[0](i);return v0} +Crazy: i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["type"]==="A"){let v0=i["nested"];Array.isArray(v0)||e[1](v0);let v4=new Array(v0.length);for(let v1=0;v1Crazy--0"](v0[v1]);v4[v1]=v2}catch(v3){v3.path="[\\"nested\\"]"+'["'+v1+'"]'+v3.path;throw v3}}i={"TAG":"A","_0":v4,}}else if(i["type"]==="Z"){let v5=i["nested"];Array.isArray(v5)||e[3](v5);let v9=new Array(v5.length);for(let v6=0;v6Crazy--0"](v5[v6]);v9[v6]=v7}catch(v8){v8.path="[\\"nested\\"]"+'["'+v6+'"]'+v8.path;throw v8}}i={"TAG":"Z","_0":v9,}}else{e[4](i)}}else if(!(typeof i==="string"&&(i==="B"||i==="C"||i==="D"||i==="E"||i==="F"||i==="G"||i==="H"||i==="I"||i==="J"||i==="K"||i==="L"||i==="M"||i==="N"||i==="O"||i==="P"||i==="Q"||i==="R"||i==="S"||i==="T"||i==="U"||i==="V"||i==="W"||i==="X"||i==="Y"))){e[5](i)}return i}`, ) }) @@ -760,7 +751,7 @@ test("json-rpc response", t => { "jsonrpc": "2.0", "id": 1, "result": ["foo", "bar"] - }`)->S.parseOrThrow(getLogsResponseSchema), + }`)->S.parseOrThrow(~to=getLogsResponseSchema), Ok(["foo", "bar"]), ) @@ -771,7 +762,7 @@ test("json-rpc response", t => { "error": { "message": "NotFound" } - }`)->S.parseOrThrow(getLogsResponseSchema), + }`)->S.parseOrThrow(~to=getLogsResponseSchema), Error(#LogsNotFound), ) @@ -783,9 +774,21 @@ test("json-rpc response", t => { "message": "Invalid", "data": "foo" } - }`)->S.parseOrThrow(getLogsResponseSchema), + }`)->S.parseOrThrow(~to=getLogsResponseSchema), Error(#InvalidData("foo")), ) + + t->U.assertCompiledCode( + ~schema=getLogsResponseSchema, + ~op=#Parse, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){try{let v0=i["result"];if(!Array.isArray(v0)){e[1](v0)}for(let v1=0;v1U.assertCompiledCode( + ~schema=getLogsResponseSchema, + ~op=#ReverseConvert, + // FIXME: Exhaustive check doesn't work + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["TAG"]==="Ok"){let v0=i["_0"];if(!Array.isArray(v0)){e[1](v0)}for(let v1=0;v1 { @@ -805,19 +808,19 @@ test("Issue https://github.com/DZakh/rescript-schema/issues/101", t => { t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i){if(i["NAME"]==="request"&&typeof i["VAL"]==="object"&&i["VAL"]){let v0=i["VAL"];}else if(i["NAME"]==="response"&&typeof i["VAL"]==="object"&&i["VAL"]){let v1=i["VAL"];let v2=v1["response"];i={"NAME":"response","VAL":{"collectionName":v1["collectionName"],"response":v2,},}}}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["NAME"]==="request"){let v0=i["VAL"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["collectionName"];typeof v1==="string"||e[0](v1);i={"NAME":i["NAME"],"VAL":{"collectionName":v1,},}}else if(i["NAME"]==="response"){let v2=i["VAL"];typeof v2==="object"&&v2||e[4](v2);let v3=v2["collectionName"],v4=v2["response"];typeof v3==="string"||e[2](v3);if(!(typeof v4==="string"&&(v4==="accepted"||v4==="rejected"))){e[3](v4)}i={"NAME":i["NAME"],"VAL":{"collectionName":v3,"response":v4,},}}else{e[5](i)}}else{e[6](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){if(i["NAME"]==="request"&&typeof i["VAL"]==="object"&&i["VAL"]){let v0=i["VAL"],v1=v0["collectionName"];if(typeof v1!=="string"){e[0](v1)}i={"NAME":"request","VAL":{"collectionName":v1,},}}else if(i["NAME"]==="response"&&typeof i["VAL"]==="object"&&i["VAL"]){let v2=i["VAL"],v3=v2["collectionName"];if(typeof v3!=="string"){e[1](v3)}let v4=v2["response"];if(!(typeof v4==="string"&&(v4==="accepted"||v4==="rejected"))){e[2](v4)}i={"NAME":"response","VAL":{"collectionName":v3,"response":v4,},}}else{e[3](i)}}else{e[4](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["NAME"]==="request"){let v0=i["VAL"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["collectionName"];typeof v1==="string"||e[0](v1);i={"NAME":i["NAME"],"VAL":{"collectionName":v1,},}}else if(i["NAME"]==="response"){let v2=i["VAL"];typeof v2==="object"&&v2||e[4](v2);let v3=v2["collectionName"],v4=v2["response"];typeof v3==="string"||e[2](v3);if(!(typeof v4==="string"&&(v4==="accepted"||v4==="rejected"))){e[3](v4)}i={"NAME":i["NAME"],"VAL":{"collectionName":v3,"response":v4,},}}else{e[5](i)}}else{e[6](i)}return i}`, ) t->Assert.deepEqual( #response({ "collectionName": "foo", "response": "accepted", - })->S.reverseConvertOrThrow(schema), + })->S.decodeOrThrow(~from=schema, ~to=S.unknown), #response({ "collectionName": "foo", "response": "accepted", @@ -828,15 +831,24 @@ test("Issue https://github.com/DZakh/rescript-schema/issues/101", t => { test("Regression https://github.com/DZakh/sury/issues/121", t => { let schema = S.union([S.literal(%raw(`null`))->S.castToUnknown, S.unknown]) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{try{if(i!==null){e[0](i)}}catch(e0){}return i}`) + t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{try{i===null||e[0](i);}catch(e1){}return i}`) let data = %raw(`{a: 'hey'}`) - t->Assert.deepEqual(data->S.parseOrThrow(schema), data) - t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(schema), %raw(`null`)) + t->Assert.deepEqual(data->S.parseOrThrow(~to=schema), data) + t->Assert.deepEqual(%raw(`null`)->S.parseOrThrow(~to=schema), %raw(`null`)) }) test("Union of strings with different refinements", t => { - let schema = S.union([S.string->S.email, S.string->S.url]) + S.enableEmail() + S.enableUrl() + let schema = S.union([S.email, S.url]) + + t->U.assertThrowsMessage( + () => %raw(`"123"`)->S.parseOrThrow(~to=schema), + `Expected email | url, received "123" +- Invalid email address +- Invalid url`, + ) t->U.assertCompiledCode( ~schema, @@ -857,11 +869,11 @@ test("Objects with the same discriminant", t => { }), ]) - t->Assert.deepEqual(%raw(`{"type":"A","value":"foo"}`)->S.parseOrThrow(schema), Ok("foo")) - t->Assert.deepEqual(%raw(`{"type":"A","value":"baz"}`)->S.parseOrThrow(schema), Error("baz")) + t->Assert.deepEqual(%raw(`{"type":"A","value":"foo"}`)->S.parseOrThrow(~to=schema), Ok("foo")) + t->Assert.deepEqual(%raw(`{"type":"A","value":"baz"}`)->S.parseOrThrow(~to=schema), Error("baz")) t->U.assertThrowsMessage( - () => %raw(`{"type":"A","value":1}`)->S.parseOrThrow(schema), - `Failed parsing: Expected { type: "A"; value: "foo" | "bar"; } | { type: "A"; value: string; }, received { type: "A"; value: 1; } + () => %raw(`{"type":"A","value":1}`)->S.parseOrThrow(~to=schema), + `Expected { type: "A"; value: "foo" | "bar"; } | { type: "A"; value: string; }, received { type: "A"; value: 1; } - At ["value"]: Expected "foo" | "bar", received 1 - At ["value"]: Expected string, received 1`, ) @@ -869,7 +881,7 @@ test("Objects with the same discriminant", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){if(i["type"]==="A"){try{let v0=i["value"];if(!(typeof v0==="string"&&(v0==="foo"||v0==="bar"))){e[0](v0)}i={"TAG":"Ok","_0":v0,}}catch(e0){try{let v1=i["value"];if(typeof v1!=="string"){e[1](v1)}i={"TAG":"Error","_0":v1,}}catch(e1){e[2](i,e0,e1)}}}else{e[3](i)}}else{e[4](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["type"]==="A"){try{let v0=i["value"];if(!(typeof v0==="string"&&(v0==="foo"||v0==="bar"))){e[0](v0)}i={"TAG":"Ok","_0":v0,}}catch(e0){try{let v1=i["value"];typeof v1==="string"||e[1](v1);i={"TAG":"Error","_0":v1,}}catch(e1){e[2](i,e0,e1)}}}else{e[3](i)}}else{e[4](i)}return i}`, ) }) @@ -910,13 +922,13 @@ module CknittelBugReport = { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i==="object"&&i){if(typeof i["payload"]==="object"&&i["payload"]){try{let v0=i["payload"];let v1=v0["a"];if(!(typeof v1==="string"||v1===void 0)){e[0](v1)}i={"TAG":"A","_0":{"payload":{"a":v1,},},}}catch(e0){try{let v2=i["payload"];let v3=v2["b"];if(!(typeof v3==="number"&&v3<2147483647&&v3>-2147483648&&v3%1===0||v3===void 0)){e[1](v3)}i={"TAG":"B","_0":{"payload":{"b":v3,},},}}catch(e1){e[2](i,e0,e1)}}}else{e[3](i)}}else{e[4](i)}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){try{let v0=i["payload"];typeof v0==="object"&&v0||e[1](v0);let v1=v0["a"];if(!(typeof v1==="string"||v1===void 0)){e[0](v1)}i={"TAG":"A","_0":{"payload":{"a":v1,},},}}catch(e0){try{let v2=i["payload"];typeof v2==="object"&&v2||e[3](v2);let v3=v2["b"];if(!(typeof v3==="number"&&!Number.isNaN(v3)&&(v3<=2147483647&&v3>=-2147483648&&v3%1===0)||v3===void 0)){e[2](v3)}i={"TAG":"B","_0":{"payload":{"b":v3,},},}}catch(e1){e[4](i,e0,e1)}}}else{e[5](i)}return i}`, ) t->U.assertCompiledCode( ~schema, ~op=#ReverseConvert, - `i=>{if(typeof i==="object"&&i){if(i["TAG"]==="A"&&typeof i["_0"]==="object"&&i["_0"]&&typeof i["_0"]["payload"]==="object"&&i["_0"]["payload"]){let v0=i["_0"];let v1=v0["payload"];i=v0}else if(i["TAG"]==="B"&&typeof i["_0"]==="object"&&i["_0"]&&typeof i["_0"]["payload"]==="object"&&i["_0"]["payload"]){let v2=i["_0"];let v3=v2["payload"];i=v2}}return i}`, + `i=>{if(typeof i==="object"&&i&&!Array.isArray(i)){if(i["TAG"]==="A"){let v0=i["_0"];typeof v0==="object"&&v0||e[2](v0);let v1=v0["payload"];typeof v1==="object"&&v1||e[1](v1);let v2=v1["a"];if(!(typeof v2==="string"||v2===void 0)){e[0](v2)}i={"payload":{"a":v2,},}}else if(i["TAG"]==="B"){let v3=i["_0"];typeof v3==="object"&&v3||e[5](v3);let v4=v3["payload"];typeof v4==="object"&&v4||e[4](v4);let v5=v4["b"];if(!(typeof v5==="number"&&!Number.isNaN(v5)&&(v5<=2147483647&&v5>=-2147483648&&v5%1===0)||v5===void 0)){e[3](v5)}i={"payload":{"b":v5,},}}else{e[6](i)}}else{e[7](i)}return i}`, ) let x = { @@ -924,12 +936,35 @@ module CknittelBugReport = { b: 42, }, } - t->Assert.deepEqual(B(x)->S.reverseConvertOrThrow(schema), %raw(`{"payload":{"b":42}}`)) + t->Assert.deepEqual(B(x)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"payload":{"b":42}}`)) let x = { A.payload: { a: "foo", }, } - t->Assert.deepEqual(A(x)->S.reverseConvertOrThrow(schema), %raw(`{"payload":{"a":"foo"}}`)) + t->Assert.deepEqual(A(x)->S.decodeOrThrow(~from=schema, ~to=S.unknown), %raw(`{"payload":{"a":"foo"}}`)) }) } + +test("Optional of int32 should keep a format validation", t => { + let schema = S.option(S.int) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{if(!(typeof i==="number"&&!Number.isNaN(i)&&(i<=2147483647&&i>=-2147483648&&i%1===0)||i===void 0)){e[0](i)}return i}`, + ) +}) + +// Tagged tuple union — dispatches on i["0"] === "a" / "b", which is what the +// `B.hoistChildChecks` helper lifts from each tuple's literal first field +// into the parent's validation list as union discriminants. +test("Tagged tuple union dispatches via literal first-field discriminant", t => { + let schema = S.union([ + S.tuple(s => (s.item(0, S.literal("a")), s.item(1, S.string))), + S.tuple(s => (s.item(0, S.literal("b")), s.item(1, S.string))), + ]) + + t->Assert.deepEqual(("a", "hello")->S.parseOrThrow(~to=schema), ("a", "hello")) + t->Assert.deepEqual(("b", "world")->S.parseOrThrow(~to=schema), ("b", "world")) +}) diff --git a/packages/sury/tests/S_unknown_test.res b/packages/sury/tests/S_unknown_test.res index 56c8db443..1032d197e 100644 --- a/packages/sury/tests/S_unknown_test.res +++ b/packages/sury/tests/S_unknown_test.res @@ -7,13 +7,13 @@ module Common = { test("Successfully parses", t => { let schema = factory() - t->Assert.deepEqual(any->S.parseOrThrow(schema), any) + t->Assert.deepEqual(any->S.parseOrThrow(~to=schema), any) }) test("Successfully serializes", t => { let schema = factory() - t->Assert.deepEqual(any->S.reverseConvertOrThrow(schema), any) + t->Assert.deepEqual(any->S.decodeOrThrow(~from=schema, ~to=S.unknown), any) }) test("Compiled parse code snapshot", t => { @@ -40,10 +40,10 @@ module Common = { }) } -test("Doesn't return refinements", t => { +test("Is Unknown variant", t => { let schema = S.unknown - t->Assert.deepEqual(schema->S.String.refinements, []) - t->Assert.deepEqual(schema->S.Array.refinements, []) - t->Assert.deepEqual(schema->S.Int.refinements, []) - t->Assert.deepEqual(schema->S.Float.refinements, []) + switch schema { + | Unknown(_) => t->Assert.is(true, true) + | _ => t->Assert.fail("Expected Unknown") + } }) diff --git a/packages/sury/tests/S_unnest_test.res b/packages/sury/tests/S_unnest_test.res deleted file mode 100644 index 17f869739..000000000 --- a/packages/sury/tests/S_unnest_test.res +++ /dev/null @@ -1,148 +0,0 @@ -open Ava - -test("Successfully parses and reverse converts a simple object with unnest", t => { - let schema = S.unnest( - S.schema(s => - { - "foo": s.matches(S.string), - "bar": s.matches(S.int), - } - ), - ) - - t->U.assertCompiledCode( - ~schema, - ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2||!Array.isArray(i["0"])||!Array.isArray(i["1"])){e[0](i)}let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v02147483647||v4<-2147483648||v4%1!==0){e[2](v4)}v1[v0]={"foo":v3,"bar":v4,};}catch(v2){if(v2&&v2.s===s){v2.path=""+\'["\'+v0+\'"]\'+v2.path}throw v2}}return v1}`, - ) - t->U.assertCompiledCode( - ~schema, - ~op=#Convert, - `i=>{let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0U.assertCompiledCode( - ~schema, - ~op=#ReverseConvert, - `i=>{let v1=[new Array(i.length),new Array(i.length),];for(let v0=0;v0U.assertCompiledCode( - ~schema, - ~op=#ReverseParse, - `i=>{if(!Array.isArray(i)){e[0](i)}let v1=[new Array(i.length),new Array(i.length),];for(let v0=0;v02147483647||v4<-2147483648||v4%1!==0){e[3](v4)}v1[0][v0]=v3;v1[1][v0]=v4;}catch(v2){if(v2&&v2.s===s){v2.path=""+\'["\'+v0+\'"]\'+v2.path}throw v2}}return v1}`, - ) - - t->Assert.deepEqual( - %raw(`[["a", "b"], [0, 1]]`)->S.parseOrThrow(schema), - [{"foo": "a", "bar": 0}, {"foo": "b", "bar": 1}], - ) - - t->Assert.deepEqual( - [{"foo": "a", "bar": 0}, {"foo": "b", "bar": 1}]->S.reverseConvertOrThrow(schema), - %raw(`[["a", "b"], [0, 1]]`), - ) - - let example = S.unnest( - S.schema(s => - { - "id": s.matches(S.string), - "name": s.matches(S.null(S.string)), - "deleted": s.matches(S.bool), - } - ), - ) - t->U.assertCompiledCode( - ~schema=example, - ~op=#ReverseConvert, - `i=>{let v1=[new Array(i.length),new Array(i.length),new Array(i.length),];for(let v0=0;v0 { - let schema = S.unnest( - S.schema(s => - { - "foo": s.matches(S.string), - "bar": s.matches(S.null(S.int)), - } - ), - ) - - t->U.assertCompiledCode( - ~schema, - ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2||!Array.isArray(i["0"])||!Array.isArray(i["1"])){e[0](i)}let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0-2147483648&&v4%1===0)){e[2](v4)}v1[v0]={"foo":v3,"bar":v4,};}catch(v2){if(v2&&v2.s===s){v2.path=""+\'["\'+v0+\'"]\'+v2.path}throw v2}}return v1}`, - ) - t->U.assertCompiledCode( - ~schema, - ~op=#ReverseConvert, - `i=>{let v1=[new Array(i.length),new Array(i.length),];for(let v0=0;v0Assert.deepEqual( - %raw(`[["a", "b"], [0, null]]`)->S.parseOrThrow(schema), - [{"foo": "a", "bar": Some(0)}, {"foo": "b", "bar": None}], - ) - - t->Assert.deepEqual( - [{"foo": "a", "bar": Some(0)}, {"foo": "b", "bar": None}]->S.reverseConvertOrThrow(schema), - %raw(`[["a", "b"], [0, null]]`), - ) -}) - -test("Case with missing item at the end", t => { - let schema = S.unnest( - S.schema(s => - { - "foo": s.matches(S.option(S.string)), - "bar": s.matches(S.bool), - } - ), - ) - - t->U.assertCompiledCode( - ~schema, - ~op=#Parse, - `i=>{if(!Array.isArray(i)||i.length!==2||!Array.isArray(i["0"])||!Array.isArray(i["1"])){e[0](i)}let v1=new Array(Math.max(i[0].length,i[1].length,));for(let v0=0;v0U.assertCompiledCode( - ~schema, - ~op=#ReverseConvert, - `i=>{let v1=[new Array(i.length),new Array(i.length),];for(let v0=0;v0Assert.deepEqual( - %raw(`[["a", "b"], [true, true, false]]`)->S.parseOrThrow(schema), - [{"foo": Some("a"), "bar": true}, {"foo": Some("b"), "bar": true}, {"foo": None, "bar": false}], - ) - - t->Assert.deepEqual( - [ - {"foo": Some("a"), "bar": true}, - {"foo": Some("b"), "bar": true}, - {"foo": None, "bar": false}, - ]->S.reverseConvertOrThrow(schema), - %raw(`[["a", "b", undefined], [true, true, false]]`), - ) -}) - -test("Handles empty objects", t => { - t->Assert.throws( - () => { - S.unnest(S.object(_ => ())) - }, - ~expectations={ - message: "[Sury] Invalid empty object for S.unnest schema.", - }, - ) -}) - -test("Handles non-object schemas", t => { - t->Assert.throws( - () => { - S.unnest(S.tuple2(S.string, S.int)) - }, - ~expectations={ - message: "[Sury] S.unnest supports only object schemas.", - }, - ) -}) diff --git a/packages/sury/tests/U.res b/packages/sury/tests/U.res index ee36837fd..b6b9cccc2 100644 --- a/packages/sury/tests/U.res +++ b/packages/sury/tests/U.res @@ -6,9 +6,7 @@ open Ava // var $_$c = $_$wf(3);␊  // return $_$w(3, 444, $_$c), i;␊  // } -let noopOpCode = ( - S.unknown->S.compile(~input=Any, ~output=Unknown, ~mode=Sync, ~typeValidation=false)->Obj.magic -)["toString"]() +let noopOpCode: string = (S.decoder(~from=S.unknown, ~to=S.unknown)->Obj.magic)["toString"]() external magic: 'a => 'b = "%identity" external castAnyToUnknown: 'any => unknown = "%identity" @@ -26,32 +24,6 @@ let unsafeGetVariantPayload = variant => (variant->Obj.magic)["_0"] exception Test let throwTestException = () => throw(Test) -type taggedFlag = - | Parse - | ParseAsync - | ReverseConvertToJson - | ReverseParse - | ReverseConvert - | Assert - -type errorPayload = {operation: taggedFlag, code: S.errorCode, path: S.Path.t} - -// TODO: Get rid of the helper -let error = ({operation, code, path}: errorPayload): S.error => { - S.ErrorClass.constructor( - ~code, - ~flag=switch operation { - | Parse => S.Flag.typeValidation - | ReverseParse => S.Flag.reverse->S.Flag.with(S.Flag.typeValidation) - | ReverseConvertToJson => S.Flag.reverse->S.Flag.with(S.Flag.jsonableOutput) - | ReverseConvert => S.Flag.reverse - | ParseAsync => S.Flag.typeValidation->S.Flag.with(S.Flag.async) - | Assert => S.Flag.typeValidation->S.Flag.with(S.Flag.assertOutput) - }, - ~path, - ) -} - let assertThrowsTestException = { (t, fn, ~message=?) => { try { @@ -67,7 +39,7 @@ let assertThrowsTestException = { let assertThrows = (t, cb, errorPayload) => { switch cb() { | any => t->Assert.fail("Asserted result is not Error. Recieved: " ++ any->unsafeStringify) - | exception S.Error({message}) => t->Assert.is(message, error(errorPayload).message) + | exception S.Exn({message}) => t->Assert.is(message, S.Error.make(errorPayload).message) } } @@ -75,17 +47,21 @@ let assertThrowsMessage = (t, cb, errorMessage, ~message=?) => { switch cb() { | any => t->Assert.fail( - `Asserted result is not S.Error "${errorMessage}". Instead got: ${any->unsafeStringify}`, + `Asserted result is not S.Exn "${errorMessage}". Instead got: ${any->unsafeStringify}`, ) - | exception S.Error({message: actualErrorMessage}) => + | exception S.Exn({message: actualErrorMessage}) => t->Assert.is(actualErrorMessage, errorMessage, ~message?) } } -let assertThrowsAsync = async (t, cb, errorPayload) => { +let asyncAssertThrowsMessage = async (t, cb, errorMessage, ~message=?) => { switch await cb() { - | any => t->Assert.fail("Asserted result is not Error. Recieved: " ++ any->unsafeStringify) - | exception S.Error({message}) => t->Assert.is(message, error(errorPayload).message) + | any => + t->Assert.fail( + `Asserted result is not S.Exn "${errorMessage}". Instead got: ${any->unsafeStringify}`, + ) + | exception S.Exn({message: actualErrorMessage}) => + t->Assert.is(actualErrorMessage, errorMessage, ~message?) } } @@ -103,59 +79,72 @@ let getCompiledCodeString = ( | #Assert | #ReverseConvertToJson ], + ~embedded=?, ) => { - let toCode = schema => - ( - switch op { - | #Parse => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=true) - fn->magic - | #ParseAsync => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Async, ~typeValidation=true) + let toFn = schema => + switch op { + | #Parse => + let fn = S.decoder(~from=S.unknown, ~to=schema) + fn->magic + | #ParseAsync => + let fn = S.asyncDecoder(~from=S.unknown, ~to=schema) + fn->magic + | #Convert => + let fn = S.decoder(~from=schema->S.reverse, ~to=S.unknown) + fn->magic + | #ConvertAsync => + let fn = S.asyncDecoder(~from=schema->S.reverse, ~to=S.unknown) + fn->magic + | #Assert => + let fn = S.decoder(~from=S.unknown, ~to=schema->S.to(S.literal()->S.noValidation(true))) + fn->magic + | #ReverseParse => { + let fn = S.decoder(~from=S.unknown, ~to=schema->S.reverse) fn->magic - | #Convert => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Sync, ~typeValidation=false) + } + | #ReverseConvert => { + let fn = S.decoder(~from=schema, ~to=S.unknown) fn->magic - | #ConvertAsync => - let fn = schema->S.compile(~input=Any, ~output=Value, ~mode=Async, ~typeValidation=false) + } + | #ReverseConvertAsync => { + let fn = S.asyncDecoder(~from=schema, ~to=S.unknown) fn->magic - | #Assert => - let fn = schema->S.compile(~input=Any, ~output=Assert, ~mode=Sync, ~typeValidation=true) + } + | #ReverseConvertToJson => { + let fn = S.decoder(~from=schema, ~to=S.json) fn->magic - | #ReverseParse => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=true) - fn->magic - } - | #ReverseConvert => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Sync, ~typeValidation=false) - fn->magic - } - | #ReverseConvertAsync => { - let fn = - schema->S.compile(~input=Value, ~output=Unknown, ~mode=Async, ~typeValidation=false) - fn->magic - } - | #ReverseConvertToJson => { - let fn = schema->S.compile(~input=Value, ~output=Json, ~mode=Sync, ~typeValidation=false) - fn->magic - } } - )["toString"]() - - let code = ref(schema->toCode) + } - switch (schema->S.untag).defs { - | Some(defs) => - defs->Dict.forEachWithKey((schema, key) => - try { - code := code.contents ++ "\n" ++ `${key}: ${schema->toCode}` - } catch { - | exn => Console.error(exn) - } - ) - | None => () + let fn = schema->toFn + let code = ref(fn["toString"]()) + + switch embedded { + | Some(embedded) => + embedded->Array.forEach(((name, index)) => { + code := code.contents ++ "\n" ++ `${name}: ${fn["embedded"]->Array.getUnsafe(index)}` + }) + | None => + switch (schema->S.untag).defs { + | Some(defs) if code.contents !== noopOpCode => + defs->Dict.forEachWithKey((schema, key) => + try { + let defFn = schema->toFn + let defCode = + defFn["toString"]()->String.replaceAll( + `${(S.unknown->S.untag).seq->Float.toString}-${( + schema->S.untag + ).seq->Float.toString}`, + `unknown->${key}`, + ) + + code := code.contents ++ "\n" ++ `${key}: ${defCode}` + } catch { + | _exn => () + } + ) + | _ => () + } } code.contents @@ -169,7 +158,9 @@ let rec cleanUpSchema = schema => { ->Array.forEach(((key, value)) => { switch key { | "output" - | "isAsync" => () + | "isAsync" + | "hasTransform" + | "seq" => () // ditemToItem leftovers FIXME: | "k" | "p" | "of" | "r" => () | _ => @@ -192,8 +183,8 @@ let unsafeAssertEqualSchemas = (t, s1: S.t<'v1>, s2: S.t<'v2>, ~message=?) => { t->Assert.unsafeDeepEqual(s1->cleanUpSchema, s2->cleanUpSchema, ~message?) } -let assertCompiledCode = (t, ~schema, ~op, code, ~message=?) => { - t->Assert.is(schema->getCompiledCodeString(~op), code, ~message?) +let assertCompiledCode = (t, ~schema, ~op, code, ~embedded=?, ~message=?) => { + t->Assert.is(schema->getCompiledCodeString(~op, ~embedded?), code, ~message?) } let assertCompiledCodeIsNoop = (t, ~schema, ~op, ~message=?) => { @@ -210,8 +201,8 @@ let assertEqualSchemas: ( let assertReverseParsesBack = (t, schema: S.t<'value>, value: 'value) => { t->Assert.unsafeDeepEqual( value - ->S.reverseConvertOrThrow(schema) - ->S.parseOrThrow(schema), + ->S.decodeOrThrow(~from=schema, ~to=S.unknown) + ->S.parseOrThrow(~to=schema), value, ) } diff --git a/packages/sury/tests/U.res.mjs b/packages/sury/tests/U.res.mjs index 675dd0645..438df4bd1 100644 --- a/packages/sury/tests/U.res.mjs +++ b/packages/sury/tests/U.res.mjs @@ -5,7 +5,7 @@ import * as Stdlib_Dict from "rescript/lib/es6/Stdlib_Dict.js"; import * as Primitive_option from "rescript/lib/es6/Primitive_option.js"; import * as Primitive_exceptions from "rescript/lib/es6/Primitive_exceptions.js"; -let noopOpCode = S.compile(S.unknown, "Any", "Input", "Sync", false).toString(); +let noopOpCode = S.decoder(S.unknown, S.unknown).toString(); function throwError(error) { throw error; @@ -24,31 +24,6 @@ function throwTestException() { }; } -function error(param) { - let tmp; - switch (param.operation) { - case "Parse" : - tmp = S.Flag.typeValidation; - break; - case "ParseAsync" : - tmp = S.Flag.typeValidation | S.Flag.async; - break; - case "ReverseConvertToJson" : - tmp = S.Flag.reverse | S.Flag.jsonableOutput; - break; - case "ReverseParse" : - tmp = S.Flag.reverse | S.Flag.typeValidation; - break; - case "ReverseConvert" : - tmp = S.Flag.reverse; - break; - case "Assert" : - tmp = S.Flag.typeValidation | S.Flag.assertOutput; - break; - } - return S.ErrorClass.constructor(param.code, tmp, param.path); -} - function assertThrowsTestException(t, fn, message) { try { fn(); @@ -70,8 +45,8 @@ function assertThrows(t, cb, errorPayload) { any = cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { - t.is(exn._1.message, error(errorPayload).message); + if (exn.RE_EXN_ID === S.Exn) { + t.is(exn._1.message, S.$$Error.make(errorPayload).message); return; } throw exn; @@ -85,63 +60,75 @@ function assertThrowsMessage(t, cb, errorMessage, message) { any = cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { + if (exn.RE_EXN_ID === S.Exn) { t.is(exn._1.message, errorMessage, message !== undefined ? Primitive_option.valFromOption(message) : undefined); return; } throw exn; } - t.fail("Asserted result is not S.Error \"" + errorMessage + "\". Instead got: " + JSON.stringify(any)); + t.fail("Asserted result is not S.Exn \"" + errorMessage + "\". Instead got: " + JSON.stringify(any)); } -async function assertThrowsAsync(t, cb, errorPayload) { +async function asyncAssertThrowsMessage(t, cb, errorMessage, message) { let any; try { any = await cb(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); - if (exn.RE_EXN_ID === S.$$Error) { - t.is(exn._1.message, error(errorPayload).message); + if (exn.RE_EXN_ID === S.Exn) { + t.is(exn._1.message, errorMessage, message !== undefined ? Primitive_option.valFromOption(message) : undefined); return; } throw exn; } - return t.fail("Asserted result is not Error. Recieved: " + JSON.stringify(any)); -} - -function getCompiledCodeString(schema, op) { - let toCode = schema => ( - op === "ParseAsync" ? S.compile(schema, "Any", "Output", "Async", true) : ( - op === "Parse" ? S.compile(schema, "Any", "Output", "Sync", true) : ( - op === "ReverseConvertToJson" ? S.compile(schema, "Output", "Json", "Sync", false) : ( - op === "ReverseConvert" ? S.compile(schema, "Output", "Input", "Sync", false) : ( - op === "Convert" ? S.compile(schema, "Any", "Output", "Sync", false) : ( - op === "Assert" ? S.compile(schema, "Any", "Assert", "Sync", true) : ( - op === "ReverseParse" ? S.compile(schema, "Output", "Input", "Sync", true) : ( - op === "ConvertAsync" ? S.compile(schema, "Any", "Output", "Async", false) : S.compile(schema, "Output", "Input", "Async", false) - ) - ) - ) - ) - ) - ) - ) - ).toString(); + return t.fail("Asserted result is not S.Exn \"" + errorMessage + "\". Instead got: " + JSON.stringify(any)); +} + +function getCompiledCodeString(schema, op, embedded) { + let toFn = schema => { + if (op === "ParseAsync") { + return S.asyncDecoder(S.unknown, schema); + } else if (op === "Parse") { + return S.decoder(S.unknown, schema); + } else if (op === "ReverseConvertToJson") { + return S.decoder(schema, S.json); + } else if (op === "ReverseConvert") { + return S.decoder(schema, S.unknown); + } else if (op === "Convert") { + return S.decoder(S.reverse(schema), S.unknown); + } else if (op === "Assert") { + return S.decoder(S.unknown, S.to(schema, S.noValidation(S.literal(), true))); + } else if (op === "ReverseParse") { + return S.decoder(S.unknown, S.reverse(schema)); + } else if (op === "ConvertAsync") { + return S.asyncDecoder(S.reverse(schema), S.unknown); + } else { + return S.asyncDecoder(schema, S.unknown); + } + }; + let fn = toFn(schema); let code = { - contents: toCode(schema) + contents: fn.toString() }; - let defs = schema.$defs; - if (defs !== undefined) { - Stdlib_Dict.forEachWithKey(defs, (schema, key) => { - try { - code.contents = code.contents + "\n" + (key + ": " + toCode(schema)); - return; - } catch (raw_exn) { - let exn = Primitive_exceptions.internalToException(raw_exn); - console.error(exn); - return; - } + if (embedded !== undefined) { + embedded.forEach(param => { + code.contents = code.contents + "\n" + (param[0] + ": " + fn.embedded[param[1]]); }); + } else { + let defs = schema.$defs; + if (defs !== undefined && code.contents !== noopOpCode) { + Stdlib_Dict.forEachWithKey(defs, (schema, key) => { + try { + let defFn = toFn(schema); + let defCode = defFn.toString().replaceAll(S.unknown.seq.toString() + "-" + schema.seq.toString(), "unknown->" + key); + code.contents = code.contents + "\n" + (key + ": " + defCode); + return; + } catch (_exn) { + return; + } + }); + } + } return code.contents; } @@ -152,12 +139,14 @@ function cleanUpSchema(schema) { let value = param[1]; let key = param[0]; switch (key) { + case "hasTransform" : case "isAsync" : case "k" : case "of" : case "output" : case "p" : case "r" : + case "seq" : return; default: if (typeof value === "function") { @@ -179,16 +168,16 @@ function unsafeAssertEqualSchemas(t, s1, s2, message) { t.deepEqual(cleanUpSchema(s1), cleanUpSchema(s2), message !== undefined ? Primitive_option.valFromOption(message) : undefined); } -function assertCompiledCode(t, schema, op, code, message) { - t.is(getCompiledCodeString(schema, op), code, message !== undefined ? Primitive_option.valFromOption(message) : undefined); +function assertCompiledCode(t, schema, op, code, embedded, message) { + t.is(getCompiledCodeString(schema, op, embedded), code, message !== undefined ? Primitive_option.valFromOption(message) : undefined); } function assertCompiledCodeIsNoop(t, schema, op, message) { - assertCompiledCode(t, schema, op, noopOpCode, message); + assertCompiledCode(t, schema, op, noopOpCode, undefined, message); } function assertReverseParsesBack(t, schema, value) { - t.deepEqual(S.parseOrThrow(S.reverseConvertOrThrow(value, schema), schema), value); + t.deepEqual(S.parseOrThrow(S.decodeOrThrow(value, schema, S.unknown), schema), value); } function assertReverseReversesBack(t, schema) { @@ -203,11 +192,10 @@ export { unsafeGetVariantPayload, Test, throwTestException, - error, assertThrowsTestException, assertThrows, assertThrowsMessage, - assertThrowsAsync, + asyncAssertThrowsMessage, getCompiledCodeString, cleanUpSchema, unsafeAssertEqualSchemas, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a6be2662..46b221591 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: dependencies: sury: specifier: ^11.0.0-alpha.2 - version: 11.0.0-alpha.2(rescript@11.1.4) + version: 11.0.0-alpha.2(rescript@12.0.0-beta.5) packages: @@ -136,6 +136,7 @@ packages: '@rescript/darwin-arm64@12.0.0-beta.5': resolution: {integrity: sha512-hIy6TQKTzy3SdP3nGAzOc1fqb2Va5ejrn5RCZu6Scst9qH4uZ+chMS4Vv+yac1tD9bkTeixeXGj98NlLgukAvw==} engines: {node: '>=20.11.0'} + cpu: [arm64] os: [darwin] '@rescript/darwin-x64@12.0.0-beta.5': @@ -810,11 +811,6 @@ packages: engines: {node: '>=10'} hasBin: true - rescript@11.1.4: - resolution: {integrity: sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==} - engines: {node: '>=10'} - hasBin: true - rescript@12.0.0-beta.5: resolution: {integrity: sha512-khIb1ABW7DEG1/HkTdP4r26IypKBfh8mrmOBWFtcSLkhQWCC5Vwl6uH+yjQ9jdpbBXXDh+xaELCmgfa2ZhdMHg==} engines: {node: '>=20.11.0'} @@ -1697,9 +1693,6 @@ snapshots: rescript@11.0.1: {} - rescript@11.1.4: - optional: true - rescript@12.0.0-beta.5: optionalDependencies: '@rescript/darwin-arm64': 12.0.0-beta.5 @@ -1804,9 +1797,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - sury@11.0.0-alpha.2(rescript@11.1.4): + sury@11.0.0-alpha.2(rescript@12.0.0-beta.5): optionalDependencies: - rescript: 11.1.4 + rescript: 12.0.0-beta.5 temp-dir@3.0.0: {} diff --git a/wallaby.conf.js b/wallaby.conf.js index 99e8cc24f..d3d981fdb 100644 --- a/wallaby.conf.js +++ b/wallaby.conf.js @@ -19,8 +19,9 @@ export default () => ({ env: { type: "node", params: { - runner: "--experimental-vm-modules", + runner: "--experimental-vm-modules", // Improtant for Ava ESM }, }, + workers: { restart: true }, // Improtant for Ava ESM testFramework: "ava", });