@reventlessdev/reventless-spec 3.0.0-alpha.84 → 3.0.0-alpha.86

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,114 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Yaml from "yaml";
5
+ import * as Nodefs from "node:fs";
6
+ import * as Nodepath from "node:path";
7
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
8
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as PlatformCodegen$Reventless from "./PlatformCodegen.res.mjs";
11
+ import * as PlatformManifests$Reventless from "./PlatformManifests.res.mjs";
12
+ import * as CapabilityManifest$Reventless from "../components/CapabilityManifest.res.mjs";
13
+
14
+ function fail(message) {
15
+ console.error("generate-platform: " + message);
16
+ process.exit(1);
17
+ }
18
+
19
+ function asObject(json) {
20
+ return Stdlib_JSON.Decode.object(json);
21
+ }
22
+
23
+ function stringAt(obj, field) {
24
+ return Stdlib_Option.flatMap(obj[field], Stdlib_JSON.Decode.string);
25
+ }
26
+
27
+ let manifestArg = Stdlib_Option.getOr(process.argv[2], "");
28
+
29
+ if (manifestArg === "") {
30
+ console.error("Usage: generate-platform <deploy-manifest.yaml>");
31
+ process.exit(1);
32
+ } else {
33
+ let manifestPath = Nodepath.resolve(manifestArg);
34
+ if (!Nodefs.existsSync(manifestPath)) {
35
+ fail(manifestPath + ` not found`);
36
+ }
37
+ let manifestDir = Nodepath.dirname(manifestPath);
38
+ let deployManifest;
39
+ try {
40
+ deployManifest = Yaml.parse(Nodefs.readFileSync(manifestPath, "utf8"));
41
+ } catch (exn) {
42
+ fail(`could not parse ` + manifestPath + ` as YAML`);
43
+ deployManifest = null;
44
+ }
45
+ let platformPath = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(deployManifest), m => m["platform"]), asObject), p => stringAt(p, "path"));
46
+ let plugins = Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(deployManifest), m => m["plugins"]), Stdlib_JSON.Decode.array), entries => Stdlib_Array.filterMap(entries, entry => {
47
+ let obj = Stdlib_JSON.Decode.object(entry);
48
+ let match = Stdlib_Option.flatMap(obj, o => stringAt(o, "name"));
49
+ let match$1 = Stdlib_Option.flatMap(obj, o => stringAt(o, "path"));
50
+ if (match !== undefined && match$1 !== undefined) {
51
+ return [
52
+ match,
53
+ match$1
54
+ ];
55
+ }
56
+ }));
57
+ if (platformPath !== undefined) {
58
+ if (plugins !== undefined) {
59
+ let pluginManifests = plugins.flatMap(param => {
60
+ let pluginName = param[0];
61
+ let pluginDir = Nodepath.resolve(manifestDir, param[1]);
62
+ if (!Nodefs.existsSync(pluginDir)) {
63
+ fail(`plugin "` + pluginName + `" points at ` + pluginDir + `, which does not exist`);
64
+ }
65
+ let manifests = PlatformManifests$Reventless.resolve(undefined, pluginDir);
66
+ if (typeof manifests !== "object") {
67
+ console.log(`Skipped: ` + pluginName + ` (` + pluginDir + `) — no plugin composition`);
68
+ return [];
69
+ }
70
+ if (manifests.TAG === "Manifests") {
71
+ return manifests._0.map(param => {
72
+ let path = param.path;
73
+ console.log(`Read: ` + pluginName + ` — ` + path + ` (via ` + PlatformManifests$Reventless.describeVia(param.via) + `)`);
74
+ let manifest;
75
+ try {
76
+ manifest = S.parseOrThrow(JSON.parse(Nodefs.readFileSync(path, "utf8")), CapabilityManifest$Reventless.schema);
77
+ } catch (exn) {
78
+ fail(`could not parse ` + path + ` as a capability manifest`);
79
+ manifest = {
80
+ capabilities: []
81
+ };
82
+ }
83
+ return {
84
+ pluginName: pluginName,
85
+ manifest: manifest
86
+ };
87
+ });
88
+ }
89
+ let match = manifests._0;
90
+ fail(`no capabilities.json found for plugin "` + pluginName + `" (` + pluginDir + `) — build the plugin first; its build emits src/capabilities.json beside Plugin.res ` + (`(` + match.evidence + `; expected ` + match.expected + `)`));
91
+ return [];
92
+ });
93
+ let message = PlatformCodegen$Reventless.render(pluginManifests);
94
+ if (message.TAG === "Ok") {
95
+ let outputPath = Nodepath.resolve(manifestDir, platformPath, "src", "PlatformCapabilities.res");
96
+ Nodefs.writeFileSync(outputPath, message._0, "utf8");
97
+ console.log("Generated: " + outputPath);
98
+ } else {
99
+ fail(message._0);
100
+ }
101
+ } else {
102
+ fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
103
+ }
104
+ } else {
105
+ fail(manifestPath + ` needs a \`platform.path\` and a \`plugins\` list with name + path`);
106
+ }
107
+ }
108
+
109
+ export {
110
+ fail,
111
+ asObject,
112
+ stringAt,
113
+ }
114
+ /* manifestArg Not a pure module */
@@ -0,0 +1,203 @@
1
+ // Where a deploy-manifest entry's capability manifests live — and, when it
2
+ // contributes none, whether that is because the entry is not a plugin at all.
3
+ //
4
+ // A deploy manifest enumerates *deployables*, not plugins. An SdkService stack,
5
+ // a static SPA, a standalone ingester Lambda are all legitimate entries with no
6
+ // `pluginStructure` to derive capabilities from; contributing nothing is the
7
+ // right answer for those. A plugin whose manifest is merely unbuilt is the
8
+ // opposite case, and silently skipping it would deprovision the stores it
9
+ // declared. The two are told apart by evidence — something in the package graph
10
+ // that emits a capability manifest — never by falling through the arms below.
11
+ //
12
+ // Resolution order, first arm that yields manifests wins:
13
+ //
14
+ // 1. `<dir>/src/capabilities.json` — the entry is itself a composition
15
+ // package, so its manifest sits beside its composition root.
16
+ // 2. `<dir>`'s own `generate: generate-plugin --aws <Namespace> <srcDir>`
17
+ // script — an `-aws` root generated from a composition `src/` elsewhere in
18
+ // the same repo. The script is the single existing record of that
19
+ // delegation, so no second hand-typed spelling of the path is introduced.
20
+ // 3. `<dir>`'s dependencies — an `-aws` root that *installs* a published
21
+ // composition package, which is the shape the framework recommends and the
22
+ // one arms 1 and 2 both miss. Every plugin dependency contributes: a root
23
+ // deploying two plugins legitimately needs both manifests.
24
+ // 4. Nothing in the package graph emits a manifest — not a plugin stack.
25
+ //
26
+ // I/O goes through `fileSystem` so the arms are testable without a fixture
27
+ // tree; `nodeFileSystem` is what the generator passes.
28
+
29
+ /** How a manifest was found — reported so the resolving arm never has to be
30
+ reconstructed from the output. */
31
+ type via =
32
+ | Composition
33
+ | Generated(string)
34
+ | Dependency(string)
35
+
36
+ type manifest = {path: string, via: via}
37
+
38
+ /** Why the entry counted as a plugin, and where its manifest was looked for. */
39
+ type unbuilt = {evidence: string, expected: string}
40
+
41
+ type t =
42
+ | Manifests(array<manifest>)
43
+ /** Something here emits a capability manifest, but the manifest is not on
44
+ disk: a plugin that has not been built. */
45
+ | Unbuilt(unbuilt)
46
+ /** No plugin composition anywhere in the entry's package graph. */
47
+ | NotAPlugin
48
+
49
+ type fileSystem = {
50
+ exists: string => bool,
51
+ readJson: string => option<JSON.t>,
52
+ }
53
+
54
+ let nodeFileSystem: fileSystem = {
55
+ exists: Generator_Node.existsSync,
56
+ readJson: path =>
57
+ try Some(Generator_Node.readFileSync(path)->JSON.parseOrThrow) catch {
58
+ | _ => None
59
+ },
60
+ }
61
+
62
+ let describeVia = (via: via): string =>
63
+ switch via {
64
+ | Composition => "its own src/"
65
+ | Generated(srcDir) => `its \`generate\` script's ${srcDir}`
66
+ | Dependency(name) => `dependency ${name}`
67
+ }
68
+
69
+ let asObject = (json: JSON.t): option<dict<JSON.t>> => json->JSON.Decode.object
70
+
71
+ let packageJsonAt = (~fs: fileSystem, ~dir: string): option<dict<JSON.t>> =>
72
+ fs.readJson(Generator_Node.join([dir, "package.json"]))->Option.flatMap(asObject)
73
+
74
+ let scriptsOf = (pkg: dict<JSON.t>): array<string> =>
75
+ pkg
76
+ ->Dict.get("scripts")
77
+ ->Option.flatMap(asObject)
78
+ ->Option.mapOr([], scripts =>
79
+ scripts->Dict.valuesToArray->Array.filterMap(JSON.Decode.string)
80
+ )
81
+
82
+ // The composition `src/` an `-aws` root is generated from, read off the script
83
+ // that performs the generation.
84
+ let generatedSrcDir = (pkg: dict<JSON.t>): option<string> =>
85
+ pkg
86
+ ->Dict.get("scripts")
87
+ ->Option.flatMap(asObject)
88
+ ->Option.flatMap(scripts => scripts->Dict.get("generate"))
89
+ ->Option.flatMap(JSON.Decode.string)
90
+ ->Option.flatMap(script => {
91
+ let tokens = script->String.split(" ")->Array.filter(t => t != "")
92
+ switch tokens {
93
+ | ["generate-plugin", "--aws", _namespace, srcDir] => Some(srcDir)
94
+ | _ => None
95
+ }
96
+ })
97
+
98
+ // A package that runs `emit-capabilities` as part of its build is a plugin
99
+ // composition package, whether or not it has been built yet. That is the whole
100
+ // signal G1 needs: it is stated once, by the package that owns the fact, and it
101
+ // is already there in every plugin package — unlike a `kind:` on the deploy
102
+ // manifest, which would restate it a second time.
103
+ //
104
+ // `reventless-local` *provides* the bin under `bin`, and so is correctly not
105
+ // matched here.
106
+ let emitsCapabilities = (pkg: dict<JSON.t>): bool =>
107
+ pkg->scriptsOf->Array.some(script => script->String.includes("emit-capabilities"))
108
+
109
+ let dependencyNames = (pkg: dict<JSON.t>): array<string> => {
110
+ let seen = Dict.make()
111
+ ["dependencies", "devDependencies"]
112
+ ->Array.flatMap(field =>
113
+ pkg->Dict.get(field)->Option.flatMap(asObject)->Option.mapOr([], Dict.keysToArray)
114
+ )
115
+ ->Array.filter(name =>
116
+ switch seen->Dict.get(name) {
117
+ | Some(_) => false
118
+ | None => {
119
+ seen->Dict.set(name, true)
120
+ true
121
+ }
122
+ }
123
+ )
124
+ }
125
+
126
+ /** Node's directory resolution: `<dir>/node_modules/<name>`, then each parent's,
127
+ up to the filesystem root. Walking it rather than binding `require.resolve`
128
+ keeps the lookup on the package directory itself — a package's entry point
129
+ may be anywhere, and it is `src/capabilities.json` that is wanted. */
130
+ let resolvePackageDir = (~fs: fileSystem, ~fromDir: string, ~name: string): option<string> => {
131
+ let rec search = dir => {
132
+ let candidate = Generator_Node.join([dir, "node_modules", name])
133
+ if fs.exists(Generator_Node.join([candidate, "package.json"])) {
134
+ Some(candidate)
135
+ } else {
136
+ let parent = Generator_Node.dirname(dir)
137
+ parent == dir ? None : search(parent)
138
+ }
139
+ }
140
+ search(fromDir)
141
+ }
142
+
143
+ // Arm 3. Unbuilt wins over what was found: a root deploying two plugins needs
144
+ // both manifests, so one of them missing is a failure even though the other
145
+ // resolved.
146
+ let fromDependencies = (~fs: fileSystem, ~pluginDir: string, ~pkg: dict<JSON.t>): t => {
147
+ let found = []
148
+ let unbuilt = ref(None)
149
+ pkg
150
+ ->dependencyNames
151
+ ->Array.forEach(name =>
152
+ switch resolvePackageDir(~fs, ~fromDir=pluginDir, ~name) {
153
+ | None => ()
154
+ | Some(packageDir) => {
155
+ let candidate = Generator_Node.join([packageDir, "src", "capabilities.json"])
156
+ if fs.exists(candidate) {
157
+ found->Array.push({path: candidate, via: Dependency(name)})
158
+ } else if packageJsonAt(~fs, ~dir=packageDir)->Option.mapOr(false, emitsCapabilities) {
159
+ switch unbuilt.contents {
160
+ | Some(_) => ()
161
+ | None =>
162
+ unbuilt :=
163
+ Some({
164
+ evidence: `dependency ${name} emits a capability manifest`,
165
+ expected: candidate,
166
+ })
167
+ }
168
+ }
169
+ }
170
+ }
171
+ )
172
+ switch (unbuilt.contents, found) {
173
+ | (Some({evidence, expected}), _) => Unbuilt({evidence, expected})
174
+ | (None, []) => NotAPlugin
175
+ | (None, manifests) => Manifests(manifests)
176
+ }
177
+ }
178
+
179
+ let resolve = (~fs: fileSystem=nodeFileSystem, ~pluginDir: string): t => {
180
+ let direct = Generator_Node.join([pluginDir, "src", "capabilities.json"])
181
+ if fs.exists(direct) {
182
+ Manifests([{path: direct, via: Composition}])
183
+ } else {
184
+ switch packageJsonAt(~fs, ~dir=pluginDir) {
185
+ | None => NotAPlugin
186
+ | Some(pkg) =>
187
+ switch generatedSrcDir(pkg) {
188
+ | Some(srcDir) => {
189
+ let expected = Generator_Node.resolve([pluginDir, srcDir, "capabilities.json"])
190
+ if fs.exists(expected) {
191
+ Manifests([{path: expected, via: Generated(srcDir)}])
192
+ } else {
193
+ Unbuilt({
194
+ evidence: `its \`generate\` script composes ${srcDir}`,
195
+ expected,
196
+ })
197
+ }
198
+ }
199
+ | None => fromDependencies(~fs, ~pluginDir, ~pkg)
200
+ }
201
+ }
202
+ }
203
+ }
@@ -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)