@reventlessdev/reventless-spec 3.0.0-alpha.127 → 3.0.0-alpha.129
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 +52 -0
- package/package.json +2 -2
- package/schema/platform-api.graphql +1 -0
- package/src/AnsiStyle.res +31 -7
- package/src/AnsiStyle.res.mjs +16 -2
- package/src/components/Plugin.res +66 -84
- package/src/components/Plugin.res.mjs +46 -81
- package/src/components/Sensitive.res +137 -0
- package/src/components/Sensitive.res.mjs +74 -0
- package/src/generator/GraftTrait.res +5 -4
- package/src/generator/GraftTrait.res.mjs +4 -4
- 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
- package/src/types/Message.res +14 -12
- package/src/types/Message.res.mjs +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
An image the platform stores, together with the text that goes with it.
|
|
3
|
+
|
|
4
|
+
## Why the text travels with the reference
|
|
5
|
+
|
|
6
|
+
A picture and the words that describe it are one thing, and holding them apart is
|
|
7
|
+
what let them drift. A view that carried an image field and a caption field
|
|
8
|
+
beside it had to keep the pair in step on every arm of its projection, and the
|
|
9
|
+
one arm that forgot produced a hero image with no alternative text — an
|
|
10
|
+
accessibility hole opened by a projection, not by a missing command.
|
|
11
|
+
|
|
12
|
+
Inside one value there is no pair to keep in step. It also puts the text where a
|
|
13
|
+
renderer can reach it: a cell renderer is handed a field's key, its schema and
|
|
14
|
+
its value, and never the row — so a caption in a *sibling field* is a caption no
|
|
15
|
+
cell can draw.
|
|
16
|
+
|
|
17
|
+
## Two texts, not one
|
|
18
|
+
|
|
19
|
+
They are different things and one does not substitute for the other.
|
|
20
|
+
**`altText` replaces** the image: what a screen reader announces instead of it,
|
|
21
|
+
what shows when it fails to load. **`caption` sits beside** it, visible to
|
|
22
|
+
everyone. `altText: "Blue running shoe, side profile"` and
|
|
23
|
+
`caption: "Front view"` are both correct and neither does the other's job.
|
|
24
|
+
|
|
25
|
+
A consumer resolves them like this, and the rules are written down because a
|
|
26
|
+
wrong one is silent:
|
|
27
|
+
|
|
28
|
+
- **the visible caption** is `caption` only. Absent means no visible text.
|
|
29
|
+
- **`alt`** is `altText`, else `caption`, else `""`.
|
|
30
|
+
|
|
31
|
+
The fallback through `caption` is deliberate and slightly impure. The pure rule —
|
|
32
|
+
`altText` else `""` — makes a host that filled only `caption` emit `alt=""` on
|
|
33
|
+
every photo, which declares them decorative. An imperfect accessible name is
|
|
34
|
+
better than a wrong one.
|
|
35
|
+
|
|
36
|
+
## No store argument on the field
|
|
37
|
+
|
|
38
|
+
The store is the host field's name, pluralised, derived by the ppx exactly as it
|
|
39
|
+
is for {!UploadableImage} — and the rule is idempotent, so `productImages:
|
|
40
|
+
array<CaptionedImage.t>` and `categoryImage?: CaptionedImage.t` both derive the
|
|
41
|
+
store their host means. The marker lands on this record rather than on the `ref`
|
|
42
|
+
inside it, which is what lets `StorageRef.getFieldStore` find the store through
|
|
43
|
+
an array wrapper without looking a level deeper.
|
|
44
|
+
|
|
45
|
+
## A pair, not a general type
|
|
46
|
+
|
|
47
|
+
Monomorphic on purpose: the ppx matches the *spelling* of a field's type under an
|
|
48
|
+
empty type-argument list, so an `Attachment.t<'ref>` would not match and no store
|
|
49
|
+
would be derived — silently. A document counterpart is a second type for the
|
|
50
|
+
reason {!UploadableImage} and {!UploadableFile} are two: they differ in what the
|
|
51
|
+
value depicts, not in how a reference to it is written. It would carry no
|
|
52
|
+
`altText`, because a document has no visual to replace.
|
|
53
|
+
|
|
54
|
+
@example
|
|
55
|
+
```rescript
|
|
56
|
+
@schema type state = {
|
|
57
|
+
productId: string,
|
|
58
|
+
productImages: array<Reventless.CaptionedImage.t>,
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** The record itself. No `@schema`: the derived schema would have to name a
|
|
64
|
+
store, and the store is the host field's — so `forField` is the only way to
|
|
65
|
+
a schema, and a field the ppx did not reach fails to compile rather than
|
|
66
|
+
provisioning a store nobody meant. */
|
|
67
|
+
type t = {
|
|
68
|
+
ref: UploadableImage.t,
|
|
69
|
+
altText?: string,
|
|
70
|
+
caption?: string,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
The sury schema for a field holding captioned images out of a named store.
|
|
75
|
+
|
|
76
|
+
Prefer typing the field `CaptionedImage.t` (or `array<CaptionedImage.t>`) and
|
|
77
|
+
letting the ppx call this. Written by hand it needs the derived store name, which
|
|
78
|
+
defeats the point of the type.
|
|
79
|
+
*/
|
|
80
|
+
let forField = (~plugin: option<string>=?, ~store: string): S.t<t> =>
|
|
81
|
+
S.schema(s => {
|
|
82
|
+
ref: s.matches(UploadableImage.forField(~plugin?, ~store)),
|
|
83
|
+
altText: ?s.matches(S.option(S.string)),
|
|
84
|
+
caption: ?s.matches(S.option(S.string)),
|
|
85
|
+
})->Semantic.mark(
|
|
86
|
+
~id=Semantic.Id.captionedImage,
|
|
87
|
+
// The store rides on the record, not on `ref`: a field reader looks through
|
|
88
|
+
// an optional wrapper and an array element and no further, so a marker one
|
|
89
|
+
// record deeper would be a store declaration nothing provisions.
|
|
90
|
+
~payload=StoredIn({plugin, store, threshold: None}),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
/** The text that replaces the image, by the rule above — `altText`, else the
|
|
94
|
+
caption, else nothing. One definition, because a second one would be the
|
|
95
|
+
drift this type exists to remove. */
|
|
96
|
+
let altTextOf = (c: t): option<string> =>
|
|
97
|
+
switch c.altText {
|
|
98
|
+
| Some(_) as text => text
|
|
99
|
+
| None => c.caption
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
|
+
import * as UploadableImage$Reventless from "./UploadableImage.res.mjs";
|
|
6
|
+
|
|
7
|
+
function forField(plugin, store) {
|
|
8
|
+
return Semantic$Reventless.mark(Sury.$schema(s => ({
|
|
9
|
+
ref: s.m(UploadableImage$Reventless.forField(plugin, store)),
|
|
10
|
+
altText: s.m(Sury.$option(Sury.string)),
|
|
11
|
+
caption: s.m(Sury.$option(Sury.string))
|
|
12
|
+
})), Semantic$Reventless.Id.captionedImage, {
|
|
13
|
+
TAG: "StoredIn",
|
|
14
|
+
_0: {
|
|
15
|
+
plugin: plugin,
|
|
16
|
+
store: store,
|
|
17
|
+
threshold: undefined
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function altTextOf(c) {
|
|
23
|
+
let text = c.altText;
|
|
24
|
+
if (text !== undefined) {
|
|
25
|
+
return text;
|
|
26
|
+
} else {
|
|
27
|
+
return c.caption;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
forField,
|
|
33
|
+
altTextOf,
|
|
34
|
+
}
|
|
35
|
+
/* sury Not a pure module */
|
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
Marks an `int` field as a length of time, in **seconds
|
|
2
|
+
Marks an `int` field as a length of time, in **seconds** — whole, zero or
|
|
3
|
+
greater. Zero is a real duration.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
reason is the reason this whole set of types is safe to add: a record is an
|
|
8
|
-
*object* on the wire, so adopting it would turn every field that used it into a
|
|
9
|
-
decode failure against events already written. These types are additive
|
|
10
|
-
precisely because they stay scalars. A duration that carries its unit is a
|
|
11
|
-
different type for a different plan, not a later version of this one — widening
|
|
12
|
-
this one would retroactively hand an upcaster obligation to every field that had
|
|
13
|
-
already adopted it.
|
|
14
|
-
|
|
15
|
-
Seconds, because that is what the renderer reads: it formats `3660` as
|
|
16
|
-
`"1h 1m"`. Milliseconds would be off by a factor of a thousand and would
|
|
17
|
-
render as weeks, which is the same class of silent wrongness `Percent` avoids by
|
|
18
|
-
matching its gauge.
|
|
19
|
-
|
|
20
|
-
`int` is right here where it was wrong for `Bytes`: int32 seconds is 68 years,
|
|
21
|
-
which no duration field needs to exceed.
|
|
22
|
-
|
|
23
|
-
## The grammar
|
|
24
|
-
|
|
25
|
-
A whole number of seconds, zero or greater. Zero is a real duration — an instant
|
|
26
|
-
timeout, a zero-length window.
|
|
5
|
+
Seconds because that is the unit `format` reads; milliseconds would render as
|
|
6
|
+
weeks. A scalar rather than `{value, unit}` so the type stays additive on the
|
|
7
|
+
wire. `int` is safe here — int32 seconds is 68 years.
|
|
27
8
|
|
|
28
9
|
@example
|
|
29
10
|
```rescript
|
|
@@ -51,3 +32,20 @@ let fromInt = (raw: int): result<t, string> =>
|
|
|
51
32
|
/** The sury schema for a duration field, in seconds.
|
|
52
33
|
Use with `@s.matches(Reventless.Duration.schema)`. */
|
|
53
34
|
let schema: S.t<t> = S.int->Semantic.refined(~id=Semantic.Id.duration, ~check=fromInt)
|
|
35
|
+
|
|
36
|
+
let scales = [(86400, "d"), (3600, "h"), (60, "m"), (1, "s")]
|
|
37
|
+
|
|
38
|
+
/** The duration as text, largest unit first and zero units dropped — `3660` is
|
|
39
|
+
`"1h 1m"`, `0` is `"0s"`. Locale-independent, the way `Money.format` is. */
|
|
40
|
+
let format = (d: t): string => {
|
|
41
|
+
let remaining = ref(d < 0 ? 0 : d)
|
|
42
|
+
let parts = []
|
|
43
|
+
scales->Array.forEach(((size, suffix)) => {
|
|
44
|
+
let count = remaining.contents / size
|
|
45
|
+
if count > 0 {
|
|
46
|
+
parts->Array.push(Int.toString(count) ++ suffix)
|
|
47
|
+
remaining := remaining.contents - count * size
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
Array.length(parts) == 0 ? "0s" : parts->Array.join(" ")
|
|
51
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
4
5
|
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
6
|
|
|
6
7
|
function fromInt(raw) {
|
|
@@ -19,8 +20,50 @@ function fromInt(raw) {
|
|
|
19
20
|
|
|
20
21
|
let schema = Semantic$Reventless.refined(Sury.int, Semantic$Reventless.Id.duration, fromInt);
|
|
21
22
|
|
|
23
|
+
let scales = [
|
|
24
|
+
[
|
|
25
|
+
86400,
|
|
26
|
+
"d"
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
3600,
|
|
30
|
+
"h"
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
60,
|
|
34
|
+
"m"
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
1,
|
|
38
|
+
"s"
|
|
39
|
+
]
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function format(d) {
|
|
43
|
+
let remaining = {
|
|
44
|
+
contents: d < 0 ? 0 : d
|
|
45
|
+
};
|
|
46
|
+
let parts = [];
|
|
47
|
+
scales.forEach(param => {
|
|
48
|
+
let size = param[0];
|
|
49
|
+
let count = Primitive_int.div(remaining.contents, size);
|
|
50
|
+
if (count > 0) {
|
|
51
|
+
parts.push(count.toString() + param[1]);
|
|
52
|
+
remaining.contents = remaining.contents - (count * size | 0) | 0;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (parts.length === 0) {
|
|
57
|
+
return "0s";
|
|
58
|
+
} else {
|
|
59
|
+
return parts.join(" ");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
22
63
|
export {
|
|
23
64
|
fromInt,
|
|
24
65
|
schema,
|
|
66
|
+
scales,
|
|
67
|
+
format,
|
|
25
68
|
}
|
|
26
69
|
/* schema Not a pure module */
|
|
@@ -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,
|