@reventlessdev/reventless-spec 3.0.0-alpha.139 → 3.0.0-alpha.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/package.json +7 -7
  4. package/src/components/DcbTag.res.mjs +2 -2
  5. package/src/components/IdentityCheck.res +110 -0
  6. package/src/components/IdentityCheck.res.mjs +103 -0
  7. package/src/components/Plugin.res.mjs +3 -3
  8. package/src/components/StateChangeSlice.res +3 -5
  9. package/src/components/StateViewSlice.res +9 -1
  10. package/src/components/TraitCertificate.res.mjs +3 -3
  11. package/src/components/TraitManifest.res.mjs +2 -2
  12. package/src/generator/PlatformGenerator.res.mjs +2 -2
  13. package/src/semantic/Bytes.res.mjs +1 -1
  14. package/src/semantic/CalendarDate.res +1 -1
  15. package/src/semantic/CalendarDate.res.mjs +1 -2
  16. package/src/semantic/DateTime.res +3 -3
  17. package/src/semantic/DateTime.res.mjs +2 -3
  18. package/src/semantic/Duration.res.mjs +1 -1
  19. package/src/semantic/Email.res +1 -1
  20. package/src/semantic/Email.res.mjs +1 -2
  21. package/src/semantic/GeoPoint.res.mjs +2 -2
  22. package/src/semantic/Money.res +1 -1
  23. package/src/semantic/Money.res.mjs +1 -1
  24. package/src/semantic/Offload.res.mjs +1 -1
  25. package/src/semantic/Percent.res.mjs +1 -1
  26. package/src/semantic/Semantic.res +11 -0
  27. package/src/semantic/Semantic.res.mjs +21 -0
  28. package/src/semantic/Template.res.mjs +3 -3
  29. package/src/semantic/Url.res +1 -1
  30. package/src/semantic/Url.res.mjs +1 -2
  31. package/src/types/AccountsManifest.res.mjs +1 -2
  32. package/src/types/Identity.res.mjs +1 -1
  33. package/src/types/Lifecycle.res.mjs +1 -1
  34. package/src/types/Message.res.mjs +2 -2
  35. package/src/types/Projection.res +48 -4
  36. package/src/types/Projection.res.mjs +121 -8
  37. package/src/types/Schedule.res.mjs +8 -8
  38. package/src/util/Util_Sury.res +6 -3
  39. package/src/util/Util_Sury.res.mjs +6 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.141 (2026-09-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **deps:** update rescript to 12.3.1 ([ea1ce37](https://github.com/ReventlessDev/reventless-core/commit/ea1ce377297640f6d0d360e6c84c778d59ecbcc5))
11
+ * **deps:** update sury to 11.0.0 ([8715b22](https://github.com/ReventlessDev/reventless-core/commit/8715b22b33856c46ba57518aad151065c123a995))
12
+
13
+
14
+ # 3.0.0-alpha.140 (2026-09-21)
15
+
16
+ * feat(spec)!: typed projection keys ([fc6a018](https://github.com/ReventlessDev/reventless-core/commit/fc6a01811ba788f3289e84a435a09f77fa627e1a))
17
+ ### Features
18
+
19
+ * **core:** references derived from identities, and identity checks ([7cb1376](https://github.com/ReventlessDev/reventless-core/commit/7cb1376e56cdb4e4d5ab5954cd97fd37fd3ba258))
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ * a projection that keys a row by a payload `string`, or uses
24
+ the envelope id as a `string`, converts explicitly with
25
+ `Target.Id.makeFromString` / `Source.Id.toString`. `StateChangeSlice.Spec` no
26
+ longer requires `module Id`.
27
+
28
+
29
+
6
30
  # 3.0.0-alpha.139 (2026-09-21)
7
31
 
8
32
  ### Features
package/README.md CHANGED
@@ -96,7 +96,7 @@ Then register it as a ReScript dependency in `rescript.json`:
96
96
  }
97
97
  ```
98
98
 
99
- Requires ReScript `^12.3.0` (peer dependency).
99
+ Requires ReScript `^12.3.1` (peer dependency).
100
100
 
101
101
  ## Links
102
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.139",
3
+ "version": "3.0.0-alpha.141",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -23,17 +23,17 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "jsonschema2graphql": "1.1.1",
26
- "sury": "11.0.0-rc.2",
27
- "sury-ppx": "11.0.0-rc.2",
26
+ "sury": "11.0.0",
27
+ "sury-ppx": "11.0.0",
28
28
  "yaml": "^2.8.3",
29
- "@reventlessdev/rescript-node": "2.0.0-alpha.12"
29
+ "@reventlessdev/rescript-node": "2.0.0-alpha.13"
30
30
  },
31
31
  "devDependencies": {
32
- "rescript": "12.3.0",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10"
32
+ "rescript": "12.3.1",
33
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.11"
34
34
  },
35
35
  "peerDependencies": {
36
- "rescript": "12.3.0"
36
+ "rescript": "^12.3.1"
37
37
  },
38
38
  "publishConfig": {
39
39
  "registry": "https://registry.npmjs.org"
@@ -54,7 +54,7 @@ function stringForKey(key) {
54
54
  return markForKey(Sury.string, key);
55
55
  }
56
56
 
57
- let int = Sury.$Metadata_set(Sury.int, dcbTagId, true);
57
+ let int = Sury.$Metadata_set(Sury.int32, dcbTagId, true);
58
58
 
59
59
  let partition = markPartition(Sury.string);
60
60
 
@@ -88,7 +88,7 @@ let derivedPartitionTagSchema = Sury.union([
88
88
  })),
89
89
  Sury.$schema(s => ({
90
90
  TAG: "ByEventType",
91
- _0: s.m(Sury.dict(Sury.string))
91
+ _0: s.m(Sury.record(Sury.string))
92
92
  }))
93
93
  ]);
94
94
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ Where a plugin's ids and their types disagree. Needs the whole plugin, not one
3
+ file: whether `orderId` names an identity depends on whether any field is typed
4
+ `OrderId.t`, which is also what makes adoption opt-in per plugin — a plugin that
5
+ types nothing is never reported.
6
+
7
+ - **A field named for one identity and typed as another** (`orderId: CustomerId.t`):
8
+ the mix-up the types exist to catch. A role name that is no identity of its own
9
+ (`sellerId: CustomerId.t`) is fine.
10
+ - **An untyped `*Id` whose key has an identity** (`productId: string` beside a
11
+ `ProductId.t`): the half-migrated state, where the compiler guards some uses and
12
+ not others.
13
+ */
14
+ type finding = {sliceName: string, message: string}
15
+
16
+ // The identity key a `*Id` / `*Ids` name gives by convention.
17
+ let nameKey = (name: string): option<string> =>
18
+ if name->String.endsWith("Ids") {
19
+ Some(name->String.slice(~start=0, ~end=name->String.length - 1))
20
+ } else if name->String.length > 2 && name->String.endsWith("Id") {
21
+ Some(name)
22
+ } else {
23
+ None
24
+ }
25
+
26
+ let fieldsOf = (schema: S.t<unknown>): array<(string, S.t<unknown>)> => {
27
+ let ofObject = (v: S.t<unknown>) =>
28
+ switch v {
29
+ | Object({properties}) => properties->Dict.toArray->Array.filter(((n, _)) => n != "TAG")
30
+ | _ => []
31
+ }
32
+ switch schema {
33
+ | AnyOf({anyOf}) => anyOf->Array.flatMap(ofObject)
34
+ | other => ofObject(other)
35
+ }
36
+ }
37
+
38
+ let slicesFields = (slice: DcbTag.sliceSchemas) =>
39
+ [slice.commandSchema, slice.consumedEventSchema, slice.eventSchema]->Array.flatMap(fieldsOf)
40
+
41
+ let check = (slices: array<DcbTag.sliceSchemas>): array<finding> => {
42
+ let declared: Set.t<string> = Set.make()
43
+ slices->Array.forEach(slice =>
44
+ slice
45
+ ->slicesFields
46
+ ->Array.forEach(((_, schema)) =>
47
+ Semantic.fieldIdentityKey(schema)->Option.forEach(k => declared->Set.add(k))
48
+ )
49
+ )
50
+ let seen: Set.t<string> = Set.make()
51
+ let findings = []
52
+ slices->Array.forEach(slice =>
53
+ slice
54
+ ->slicesFields
55
+ ->Array.forEach(((name, schema)) => {
56
+ let message = switch (Semantic.fieldIdentityKey(schema), nameKey(name)) {
57
+ | (Some(key), Some(named)) if named != key && declared->Set.has(named) =>
58
+ Some(
59
+ `${name} is typed as a ${key}, but ${named} is an identity of its own in this plugin. Type it as ${named}'s identity, or rename the field.`,
60
+ )
61
+ | (None, Some(named)) if declared->Set.has(named) =>
62
+ Some(
63
+ `${name} is a plain string, but ${named} has an identity in this plugin. Type the field with it, so the compiler guards every use.`,
64
+ )
65
+ | _ => None
66
+ }
67
+ message->Option.forEach(
68
+ message => {
69
+ let id = slice.name ++ "\n" ++ message
70
+ if !(seen->Set.has(id)) {
71
+ seen->Set.add(id)
72
+ findings->Array.push({sliceName: slice.name, message})
73
+ }
74
+ },
75
+ )
76
+ })
77
+ )
78
+ findings
79
+ }
80
+
81
+ /**
82
+ A slice partitioned by an identity another chapter declares: `categoryId`
83
+ partitioning a slice under `Product/`. The chapter is the default home of the
84
+ identity its slices decide about, so the two disagreeing usually means the slice
85
+ or the identity is in the wrong folder. `identityChapters` maps a key to the
86
+ chapter whose folder declares it; an identity declared in another package has
87
+ none and is not checked.
88
+ */
89
+ let checkChapters = (
90
+ ~identityChapters: dict<string>,
91
+ ~partitionBySlice: dict<string>,
92
+ slices: array<DcbTag.sliceSchemas>,
93
+ ): array<finding> =>
94
+ slices->Array.filterMap(slice =>
95
+ switch (
96
+ partitionBySlice->Dict.get(slice.name),
97
+ slice.moduleUrl->Option.flatMap(DcbTag.chapterOfModuleUrl),
98
+ ) {
99
+ | (Some(key), Some(chapter)) =>
100
+ switch identityChapters->Dict.get(key) {
101
+ | Some(home) if home != chapter =>
102
+ Some({
103
+ sliceName: slice.name,
104
+ message: `is partitioned by ${key}, which ${home}/ declares, but sits in ${chapter}/. Move the slice or the identity so they share a chapter.`,
105
+ })
106
+ | _ => None
107
+ }
108
+ | _ => None
109
+ }
110
+ )
@@ -0,0 +1,103 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as DcbTag$Reventless from "./DcbTag.res.mjs";
6
+ import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
7
+
8
+ function nameKey(name) {
9
+ if (name.endsWith("Ids")) {
10
+ return name.slice(0, name.length - 1 | 0);
11
+ } else if (name.length > 2 && name.endsWith("Id")) {
12
+ return name;
13
+ } else {
14
+ return;
15
+ }
16
+ }
17
+
18
+ function fieldsOf(schema) {
19
+ let ofObject = v => {
20
+ if (v.type === "object") {
21
+ return Object.entries(v.properties).filter(param => param[0] !== "TAG");
22
+ } else {
23
+ return [];
24
+ }
25
+ };
26
+ if (schema.type === "anyOf") {
27
+ return schema.anyOf.flatMap(ofObject);
28
+ } else {
29
+ return ofObject(schema);
30
+ }
31
+ }
32
+
33
+ function slicesFields(slice) {
34
+ return [
35
+ slice.commandSchema,
36
+ slice.consumedEventSchema,
37
+ slice.eventSchema
38
+ ].flatMap(fieldsOf);
39
+ }
40
+
41
+ function check(slices) {
42
+ let declared = new Set();
43
+ slices.forEach(slice => {
44
+ slicesFields(slice).forEach(param => Stdlib_Option.forEach(Semantic$Reventless.fieldIdentityKey(param[1]), k => {
45
+ declared.add(k);
46
+ }));
47
+ });
48
+ let seen = new Set();
49
+ let findings = [];
50
+ slices.forEach(slice => {
51
+ slicesFields(slice).forEach(param => {
52
+ let name = param[0];
53
+ let match = Semantic$Reventless.fieldIdentityKey(param[1]);
54
+ let match$1 = nameKey(name);
55
+ let message = match !== undefined ? (
56
+ match$1 !== undefined && match$1 !== match && declared.has(match$1) ? name + ` is typed as a ` + match + `, but ` + match$1 + ` is an identity of its own in this plugin. Type it as ` + match$1 + `'s identity, or rename the field.` : undefined
57
+ ) : (
58
+ match$1 !== undefined && declared.has(match$1) ? name + ` is a plain string, but ` + match$1 + ` has an identity in this plugin. Type the field with it, so the compiler guards every use.` : undefined
59
+ );
60
+ Stdlib_Option.forEach(message, message => {
61
+ let id = slice.name + "\n" + message;
62
+ if (!seen.has(id)) {
63
+ seen.add(id);
64
+ findings.push({
65
+ sliceName: slice.name,
66
+ message: message
67
+ });
68
+ return;
69
+ }
70
+ });
71
+ });
72
+ });
73
+ return findings;
74
+ }
75
+
76
+ function checkChapters(identityChapters, partitionBySlice, slices) {
77
+ return Stdlib_Array.filterMap(slices, slice => {
78
+ let match = partitionBySlice[slice.name];
79
+ let match$1 = Stdlib_Option.flatMap(slice.moduleUrl, DcbTag$Reventless.chapterOfModuleUrl);
80
+ if (match === undefined) {
81
+ return;
82
+ }
83
+ if (match$1 === undefined) {
84
+ return;
85
+ }
86
+ let home = identityChapters[match];
87
+ if (home !== undefined && home !== match$1) {
88
+ return {
89
+ sliceName: slice.name,
90
+ message: `is partitioned by ` + match + `, which ` + home + `/ declares, but sits in ` + match$1 + `/. Move the slice or the identity so they share a chapter.`
91
+ };
92
+ }
93
+ });
94
+ }
95
+
96
+ export {
97
+ nameKey,
98
+ fieldsOf,
99
+ slicesFields,
100
+ check,
101
+ checkChapters,
102
+ }
103
+ /* DcbTag-Reventless Not a pure module */
@@ -61,7 +61,7 @@ let menuEntrySchema = Sury.$schema(s => ({
61
61
  label: s.m(Sury.string),
62
62
  icon: s.m(Sury.$option(Sury.string)),
63
63
  group: s.m(Sury.$option(Sury.string)),
64
- sortOrder: s.m(Sury.int)
64
+ sortOrder: s.m(Sury.int32)
65
65
  }));
66
66
 
67
67
  let pageManifestEntrySchema = Sury.$schema(s => ({
@@ -100,7 +100,7 @@ let commandDefSchema = Sury.$schema(s => ({
100
100
  allowedStates: s.m(Sury.$option(Sury.array(Sury.string))),
101
101
  targetState: s.m(Sury.$option(Sury.string)),
102
102
  allowedStatesSource: s.m(Sury.$option(Sury.string)),
103
- apiExposed: s.m(Sury.$option(Sury.bool)),
103
+ apiExposed: s.m(Sury.$option(Sury.boolean)),
104
104
  requiredAccess: s.m(Sury.$option(Sury.array(Sury.string))),
105
105
  ownerField: s.m(Sury.$option(Sury.string))
106
106
  }));
@@ -118,7 +118,7 @@ let queryableDefSchema = Sury.$schema(s => ({
118
118
  ownerField: s.m(Sury.$option(Sury.string)),
119
119
  retiredField: s.m(Sury.$option(Sury.string)),
120
120
  retiredValues: s.m(Sury.$option(Sury.array(Sury.string))),
121
- namedWhenRetired: s.m(Sury.$option(Sury.bool)),
121
+ namedWhenRetired: s.m(Sury.$option(Sury.boolean)),
122
122
  visibility: s.m(Sury.$option(Sury.string)),
123
123
  chapter: s.m(Sury.$option(Sury.string)),
124
124
  singleQueryField: s.m(Sury.$option(Sury.string)),
@@ -42,17 +42,15 @@ let decide = (state, command) => switch command {
42
42
  ```
43
43
  */
44
44
  /**
45
- The lean Spec for a StateChangeSlice — types, identity, schemas. State and
46
- state-evolution functions live in the sibling `Behavior` module type.
45
+ The lean Spec for a StateChangeSlice — types and schemas. State and
46
+ state-evolution functions live in the sibling `Behavior` module type. A slice has
47
+ no identity of its own: it decides about the one its partition field carries.
47
48
  */
48
49
  module type Spec = {
49
50
  /** Logical name of this slice (used as a command topic prefix). */
50
51
  let name: string
51
52
  let moduleUrl: string
52
53
 
53
- /** Identity type — always `Id.String` for DCB slices. */
54
- module Id: Id.T
55
-
56
54
  /**
57
55
  Events this slice reads to build its decision model (in `evolve`).
58
56
  Only needs the fields required for the decision — no tag annotations needed.
@@ -51,6 +51,14 @@ module type Spec = {
51
51
  /** Sury schema for the state type — generated automatically by `@schema`. */
52
52
  let stateSchema: S.t<state>
53
53
 
54
+ /**
55
+ What a row is keyed by. `Id.StringPure` (a plain `string`) unless the view
56
+ declares its identity — `module Key = OrderId` — in which case a projection
57
+ that keys a row by another entity's id does not compile. Auto-injected by
58
+ `@@reventless.spec` in a `StateView/` folder.
59
+ */
60
+ module Key: Id.T
61
+
54
62
  /**
55
63
  Events this view slice projects. Only needs the fields required for the projection —
56
64
  no tag annotations needed. May be payload-less where only existence matters.
@@ -114,7 +122,7 @@ module type Projection = {
114
122
  Receives the event wrapped in a `consumed` envelope (event + `meta` +
115
123
  `recordedAt`); only events declared in `Spec.consumedEvent` — no wildcard needed.
116
124
  */
117
- let project: consumed<Spec.consumedEvent> => array<Projection.action<string, Spec.state>>
125
+ let project: consumed<Spec.consumedEvent> => array<Projection.action<Spec.Key.t, Spec.state>>
118
126
 
119
127
  /** File URL of this Projection module (`import.meta.url`). */
120
128
  let moduleUrl: string
@@ -5,7 +5,7 @@ import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
5
5
 
6
6
  let assertionSchema = Sury.$schema(s => ({
7
7
  name: s.m(Sury.string),
8
- passed: s.m(Sury.bool)
8
+ passed: s.m(Sury.boolean)
9
9
  }));
10
10
 
11
11
  let schema = Sury.$schema(s => ({
@@ -15,8 +15,8 @@ let schema = Sury.$schema(s => ({
15
15
  host: s.m(Sury.string),
16
16
  suite: s.m(Sury.string),
17
17
  assertions: s.m(Sury.array(assertionSchema)),
18
- passed: s.m(Sury.int),
19
- failed: s.m(Sury.int)
18
+ passed: s.m(Sury.int32),
19
+ failed: s.m(Sury.int32)
20
20
  }));
21
21
 
22
22
  function verified(certificate) {
@@ -6,7 +6,7 @@ import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
6
6
 
7
7
  let configFieldSchema = Sury.$schema(s => ({
8
8
  name: s.m(Sury.string),
9
- required: s.m(Sury.bool)
9
+ required: s.m(Sury.boolean)
10
10
  }));
11
11
 
12
12
  let schema = Sury.$schema(s => ({
@@ -16,7 +16,7 @@ let schema = Sury.$schema(s => ({
16
16
  license: s.m(Sury.string),
17
17
  capabilities: s.m(Sury.array(Sury.string)),
18
18
  config: s.m(Sury.array(configFieldSchema)),
19
- scaffolded: s.m(Sury.bool)
19
+ scaffolded: s.m(Sury.boolean)
20
20
  }));
21
21
 
22
22
  function render(manifest) {
@@ -1,6 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
3
+ import * as Sury from "sury";
4
4
  import * as Yaml from "yaml";
5
5
  import * as Nodefs from "node:fs";
6
6
  import * as Nodepath from "node:path";
@@ -73,7 +73,7 @@ if (manifestArg === "") {
73
73
  console.log(`Read: ` + pluginName + ` — ` + path + ` (via ` + PlatformManifests$Reventless.describeVia(param.via) + `)`);
74
74
  let manifest;
75
75
  try {
76
- manifest = S.parseOrThrow(JSON.parse(Nodefs.readFileSync(path, "utf8")), CapabilityManifest$Reventless.schema);
76
+ manifest = Sury.parseOrThrow(JSON.parse(Nodefs.readFileSync(path, "utf8")), CapabilityManifest$Reventless.schema);
77
77
  } catch (exn) {
78
78
  fail(`could not parse ` + path + ` as a capability manifest`);
79
79
  manifest = {
@@ -29,7 +29,7 @@ function fromFloat(raw) {
29
29
  }
30
30
  }
31
31
 
32
- let schema = Semantic$Reventless.refined(Sury.float, Semantic$Reventless.Id.bytes, fromFloat);
32
+ let schema = Semantic$Reventless.refined(Sury.number, Semantic$Reventless.Id.bytes, fromFloat);
33
33
 
34
34
  let units = [
35
35
  "B",
@@ -40,7 +40,7 @@ external toString: t => string = "%identity"
40
40
 
41
41
  // Sury's rule, held once. `fromString` runs it rather than restating it, and
42
42
  // `schema` is built from `fromString`, so there is exactly one grammar here.
43
- let grammar: S.t<string> = S.isoDate
43
+ let grammar: S.t<string> = (S.isoDate :> S.t<string>)
44
44
 
45
45
  /** Validate a raw string as an ISO-8601 calendar day, saying why when it is not one. */
46
46
  let fromString = (raw: string): result<t, string> =>
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Sury from "sury";
5
4
  import * as Semantic$Reventless from "./Semantic.res.mjs";
6
5
 
@@ -9,7 +8,7 @@ let grammar = Sury.isoDate;
9
8
  function fromString(raw) {
10
9
  let value;
11
10
  try {
12
- value = S.parseOrThrow(raw, grammar);
11
+ value = Sury.parseOrThrow(raw, grammar);
13
12
  } catch (exn) {
14
13
  return {
15
14
  TAG: "Error",
@@ -3,14 +3,14 @@ Marks a `string` field as an ISO-8601 instant.
3
3
 
4
4
  ## The grammar
5
5
 
6
- Sury's `S.isoDateTime`, and nothing added — the same rule the branded scalars
6
+ Sury's `S.utcDateTime`, and nothing added — the same rule the branded scalars
7
7
  follow. `S.datetime` is the other binding and is not this one: it *transforms* to
8
8
  `Js.Date.t`, changing the field's runtime type, where this keeps the string the
9
9
  projection wrote.
10
10
 
11
11
  ## UTC only
12
12
 
13
- `S.isoDateTime` accepts a `Z` instant and rejects an offset — `2026-03-02T09:00:00+01:00`
13
+ `S.utcDateTime` accepts a `Z` instant and rejects an offset — `2026-03-02T09:00:00+01:00`
14
14
  and a bare `2026-03-02T09:00:00` both fail. That is stricter than RFC 3339 and it
15
15
  is the strictness worth adopting: every instant the framework produces comes from
16
16
  `Message.nowAsISOString`, which is always `Z`. "Instants are stored in UTC" is a
@@ -48,7 +48,7 @@ external toString: t => string = "%identity"
48
48
 
49
49
  // Sury's rule, held once. `fromString` runs it rather than restating it, and
50
50
  // `schema` is built from `fromString`, so there is exactly one grammar here.
51
- let grammar: S.t<string> = S.isoDateTime
51
+ let grammar: S.t<string> = (S.utcDateTime :> S.t<string>)
52
52
 
53
53
  /** Validate a raw string as a UTC ISO-8601 instant, saying why when it is not one. */
54
54
  let fromString = (raw: string): result<t, string> =>
@@ -1,16 +1,15 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Sury from "sury";
5
4
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
6
5
  import * as Semantic$Reventless from "./Semantic.res.mjs";
7
6
 
8
- let grammar = Sury.isoDateTime;
7
+ let grammar = Sury.utcDateTime;
9
8
 
10
9
  function fromString(raw) {
11
10
  let value;
12
11
  try {
13
- value = S.parseOrThrow(raw, grammar);
12
+ value = Sury.parseOrThrow(raw, grammar);
14
13
  } catch (exn) {
15
14
  return {
16
15
  TAG: "Error",
@@ -18,7 +18,7 @@ function fromInt(raw) {
18
18
  }
19
19
  }
20
20
 
21
- let schema = Semantic$Reventless.refined(Sury.int, Semantic$Reventless.Id.duration, fromInt);
21
+ let schema = Semantic$Reventless.refined(Sury.int32, Semantic$Reventless.Id.duration, fromInt);
22
22
 
23
23
  let scales = [
24
24
  [
@@ -37,7 +37,7 @@ external toString: t => string = "%identity"
37
37
 
38
38
  // Sury's check, held once. `fromString` runs it rather than restating it, and
39
39
  // `schema` is built from `fromString`, so there is exactly one grammar here.
40
- let grammar: S.t<string> = S.email
40
+ let grammar: S.t<string> = (S.email :> S.t<string>)
41
41
 
42
42
  /** Validate a raw string as an email address, saying why when it is not one. */
43
43
  let fromString = (raw: string): result<t, string> =>
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Sury from "sury";
5
4
  import * as Semantic$Reventless from "./Semantic.res.mjs";
6
5
 
@@ -9,7 +8,7 @@ let grammar = Sury.email;
9
8
  function fromString(raw) {
10
9
  let value;
11
10
  try {
12
- value = S.parseOrThrow(raw, grammar);
11
+ value = Sury.parseOrThrow(raw, grammar);
13
12
  } catch (exn) {
14
13
  return {
15
14
  TAG: "Error",
@@ -48,12 +48,12 @@ function validateLng(raw) {
48
48
  }
49
49
  }
50
50
 
51
- let latSchema = S.refine(Sury.float, raw => {
51
+ let latSchema = S.refine(Sury.number, raw => {
52
52
  let match = validateLat(raw);
53
53
  return match.TAG === "Ok";
54
54
  }, "expected a latitude in -90…90", undefined);
55
55
 
56
- let lngSchema = S.refine(Sury.float, raw => {
56
+ let lngSchema = S.refine(Sury.number, raw => {
57
57
  let match = validateLng(raw);
58
58
  return match.TAG === "Ok";
59
59
  }, "expected a longitude in -180…180", undefined);
@@ -91,7 +91,7 @@ let validateAmount = (amount: float): result<float, string> =>
91
91
  The check sits on the field rather than on the pair because wholeness is a
92
92
  property of the amount: it does not need the currency, and a rule placed
93
93
  where it needs nothing else is a rule that cannot be read wrong. (A
94
- record-level refinement is available — sury 11.0.0-rc.2 compiles one
94
+ record-level refinement is available — sury 11 compiles one
95
95
  correctly, where 11-alpha hoisted the result object above the field reads and
96
96
  threw `Cannot access 'v0' before initialization` — it is simply not what this
97
97
  check wants.) */
@@ -28,7 +28,7 @@ function validateAmount(amount) {
28
28
  }
29
29
  }
30
30
 
31
- let amountSchema = S.refine(Sury.float, amount => {
31
+ let amountSchema = S.refine(Sury.number, amount => {
32
32
  let match = validateAmount(amount);
33
33
  return match.TAG === "Ok";
34
34
  }, "expected a monetary amount", undefined);
@@ -10,7 +10,7 @@ let offloadedRefSchema = Sury.$schema(s => ({
10
10
  store: s.m(Sury.string),
11
11
  key: s.m(Sury.string),
12
12
  hash: s.m(Sury.string),
13
- bytes: s.m(Sury.int)
13
+ bytes: s.m(Sury.int32)
14
14
  }));
15
15
 
16
16
  let sentinelKey = "$offload";
@@ -24,7 +24,7 @@ function fromFloat(raw) {
24
24
  }
25
25
  }
26
26
 
27
- let schema = Semantic$Reventless.refined(Sury.float, Semantic$Reventless.Id.percent, fromFloat);
27
+ let schema = Semantic$Reventless.refined(Sury.number, Semantic$Reventless.Id.percent, fromFloat);
28
28
 
29
29
  function format(p) {
30
30
  return p.toString() + "%";
@@ -258,6 +258,17 @@ let identityKey = (fieldSchema: S.t<'a>): option<string> =>
258
258
  | _ => None
259
259
  }
260
260
 
261
+ /** The identity a field names, on its own value or on an array's elements. */
262
+ let rec fieldIdentityKey = (fieldSchema: S.t<unknown>): option<string> =>
263
+ switch identityKey(fieldSchema) {
264
+ | Some(_) as found => found
265
+ | None =>
266
+ switch fieldSchema->unwrapOptional->Option.getOr(fieldSchema) {
267
+ | Array({additionalItems: Schema(item)}) => fieldIdentityKey(item)
268
+ | _ => None
269
+ }
270
+ }
271
+
261
272
  /** Whether a field's schema carries this specific semantic. */
262
273
  let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
263
274
  switch get(fieldSchema) {
@@ -225,6 +225,26 @@ function identityKey(fieldSchema) {
225
225
  }
226
226
  }
227
227
 
228
+ function fieldIdentityKey(_fieldSchema) {
229
+ while (true) {
230
+ let fieldSchema = _fieldSchema;
231
+ let found = identityKey(fieldSchema);
232
+ if (found !== undefined) {
233
+ return found;
234
+ }
235
+ let match = Stdlib_Option.getOr(unwrapOptional(fieldSchema), fieldSchema);
236
+ if (match.type !== "array") {
237
+ return;
238
+ }
239
+ let item = match.additionalItems;
240
+ if (item === "strip" || item === "strict") {
241
+ return;
242
+ }
243
+ _fieldSchema = item;
244
+ continue;
245
+ };
246
+ }
247
+
228
248
  function has(fieldSchema, id) {
229
249
  let s = getFrom(fieldSchema);
230
250
  if (s !== undefined) {
@@ -246,6 +266,7 @@ export {
246
266
  getFrom,
247
267
  get,
248
268
  identityKey,
269
+ fieldIdentityKey,
249
270
  has,
250
271
  }
251
272
  /* semanticId Not a pure module */
@@ -1,6 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
3
+ import * as Sury from "sury";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
@@ -347,7 +347,7 @@ function isWithheld(schema) {
347
347
 
348
348
  function parseSafely(json, schema) {
349
349
  try {
350
- return Primitive_option.some(S.parseOrThrow(json, schema));
350
+ return Primitive_option.some(Sury.parseOrThrow(json, schema));
351
351
  } catch (exn) {
352
352
  return;
353
353
  }
@@ -542,4 +542,4 @@ export {
542
542
  renderSource,
543
543
  variantSchema,
544
544
  }
545
- /* S Not a pure module */
545
+ /* sury Not a pure module */
@@ -38,7 +38,7 @@ external toString: t => string = "%identity"
38
38
  // to an append-only log, so the address has to survive the round trip as the
39
39
  // caller wrote it. `S.uri` is the string-preserving arm — it still rejects what
40
40
  // does not parse, and returns exactly what it was given.
41
- let grammar: S.t<string> = S.uri
41
+ let grammar: S.t<string> = (S.uri :> S.t<string>)
42
42
 
43
43
  // Schemes are case-insensitive per RFC 3986, and `HTTPS://x` parses fine, so the
44
44
  // allowlist has to fold case or it rejects a valid address on a technicality.
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Sury from "sury";
5
4
  import * as Semantic$Reventless from "./Semantic.res.mjs";
6
5
 
@@ -18,7 +17,7 @@ function hasWebScheme(raw) {
18
17
  function fromString(raw) {
19
18
  let value;
20
19
  try {
21
- value = S.parseOrThrow(raw, grammar);
20
+ value = Sury.parseOrThrow(raw, grammar);
22
21
  } catch (exn) {
23
22
  return {
24
23
  TAG: "Error",
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Sury from "sury";
5
4
  import * as Yaml from "yaml";
6
5
  import * as Nodefs from "node:fs";
@@ -25,7 +24,7 @@ function parseString(yamlText) {
25
24
  try {
26
25
  return {
27
26
  TAG: "Ok",
28
- _0: S.parseOrThrow(Yaml.parse(yamlText), entriesSchema)
27
+ _0: Sury.parseOrThrow(Yaml.parse(yamlText), entriesSchema)
29
28
  };
30
29
  } catch (raw_err) {
31
30
  let err = Primitive_exceptions.internalToException(raw_err);
@@ -15,7 +15,7 @@ let schema = Sury.$schema(s => ({
15
15
  userId: s.m(Sury.string),
16
16
  username: s.m(Sury.string),
17
17
  groups: s.m(Sury.array(Sury.string)),
18
- claims: s.m(Sury.$option(Sury.dict(Sury.string))),
18
+ claims: s.m(Sury.$option(Sury.record(Sury.string))),
19
19
  provider: s.m(providerSchema)
20
20
  }));
21
21
 
@@ -62,7 +62,7 @@ function entrySchema(_stateSchema) {
62
62
  return Sury.$schema(s => ({
63
63
  state: s.m(_stateSchema),
64
64
  at: s.m(DateTime$Reventless.schema),
65
- afterGap: s.m(Sury.$option(Sury.bool))
65
+ afterGap: s.m(Sury.$option(Sury.boolean))
66
66
  }));
67
67
  }
68
68
 
@@ -16,7 +16,7 @@ let metaSchema = Sury.$schema(s => ({
16
16
  causationId: s.m(Sury.$option(Sury.string)),
17
17
  traceparent: s.m(Sury.$option(Sury.string)),
18
18
  schemaVersion: s.m(Sury.$option(Sury.string)),
19
- headers: s.m(Sury.$option(Sury.dict(Sury.string)))
19
+ headers: s.m(Sury.$option(Sury.record(Sury.string)))
20
20
  }));
21
21
 
22
22
  let contextSchema = Sury.$schema(s => ({
@@ -33,7 +33,7 @@ let commandJsonSchema = Sury.$schema(s => ({
33
33
  id: s.m(Sury.string),
34
34
  meta: s.m(metaSchema),
35
35
  commandJson: s.m(Sury.json),
36
- delay: s.m(Sury.$option(Sury.int))
36
+ delay: s.m(Sury.$option(Sury.int32))
37
37
  }));
38
38
 
39
39
  let fillMissingDefaults = (function(schema, json, scalarFills){
@@ -79,23 +79,60 @@ type action<'id, 'state> =
79
79
  /** No-op. Return this when an event should not affect the read model. */
80
80
  | Ignore
81
81
 
82
+ /**
83
+ The same action keyed by another representation of its ids. `to_` converts the
84
+ ids the action carries; `from` converts back the ids its callbacks receive. The
85
+ storage edge uses it to turn a typed row key into the string a table is keyed by.
86
+ */
87
+ let mapActionId = (action: action<'a, 's>, ~to_: 'a => 'b, ~from: 'b => 'a): action<'b, 's> =>
88
+ switch action {
89
+ | Create(id, state) => Create(to_(id), state)
90
+ | CreateMany(rows) => CreateMany(rows->Array.map(((id, state)) => (to_(id), state)))
91
+ | Update(id, f) => Update(to_(id), f)
92
+ | UpdateMany(ids, f) => UpdateMany(ids->Array.map(to_), (id, state) => f(from(id), state))
93
+ | UpdateWithDefault(id, default, f) => UpdateWithDefault(to_(id), default, f)
94
+ | UpdateManyWithDefault(ids, default, f) =>
95
+ UpdateManyWithDefault(
96
+ ids->Array.map(to_),
97
+ id => default(from(id)),
98
+ (id, state) => f(from(id), state),
99
+ )
100
+ | Set(id, state) => Set(to_(id), state)
101
+ | SetMany(ids, f) => SetMany(ids->Array.map(to_), id => f(from(id)))
102
+ | Delete(id) => Delete(to_(id))
103
+ | DeleteMany(ids) => DeleteMany(ids->Array.map(to_))
104
+ | DeleteIf(id, p) => DeleteIf(to_(id), p)
105
+ | DeleteManyIf(ids, p) => DeleteManyIf(ids->Array.map(to_), (id, state) => p(from(id), state))
106
+ | CreateMultiState(id, states) => CreateMultiState(to_(id), states)
107
+ | UpdateMultiState(id, f) => UpdateMultiState(to_(id), f)
108
+ | UpdateManyMultiStates(ids, f) =>
109
+ UpdateManyMultiStates(ids->Array.map(to_), (id, states) => f(from(id), states))
110
+ | Ignore => Ignore
111
+ }
112
+
82
113
  /**
83
114
  A compiled single-source-to-single-target mapping.
84
115
 
85
116
  Created by `Projection.Mapping.Make(Source, Target, MappingImpl)`.
86
117
  The `project` function receives a full `Message.event'` envelope and returns
87
- one `action` value.
118
+ one `action` value. Both ends are typed: the envelope id is the source's `Id.t`
119
+ and the row key the target's, so a projection that keys a view by another
120
+ entity's id does not compile. A mapping between different ids converts
121
+ explicitly (`Target.Id.makeFromString`), which marks the seam.
88
122
  */
89
123
  module type Mapping = {
90
124
  //module Source: Source
91
125
  //module Target: Target // NOTE: to be destructive substituted
92
126
  module SourceId: Id.T
127
+ type targetId
93
128
  @schema
94
129
  type sourceEvent
95
130
  @schema
96
131
  type targetState
97
132
 
98
- let project: Message.event'<string, sourceEvent> => action<string, targetState>
133
+ let project: Message.event'<SourceId.t, sourceEvent> => action<targetId, targetState>
134
+ let targetIdToString: targetId => string
135
+ let targetIdFromString: string => targetId
99
136
  let sourceEventSchema: S.t<sourceEvent>
100
137
  let sourceName: string
101
138
  let subIdConfig: option<ReadModel.subIdConfig<targetState>>
@@ -116,9 +153,11 @@ module type Mappings = {
116
153
  }
117
154
 
118
155
  module type MappingImpl = {
156
+ type sourceId
157
+ type targetId
119
158
  type sourceEvent
120
159
  type targetState
121
- let project: Message.event'<string, sourceEvent> => action<string, targetState>
160
+ let project: Message.event'<sourceId, sourceEvent> => action<targetId, targetState>
122
161
  }
123
162
 
124
163
  /**
@@ -147,7 +186,9 @@ module Mapping = {
147
186
  Target: Target,
148
187
  MappingImpl: MappingImpl
149
188
  with type sourceEvent := Source.event
150
- and type targetState := Target.state,
189
+ and type targetState := Target.state
190
+ and type sourceId := Source.Id.t
191
+ and type targetId := Target.Id.t,
151
192
  ): (
152
193
  Mapping
153
194
  with type targetState = Target.state
@@ -155,11 +196,14 @@ module Mapping = {
155
196
  and module SourceId = Source.Id
156
197
  ) => {
157
198
  module SourceId = Source.Id
199
+ type targetId = Target.Id.t
158
200
  @schema
159
201
  type sourceEvent = Source.event
160
202
  @schema
161
203
  type targetState = Target.state
162
204
  let project = MappingImpl.project
205
+ let targetIdToString = Target.Id.toString
206
+ let targetIdFromString = Target.Id.makeFromString
163
207
  let sourceName = Source.name
164
208
  let subIdConfig = Target.subIdConfig
165
209
  }
@@ -1,15 +1,127 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
 
4
+ function mapActionId(action, to_, from) {
5
+ if (typeof action !== "object") {
6
+ return "Ignore";
7
+ }
8
+ switch (action.TAG) {
9
+ case "Create" :
10
+ return {
11
+ TAG: "Create",
12
+ _0: to_(action._0),
13
+ _1: action._1
14
+ };
15
+ case "CreateMany" :
16
+ return {
17
+ TAG: "CreateMany",
18
+ _0: action._0.map(param => [
19
+ to_(param[0]),
20
+ param[1]
21
+ ])
22
+ };
23
+ case "Update" :
24
+ return {
25
+ TAG: "Update",
26
+ _0: to_(action._0),
27
+ _1: action._1
28
+ };
29
+ case "UpdateMany" :
30
+ let f = action._1;
31
+ return {
32
+ TAG: "UpdateMany",
33
+ _0: action._0.map(to_),
34
+ _1: (id, state) => f(from(id), state)
35
+ };
36
+ case "UpdateWithDefault" :
37
+ return {
38
+ TAG: "UpdateWithDefault",
39
+ _0: to_(action._0),
40
+ _1: action._1,
41
+ _2: action._2
42
+ };
43
+ case "UpdateManyWithDefault" :
44
+ let f$1 = action._2;
45
+ let $$default = action._1;
46
+ return {
47
+ TAG: "UpdateManyWithDefault",
48
+ _0: action._0.map(to_),
49
+ _1: id => $$default(from(id)),
50
+ _2: (id, state) => f$1(from(id), state)
51
+ };
52
+ case "Set" :
53
+ return {
54
+ TAG: "Set",
55
+ _0: to_(action._0),
56
+ _1: action._1
57
+ };
58
+ case "SetMany" :
59
+ let f$2 = action._1;
60
+ return {
61
+ TAG: "SetMany",
62
+ _0: action._0.map(to_),
63
+ _1: id => f$2(from(id))
64
+ };
65
+ case "Delete" :
66
+ return {
67
+ TAG: "Delete",
68
+ _0: to_(action._0)
69
+ };
70
+ case "DeleteMany" :
71
+ return {
72
+ TAG: "DeleteMany",
73
+ _0: action._0.map(to_)
74
+ };
75
+ case "DeleteIf" :
76
+ return {
77
+ TAG: "DeleteIf",
78
+ _0: to_(action._0),
79
+ _1: action._1
80
+ };
81
+ case "DeleteManyIf" :
82
+ let p = action._1;
83
+ return {
84
+ TAG: "DeleteManyIf",
85
+ _0: action._0.map(to_),
86
+ _1: (id, state) => p(from(id), state)
87
+ };
88
+ case "CreateMultiState" :
89
+ return {
90
+ TAG: "CreateMultiState",
91
+ _0: to_(action._0),
92
+ _1: action._1
93
+ };
94
+ case "UpdateMultiState" :
95
+ return {
96
+ TAG: "UpdateMultiState",
97
+ _0: to_(action._0),
98
+ _1: action._1
99
+ };
100
+ case "UpdateManyMultiStates" :
101
+ let f$3 = action._1;
102
+ return {
103
+ TAG: "UpdateManyMultiStates",
104
+ _0: action._0.map(to_),
105
+ _1: (id, states) => f$3(from(id), states)
106
+ };
107
+ }
108
+ }
109
+
4
110
  function Make(Source) {
5
- return Target => (MappingImpl => ({
6
- SourceId: Source.Id,
7
- project: MappingImpl.project,
8
- sourceEventSchema: Source.eventSchema,
9
- sourceName: Source.name,
10
- subIdConfig: Target.subIdConfig,
11
- targetStateSchema: Target.stateSchema
12
- }));
111
+ return Target => (MappingImpl => {
112
+ let targetIdToString = Target.Id.toString;
113
+ let targetIdFromString = Target.Id.makeFromString;
114
+ return {
115
+ SourceId: Source.Id,
116
+ project: MappingImpl.project,
117
+ targetIdToString: targetIdToString,
118
+ targetIdFromString: targetIdFromString,
119
+ sourceEventSchema: Source.eventSchema,
120
+ sourceName: Source.name,
121
+ subIdConfig: Target.subIdConfig,
122
+ targetStateSchema: Target.stateSchema
123
+ };
124
+ });
13
125
  }
14
126
 
15
127
  let Mapping = {
@@ -37,6 +149,7 @@ let DcbSource = {
37
149
  };
38
150
 
39
151
  export {
152
+ mapActionId,
40
153
  Mapping,
41
154
  Mappings,
42
155
  DcbSource,
@@ -2,15 +2,15 @@
2
2
 
3
3
  import * as Sury from "sury";
4
4
 
5
- let yearSchema = Sury.int;
5
+ let yearSchema = Sury.int32;
6
6
 
7
- let monthSchema = Sury.int;
7
+ let monthSchema = Sury.int32;
8
8
 
9
- let daySchema = Sury.int;
9
+ let daySchema = Sury.int32;
10
10
 
11
- let hourSchema = Sury.int;
11
+ let hourSchema = Sury.int32;
12
12
 
13
- let minuteSchema = Sury.int;
13
+ let minuteSchema = Sury.int32;
14
14
 
15
15
  let rateSchema = Sury.union([
16
16
  Sury.$schema(s => ({
@@ -23,15 +23,15 @@ let rateSchema = Sury.union([
23
23
  })),
24
24
  Sury.$schema(s => ({
25
25
  TAG: "Minutes",
26
- _0: s.m(Sury.int)
26
+ _0: s.m(Sury.int32)
27
27
  })),
28
28
  Sury.$schema(s => ({
29
29
  TAG: "Hours",
30
- _0: s.m(Sury.int)
30
+ _0: s.m(Sury.int32)
31
31
  })),
32
32
  Sury.$schema(s => ({
33
33
  TAG: "Days",
34
- _0: s.m(Sury.int)
34
+ _0: s.m(Sury.int32)
35
35
  })),
36
36
  Sury.$schema(s => ({
37
37
  TAG: "Daily",
@@ -7,16 +7,19 @@
7
7
  // See docs/plans/sury-11-migration.md.
8
8
 
9
9
  let toJson: ('a, S.t<'a>) => JSON.t = (value, schema) =>
10
- value->S.decodeOrThrow(~from=schema, ~to=S.json)
10
+ value->S.convertOrThrow(~from=schema, ~to=S.json)
11
11
 
12
12
  // `~space` defaults to compact, matching the removed `reverseConvertToJsonStringOrThrow`.
13
13
  let toJsonString = (value: 'a, schema: S.t<'a>, ~space: int=0): string =>
14
- value->S.decodeOrThrow(~from=schema, ~to=space == 0 ? S.jsonString : S.jsonStringWithSpace(space))
14
+ (value->S.convertOrThrow(
15
+ ~from=schema,
16
+ ~to=space == 0 ? S.jsonString : S.jsonStringWithSpace(space),
17
+ ) :> string)
15
18
 
16
19
  let fromJson: (JSON.t, S.t<'a>) => 'a = (json, schema) => json->S.parseOrThrow(~to=schema)
17
20
 
18
21
  let fromJsonString: (string, S.t<'a>) => 'a = (str, schema) =>
19
- str->S.decodeOrThrow(~from=S.jsonString, ~to=schema)
22
+ S.JsonString(str)->S.convertOrThrow(~from=S.jsonString, ~to=schema)
20
23
 
21
24
  // Sury raises `S.Exn`, which carries `RE_EXN_ID: "S.Exn"` and so is not a `JsExn`
22
25
  // even though it is a JS Error at runtime — the usual
@@ -6,18 +6,20 @@ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
 
8
8
  function toJson(value, schema) {
9
- return S.decodeOrThrow(value, schema, Sury.json);
9
+ return S.convertOrThrow(value, schema, undefined, Sury.json);
10
10
  }
11
11
 
12
12
  function toJsonString(value, schema, spaceOpt) {
13
13
  let space = spaceOpt !== undefined ? spaceOpt : 0;
14
- return S.decodeOrThrow(value, schema, space === 0 ? Sury.jsonString : Sury.jsonStringWithSpace(space));
14
+ return S.convertOrThrow(value, schema, undefined, space === 0 ? Sury.jsonString : Sury.jsonStringWithSpace(space));
15
15
  }
16
16
 
17
- let fromJson = S.parseOrThrow;
17
+ function fromJson(json, schema) {
18
+ return Sury.parseOrThrow(json, schema);
19
+ }
18
20
 
19
21
  function fromJsonString(str, schema) {
20
- return S.decodeOrThrow(str, Sury.jsonString, schema);
22
+ return S.convertOrThrow(str, Sury.jsonString, undefined, schema);
21
23
  }
22
24
 
23
25
  function exnMessage(exn, fallbackOpt) {