@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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/schema/platform-api.graphql +1 -0
- package/src/components/Plugin.res +8 -0
- package/src/components/Plugin.res.mjs +2 -1
- package/src/components/Sensitive.res +137 -0
- package/src/components/Sensitive.res.mjs +74 -0
- package/src/semantic/Bytes.res +21 -21
- package/src/semantic/Bytes.res.mjs +35 -0
- package/src/semantic/CaptionedImage.res +100 -0
- package/src/semantic/CaptionedImage.res.mjs +35 -0
- package/src/semantic/Duration.res +22 -24
- package/src/semantic/Duration.res.mjs +43 -0
- package/src/semantic/MemberRef.res +119 -0
- package/src/semantic/MemberRef.res.mjs +57 -0
- package/src/semantic/Offload.res.mjs +2 -2
- package/src/semantic/Percent.res +9 -20
- package/src/semantic/Percent.res.mjs +5 -0
- package/src/semantic/Semantic.res +63 -0
- package/src/semantic/Semantic.res.mjs +39 -0
- package/src/semantic/StorageRef.res.mjs +1 -1
- package/src/semantic/Template.res +412 -0
- package/src/semantic/Template.res.mjs +545 -0
|
@@ -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
|
|
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
|
|
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;
|
package/src/semantic/Percent.res
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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,
|