@reventlessdev/reventless-spec 3.0.0-alpha.100

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 (139) hide show
  1. package/CHANGELOG.md +931 -0
  2. package/LICENSE +202 -0
  3. package/README.md +109 -0
  4. package/package.json +49 -0
  5. package/rescript.json +32 -0
  6. package/run-generator.mjs +2 -0
  7. package/run-platform-generator.mjs +2 -0
  8. package/scripts/generate-currency.mjs +215 -0
  9. package/scripts/iso-4217-list-one.xml +1956 -0
  10. package/src/AnsiStyle.res +40 -0
  11. package/src/AnsiStyle.res.mjs +54 -0
  12. package/src/LogPrefix.res +192 -0
  13. package/src/LogPrefix.res.mjs +159 -0
  14. package/src/PackageVersion.res +67 -0
  15. package/src/PackageVersion.res.mjs +81 -0
  16. package/src/components/Aggregate.res +64 -0
  17. package/src/components/Aggregate.res.mjs +2 -0
  18. package/src/components/AutomationSlice.res +279 -0
  19. package/src/components/AutomationSlice.res.mjs +30 -0
  20. package/src/components/CapabilityManifest.res +74 -0
  21. package/src/components/CapabilityManifest.res.mjs +61 -0
  22. package/src/components/ComponentKind.res +99 -0
  23. package/src/components/ComponentKind.res.mjs +125 -0
  24. package/src/components/Counter.res +24 -0
  25. package/src/components/Counter.res.mjs +2 -0
  26. package/src/components/DcbDecode.res +118 -0
  27. package/src/components/DcbDecode.res.mjs +100 -0
  28. package/src/components/DcbScopeInference.res +244 -0
  29. package/src/components/DcbScopeInference.res.mjs +177 -0
  30. package/src/components/DcbTag.res +1335 -0
  31. package/src/components/DcbTag.res.mjs +898 -0
  32. package/src/components/DcbValidation.res +427 -0
  33. package/src/components/DcbValidation.res.mjs +423 -0
  34. package/src/components/DisplayName.res +40 -0
  35. package/src/components/DisplayName.res.mjs +26 -0
  36. package/src/components/ExtensionPoint.res +27 -0
  37. package/src/components/ExtensionPoint.res.mjs +2 -0
  38. package/src/components/InboundTranslationSlice.res +85 -0
  39. package/src/components/InboundTranslationSlice.res.mjs +2 -0
  40. package/src/components/OutboundTranslationSlice.res +153 -0
  41. package/src/components/OutboundTranslationSlice.res.mjs +2 -0
  42. package/src/components/Plugin.res +538 -0
  43. package/src/components/Plugin.res.mjs +264 -0
  44. package/src/components/PluginName.res +39 -0
  45. package/src/components/PluginName.res.mjs +45 -0
  46. package/src/components/ReadModel.res +199 -0
  47. package/src/components/ReadModel.res.mjs +18 -0
  48. package/src/components/Reference.res +55 -0
  49. package/src/components/Reference.res.mjs +50 -0
  50. package/src/components/Snapshot.res +26 -0
  51. package/src/components/Snapshot.res.mjs +2 -0
  52. package/src/components/StateAnnotations.res +97 -0
  53. package/src/components/StateAnnotations.res.mjs +15 -0
  54. package/src/components/StateChangeSlice.res +131 -0
  55. package/src/components/StateChangeSlice.res.mjs +2 -0
  56. package/src/components/StateViewSlice.res +123 -0
  57. package/src/components/StateViewSlice.res.mjs +2 -0
  58. package/src/components/Task.res +62 -0
  59. package/src/components/Task.res.mjs +2 -0
  60. package/src/generator/Codegen.res +842 -0
  61. package/src/generator/Codegen.res.mjs +565 -0
  62. package/src/generator/Config.res +106 -0
  63. package/src/generator/Config.res.mjs +69 -0
  64. package/src/generator/Discovery.res +230 -0
  65. package/src/generator/Discovery.res.mjs +198 -0
  66. package/src/generator/Generator_Node.res +14 -0
  67. package/src/generator/Generator_Node.res.mjs +18 -0
  68. package/src/generator/Pairing.res +460 -0
  69. package/src/generator/Pairing.res.mjs +415 -0
  70. package/src/generator/PlatformCodegen.res +207 -0
  71. package/src/generator/PlatformCodegen.res.mjs +154 -0
  72. package/src/generator/PlatformGenerator.res +126 -0
  73. package/src/generator/PlatformGenerator.res.mjs +114 -0
  74. package/src/generator/PlatformManifests.res +203 -0
  75. package/src/generator/PlatformManifests.res.mjs +212 -0
  76. package/src/generator/PluginGenerator.res +57 -0
  77. package/src/generator/PluginGenerator.res.mjs +73 -0
  78. package/src/semantic/Bytes.res +54 -0
  79. package/src/semantic/Bytes.res.mjs +38 -0
  80. package/src/semantic/Capabilities.res +43 -0
  81. package/src/semantic/Capabilities.res.mjs +17 -0
  82. package/src/semantic/Color.res +51 -0
  83. package/src/semantic/Color.res.mjs +29 -0
  84. package/src/semantic/Currency.res +598 -0
  85. package/src/semantic/Currency.res.mjs +743 -0
  86. package/src/semantic/DateRange.res +148 -0
  87. package/src/semantic/DateRange.res.mjs +74 -0
  88. package/src/semantic/Duration.res +53 -0
  89. package/src/semantic/Duration.res.mjs +26 -0
  90. package/src/semantic/Email.res +51 -0
  91. package/src/semantic/Email.res.mjs +31 -0
  92. package/src/semantic/GeoPoint.res +226 -0
  93. package/src/semantic/GeoPoint.res.mjs +190 -0
  94. package/src/semantic/Geocoding.res +127 -0
  95. package/src/semantic/Geocoding.res.mjs +36 -0
  96. package/src/semantic/Money.res +196 -0
  97. package/src/semantic/Money.res.mjs +138 -0
  98. package/src/semantic/Offload.res +294 -0
  99. package/src/semantic/Offload.res.mjs +191 -0
  100. package/src/semantic/Percent.res +53 -0
  101. package/src/semantic/Percent.res.mjs +33 -0
  102. package/src/semantic/Phone.res +55 -0
  103. package/src/semantic/Phone.res.mjs +29 -0
  104. package/src/semantic/Semantic.res +162 -0
  105. package/src/semantic/Semantic.res.mjs +95 -0
  106. package/src/semantic/StorageRef.res +164 -0
  107. package/src/semantic/StorageRef.res.mjs +111 -0
  108. package/src/semantic/Url.res +66 -0
  109. package/src/semantic/Url.res.mjs +48 -0
  110. package/src/types/Authorization.res +23 -0
  111. package/src/types/Authorization.res.mjs +33 -0
  112. package/src/types/Behavior.res +86 -0
  113. package/src/types/Behavior.res.mjs +2 -0
  114. package/src/types/DateTime.res +29 -0
  115. package/src/types/DateTime.res.mjs +16 -0
  116. package/src/types/EventMapping.res +100 -0
  117. package/src/types/EventMapping.res.mjs +15 -0
  118. package/src/types/Handler.res +30 -0
  119. package/src/types/Handler.res.mjs +2 -0
  120. package/src/types/Id.res +75 -0
  121. package/src/types/Id.res.mjs +37 -0
  122. package/src/types/Identity.res +46 -0
  123. package/src/types/Identity.res.mjs +51 -0
  124. package/src/types/Message.res +326 -0
  125. package/src/types/Message.res.mjs +186 -0
  126. package/src/types/Projection.res +220 -0
  127. package/src/types/Projection.res.mjs +44 -0
  128. package/src/types/QueryEngine.res +123 -0
  129. package/src/types/QueryEngine.res.mjs +12 -0
  130. package/src/types/ReadConsistency.res +38 -0
  131. package/src/types/ReadConsistency.res.mjs +29 -0
  132. package/src/types/Schedule.res +65 -0
  133. package/src/types/Schedule.res.mjs +68 -0
  134. package/src/types/SideEffect.res +45 -0
  135. package/src/types/SideEffect.res.mjs +2 -0
  136. package/src/types/StoredEvent.res +46 -0
  137. package/src/types/StoredEvent.res.mjs +32 -0
  138. package/src/types/Visibility.res +24 -0
  139. package/src/types/Visibility.res.mjs +25 -0
@@ -0,0 +1,294 @@
1
+ /**
2
+ A field whose large value lives in a content-addressed object store, carried by
3
+ reference — or inline when it is small enough not to be worth a round trip.
4
+
5
+ ## Sibling of `StorageRef`, not the same thing
6
+
7
+ `StorageRef` and `Offload` are two members of one family: both say "this field's
8
+ value lives in an object store", both declare that store through the shared
9
+ `Semantic.StoredIn` marker, and both are produced by a **client** that uploads
10
+ the bytes before the command is issued — never by the framework inside `decide`.
11
+ They differ in what the field carries:
12
+
13
+ - `@storageRef` is always a reference (an origin-relative path a store minted),
14
+ and the reference *is* the value the reader sees (a URL it renders).
15
+ - `@offload` is an **inline-or-reference** value. Below a size threshold the
16
+ value stays embedded; above it the client stores the bytes under a
17
+ content-addressed key and the field carries `Offloaded{store, key, hash, bytes}`.
18
+ A reader resolves either arm back to the value.
19
+
20
+ Content addressing (the key is the SHA-256 of the bytes) makes the store write
21
+ idempotent and deduplicating: the same value stored twice lands on the same key,
22
+ so identical payloads across versions or tenants hold one object, not many.
23
+
24
+ ## The backward-compatible wire form
25
+
26
+ Every event already in history stored the value **inline and unwrapped** — a
27
+ plain record, with no variant tag. So the codec here must decode those bytes
28
+ unchanged, which rules out sury's default tagged-union encoding (`{TAG, _0}`).
29
+
30
+ Instead the codec is *untagged* and sniffs a reserved sentinel key:
31
+
32
+ - an `Offloaded` value encodes as `{"$offload": {store, key, hash, bytes}}`;
33
+ - an `Inline` value encodes as the raw value, exactly as before.
34
+
35
+ On decode, a JSON object carrying the `$offload` key is an `Offloaded`; anything
36
+ else is decoded as the inner value into `Inline`. A record field name can never
37
+ be `$offload` (identifiers cannot start with `$`), so a legacy inline payload can
38
+ never be mistaken for a reference, and vice versa — no migration, and a
39
+ pre-change fixture decodes as `Inline` untouched.
40
+
41
+ @example
42
+ ```rescript
43
+ // an event/command field, optional and offloadable to the "pluginStructures" store
44
+ structure: @s.matches(Offload.optionSchema(~store="pluginStructures", pluginStructureSchema))
45
+ option<Offload.payload<pluginStructure>>
46
+ ```
47
+ */
48
+
49
+ /** The reference an offloaded value carries: which store holds it, the
50
+ content-addressed key, the content hash (== the key's basis), and the byte
51
+ length. `hash` is redundant with `key` today (`key` is `sha256/<hash>`) but
52
+ named so a reader can verify integrity without parsing the key. */
53
+ @schema
54
+ type offloadedRef = {
55
+ store: string,
56
+ key: string,
57
+ hash: string,
58
+ bytes: int,
59
+ }
60
+
61
+ /** A field's value: embedded, or a reference to bytes the client stored. */
62
+ type payload<'a> =
63
+ | Inline('a)
64
+ | Offloaded(offloadedRef)
65
+
66
+ /** `nullableAsOption` emits `T | undefined | null`, which fails
67
+ `jsonableValidation` inside an event union; `js_nullable` emits `T | null`,
68
+ which is JSON-safe there. Same reason `Plugin.res` reaches for it. */
69
+ @module("sury/src/Sury.res.mjs")
70
+ external _jsNullable: (S.t<'a>, unit) => S.t<option<'a>> = "js_nullable"
71
+
72
+ /** The codec builds on `S.json`, which sury 11 gates behind an explicit enable.
73
+ Doing it here (at module load, before any `schema` call) makes the primitive
74
+ self-contained: importing `Offload` is enough, no consumer has to remember. */
75
+ S.enableJson()
76
+
77
+ /** The object key under which an `Offloaded` value hides. Reserved: no ReScript
78
+ record field encodes to a key starting with `$`, so it cannot collide with an
79
+ inline payload's own fields. */
80
+ let sentinelKey = "$offload"
81
+
82
+ /**
83
+ The untagged inline-or-reference codec for a field of inner type `'a`.
84
+
85
+ Parameterised by the inner value's schema because the `Inline` arm round-trips
86
+ through it. The `Offloaded` arm round-trips through `offloadedRefSchema` under the
87
+ sentinel key. See the module doc for why this is untagged.
88
+ */
89
+ let schema = (inner: S.t<'a>): S.t<payload<'a>> => {
90
+ // Offloaded arm: recognised by the reserved sentinel key, strict, and tried
91
+ // first so an offloaded value is never mistaken for an inline one. The ref is
92
+ // a fixed new shape that needs no healing, so a direct json-transform is fine.
93
+ let offloadedArm = S.json->S.transform(s => {
94
+ parser: json =>
95
+ switch json->JSON.Decode.object->Option.flatMap(dict => dict->Dict.get(sentinelKey)) {
96
+ | Some(refJson) => Offloaded(refJson->S.parseJsonOrThrow(offloadedRefSchema))
97
+ | None => s.fail("not an offloaded reference")
98
+ },
99
+ serializer: payload =>
100
+ switch payload {
101
+ | Offloaded(ref) =>
102
+ Dict.fromArray([(sentinelKey, ref->S.reverseConvertToJsonOrThrow(offloadedRefSchema))])
103
+ ->JSON.Encode.object
104
+ | Inline(_) => s.fail("not an offloaded reference")
105
+ },
106
+ })
107
+ // Inline arm: the inner schema applied through sury's own pipeline, so it
108
+ // inherits whatever tolerance the surrounding decode uses — the lifecycle
109
+ // Message decoder heals older payloads with missing fields. This is why the
110
+ // codec is a union rather than one json-transform with a nested
111
+ // parseJsonOrThrow: a nested parse runs strict and breaks the frozen corpus.
112
+ let inlineArm = inner->S.transform(s => {
113
+ parser: value => Inline(value),
114
+ serializer: payload =>
115
+ switch payload {
116
+ | Inline(value) => value
117
+ | Offloaded(_) => s.fail("not an inline value")
118
+ },
119
+ })
120
+ S.union([offloadedArm, inlineArm])
121
+ }
122
+
123
+ /**
124
+ The codec plus the `StoredIn` marker declaring which store the field's references
125
+ live in, for a **non-optional** field. `plugin` is absent for the declaring
126
+ plugin's own store; qualify as `"<plugin>.<store>"` to point at another's.
127
+
128
+ Prefer the `@offload("<store>")` ppx shorthand over calling this by hand.
129
+ */
130
+ let forStore = (
131
+ ~plugin: option<string>=?,
132
+ ~store: string,
133
+ ~threshold: option<int>=?,
134
+ inner: S.t<'a>,
135
+ ): S.t<payload<'a>> =>
136
+ schema(inner)->Semantic.mark(~id=Semantic.Id.offload, ~payload=StoredIn({plugin, store, threshold}))
137
+
138
+ /**
139
+ The codec wrapped for an **optional** field (`js_nullable`), plus the `StoredIn`
140
+ marker. This is the common case: offloadable fields are usually optional (absent
141
+ for older protocol versions, say). The marker sits on the outer schema, where
142
+ `Semantic.get` reads it first.
143
+ */
144
+ let optionSchema = (
145
+ ~plugin: option<string>=?,
146
+ ~store: string,
147
+ ~threshold: option<int>=?,
148
+ inner: S.t<'a>,
149
+ ): S.t<option<payload<'a>>> =>
150
+ _jsNullable(schema(inner), ())->Semantic.mark(
151
+ ~id=Semantic.Id.offload,
152
+ ~payload=StoredIn({plugin, store, threshold}),
153
+ )
154
+
155
+ /** Serialize a payload to its **untagged** wire JSON: `Inline` becomes the bare
156
+ inner value's JSON, `Offloaded` becomes `{"$offload": {...}}`. Used where a
157
+ payload must be stored as a plain JSON blob rather than the ReScript variant —
158
+ e.g. the plugin read model, whose DynamoDB write path marshals the raw value
159
+ and would otherwise persist the variant's runtime `{TAG, _0}` shape. */
160
+ let toJson = (inner: S.t<'a>, payload: payload<'a>): JSON.t =>
161
+ payload->S.reverseConvertToJsonOrThrow(schema(inner))
162
+
163
+ /** The inline value, if this payload is `Inline`. `None` for `Offloaded` — a
164
+ caller that must handle both arms uses `resolve` (async, fetches the ref); a
165
+ caller that only ever sees inline values (a test, or a path where offloading
166
+ is not yet wired) uses this. */
167
+ let getInline = (payload: payload<'a>): option<'a> =>
168
+ switch payload {
169
+ | Inline(value) => Some(value)
170
+ | Offloaded(_) => None
171
+ }
172
+
173
+ // ── Client helpers ─────────────────────────────────────────────────────────
174
+ //
175
+ // The producer/reader surface that makes offloading easy for a client to drive.
176
+ // Both take their store I/O as injected transports so this module stays pure
177
+ // and provider-agnostic: a browser passes a presigned PUT / fetch, the Node seed
178
+ // passes a direct SDK call, a test passes an in-memory map. The plugin's own
179
+ // deploy-time producer is the exception — it offloads Pulumi-natively (declares a
180
+ // content-addressed object resource) rather than through `prepare`, because its
181
+ // value is a `Pulumi.Output` and a resource cannot be created inside `.apply`.
182
+
183
+ /**
184
+ Decide inline-or-offloaded for one value and, when it is large, upload it.
185
+
186
+ Serializes `value` with `schema`; if the JSON is below `threshold` it stays
187
+ `Inline` (no round trip). Otherwise it is hashed, uploaded under the
188
+ content-addressed key `sha256/<hash>`, and returned as `Offloaded`. Because the
189
+ key is the content hash, re-uploading identical bytes writes the same object —
190
+ idempotent, and the source of cross-version/cross-client dedupe.
191
+
192
+ `~hash` and `~upload` are injected so this stays provider-agnostic; `~hash` must
193
+ be a stable content hash (the same bytes must always hash the same). Size is the
194
+ JSON string's length in characters — a close proxy for byte length on the
195
+ mostly-ASCII JSON these payloads are, and only ever used for the threshold cut.
196
+ */
197
+ let prepare = (
198
+ value: 'a,
199
+ ~schema: S.t<'a>,
200
+ ~store: string,
201
+ ~threshold: int,
202
+ ~hash: string => string,
203
+ ~upload: (~key: string, ~bytes: string) => promise<unit>,
204
+ ): promise<payload<'a>> => {
205
+ let bytes = value->S.reverseConvertToJsonStringOrThrow(schema)
206
+ if bytes->String.length < threshold {
207
+ Promise.resolve(Inline(value))
208
+ } else {
209
+ let digest = hash(bytes)
210
+ let key = "sha256/" ++ digest
211
+ upload(~key, ~bytes)->Promise.then(() =>
212
+ Promise.resolve(Offloaded({store, key, hash: digest, bytes: bytes->String.length}))
213
+ )
214
+ }
215
+ }
216
+
217
+ /**
218
+ Resolve a payload back to its value: `Inline` directly, `Offloaded` by fetching
219
+ the object's bytes (via the injected `~fetch`) and decoding with `schema`.
220
+
221
+ Offloaded objects are written by current code, so their bytes decode strictly —
222
+ unlike the inline arm of the wire codec, which heals older event payloads.
223
+ */
224
+ let resolve = (payload: payload<'a>, ~schema: S.t<'a>, ~fetch: string => promise<string>): promise<
225
+ 'a,
226
+ > =>
227
+ switch payload {
228
+ | Inline(value) => Promise.resolve(value)
229
+ | Offloaded({key}) =>
230
+ fetch(key)->Promise.then(bytes => Promise.resolve(bytes->S.parseJsonStringOrThrow(schema)))
231
+ }
232
+
233
+ /**
234
+ Wrap a `~fetch` so each content-addressed key is fetched at most once per process.
235
+
236
+ Keys are immutable (they are content hashes), so a fetched object can be cached
237
+ forever — every replay/projection that references the same hash reuses it, and
238
+ concurrent resolves of the same key share the one in-flight promise.
239
+ */
240
+ let cachedFetch = (fetch: string => promise<string>): (string => promise<string>) => {
241
+ let cache: dict<promise<string>> = Dict.make()
242
+ key =>
243
+ switch cache->Dict.get(key) {
244
+ | Some(inflight) => inflight
245
+ | None =>
246
+ let inflight = fetch(key)
247
+ cache->Dict.set(key, inflight)
248
+ inflight
249
+ }
250
+ }
251
+
252
+ /** The store an `@offload` field declares, if any — the read side of the marker,
253
+ used by provisioning to know the field requires this store to exist. Distinct
254
+ from `StorageRef.getStore` by the semantic id, so the two families stay
255
+ separable (offload objects are content-addressed and durable up front; they
256
+ must not be swept by the pending-upload claimer). */
257
+ let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
258
+ switch Semantic.get(schema) {
259
+ | Some({id, payload: StoredIn(target)}) if id == Semantic.Id.offload => Some(target)
260
+ | _ => None
261
+ }
262
+
263
+ /** The framework-default inline-vs-offloaded byte cut, used when neither the field
264
+ marker nor the platform declares one. Retuning any level is safe: both arms of
265
+ the codec read back to identical bytes, so the threshold only decides how
266
+ *future* values are split — no wire change, no re-encoding, existing events
267
+ stay valid. */
268
+ let defaultThreshold = 8192
269
+
270
+ /** The per-field threshold an `@offload` field declares, if any — the top of the
271
+ precedence chain (`@offload({..., threshold})`). `None` when the field left it
272
+ unset, which defers to the platform default and then {!defaultThreshold}. */
273
+ let getThreshold = (schema: S.t<'a>): option<int> =>
274
+ switch Semantic.get(schema) {
275
+ | Some({id, payload: StoredIn({threshold})}) if id == Semantic.Id.offload => threshold
276
+ | _ => None
277
+ }
278
+
279
+ /**
280
+ The effective threshold for a field, resolving the precedence chain most-specific
281
+ first: the per-field `@offload({threshold})` (read from the field's schema), then
282
+ the platform config default (`~platformDefault`, e.g. `MakeWithConfig`'s
283
+ `offloadThreshold`), then {!defaultThreshold}.
284
+
285
+ A client drives an offloadable field by reading its field schema, calling this to
286
+ get the cut, and passing the result as {!prepare}'s `~threshold`. `prepare` stays
287
+ threshold-explicit (it holds the *value* schema, not the field schema that carries
288
+ the marker), so this is the seam that turns the declaration into a number.
289
+ */
290
+ let effectiveThreshold = (schema: S.t<'a>, ~platformDefault: option<int>=?, ()): int =>
291
+ switch getThreshold(schema) {
292
+ | Some(t) => t
293
+ | None => platformDefault->Option.getOr(defaultThreshold)
294
+ }
@@ -0,0 +1,191 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
8
+ import * as SuryResMjs from "sury/src/Sury.res.mjs";
9
+
10
+ let offloadedRefSchema = S.schema(s => ({
11
+ store: s.m(S.string),
12
+ key: s.m(S.string),
13
+ hash: s.m(S.string),
14
+ bytes: s.m(S.int)
15
+ }));
16
+
17
+ S.enableJson();
18
+
19
+ let sentinelKey = "$offload";
20
+
21
+ function schema(inner) {
22
+ let offloadedArm = S.transform(S.json, s => ({
23
+ p: json => {
24
+ let refJson = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), dict => dict[sentinelKey]);
25
+ if (refJson !== undefined) {
26
+ return {
27
+ TAG: "Offloaded",
28
+ _0: S.parseJsonOrThrow(refJson, offloadedRefSchema)
29
+ };
30
+ } else {
31
+ return s.fail("not an offloaded reference", undefined);
32
+ }
33
+ },
34
+ s: payload => {
35
+ if (payload.TAG === "Inline") {
36
+ return s.fail("not an offloaded reference", undefined);
37
+ } else {
38
+ return Object.fromEntries([[
39
+ sentinelKey,
40
+ S.reverseConvertToJsonOrThrow(payload._0, offloadedRefSchema)
41
+ ]]);
42
+ }
43
+ }
44
+ }));
45
+ let inlineArm = S.transform(inner, s => ({
46
+ p: value => ({
47
+ TAG: "Inline",
48
+ _0: value
49
+ }),
50
+ s: payload => {
51
+ if (payload.TAG === "Inline") {
52
+ return payload._0;
53
+ } else {
54
+ return s.fail("not an inline value", undefined);
55
+ }
56
+ }
57
+ }));
58
+ return S.union([
59
+ offloadedArm,
60
+ inlineArm
61
+ ]);
62
+ }
63
+
64
+ function forStore(plugin, store, threshold, inner) {
65
+ return Semantic$Reventless.mark(schema(inner), Semantic$Reventless.Id.offload, {
66
+ TAG: "StoredIn",
67
+ _0: {
68
+ plugin: plugin,
69
+ store: store,
70
+ threshold: threshold
71
+ }
72
+ });
73
+ }
74
+
75
+ function optionSchema(plugin, store, threshold, inner) {
76
+ return Semantic$Reventless.mark(SuryResMjs.js_nullable(schema(inner)), Semantic$Reventless.Id.offload, {
77
+ TAG: "StoredIn",
78
+ _0: {
79
+ plugin: plugin,
80
+ store: store,
81
+ threshold: threshold
82
+ }
83
+ });
84
+ }
85
+
86
+ function toJson(inner, payload) {
87
+ return S.reverseConvertToJsonOrThrow(payload, schema(inner));
88
+ }
89
+
90
+ function getInline(payload) {
91
+ if (payload.TAG === "Inline") {
92
+ return Primitive_option.some(payload._0);
93
+ }
94
+ }
95
+
96
+ function prepare(value, schema, store, threshold, hash, upload) {
97
+ let bytes = S.reverseConvertToJsonStringOrThrow(value, schema, undefined);
98
+ if (bytes.length < threshold) {
99
+ return Promise.resolve({
100
+ TAG: "Inline",
101
+ _0: value
102
+ });
103
+ }
104
+ let digest = hash(bytes);
105
+ let key = "sha256/" + digest;
106
+ return upload(key, bytes).then(() => Promise.resolve({
107
+ TAG: "Offloaded",
108
+ _0: {
109
+ store: store,
110
+ key: key,
111
+ hash: digest,
112
+ bytes: bytes.length
113
+ }
114
+ }));
115
+ }
116
+
117
+ function resolve(payload, schema, fetch) {
118
+ if (payload.TAG === "Inline") {
119
+ return Promise.resolve(payload._0);
120
+ } else {
121
+ return fetch(payload._0.key).then(bytes => Promise.resolve(S.parseJsonStringOrThrow(bytes, schema)));
122
+ }
123
+ }
124
+
125
+ function cachedFetch(fetch) {
126
+ let cache = {};
127
+ return key => {
128
+ let inflight = cache[key];
129
+ if (inflight !== undefined) {
130
+ return inflight;
131
+ }
132
+ let inflight$1 = fetch(key);
133
+ cache[key] = inflight$1;
134
+ return inflight$1;
135
+ };
136
+ }
137
+
138
+ function getStore(schema) {
139
+ let match = Semantic$Reventless.get(schema);
140
+ if (match === undefined) {
141
+ return;
142
+ }
143
+ let target = match.payload;
144
+ if (typeof target !== "object" || target.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
145
+ return;
146
+ } else {
147
+ return target._0;
148
+ }
149
+ }
150
+
151
+ function getThreshold(schema) {
152
+ let match = Semantic$Reventless.get(schema);
153
+ if (match === undefined) {
154
+ return;
155
+ }
156
+ let match$1 = match.payload;
157
+ if (typeof match$1 !== "object" || match$1.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
158
+ return;
159
+ } else {
160
+ return match$1._0.threshold;
161
+ }
162
+ }
163
+
164
+ function effectiveThreshold(schema, platformDefault, param) {
165
+ let t = getThreshold(schema);
166
+ if (t !== undefined) {
167
+ return t;
168
+ } else {
169
+ return Stdlib_Option.getOr(platformDefault, 8192);
170
+ }
171
+ }
172
+
173
+ let defaultThreshold = 8192;
174
+
175
+ export {
176
+ offloadedRefSchema,
177
+ sentinelKey,
178
+ schema,
179
+ forStore,
180
+ optionSchema,
181
+ toJson,
182
+ getInline,
183
+ prepare,
184
+ resolve,
185
+ cachedFetch,
186
+ getStore,
187
+ defaultThreshold,
188
+ getThreshold,
189
+ effectiveThreshold,
190
+ }
191
+ /* offloadedRefSchema Not a pure module */
@@ -0,0 +1,53 @@
1
+ /**
2
+ Marks a `float` field as a percentage, expressed **0–100**.
3
+
4
+ ## Why 0–100 and not 0–1
5
+
6
+ Both conventions are defensible in the abstract, so the tie is broken by the
7
+ consumer that already exists: the dashboard gauges a field with this semantic
8
+ against fixed bounds of 0 and 100, and formats `42.0` as `"42%"`. Under a 0–1
9
+ convention every value would render as a rounding error near zero — a gauge
10
+ pinned at empty and a label reading `"0.42%"`.
11
+
12
+ That failure is quiet, and it is quiet in the worst way: the numbers are
13
+ *present* and *wrong*, and the layer at fault is not the one showing the symptom.
14
+ Agreeing with the renderer costs nothing; disagreeing costs an afternoon.
15
+
16
+ A fraction is still perfectly good arithmetic — it just multiplies by 100 before
17
+ it becomes this type.
18
+
19
+ ## The grammar
20
+
21
+ A finite number in `[0, 100]`. Fractions are allowed: `99.95` is a percentage.
22
+
23
+ @example
24
+ ```rescript
25
+ @schema type state = {
26
+ productId: string,
27
+ taxRate: @s.matches(Reventless.Percent.schema) float,
28
+ }
29
+ ```
30
+ */
31
+
32
+ /** The percentage's representation. Transparent `float`: the marker refines an
33
+ existing numeric field rather than replacing it, so nothing stored changes. */
34
+ type t = float
35
+
36
+ external unsafe: float => t = "%identity"
37
+ external toFloat: t => float = "%identity"
38
+
39
+ /** Validate a number as a percentage, saying why when it is not one. */
40
+ let fromFloat = (raw: float): result<t, string> =>
41
+ if !Float.isFinite(raw) {
42
+ Error(`a percentage must be a finite number, got ${Float.toString(raw)}`)
43
+ } else if raw < 0.0 || raw > 100.0 {
44
+ Error(
45
+ `a percentage runs from 0 to 100, got ${Float.toString(raw)}. ` ++
46
+ `This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`,
47
+ )
48
+ } else {
49
+ Ok(raw)
50
+ }
51
+
52
+ /** The sury schema for a percentage field. Use with `@s.matches(Reventless.Percent.schema)`. */
53
+ let schema: S.t<t> = S.float->Semantic.refined(~id=Semantic.Id.percent, ~check=fromFloat)
@@ -0,0 +1,33 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
5
+
6
+ function fromFloat(raw) {
7
+ if (isFinite(raw)) {
8
+ if (raw < 0.0 || raw > 100.0) {
9
+ return {
10
+ TAG: "Error",
11
+ _0: `a percentage runs from 0 to 100, got ` + raw.toString() + `. This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`
12
+ };
13
+ } else {
14
+ return {
15
+ TAG: "Ok",
16
+ _0: raw
17
+ };
18
+ }
19
+ } else {
20
+ return {
21
+ TAG: "Error",
22
+ _0: `a percentage must be a finite number, got ` + raw.toString()
23
+ };
24
+ }
25
+ }
26
+
27
+ let schema = Semantic$Reventless.refined(S.float, Semantic$Reventless.Id.percent, fromFloat);
28
+
29
+ export {
30
+ fromFloat,
31
+ schema,
32
+ }
33
+ /* schema Not a pure module */
@@ -0,0 +1,55 @@
1
+ /**
2
+ Marks a `string` field as a phone number in E.164 form.
3
+
4
+ ## Why E.164, and only E.164
5
+
6
+ A phone number written the way a person says it out loud — `(030) 12 34 56`,
7
+ `+49 30 123456`, `0049-30-123456` — is three different strings for one number,
8
+ so a log full of them cannot be searched, deduplicated or dialled reliably. E.164
9
+ is the one form that is unambiguous internationally, and it is the form the
10
+ `tel:` link this field renders as actually wants.
11
+
12
+ That makes this the one branded scalar that is likely to *reject* input a form
13
+ would otherwise have accepted, and it should: the alternative is storing an
14
+ un-dialable string permanently. Normalising a local number into E.164 needs a
15
+ default country the framework does not know, so that belongs to the caller,
16
+ before the command.
17
+
18
+ ## The grammar
19
+
20
+ `+`, then a country digit 1–9, then up to 14 more digits — at most 15 in total,
21
+ which is the E.164 limit. No spaces, no punctuation, no leading zero after the
22
+ `+`.
23
+
24
+ @example
25
+ ```rescript
26
+ @schema type command =
27
+ | SetContactPhone({
28
+ customerId: @s.matches(DcbTag.string) string,
29
+ phone: @s.matches(Reventless.Phone.schema) string,
30
+ })
31
+ ```
32
+ */
33
+
34
+ /** The number's representation. Transparent `string`; see `Email.t`. */
35
+ type t = string
36
+
37
+ external unsafe: string => t = "%identity"
38
+ external toString: t => string = "%identity"
39
+
40
+ let e164 = /^\+[1-9]\d{0,14}$/
41
+
42
+ /** Validate a raw string as an E.164 number, saying why when it is not one. */
43
+ let fromString = (raw: string): result<t, string> =>
44
+ if e164->RegExp.test(raw) {
45
+ Ok(raw)
46
+ } else {
47
+ Error(
48
+ `expected a phone number in E.164 form — "+" then up to 15 digits, as in "+4930123456" — got ${Semantic.showString(
49
+ raw,
50
+ )}. Spaces, dashes and brackets are not part of the stored form.`,
51
+ )
52
+ }
53
+
54
+ /** The sury schema for a phone field. Use with `@s.matches(Reventless.Phone.schema)`. */
55
+ let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.phone, ~check=fromString)
@@ -0,0 +1,29 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
5
+
6
+ let e164 = /^\+[1-9]\d{0,14}$/;
7
+
8
+ function fromString(raw) {
9
+ if (e164.test(raw)) {
10
+ return {
11
+ TAG: "Ok",
12
+ _0: raw
13
+ };
14
+ } else {
15
+ return {
16
+ TAG: "Error",
17
+ _0: `expected a phone number in E.164 form — "+" then up to 15 digits, as in "+4930123456" — got ` + Semantic$Reventless.showString(raw) + `. Spaces, dashes and brackets are not part of the stored form.`
18
+ };
19
+ }
20
+ }
21
+
22
+ let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.phone, fromString);
23
+
24
+ export {
25
+ e164,
26
+ fromString,
27
+ schema,
28
+ }
29
+ /* schema Not a pure module */