@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,162 @@
1
+ /**
2
+ The one marker every typed semantic marks itself with.
3
+
4
+ A semantic type says what a field's value *is* — a date-time, a reference to
5
+ another entity, a ref into an object store — as a property of the field's
6
+ **type**, not as a string stapled beside it. Every layer downstream then derives
7
+ from that single declaration: validation, the wire contract, the UI widget it
8
+ gets rendered with, and eventually the infrastructure provisioned for it.
9
+
10
+ Typed markers predate this module, and each was bespoke: `DateTime` carried its
11
+ own metadata id, `Reference` carried another, and the schema walk detected both
12
+ by hardcoded special case. That made every new typed marker new detection code.
13
+ One shared marker means the walk reads a semantic generically and a new semantic
14
+ type is a new *value*, not a new branch.
15
+
16
+ The payload is a real variant rather than free-form JSON. The semantic
17
+ vocabulary is framework-owned — an application declares a field *is* a storage
18
+ ref, it does not invent what a storage ref means — so the set is closed, and a
19
+ closed set typed here is one the compiler checks at every producer and consumer.
20
+ It also keeps `Reference.getTarget` a total typed function instead of a decode
21
+ that can fail at runtime.
22
+ */
23
+
24
+ /** Which entity a reference field points to. */
25
+ type referenceTarget = {entity: string, plugin: option<string>}
26
+
27
+ /** Which object store a storage-ref / offload field's value lives in. `plugin` is
28
+ absent when the store belongs to the declaring plugin, which is the common
29
+ case. `threshold` is the per-field inline-vs-offloaded byte cut an `@offload`
30
+ field may declare (`None` for `@storageRef`, which is always a ref, and for
31
+ `@offload` fields that leave it to the platform default). */
32
+ type storeTarget = {plugin: option<string>, store: string, threshold: option<int>}
33
+
34
+ /** Per-semantic detail, for the semantics that carry any. */
35
+ type payload =
36
+ | Plain
37
+ | ReferenceTo(referenceTarget)
38
+ | StoredIn(storeTarget)
39
+
40
+ /** A field's semantic: the vocabulary id, plus its detail. */
41
+ type t = {id: string, payload: payload}
42
+
43
+ /**
44
+ The semantic ids the framework itself defines.
45
+
46
+ These strings are the wire vocabulary — they are what `x-reventless-semantic`
47
+ carries, and the same vocabulary the string annotation path already uses, so the
48
+ type path and the annotation path converge on one wire format rather than two.
49
+ */
50
+ module Id = {
51
+ let dateTime = "dateTime"
52
+ let reference = "reference"
53
+ let storageRef = "storageRef"
54
+ // A field whose large value the client stored in a content-addressed object
55
+ // store and carries by reference (inline below a size threshold). Sibling of
56
+ // `storageRef`: same `StoredIn` store declaration, but an inline-or-reference
57
+ // value rather than an always-a-ref path string.
58
+ let offload = "offload"
59
+
60
+ // The branded scalars. Each refines a `string` or a number without changing
61
+ // its shape, so a field gains one of these without anything stored changing.
62
+ let email = "email"
63
+ let phone = "phone"
64
+ let url = "url"
65
+ let percent = "percent"
66
+ let bytes = "bytes"
67
+ let duration = "duration"
68
+ let color = "color"
69
+
70
+ // The first composite that is not infrastructure. Unlike the seven above it
71
+ // this one changes a field's *shape* — a number becomes an object — so it is
72
+ // a wire-breaking declaration rather than a refinement of one.
73
+ let money = "money"
74
+
75
+ // The second composite. A pair of ISO-8601 instants as one value, replacing a
76
+ // span the UI used to guess from a `start*`/`end*` name pair. Like `money` it
77
+ // is an object on the wire; unlike it, adopting it as a *new* optional field
78
+ // is additive — an absent optional decodes to `None`.
79
+ let dateRange = "dateRange"
80
+
81
+ // The third composite, and the cheapest to adopt. A latitude/longitude pair as
82
+ // one value, replacing a point the UI used to guess from a `lat`/`lng` name
83
+ // pair. Most coordinate fields already store `{lat, lng}` as a hand-rolled
84
+ // record, so retyping one is shape-preserving: the wire is unchanged and
85
+ // nothing stored needs upcasting.
86
+ let geoPoint = "geoPoint"
87
+ }
88
+
89
+ let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless", ~name="semantic")
90
+
91
+ /** Mark a schema as carrying a semantic. */
92
+ let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
93
+ schema->S.Metadata.set(~id=semanticId, {id, payload})
94
+
95
+ /**
96
+ A schema that validates with `check` and carries the semantic `id`.
97
+
98
+ The branded scalars all have the same shape — one constructor function that
99
+ defines the grammar, and a schema that must agree with it — and `StorageRef`
100
+ established that the schema is *derived* from the constructor rather than
101
+ hand-rolling a second check beside it. Deriving it here makes that structural:
102
+ there is one place a grammar can be written, so there is nowhere for a second
103
+ one to drift.
104
+ */
105
+ let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.t<'a> =>
106
+ base
107
+ ->S.refine(s => value =>
108
+ switch check(value) {
109
+ | Ok(_) => ()
110
+ | Error(why) => s.fail(why)
111
+ }
112
+ )
113
+ ->mark(~id)
114
+
115
+ /** A value as it should read back to the person who typed it. Rejection messages
116
+ reach forms through `validateInput`, so they quote the offending value. */
117
+ let showString = (raw: string): string => raw->JSON.Encode.string->JSON.stringify
118
+
119
+ /**
120
+ The semantic a field's schema carries, if any.
121
+
122
+ An **optional** field keeps its marker one level down. The ppx annotates the
123
+ field's `string`, and sury-ppx then wraps that schema in a union with
124
+ `Undefined`/`Null`; the wrapper is a new schema and carries no metadata of its
125
+ own. So a walk that reads only the outer schema sees `imageUrl?: string` as
126
+ carrying no semantic at all — the store goes undeclared, the reference goes
127
+ uncollected, the branded scalar loses its brand. Every reader converges here, so
128
+ following the wrapper once here is what keeps "optional" a statement about
129
+ presence rather than a way to lose the field's type.
130
+
131
+ The outer schema is read first, so a marker set on the wrapper itself still wins.
132
+ Only a union with exactly one non-null variant is followed: that is the shape an
133
+ optional field has, and a genuine multi-variant union has no single inner schema
134
+ whose semantic could stand for the whole.
135
+ */
136
+ let rec getFrom = (schema: S.t<unknown>): option<t> =>
137
+ switch S.Metadata.get(schema, ~id=semanticId) {
138
+ | Some(_) as found => found
139
+ | None =>
140
+ switch schema {
141
+ | Union({anyOf}) =>
142
+ switch anyOf->Array.filter(v =>
143
+ switch v {
144
+ | Null(_) | Undefined(_) => false
145
+ | _ => true
146
+ }
147
+ ) {
148
+ | [inner] => getFrom(inner)
149
+ | _ => None
150
+ }
151
+ | _ => None
152
+ }
153
+ }
154
+
155
+ let get = (fieldSchema: S.t<'a>): option<t> => fieldSchema->S.castToUnknown->getFrom
156
+
157
+ /** Whether a field's schema carries this specific semantic. */
158
+ let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
159
+ switch get(fieldSchema) {
160
+ | Some(s) => s.id === id
161
+ | None => false
162
+ }
@@ -0,0 +1,95 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+
5
+ let Id = {
6
+ dateTime: "dateTime",
7
+ reference: "reference",
8
+ storageRef: "storageRef",
9
+ offload: "offload",
10
+ email: "email",
11
+ phone: "phone",
12
+ url: "url",
13
+ percent: "percent",
14
+ bytes: "bytes",
15
+ duration: "duration",
16
+ color: "color",
17
+ money: "money",
18
+ dateRange: "dateRange",
19
+ geoPoint: "geoPoint"
20
+ };
21
+
22
+ let semanticId = S.Metadata.Id.make("reventless", "semantic");
23
+
24
+ function mark(schema, id, payloadOpt) {
25
+ let payload = payloadOpt !== undefined ? payloadOpt : "Plain";
26
+ return S.Metadata.set(schema, semanticId, {
27
+ id: id,
28
+ payload: payload
29
+ });
30
+ }
31
+
32
+ function refined(base, id, check) {
33
+ return mark(S.refine(base, s => (value => {
34
+ let why = check(value);
35
+ if (why.TAG === "Ok") {
36
+ return;
37
+ } else {
38
+ return s.fail(why._0, undefined);
39
+ }
40
+ })), id, undefined);
41
+ }
42
+
43
+ function showString(raw) {
44
+ return JSON.stringify(raw);
45
+ }
46
+
47
+ function getFrom(_schema) {
48
+ while (true) {
49
+ let schema = _schema;
50
+ let found = S.Metadata.get(schema, semanticId);
51
+ if (found !== undefined) {
52
+ return found;
53
+ }
54
+ if (schema.type !== "union") {
55
+ return;
56
+ }
57
+ let match = schema.anyOf.filter(v => {
58
+ switch (v.type) {
59
+ case "null" :
60
+ case "undefined" :
61
+ return false;
62
+ default:
63
+ return true;
64
+ }
65
+ });
66
+ if (match.length !== 1) {
67
+ return;
68
+ }
69
+ _schema = match[0];
70
+ continue;
71
+ };
72
+ }
73
+
74
+ let get = getFrom;
75
+
76
+ function has(fieldSchema, id) {
77
+ let s = getFrom(fieldSchema);
78
+ if (s !== undefined) {
79
+ return s.id === id;
80
+ } else {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ export {
86
+ Id,
87
+ semanticId,
88
+ mark,
89
+ refined,
90
+ showString,
91
+ getFrom,
92
+ get,
93
+ has,
94
+ }
95
+ /* semanticId Not a pure module */
@@ -0,0 +1,164 @@
1
+ /**
2
+ A reference to an object living in one of the platform's object stores.
3
+
4
+ The value is the ref string a store's presign service minted — an origin-relative
5
+ path rooted at the store's served prefix, which the UI renders directly because
6
+ the store is fronted read-only on the app's own origin.
7
+
8
+ ## Why this is a type and not a convention
9
+
10
+ An event log is append-only, so whatever a command accepts into it is permanent.
11
+ Before this type, an `imageUrl: string` field accepted *anything* — including an
12
+ `https://` URL pointing at somebody else's server, or a multi-megabyte `data:`
13
+ URI inlined into the event itself. Both deploy green, both are unfixable after
14
+ the fact, and neither is what the field means. Declaring the field's type makes
15
+ the wrong values unrepresentable at the boundary, before `decide` ever runs.
16
+
17
+ The declaration also states a *requirement*: a field of this type says the
18
+ deployment needs a store called `store` to exist. Nothing provisions that store
19
+ yet, and that is a deliberate resting point — the validation hole is closed and
20
+ the requirement is written down, which is strictly better than the status quo
21
+ even if automatic provisioning never lands.
22
+
23
+ ## The grammar
24
+
25
+ A ref is an absolute, origin-relative path of at least two non-empty segments:
26
+
27
+ /uploads/2f8c1e94-.../photo.jpg
28
+ /uploads/user-42/2f8c1e94-.../photo.jpg
29
+
30
+ Rejected: anything with a scheme (`https://…`, `data:…`), protocol-relative
31
+ `//host/path`, relative paths, empty segments, and `.`/`..` traversal.
32
+
33
+ The framework mints refs in exactly this form — the presign service builds the
34
+ object key as `{servedPrefix}/{identity}{uuid}/{fileName}` and returns `/{key}` —
35
+ so the grammar is the framework's to define, not an application's.
36
+
37
+ Note what is *not* checked: that the ref's prefix belongs to this specific store.
38
+ Today every store shares one served prefix, so there is nothing store-specific to
39
+ check against; the store identity is carried in the field's semantic payload,
40
+ where provisioning and the UI read it. When stores gain per-store prefixes, this
41
+ check tightens from a structural one to a per-store one without the type, the
42
+ wire format, or any stored value changing.
43
+
44
+ @example
45
+ ```rescript
46
+ @schema type command =
47
+ | ChangeProductImage({
48
+ productId: @s.matches(DcbTag.string) string,
49
+ imageUrl: @storageRef("productImages") string,
50
+ })
51
+ ```
52
+ */
53
+
54
+ /** The ref's representation. Transparent `string` on purpose: the marker refines
55
+ an existing `string` field rather than replacing it, so the field's runtime
56
+ representation — and therefore every stored event — is unchanged. A sealed
57
+ type here would defeat that, and would also be unattachable via `@s.matches`,
58
+ which requires the schema's type to match the field's. */
59
+ type t = string
60
+
61
+ external unsafe: string => t = "%identity"
62
+ external toString: t => string = "%identity"
63
+
64
+ let segmentIsSafe = (segment: string) =>
65
+ segment !== "" && segment !== "." && segment !== ".."
66
+
67
+ /**
68
+ Validate a raw string as a storage ref, saying why when it is not one.
69
+
70
+ This is the single definition of the grammar. `forStore`'s sury schema is derived
71
+ from it rather than hand-rolling a second check, so the constructor and the
72
+ schema validation cannot drift apart.
73
+ */
74
+ let fromString = (raw: string): result<t, string> =>
75
+ if !String.startsWith(raw, "/") {
76
+ Error(
77
+ `expected an origin-relative storage ref starting with "/", got ${raw->JSON.Encode.string->JSON.stringify}. External URLs and data: URIs are not storage refs.`,
78
+ )
79
+ } else if String.startsWith(raw, "//") {
80
+ Error(`protocol-relative refs are not storage refs: ${raw}`)
81
+ } else {
82
+ let segments = raw->String.slice(~start=1, ~end=String.length(raw))->String.split("/")
83
+ if segments->Array.length < 2 {
84
+ Error(`a storage ref needs a prefix and an object path, got ${raw}`)
85
+ } else if !(segments->Array.every(segmentIsSafe)) {
86
+ Error(`a storage ref may not contain empty or traversal segments, got ${raw}`)
87
+ } else {
88
+ Ok(raw)
89
+ }
90
+ }
91
+
92
+ /**
93
+ The sury schema for a field holding a ref into a named store.
94
+
95
+ Prefer the `@storageRef("<store>")` ppx shorthand over writing this by hand.
96
+ Qualify the store as `"<plugin>.<store>"` to point at another plugin's store.
97
+ */
98
+ let forStore = (~plugin: option<string>=?, ~store: string): S.t<t> =>
99
+ S.string
100
+ ->S.refine(s => value =>
101
+ // The empty string is admitted as the "no object" sentinel. The fields this
102
+ // marks are non-optional today, and a producer with nothing to reference —
103
+ // a supplier feed carrying no image, say — already writes `""` to mean
104
+ // absence. Rejecting it here would break a legitimate existing value and
105
+ // force an event-schema change, which this marker exists to avoid: it
106
+ // refines an existing `string` field without altering what is stored.
107
+ //
108
+ // Note this is a strictly weaker guarantee than `fromString`, which stays
109
+ // exact. Making these fields properly optional would let the sentinel go.
110
+ if value !== "" {
111
+ switch fromString(value) {
112
+ | Ok(_) => ()
113
+ | Error(why) => s.fail(why)
114
+ }
115
+ }
116
+ )
117
+ ->Semantic.mark(~id=Semantic.Id.storageRef, ~payload=StoredIn({plugin, store, threshold: None}))
118
+
119
+ /** The store a field's schema declares its refs live in, if any. */
120
+ let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
121
+ switch Semantic.get(schema) {
122
+ | Some({payload: StoredIn(target)}) => Some(target)
123
+ | _ => None
124
+ }
125
+
126
+ /** How many refs one field holds. */
127
+ type arity =
128
+ | /** A `string` field: one ref. */ Single
129
+ | /** An `array<string>` field: zero or more refs. */ Multiple
130
+
131
+ /**
132
+ The store a *field* declares, looking through an array wrapper, with the arity
133
+ that tells a reader how to get at the value.
134
+
135
+ `getStore` asks about a schema; this asks about a field, and those differ for
136
+ `@storageRef("s") urls: array<string>` — the ppx attaches the marker to the
137
+ element type, because `@s.matches` requires the schema's type to match what it
138
+ is attached to, so the array itself carries no marker and `getStore` on it
139
+ answers `None`.
140
+
141
+ Every reader of a *field* wants this one. Reading `getStore` directly is what
142
+ made a multi-valued store declaration invisible: the store went unprovisioned,
143
+ which is the same silence as not having written the annotation at all.
144
+ */
145
+ let getFieldStore = (schema: S.t<'a>): option<(Semantic.storeTarget, arity)> =>
146
+ switch getStore(schema) {
147
+ | Some(target) => Some(target, Single)
148
+ | None =>
149
+ switch schema->(Obj.magic: S.t<'a> => S.t<unknown>) {
150
+ | Array({items, additionalItems}) =>
151
+ // sury keeps a homogeneous `S.array` element schema in `additionalItems`
152
+ // and a tuple's positional schemas in `items`; read both so the marker is
153
+ // found wherever the element sits.
154
+ switch items->Array.get(0) {
155
+ | Some({schema: itemSchema}) => getStore(itemSchema)
156
+ | None =>
157
+ switch additionalItems {
158
+ | Schema(itemSchema) => getStore(itemSchema)
159
+ | _ => None
160
+ }
161
+ }->Option.map(target => (target, Multiple))
162
+ | _ => None
163
+ }
164
+ }
@@ -0,0 +1,111 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
6
+
7
+ function segmentIsSafe(segment) {
8
+ if (segment !== "" && segment !== ".") {
9
+ return segment !== "..";
10
+ } else {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ function fromString(raw) {
16
+ if (!raw.startsWith("/")) {
17
+ return {
18
+ TAG: "Error",
19
+ _0: `expected an origin-relative storage ref starting with "/", got ` + JSON.stringify(raw) + `. External URLs and data: URIs are not storage refs.`
20
+ };
21
+ }
22
+ if (raw.startsWith("//")) {
23
+ return {
24
+ TAG: "Error",
25
+ _0: `protocol-relative refs are not storage refs: ` + raw
26
+ };
27
+ }
28
+ let segments = raw.slice(1, raw.length).split("/");
29
+ if (segments.length < 2) {
30
+ return {
31
+ TAG: "Error",
32
+ _0: `a storage ref needs a prefix and an object path, got ` + raw
33
+ };
34
+ } else if (segments.every(segmentIsSafe)) {
35
+ return {
36
+ TAG: "Ok",
37
+ _0: raw
38
+ };
39
+ } else {
40
+ return {
41
+ TAG: "Error",
42
+ _0: `a storage ref may not contain empty or traversal segments, got ` + raw
43
+ };
44
+ }
45
+ }
46
+
47
+ function forStore(plugin, store) {
48
+ return Semantic$Reventless.mark(S.refine(S.string, s => (value => {
49
+ if (value === "") {
50
+ return;
51
+ }
52
+ let why = fromString(value);
53
+ if (why.TAG === "Ok") {
54
+ return;
55
+ } else {
56
+ return s.fail(why._0, undefined);
57
+ }
58
+ })), Semantic$Reventless.Id.storageRef, {
59
+ TAG: "StoredIn",
60
+ _0: {
61
+ plugin: plugin,
62
+ store: store,
63
+ threshold: undefined
64
+ }
65
+ });
66
+ }
67
+
68
+ function getStore(schema) {
69
+ let match = Semantic$Reventless.get(schema);
70
+ if (match === undefined) {
71
+ return;
72
+ }
73
+ let target = match.payload;
74
+ if (typeof target !== "object" || target.TAG === "ReferenceTo") {
75
+ return;
76
+ } else {
77
+ return target._0;
78
+ }
79
+ }
80
+
81
+ function getFieldStore(schema) {
82
+ let target = getStore(schema);
83
+ if (target !== undefined) {
84
+ return [
85
+ target,
86
+ "Single"
87
+ ];
88
+ }
89
+ if (schema.type !== "array") {
90
+ return;
91
+ }
92
+ let additionalItems = schema.additionalItems;
93
+ let match = schema.items[0];
94
+ let tmp;
95
+ tmp = match !== undefined ? getStore(match.schema) : (
96
+ additionalItems === "strip" || additionalItems === "strict" ? undefined : getStore(additionalItems)
97
+ );
98
+ return Stdlib_Option.map(tmp, target => [
99
+ target,
100
+ "Multiple"
101
+ ]);
102
+ }
103
+
104
+ export {
105
+ segmentIsSafe,
106
+ fromString,
107
+ forStore,
108
+ getStore,
109
+ getFieldStore,
110
+ }
111
+ /* S Not a pure module */
@@ -0,0 +1,66 @@
1
+ /**
2
+ Marks a `string` field as a web address.
3
+
4
+ ## The grammar: parseable, **and** `http`/`https`
5
+
6
+ Sury's `S.url` is the parse — it is `new URL()`, so it settles host, port and
7
+ escaping without this module having an opinion. But `new URL()` accepts every
8
+ scheme, `javascript:alert(1)` included, and that is not an academic gap here: a
9
+ field carrying this semantic renders as an anchor whose `href` is the stored
10
+ value. An append-only log plus a scheme nobody checked is a stored XSS that
11
+ cannot be deleted afterwards — the same shape of hole `StorageRef` exists to
12
+ close, arriving through a different field.
13
+
14
+ So the grammar is sury's parse plus a two-scheme allowlist. `mailto:` and `tel:`
15
+ are deliberately outside it: those are `Email` and `Phone`, which validate what
16
+ they actually hold and render correctly on their own.
17
+
18
+ @example
19
+ ```rescript
20
+ @schema type command =
21
+ | SetSupplierSite({
22
+ supplierId: @s.matches(DcbTag.string) string,
23
+ website: @s.matches(Reventless.Url.schema) string,
24
+ })
25
+ ```
26
+ */
27
+
28
+ /** The address's representation. Transparent `string`; see `Email.t`. */
29
+ type t = string
30
+
31
+ external unsafe: string => t = "%identity"
32
+ external toString: t => string = "%identity"
33
+
34
+ // Sury's parse, held once — `fromString` runs it and adds the scheme check, and
35
+ // `schema` derives from `fromString`. One grammar, in one place.
36
+ let grammar: S.t<string> = S.string->S.url
37
+
38
+ // Schemes are case-insensitive per RFC 3986, and `HTTPS://x` parses fine, so the
39
+ // allowlist has to fold case or it rejects a valid address on a technicality.
40
+ let hasWebScheme = (raw: string): bool => {
41
+ let lower = String.toLowerCase(raw)
42
+ String.startsWith(lower, "http://") || String.startsWith(lower, "https://")
43
+ }
44
+
45
+ /** Validate a raw string as an `http`/`https` URL, saying why when it is not one. */
46
+ let fromString = (raw: string): result<t, string> =>
47
+ switch raw->S.parseOrThrow(grammar) {
48
+ | value =>
49
+ if hasWebScheme(value) {
50
+ Ok(value)
51
+ } else {
52
+ Error(
53
+ `a URL field takes an http:// or https:// address, got ${Semantic.showString(raw)}. ` ++
54
+ `Use Email or Phone for mailto:/tel:, and a storage ref for an uploaded object.`,
55
+ )
56
+ }
57
+ | exception _ =>
58
+ Error(
59
+ `expected an absolute URL, got ${Semantic.showString(
60
+ raw,
61
+ )}. A relative path is not a URL — include the scheme and host.`,
62
+ )
63
+ }
64
+
65
+ /** The sury schema for a URL field. Use with `@s.matches(Reventless.Url.schema)`. */
66
+ let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.url, ~check=fromString)
@@ -0,0 +1,48 @@
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 grammar = S.url(S.string, undefined);
7
+
8
+ function hasWebScheme(raw) {
9
+ let lower = raw.toLowerCase();
10
+ if (lower.startsWith("http://")) {
11
+ return true;
12
+ } else {
13
+ return lower.startsWith("https://");
14
+ }
15
+ }
16
+
17
+ function fromString(raw) {
18
+ let value;
19
+ try {
20
+ value = S.parseOrThrow(raw, grammar);
21
+ } catch (exn) {
22
+ return {
23
+ TAG: "Error",
24
+ _0: `expected an absolute URL, got ` + Semantic$Reventless.showString(raw) + `. A relative path is not a URL — include the scheme and host.`
25
+ };
26
+ }
27
+ if (hasWebScheme(value)) {
28
+ return {
29
+ TAG: "Ok",
30
+ _0: value
31
+ };
32
+ } else {
33
+ return {
34
+ TAG: "Error",
35
+ _0: `a URL field takes an http:// or https:// address, got ` + Semantic$Reventless.showString(raw) + `. Use Email or Phone for mailto:/tel:, and a storage ref for an uploaded object.`
36
+ };
37
+ }
38
+ }
39
+
40
+ let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.url, fromString);
41
+
42
+ export {
43
+ grammar,
44
+ hasWebScheme,
45
+ fromString,
46
+ schema,
47
+ }
48
+ /* grammar Not a pure module */
@@ -0,0 +1,23 @@
1
+ // Provider-agnostic authorization rules. Evaluated against the
2
+ // `Identity.t` resolved by an `Auth_Adapter.Provider.authenticate` call.
3
+ //
4
+ // `array<string>` for groups (not a parameterised variant) keeps the
5
+ // framework decoupled from any application's specific group set —
6
+ // applications can define their own typed `group` variant and convert via
7
+ // a thin helper, see docs/analysis/authentication-authorization.md §4.2.
8
+
9
+ @schema
10
+ type permission =
11
+ | AllowGroups(array<string>)
12
+ | AllowAuthenticated
13
+ | AllowAnonymous
14
+ | DenyAll
15
+
16
+ let isAllowed = (rule: permission, identity: Identity.t): bool =>
17
+ switch rule {
18
+ | DenyAll => false
19
+ | AllowAnonymous => true
20
+ | AllowAuthenticated => identity.userId !== "anonymous"
21
+ | AllowGroups(groups) =>
22
+ groups->Array.some(group => identity.groups->Array.includes(group))
23
+ }