@reventlessdev/reventless-spec 3.0.0-alpha.85 → 3.0.0-alpha.87
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 +16 -0
- package/package.json +1 -1
- package/src/components/CapabilityManifest.res +10 -3
- package/src/components/CapabilityManifest.res.mjs +4 -2
- package/src/components/Plugin.res +8 -0
- package/src/components/Plugin.res.mjs +2 -1
- package/src/generator/PlatformCodegen.res +20 -16
- package/src/generator/PlatformCodegen.res.mjs +11 -12
- package/src/generator/PlatformGenerator.res +29 -50
- package/src/generator/PlatformGenerator.res.mjs +30 -56
- package/src/generator/PlatformManifests.res +203 -0
- package/src/generator/PlatformManifests.res.mjs +212 -0
- package/src/semantic/Bytes.res +54 -0
- package/src/semantic/Bytes.res.mjs +38 -0
- package/src/semantic/Color.res +51 -0
- package/src/semantic/Color.res.mjs +29 -0
- package/src/semantic/Duration.res +53 -0
- package/src/semantic/Duration.res.mjs +26 -0
- package/src/semantic/Email.res +51 -0
- package/src/semantic/Email.res.mjs +31 -0
- package/src/semantic/Percent.res +53 -0
- package/src/semantic/Percent.res.mjs +33 -0
- package/src/semantic/Phone.res +55 -0
- package/src/semantic/Phone.res.mjs +29 -0
- package/src/semantic/Semantic.res +71 -2
- package/src/semantic/Semantic.res.mjs +54 -4
- package/src/semantic/Url.res +66 -0
- package/src/semantic/Url.res.mjs +48 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
|
|
9
|
+
function nodeFileSystem_exists(prim) {
|
|
10
|
+
return Nodefs.existsSync(prim);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nodeFileSystem_readJson(path) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(Nodefs.readFileSync(path, "utf8"));
|
|
16
|
+
} catch (exn) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let nodeFileSystem = {
|
|
22
|
+
exists: nodeFileSystem_exists,
|
|
23
|
+
readJson: nodeFileSystem_readJson
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function describeVia(via) {
|
|
27
|
+
if (typeof via !== "object") {
|
|
28
|
+
return "its own src/";
|
|
29
|
+
} else if (via.TAG === "Generated") {
|
|
30
|
+
return `its \`generate\` script's ` + via._0;
|
|
31
|
+
} else {
|
|
32
|
+
return `dependency ` + via._0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asObject(json) {
|
|
37
|
+
return Stdlib_JSON.Decode.object(json);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function packageJsonAt(fs, dir) {
|
|
41
|
+
return Stdlib_Option.flatMap(fs.readJson(Nodepath.join(dir, "package.json")), asObject);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scriptsOf(pkg) {
|
|
45
|
+
return Stdlib_Option.mapOr(Stdlib_Option.flatMap(pkg["scripts"], asObject), [], scripts => Stdlib_Array.filterMap(Object.values(scripts), Stdlib_JSON.Decode.string));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function generatedSrcDir(pkg) {
|
|
49
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(pkg["scripts"], asObject), scripts => scripts["generate"]), Stdlib_JSON.Decode.string), script => {
|
|
50
|
+
let tokens = script.split(" ").filter(t => t !== "");
|
|
51
|
+
if (tokens.length !== 4) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let match = tokens[0];
|
|
55
|
+
if (match !== "generate-plugin") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let match$1 = tokens[1];
|
|
59
|
+
if (match$1 === "--aws") {
|
|
60
|
+
return tokens[3];
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emitsCapabilities(pkg) {
|
|
66
|
+
return scriptsOf(pkg).some(script => script.includes("emit-capabilities"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function dependencyNames(pkg) {
|
|
70
|
+
let seen = {};
|
|
71
|
+
return [
|
|
72
|
+
"dependencies",
|
|
73
|
+
"devDependencies"
|
|
74
|
+
].flatMap(field => Stdlib_Option.mapOr(Stdlib_Option.flatMap(pkg[field], asObject), [], prim => Object.keys(prim))).filter(name => {
|
|
75
|
+
let match = seen[name];
|
|
76
|
+
if (match !== undefined) {
|
|
77
|
+
return false;
|
|
78
|
+
} else {
|
|
79
|
+
seen[name] = true;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolvePackageDir(fs, fromDir, name) {
|
|
86
|
+
let _dir = fromDir;
|
|
87
|
+
while (true) {
|
|
88
|
+
let dir = _dir;
|
|
89
|
+
let candidate = Nodepath.join(dir, "node_modules", name);
|
|
90
|
+
if (fs.exists(Nodepath.join(candidate, "package.json"))) {
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
let parent = Nodepath.dirname(dir);
|
|
94
|
+
if (parent === dir) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
_dir = parent;
|
|
98
|
+
continue;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function fromDependencies(fs, pluginDir, pkg) {
|
|
103
|
+
let found = [];
|
|
104
|
+
let unbuilt = {
|
|
105
|
+
contents: undefined
|
|
106
|
+
};
|
|
107
|
+
dependencyNames(pkg).forEach(name => {
|
|
108
|
+
let packageDir = resolvePackageDir(fs, pluginDir, name);
|
|
109
|
+
if (packageDir === undefined) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let candidate = Nodepath.join(packageDir, "src", "capabilities.json");
|
|
113
|
+
if (fs.exists(candidate)) {
|
|
114
|
+
found.push({
|
|
115
|
+
path: candidate,
|
|
116
|
+
via: {
|
|
117
|
+
TAG: "Dependency",
|
|
118
|
+
_0: name
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!Stdlib_Option.mapOr(packageJsonAt(fs, packageDir), false, emitsCapabilities)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let match = unbuilt.contents;
|
|
127
|
+
if (match !== undefined) {
|
|
128
|
+
return;
|
|
129
|
+
} else {
|
|
130
|
+
unbuilt.contents = {
|
|
131
|
+
evidence: `dependency ` + name + ` emits a capability manifest`,
|
|
132
|
+
expected: candidate
|
|
133
|
+
};
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
let match = unbuilt.contents;
|
|
138
|
+
if (match !== undefined) {
|
|
139
|
+
return {
|
|
140
|
+
TAG: "Unbuilt",
|
|
141
|
+
_0: {
|
|
142
|
+
evidence: match.evidence,
|
|
143
|
+
expected: match.expected
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
} else if (found.length !== 0) {
|
|
147
|
+
return {
|
|
148
|
+
TAG: "Manifests",
|
|
149
|
+
_0: found
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
return "NotAPlugin";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolve(fsOpt, pluginDir) {
|
|
157
|
+
let fs = fsOpt !== undefined ? fsOpt : nodeFileSystem;
|
|
158
|
+
let direct = Nodepath.join(pluginDir, "src", "capabilities.json");
|
|
159
|
+
if (fs.exists(direct)) {
|
|
160
|
+
return {
|
|
161
|
+
TAG: "Manifests",
|
|
162
|
+
_0: [{
|
|
163
|
+
path: direct,
|
|
164
|
+
via: "Composition"
|
|
165
|
+
}]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
let pkg = packageJsonAt(fs, pluginDir);
|
|
169
|
+
if (pkg === undefined) {
|
|
170
|
+
return "NotAPlugin";
|
|
171
|
+
}
|
|
172
|
+
let srcDir = generatedSrcDir(pkg);
|
|
173
|
+
if (srcDir === undefined) {
|
|
174
|
+
return fromDependencies(fs, pluginDir, pkg);
|
|
175
|
+
}
|
|
176
|
+
let expected = Nodepath.resolve(pluginDir, srcDir, "capabilities.json");
|
|
177
|
+
if (fs.exists(expected)) {
|
|
178
|
+
return {
|
|
179
|
+
TAG: "Manifests",
|
|
180
|
+
_0: [{
|
|
181
|
+
path: expected,
|
|
182
|
+
via: {
|
|
183
|
+
TAG: "Generated",
|
|
184
|
+
_0: srcDir
|
|
185
|
+
}
|
|
186
|
+
}]
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
return {
|
|
190
|
+
TAG: "Unbuilt",
|
|
191
|
+
_0: {
|
|
192
|
+
evidence: `its \`generate\` script composes ` + srcDir,
|
|
193
|
+
expected: expected
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
nodeFileSystem,
|
|
201
|
+
describeVia,
|
|
202
|
+
asObject,
|
|
203
|
+
packageJsonAt,
|
|
204
|
+
scriptsOf,
|
|
205
|
+
generatedSrcDir,
|
|
206
|
+
emitsCapabilities,
|
|
207
|
+
dependencyNames,
|
|
208
|
+
resolvePackageDir,
|
|
209
|
+
fromDependencies,
|
|
210
|
+
resolve,
|
|
211
|
+
}
|
|
212
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a numeric field as a count of bytes.
|
|
3
|
+
|
|
4
|
+
## Why `float` and not `int`
|
|
5
|
+
|
|
6
|
+
A byte count is a whole number and cannot be negative, so `int` is the type it
|
|
7
|
+
wants to be. It is the wrong one here anyway: ReScript's `int` is int32, and
|
|
8
|
+
sury enforces that, so an `int` byte count silently caps at 2,147,483,647 — just
|
|
9
|
+
under 2 GiB. A type whose stated job is file sizes cannot stop at 2 GB.
|
|
10
|
+
|
|
11
|
+
`float` is a JS number, exact for every integer up to 2^53 — nine petabytes,
|
|
12
|
+
which is enough. The discreteness `int` would have given for free is recovered
|
|
13
|
+
by checking it: the grammar below rejects a fractional byte count, so the type
|
|
14
|
+
still means what `int` meant, minus the ceiling.
|
|
15
|
+
|
|
16
|
+
The wire is unaffected either way — both are JSON numbers — so this is a source
|
|
17
|
+
choice, not a format one. A field genuinely bounded below 2 GiB can still be
|
|
18
|
+
declared `int` and left unmarked; this type is for the ones that are not.
|
|
19
|
+
|
|
20
|
+
## The grammar
|
|
21
|
+
|
|
22
|
+
A finite, whole number, zero or greater. Zero is a real byte count — an empty
|
|
23
|
+
object — so it is accepted.
|
|
24
|
+
|
|
25
|
+
@example
|
|
26
|
+
```rescript
|
|
27
|
+
@schema type state = {
|
|
28
|
+
documentId: string,
|
|
29
|
+
size: @s.matches(Reventless.Bytes.schema) float,
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The count's representation. Transparent `float`; see the note above on why
|
|
35
|
+
it is not `int`. */
|
|
36
|
+
type t = float
|
|
37
|
+
|
|
38
|
+
external unsafe: float => t = "%identity"
|
|
39
|
+
external toFloat: t => float = "%identity"
|
|
40
|
+
|
|
41
|
+
/** Validate a number as a byte count, saying why when it is not one. */
|
|
42
|
+
let fromFloat = (raw: float): result<t, string> =>
|
|
43
|
+
if !Float.isFinite(raw) {
|
|
44
|
+
Error(`a byte count must be a finite number, got ${Float.toString(raw)}`)
|
|
45
|
+
} else if raw < 0.0 {
|
|
46
|
+
Error(`a byte count cannot be negative, got ${Float.toString(raw)}`)
|
|
47
|
+
} else if Math.floor(raw) !== raw {
|
|
48
|
+
Error(`a byte count is a whole number of bytes, got ${Float.toString(raw)}`)
|
|
49
|
+
} else {
|
|
50
|
+
Ok(raw)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The sury schema for a byte-count field. Use with `@s.matches(Reventless.Bytes.schema)`. */
|
|
54
|
+
let schema: S.t<t> = S.float->Semantic.refined(~id=Semantic.Id.bytes, ~check=fromFloat)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
|
+
|
|
6
|
+
function fromFloat(raw) {
|
|
7
|
+
if (isFinite(raw)) {
|
|
8
|
+
if (raw < 0.0) {
|
|
9
|
+
return {
|
|
10
|
+
TAG: "Error",
|
|
11
|
+
_0: `a byte count cannot be negative, got ` + raw.toString()
|
|
12
|
+
};
|
|
13
|
+
} else if (Math.floor(raw) !== raw) {
|
|
14
|
+
return {
|
|
15
|
+
TAG: "Error",
|
|
16
|
+
_0: `a byte count is a whole number of bytes, got ` + raw.toString()
|
|
17
|
+
};
|
|
18
|
+
} else {
|
|
19
|
+
return {
|
|
20
|
+
TAG: "Ok",
|
|
21
|
+
_0: raw
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
return {
|
|
26
|
+
TAG: "Error",
|
|
27
|
+
_0: `a byte count must be a finite number, got ` + raw.toString()
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let schema = Semantic$Reventless.refined(S.float, Semantic$Reventless.Id.bytes, fromFloat);
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
fromFloat,
|
|
36
|
+
schema,
|
|
37
|
+
}
|
|
38
|
+
/* schema Not a pure module */
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a `string` field as a colour, written as a hex triplet.
|
|
3
|
+
|
|
4
|
+
## Why hex only
|
|
5
|
+
|
|
6
|
+
A field carrying this semantic is rendered as a swatch by setting the value as a
|
|
7
|
+
CSS `background`. CSS accepts far more than a colour there — `url(…)`, gradients,
|
|
8
|
+
`var(…)` — so "any CSS colour" would make a colour field a way to write arbitrary
|
|
9
|
+
CSS into a log that cannot be edited afterwards. Hex is the form that is a
|
|
10
|
+
colour and nothing else, and it is what a colour picker emits anyway.
|
|
11
|
+
|
|
12
|
+
Named colours (`rebeccapurple`) and functional forms (`rgb(…)`, `oklch(…)`) are
|
|
13
|
+
rejected for the same reason, not because they are worse notation.
|
|
14
|
+
|
|
15
|
+
## The grammar
|
|
16
|
+
|
|
17
|
+
`#` then 3, 4, 6 or 8 hex digits — the shorthand, the shorthand with alpha, the
|
|
18
|
+
full triplet, and the triplet with alpha. Case is not significant.
|
|
19
|
+
|
|
20
|
+
@example
|
|
21
|
+
```rescript
|
|
22
|
+
@schema type command =
|
|
23
|
+
| SetLabelColour({
|
|
24
|
+
labelId: @s.matches(DcbTag.string) string,
|
|
25
|
+
colour: @s.matches(Reventless.Color.schema) string,
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The colour's representation. Transparent `string`; see `Email.t`. */
|
|
31
|
+
type t = string
|
|
32
|
+
|
|
33
|
+
external unsafe: string => t = "%identity"
|
|
34
|
+
external toString: t => string = "%identity"
|
|
35
|
+
|
|
36
|
+
let hex = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
|
|
37
|
+
|
|
38
|
+
/** Validate a raw string as a hex colour, saying why when it is not one. */
|
|
39
|
+
let fromString = (raw: string): result<t, string> =>
|
|
40
|
+
if hex->RegExp.test(raw) {
|
|
41
|
+
Ok(raw)
|
|
42
|
+
} else {
|
|
43
|
+
Error(
|
|
44
|
+
`expected a hex colour such as "#1e90ff" or "#1e90ffcc", got ${Semantic.showString(
|
|
45
|
+
raw,
|
|
46
|
+
)}. Named colours and rgb()/oklch() forms are not accepted.`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The sury schema for a colour field. Use with `@s.matches(Reventless.Color.schema)`. */
|
|
51
|
+
let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.color, ~check=fromString)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
|
+
|
|
6
|
+
let hex = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
7
|
+
|
|
8
|
+
function fromString(raw) {
|
|
9
|
+
if (hex.test(raw)) {
|
|
10
|
+
return {
|
|
11
|
+
TAG: "Ok",
|
|
12
|
+
_0: raw
|
|
13
|
+
};
|
|
14
|
+
} else {
|
|
15
|
+
return {
|
|
16
|
+
TAG: "Error",
|
|
17
|
+
_0: `expected a hex colour such as "#1e90ff" or "#1e90ffcc", got ` + Semantic$Reventless.showString(raw) + `. Named colours and rgb()/oklch() forms are not accepted.`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.color, fromString);
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
hex,
|
|
26
|
+
fromString,
|
|
27
|
+
schema,
|
|
28
|
+
}
|
|
29
|
+
/* schema Not a pure module */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks an `int` field as a length of time, in **seconds**.
|
|
3
|
+
|
|
4
|
+
## Why a scalar, and why seconds are part of the type
|
|
5
|
+
|
|
6
|
+
The obvious richer design is `{value, unit}`. It is not available here, and the
|
|
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.
|
|
27
|
+
|
|
28
|
+
@example
|
|
29
|
+
```rescript
|
|
30
|
+
@schema type state = {
|
|
31
|
+
jobId: string,
|
|
32
|
+
runtimeSeconds: @s.matches(Reventless.Duration.schema) int,
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** The duration's representation, in seconds. Transparent `int`. */
|
|
38
|
+
type t = int
|
|
39
|
+
|
|
40
|
+
external unsafe: int => t = "%identity"
|
|
41
|
+
external toInt: t => int = "%identity"
|
|
42
|
+
|
|
43
|
+
/** Validate a number of seconds as a duration, saying why when it is not one. */
|
|
44
|
+
let fromInt = (raw: int): result<t, string> =>
|
|
45
|
+
if raw < 0 {
|
|
46
|
+
Error(`a duration cannot be negative, got ${Int.toString(raw)} seconds`)
|
|
47
|
+
} else {
|
|
48
|
+
Ok(raw)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The sury schema for a duration field, in seconds.
|
|
52
|
+
Use with `@s.matches(Reventless.Duration.schema)`. */
|
|
53
|
+
let schema: S.t<t> = S.int->Semantic.refined(~id=Semantic.Id.duration, ~check=fromInt)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
|
+
|
|
6
|
+
function fromInt(raw) {
|
|
7
|
+
if (raw < 0) {
|
|
8
|
+
return {
|
|
9
|
+
TAG: "Error",
|
|
10
|
+
_0: `a duration cannot be negative, got ` + raw.toString() + ` seconds`
|
|
11
|
+
};
|
|
12
|
+
} else {
|
|
13
|
+
return {
|
|
14
|
+
TAG: "Ok",
|
|
15
|
+
_0: raw
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let schema = Semantic$Reventless.refined(S.int, Semantic$Reventless.Id.duration, fromInt);
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
fromInt,
|
|
24
|
+
schema,
|
|
25
|
+
}
|
|
26
|
+
/* schema Not a pure module */
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a `string` field as an email address.
|
|
3
|
+
|
|
4
|
+
## Why a type rather than a name
|
|
5
|
+
|
|
6
|
+
An `email` field already renders as a mailto link today, because the UI guesses
|
|
7
|
+
from the field's *name*. The guess is the whole problem: `contact`, `owner` and
|
|
8
|
+
`notifyTo` hold addresses and are not guessed, `emailTemplate` is guessed and is
|
|
9
|
+
not an address, and nothing anywhere checks that what was written is one. A type
|
|
10
|
+
says it, and the check comes with it.
|
|
11
|
+
|
|
12
|
+
## The grammar
|
|
13
|
+
|
|
14
|
+
Sury's `S.email`, and nothing added. This is a case where the framework has no
|
|
15
|
+
opinion worth having: address syntax is somebody else's specification, and a
|
|
16
|
+
second regex here would only be a way to disagree with it.
|
|
17
|
+
|
|
18
|
+
Note that a syntactically valid address is not a deliverable one — nothing here
|
|
19
|
+
sends a probe. This rejects what is not an address, which is the part a boundary
|
|
20
|
+
check can honestly do.
|
|
21
|
+
|
|
22
|
+
@example
|
|
23
|
+
```rescript
|
|
24
|
+
@schema type command =
|
|
25
|
+
| InviteMember({
|
|
26
|
+
teamId: @s.matches(DcbTag.string) string,
|
|
27
|
+
email: @s.matches(Reventless.Email.schema) string,
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The address's representation. Transparent `string`: the marker refines an
|
|
33
|
+
existing field rather than replacing it, so nothing stored changes. */
|
|
34
|
+
type t = string
|
|
35
|
+
|
|
36
|
+
external unsafe: string => t = "%identity"
|
|
37
|
+
external toString: t => string = "%identity"
|
|
38
|
+
|
|
39
|
+
// Sury's check, held once. `fromString` runs it rather than restating it, and
|
|
40
|
+
// `schema` is built from `fromString`, so there is exactly one grammar here.
|
|
41
|
+
let grammar: S.t<string> = S.string->S.email
|
|
42
|
+
|
|
43
|
+
/** Validate a raw string as an email address, saying why when it is not one. */
|
|
44
|
+
let fromString = (raw: string): result<t, string> =>
|
|
45
|
+
switch raw->S.parseOrThrow(grammar) {
|
|
46
|
+
| value => Ok(value)
|
|
47
|
+
| exception _ => Error(`expected an email address, got ${Semantic.showString(raw)}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The sury schema for an email field. Use with `@s.matches(Reventless.Email.schema)`. */
|
|
51
|
+
let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.email, ~check=fromString)
|
|
@@ -0,0 +1,31 @@
|
|
|
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.email(S.string, undefined);
|
|
7
|
+
|
|
8
|
+
function fromString(raw) {
|
|
9
|
+
let value;
|
|
10
|
+
try {
|
|
11
|
+
value = S.parseOrThrow(raw, grammar);
|
|
12
|
+
} catch (exn) {
|
|
13
|
+
return {
|
|
14
|
+
TAG: "Error",
|
|
15
|
+
_0: `expected an email address, got ` + Semantic$Reventless.showString(raw)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
TAG: "Ok",
|
|
20
|
+
_0: value
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.email, fromString);
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
grammar,
|
|
28
|
+
fromString,
|
|
29
|
+
schema,
|
|
30
|
+
}
|
|
31
|
+
/* grammar Not a pure module */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a `float` field as a percentage, expressed **0–100**.
|
|
3
|
+
|
|
4
|
+
## Why 0–100 and not 0–1
|
|
5
|
+
|
|
6
|
+
Both conventions are defensible in the abstract, so the tie is broken by the
|
|
7
|
+
consumer that already exists: the dashboard gauges a field with this semantic
|
|
8
|
+
against fixed bounds of 0 and 100, and formats `42.0` as `"42%"`. Under a 0–1
|
|
9
|
+
convention every value would render as a rounding error near zero — a gauge
|
|
10
|
+
pinned at empty and a label reading `"0.42%"`.
|
|
11
|
+
|
|
12
|
+
That failure is quiet, and it is quiet in the worst way: the numbers are
|
|
13
|
+
*present* and *wrong*, and the layer at fault is not the one showing the symptom.
|
|
14
|
+
Agreeing with the renderer costs nothing; disagreeing costs an afternoon.
|
|
15
|
+
|
|
16
|
+
A fraction is still perfectly good arithmetic — it just multiplies by 100 before
|
|
17
|
+
it becomes this type.
|
|
18
|
+
|
|
19
|
+
## The grammar
|
|
20
|
+
|
|
21
|
+
A finite number in `[0, 100]`. Fractions are allowed: `99.95` is a percentage.
|
|
22
|
+
|
|
23
|
+
@example
|
|
24
|
+
```rescript
|
|
25
|
+
@schema type state = {
|
|
26
|
+
productId: string,
|
|
27
|
+
taxRate: @s.matches(Reventless.Percent.schema) float,
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The percentage's representation. Transparent `float`: the marker refines an
|
|
33
|
+
existing numeric field rather than replacing it, so nothing stored changes. */
|
|
34
|
+
type t = float
|
|
35
|
+
|
|
36
|
+
external unsafe: float => t = "%identity"
|
|
37
|
+
external toFloat: t => float = "%identity"
|
|
38
|
+
|
|
39
|
+
/** Validate a number as a percentage, saying why when it is not one. */
|
|
40
|
+
let fromFloat = (raw: float): result<t, string> =>
|
|
41
|
+
if !Float.isFinite(raw) {
|
|
42
|
+
Error(`a percentage must be a finite number, got ${Float.toString(raw)}`)
|
|
43
|
+
} else if raw < 0.0 || raw > 100.0 {
|
|
44
|
+
Error(
|
|
45
|
+
`a percentage runs from 0 to 100, got ${Float.toString(raw)}. ` ++
|
|
46
|
+
`This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`,
|
|
47
|
+
)
|
|
48
|
+
} else {
|
|
49
|
+
Ok(raw)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The sury schema for a percentage field. Use with `@s.matches(Reventless.Percent.schema)`. */
|
|
53
|
+
let schema: S.t<t> = S.float->Semantic.refined(~id=Semantic.Id.percent, ~check=fromFloat)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
5
|
+
|
|
6
|
+
function fromFloat(raw) {
|
|
7
|
+
if (isFinite(raw)) {
|
|
8
|
+
if (raw < 0.0 || raw > 100.0) {
|
|
9
|
+
return {
|
|
10
|
+
TAG: "Error",
|
|
11
|
+
_0: `a percentage runs from 0 to 100, got ` + raw.toString() + `. This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`
|
|
12
|
+
};
|
|
13
|
+
} else {
|
|
14
|
+
return {
|
|
15
|
+
TAG: "Ok",
|
|
16
|
+
_0: raw
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
return {
|
|
21
|
+
TAG: "Error",
|
|
22
|
+
_0: `a percentage must be a finite number, got ` + raw.toString()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let schema = Semantic$Reventless.refined(S.float, Semantic$Reventless.Id.percent, fromFloat);
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
fromFloat,
|
|
31
|
+
schema,
|
|
32
|
+
}
|
|
33
|
+
/* schema Not a pure module */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a `string` field as a phone number in E.164 form.
|
|
3
|
+
|
|
4
|
+
## Why E.164, and only E.164
|
|
5
|
+
|
|
6
|
+
A phone number written the way a person says it out loud — `(030) 12 34 56`,
|
|
7
|
+
`+49 30 123456`, `0049-30-123456` — is three different strings for one number,
|
|
8
|
+
so a log full of them cannot be searched, deduplicated or dialled reliably. E.164
|
|
9
|
+
is the one form that is unambiguous internationally, and it is the form the
|
|
10
|
+
`tel:` link this field renders as actually wants.
|
|
11
|
+
|
|
12
|
+
That makes this the one branded scalar that is likely to *reject* input a form
|
|
13
|
+
would otherwise have accepted, and it should: the alternative is storing an
|
|
14
|
+
un-dialable string permanently. Normalising a local number into E.164 needs a
|
|
15
|
+
default country the framework does not know, so that belongs to the caller,
|
|
16
|
+
before the command.
|
|
17
|
+
|
|
18
|
+
## The grammar
|
|
19
|
+
|
|
20
|
+
`+`, then a country digit 1–9, then up to 14 more digits — at most 15 in total,
|
|
21
|
+
which is the E.164 limit. No spaces, no punctuation, no leading zero after the
|
|
22
|
+
`+`.
|
|
23
|
+
|
|
24
|
+
@example
|
|
25
|
+
```rescript
|
|
26
|
+
@schema type command =
|
|
27
|
+
| SetContactPhone({
|
|
28
|
+
customerId: @s.matches(DcbTag.string) string,
|
|
29
|
+
phone: @s.matches(Reventless.Phone.schema) string,
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The number's representation. Transparent `string`; see `Email.t`. */
|
|
35
|
+
type t = string
|
|
36
|
+
|
|
37
|
+
external unsafe: string => t = "%identity"
|
|
38
|
+
external toString: t => string = "%identity"
|
|
39
|
+
|
|
40
|
+
let e164 = /^\+[1-9]\d{0,14}$/
|
|
41
|
+
|
|
42
|
+
/** Validate a raw string as an E.164 number, saying why when it is not one. */
|
|
43
|
+
let fromString = (raw: string): result<t, string> =>
|
|
44
|
+
if e164->RegExp.test(raw) {
|
|
45
|
+
Ok(raw)
|
|
46
|
+
} else {
|
|
47
|
+
Error(
|
|
48
|
+
`expected a phone number in E.164 form — "+" then up to 15 digits, as in "+4930123456" — got ${Semantic.showString(
|
|
49
|
+
raw,
|
|
50
|
+
)}. Spaces, dashes and brackets are not part of the stored form.`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The sury schema for a phone field. Use with `@s.matches(Reventless.Phone.schema)`. */
|
|
55
|
+
let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.phone, ~check=fromString)
|