@reventlessdev/reventless-spec 3.0.0-alpha.127 → 3.0.0-alpha.128

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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ A reference to a member of a collection the row already holds.
3
+
4
+ The distinction this type exists to make is **input versus selection**. A field
5
+ typed {!UploadableImage} says "give me a new file", and a UI reading it binds an
6
+ upload endpoint. A field that must name a picture the row *already has* — remove
7
+ this one, make that one primary, caption the third — says something else
8
+ entirely, and before this type there was no way to say it: every such field was
9
+ typed as the uploadable it selected among, so every one of them offered an
10
+ uploader. On a remove command that is not merely odd, it is the opposite of what
11
+ the command does.
12
+
13
+ ## What it declares, and what it does not
14
+
15
+ It declares where the candidates are: a field of the row, by name. It declares
16
+ no store — nothing is provisioned, no upload endpoint is bound — for the same
17
+ reason {!ImageRef} declares none, and with the same consequence: a consumer
18
+ asking `StorageRef.getFieldStore` gets `None`.
19
+
20
+ The *semantic* is content-agnostic: one id serves images and documents, and what
21
+ a member is comes from `~content`. That argument is optional and, where a caller
22
+ gives it, redundant with the collection's own element type — deliberately, and
23
+ for the reason {!UploadableImage} exists at all rather than a storage ref beside
24
+ a content annotation. A consumer rendering one cell holds that field's schema and
25
+ nothing else; it cannot reach a sibling's element type, so a declaration that
26
+ made it look would be a declaration it could not read. Omit it and such a
27
+ consumer falls back to its own rules, which is the right answer for a member type
28
+ this vocabulary has no word for.
29
+
30
+ ## Why not `Reference.to_`
31
+
32
+ A reference resolves its candidates by querying the target view's table. These
33
+ candidates are on the row already in hand, so there is no query: same shape of
34
+ answer, different resolution path, and pointing a query at it would scan a table
35
+ to re-read a value the caller is holding. `Reference.toWithoutDcbTag` is the
36
+ precedent that a payload can be reused without dragging DCB tagging along; this
37
+ is the converse — a payload that resolves locally.
38
+
39
+ ## Where it goes
40
+
41
+ **On a command field**, and there it means *pick one of these*:
42
+
43
+ ```rescript
44
+ @schema type command =
45
+ | RemoveProductImage({
46
+ productId: @s.matches(DcbTag.string) string,
47
+ productImage: @s.matches(Reventless.MemberRef.of_(~view="Products", ~field="productImages")) string,
48
+ })
49
+ ```
50
+
51
+ `~view` names the collection's view. Omit it on a declaration made *on* that
52
+ view's own state, where the answer is "this record" — the wrapper walk below
53
+ supports that position, and `getFieldTarget` reads it. Nothing in the framework
54
+ declares one there today: a view that carried a scalar *and* the set it was
55
+ drawn from needed a marker saying the two were one thing, and a view whose
56
+ primary is simply the first member has no second field to reconcile.
57
+ */
58
+
59
+ /** Transparent `string`, as every ref-shaped semantic here is: the marker
60
+ refines an existing field rather than replacing it, so nothing stored
61
+ changes when a field adopts it. */
62
+ type t = string
63
+
64
+ external unsafe: string => t = "%identity"
65
+ external toString: t => string = "%identity"
66
+
67
+ /** Which collection a field names a member of. */
68
+ type target = Semantic.memberTarget
69
+
70
+ /**
71
+ The sury schema for a field that selects a member of `field`.
72
+
73
+ `~view` names the collection's view; omit it on a declaration made *on* that
74
+ view's own state, where the answer is "this record". `~plugin` qualifies a view
75
+ another plugin owns. `~content` names what the members are — pass
76
+ `Semantic.Id.imageRef` for a set of pictures — so a reader holding this field
77
+ alone still knows what it is drawing.
78
+
79
+ No grammar is checked. The value is whatever the collection's own element type
80
+ admits — a storage ref today, and checking the ref grammar a second time here
81
+ would put {!StorageRef}'s rules in a second place to drift from. What makes a
82
+ selection valid is that the row holds it, and only the decider knows that.
83
+ */
84
+ let of_ = (
85
+ ~plugin: option<string>=?,
86
+ ~view: option<string>=?,
87
+ ~content: option<string>=?,
88
+ ~field: string,
89
+ ): S.t<t> =>
90
+ S.string->Semantic.mark(
91
+ ~id=Semantic.Id.memberRef,
92
+ ~payload=MemberOf({view, field, plugin, content}),
93
+ )
94
+
95
+ /** The collection a schema names, if it carries the marker. */
96
+ let getTarget = (schema: S.t<'a>): option<target> =>
97
+ switch Semantic.get(schema) {
98
+ | Some({payload: MemberOf(target)}) => Some(target)
99
+ | _ => None
100
+ }
101
+
102
+ /**
103
+ The collection a *field* names, looking through the wrappers around its value.
104
+
105
+ The distinction `Reference.getFieldTarget` draws applies here for the same
106
+ reason: `of_` returns an element schema, so on an `array<string>` of selections
107
+ the marker sits on the string inside the array and the field's own schema
108
+ carries nothing. Only the optional wrapper and the array element are followed —
109
+ a marker on a nested record's field belongs to that field.
110
+ */
111
+ let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
112
+ switch getTarget(schema) {
113
+ | Some(_) as found => found
114
+ | None =>
115
+ switch schema->Semantic.unwrapOptional->Option.getOr(schema) {
116
+ | Array({additionalItems: Schema(item)}) => getFieldTarget(item)
117
+ | _ => None
118
+ }
119
+ }
@@ -0,0 +1,57 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
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 of_(plugin, view, content, field) {
8
+ return Semantic$Reventless.mark(Sury.string, Semantic$Reventless.Id.memberRef, {
9
+ TAG: "MemberOf",
10
+ _0: {
11
+ view: view,
12
+ field: field,
13
+ plugin: plugin,
14
+ content: content
15
+ }
16
+ });
17
+ }
18
+
19
+ function getTarget(schema) {
20
+ let match = Semantic$Reventless.get(schema);
21
+ if (match === undefined) {
22
+ return;
23
+ }
24
+ let target = match.payload;
25
+ if (typeof target !== "object" || target.TAG !== "MemberOf") {
26
+ return;
27
+ } else {
28
+ return target._0;
29
+ }
30
+ }
31
+
32
+ function getFieldTarget(_schema) {
33
+ while (true) {
34
+ let schema = _schema;
35
+ let found = getTarget(schema);
36
+ if (found !== undefined) {
37
+ return found;
38
+ }
39
+ let match = Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(schema), schema);
40
+ if (match.type !== "array") {
41
+ return;
42
+ }
43
+ let item = match.additionalItems;
44
+ if (item === "strip" || item === "strict") {
45
+ return;
46
+ }
47
+ _schema = item;
48
+ continue;
49
+ };
50
+ }
51
+
52
+ export {
53
+ of_,
54
+ getTarget,
55
+ getFieldTarget,
56
+ }
57
+ /* sury Not a pure module */
@@ -110,7 +110,7 @@ function getStore(schema) {
110
110
  return;
111
111
  }
112
112
  let target = match.payload;
113
- if (typeof target !== "object" || target.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
113
+ if (typeof target !== "object" || target.TAG !== "StoredIn" || match.id !== Semantic$Reventless.Id.offload) {
114
114
  return;
115
115
  } else {
116
116
  return target._0;
@@ -123,7 +123,7 @@ function getThreshold(schema) {
123
123
  return;
124
124
  }
125
125
  let match$1 = match.payload;
126
- if (typeof match$1 !== "object" || match$1.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
126
+ if (typeof match$1 !== "object" || match$1.TAG !== "StoredIn" || match.id !== Semantic$Reventless.Id.offload) {
127
127
  return;
128
128
  } else {
129
129
  return match$1._0.threshold;
@@ -1,24 +1,9 @@
1
1
  /**
2
- Marks a `float` field as a percentage, expressed **0–100**.
2
+ Marks a `float` field as a percentage, expressed **0–100**, not 0–1.
3
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.
4
+ The scale is what every consumer gauges and formats against, so a fraction
5
+ multiplies by 100 before it becomes this type. A finite number in `[0, 100]`;
6
+ fractions are allowed — `99.95` is a percentage.
22
7
 
23
8
  @example
24
9
  ```rescript
@@ -30,7 +15,7 @@ A finite number in `[0, 100]`. Fractions are allowed: `99.95` is a percentage.
30
15
  */
31
16
 
32
17
  /** The percentage's representation. Transparent `float`: the marker refines an
33
- existing numeric field rather than replacing it, so nothing stored changes. */
18
+ existing numeric field, so nothing stored changes. */
34
19
  type t = float
35
20
 
36
21
  external unsafe: float => t = "%identity"
@@ -51,3 +36,7 @@ let fromFloat = (raw: float): result<t, string> =>
51
36
 
52
37
  /** The sury schema for a percentage field. Use with `@s.matches(Reventless.Percent.schema)`. */
53
38
  let schema: S.t<t> = S.float->Semantic.refined(~id=Semantic.Id.percent, ~check=fromFloat)
39
+
40
+ /** The percentage as text — `"42%"`, `"99.95%"`. Locale-independent, the way
41
+ `Money.format` is. */
42
+ let format = (p: t): string => Float.toString(p) ++ "%"
@@ -26,8 +26,13 @@ function fromFloat(raw) {
26
26
 
27
27
  let schema = Semantic$Reventless.refined(Sury.float, Semantic$Reventless.Id.percent, fromFloat);
28
28
 
29
+ function format(p) {
30
+ return p.toString() + "%";
31
+ }
32
+
29
33
  export {
30
34
  fromFloat,
31
35
  schema,
36
+ format,
32
37
  }
33
38
  /* schema Not a pure module */
@@ -18,11 +18,39 @@ type referenceTarget = {entity: string, plugin: option<string>}
18
18
  when it defers to the platform default. */
19
19
  type storeTarget = {plugin: option<string>, store: string, threshold: option<int>}
20
20
 
21
+ /**
22
+ Which collection a value names a member of.
23
+
24
+ `field` is the collection's field name on the row — the only thing a consumer
25
+ needs, because the candidates are already loaded. `view` names the view that
26
+ collection belongs to and is absent where the answer is "the row this field is
27
+ on", which is every declaration made on a view's own state. `plugin` is absent
28
+ for the declaring plugin's own view, as everywhere else here.
29
+
30
+ Unlike `referenceTarget` this is deliberately not resolvable by query: the
31
+ members are on the row, so a consumer that went looking for a table has
32
+ misread it.
33
+
34
+ `content` names what a member *is* — an `imageRef`/`fileRef` id — and is the one
35
+ part of this that is not about the collection. It rides here for the reason
36
+ `uploadableImage` exists at all rather than `storageRef` beside a content
37
+ annotation: the collection's own element type already says it, but a consumer
38
+ holding one field's schema cannot reach a sibling's, and one that renders a cell
39
+ holds exactly that. Absent leaves the reader to its own rules, which is what a
40
+ host attaching something the vocabulary has no word for should get. */
41
+ type memberTarget = {
42
+ view: option<string>,
43
+ field: string,
44
+ plugin: option<string>,
45
+ content: option<string>,
46
+ }
47
+
21
48
  /** Per-semantic detail, for the semantics that carry any. */
22
49
  type payload =
23
50
  | Plain
24
51
  | ReferenceTo(referenceTarget)
25
52
  | StoredIn(storeTarget)
53
+ | MemberOf(memberTarget)
26
54
 
27
55
  /** A field's semantic: the vocabulary id, plus its detail. */
28
56
  type t = {id: string, payload: payload}
@@ -45,6 +73,16 @@ module Id = {
45
73
  let imageRef = "imageRef"
46
74
  let fileRef = "fileRef"
47
75
 
76
+ // An image and the text that goes with it, in one value. Declares its store
77
+ // the way `uploadableImage` does, on the record rather than on the reference
78
+ // inside it, so a field reader finds it through an array wrapper.
79
+ let captionedImage = "captionedImage"
80
+
81
+ // A selection rather than an input: the value names a member of a collection
82
+ // the row already holds. Declares no store, so nothing is provisioned and no
83
+ // upload endpoint is bound — the distinction `uploadableImage` could not make.
84
+ let memberRef = "memberRef"
85
+
48
86
  // Branded scalars: a refinement, so adopting one changes nothing stored.
49
87
  let email = "email"
50
88
  let phone = "phone"
@@ -116,6 +154,31 @@ let unwrapOptional = (schema: S.t<unknown>): option<S.t<unknown>> =>
116
154
  | _ => None
117
155
  }
118
156
 
157
+ /**
158
+ One arm of a command or event union, found by its tag.
159
+
160
+ A union with exactly one constructor is emitted as a bare object carrying the
161
+ TAG rather than a one-element `AnyOf`, so both shapes are matched.
162
+ */
163
+ let unionVariant = (schema: S.t<unknown>, ~variant: string): option<S.t<unknown>> => {
164
+ let isVariant = (properties: dict<S.t<unknown>>) =>
165
+ switch properties->Dict.get("TAG") {
166
+ | Some(String({const: ?Some(name)})) => name == variant
167
+ | _ => false
168
+ }
169
+ switch schema {
170
+ | AnyOf({anyOf}) =>
171
+ anyOf->Array.find(arm =>
172
+ switch arm {
173
+ | Object({properties}) => isVariant(properties)
174
+ | _ => false
175
+ }
176
+ )
177
+ | Object({properties}) => isVariant(properties) ? Some(schema) : None
178
+ | _ => None
179
+ }
180
+ }
181
+
119
182
  /**
120
183
  The semantic a field's schema carries, if any.
121
184
 
@@ -13,6 +13,8 @@ let Id = {
13
13
  uploadableFile: "uploadableFile",
14
14
  imageRef: "imageRef",
15
15
  fileRef: "fileRef",
16
+ captionedImage: "captionedImage",
17
+ memberRef: "memberRef",
16
18
  email: "email",
17
19
  phone: "phone",
18
20
  url: "url",
@@ -67,6 +69,42 @@ function unwrapOptional(schema) {
67
69
  }
68
70
  }
69
71
 
72
+ function unionVariant(schema, variant) {
73
+ let isVariant = properties => {
74
+ let match = properties["TAG"];
75
+ if (match === undefined) {
76
+ return false;
77
+ }
78
+ if (match.type !== "string") {
79
+ return false;
80
+ }
81
+ let name = match.const;
82
+ if (name !== undefined) {
83
+ return name === variant;
84
+ } else {
85
+ return false;
86
+ }
87
+ };
88
+ switch (schema.type) {
89
+ case "object" :
90
+ if (isVariant(schema.properties)) {
91
+ return schema;
92
+ } else {
93
+ return;
94
+ }
95
+ case "anyOf" :
96
+ return schema.anyOf.find(arm => {
97
+ if (arm.type === "object") {
98
+ return isVariant(arm.properties);
99
+ } else {
100
+ return false;
101
+ }
102
+ });
103
+ default:
104
+ return;
105
+ }
106
+ }
107
+
70
108
  function getFrom(schema) {
71
109
  let found = Sury.$Metadata_get(schema, semanticId);
72
110
  if (found !== undefined) {
@@ -94,6 +132,7 @@ export {
94
132
  refined,
95
133
  showString,
96
134
  unwrapOptional,
135
+ unionVariant,
97
136
  getFrom,
98
137
  get,
99
138
  has,
@@ -70,7 +70,7 @@ function getStore(schema) {
70
70
  return;
71
71
  }
72
72
  let target = match.payload;
73
- if (typeof target !== "object" || target.TAG === "ReferenceTo") {
73
+ if (typeof target !== "object" || target.TAG !== "StoredIn") {
74
74
  return;
75
75
  } else {
76
76
  return target._0;