@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,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 e164 = /^\+[1-9]\d{0,14}$/;
|
|
7
|
+
|
|
8
|
+
function fromString(raw) {
|
|
9
|
+
if (e164.test(raw)) {
|
|
10
|
+
return {
|
|
11
|
+
TAG: "Ok",
|
|
12
|
+
_0: raw
|
|
13
|
+
};
|
|
14
|
+
} else {
|
|
15
|
+
return {
|
|
16
|
+
TAG: "Error",
|
|
17
|
+
_0: `expected a phone number in E.164 form — "+" then up to 15 digits, as in "+4930123456" — got ` + Semantic$Reventless.showString(raw) + `. Spaces, dashes and brackets are not part of the stored form.`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.phone, fromString);
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
e164,
|
|
26
|
+
fromString,
|
|
27
|
+
schema,
|
|
28
|
+
}
|
|
29
|
+
/* schema Not a pure module */
|
|
@@ -48,6 +48,16 @@ module Id = {
|
|
|
48
48
|
let dateTime = "dateTime"
|
|
49
49
|
let reference = "reference"
|
|
50
50
|
let storageRef = "storageRef"
|
|
51
|
+
|
|
52
|
+
// The branded scalars. Each refines a `string` or a number without changing
|
|
53
|
+
// its shape, so a field gains one of these without anything stored changing.
|
|
54
|
+
let email = "email"
|
|
55
|
+
let phone = "phone"
|
|
56
|
+
let url = "url"
|
|
57
|
+
let percent = "percent"
|
|
58
|
+
let bytes = "bytes"
|
|
59
|
+
let duration = "duration"
|
|
60
|
+
let color = "color"
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless", ~name="semantic")
|
|
@@ -56,8 +66,67 @@ let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless",
|
|
|
56
66
|
let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
|
|
57
67
|
schema->S.Metadata.set(~id=semanticId, {id, payload})
|
|
58
68
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
69
|
+
/**
|
|
70
|
+
A schema that validates with `check` and carries the semantic `id`.
|
|
71
|
+
|
|
72
|
+
The branded scalars all have the same shape — one constructor function that
|
|
73
|
+
defines the grammar, and a schema that must agree with it — and `StorageRef`
|
|
74
|
+
established that the schema is *derived* from the constructor rather than
|
|
75
|
+
hand-rolling a second check beside it. Deriving it here makes that structural:
|
|
76
|
+
there is one place a grammar can be written, so there is nowhere for a second
|
|
77
|
+
one to drift.
|
|
78
|
+
*/
|
|
79
|
+
let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.t<'a> =>
|
|
80
|
+
base
|
|
81
|
+
->S.refine(s => value =>
|
|
82
|
+
switch check(value) {
|
|
83
|
+
| Ok(_) => ()
|
|
84
|
+
| Error(why) => s.fail(why)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
->mark(~id)
|
|
88
|
+
|
|
89
|
+
/** A value as it should read back to the person who typed it. Rejection messages
|
|
90
|
+
reach forms through `validateInput`, so they quote the offending value. */
|
|
91
|
+
let showString = (raw: string): string => raw->JSON.Encode.string->JSON.stringify
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
The semantic a field's schema carries, if any.
|
|
95
|
+
|
|
96
|
+
An **optional** field keeps its marker one level down. The ppx annotates the
|
|
97
|
+
field's `string`, and sury-ppx then wraps that schema in a union with
|
|
98
|
+
`Undefined`/`Null`; the wrapper is a new schema and carries no metadata of its
|
|
99
|
+
own. So a walk that reads only the outer schema sees `imageUrl?: string` as
|
|
100
|
+
carrying no semantic at all — the store goes undeclared, the reference goes
|
|
101
|
+
uncollected, the branded scalar loses its brand. Every reader converges here, so
|
|
102
|
+
following the wrapper once here is what keeps "optional" a statement about
|
|
103
|
+
presence rather than a way to lose the field's type.
|
|
104
|
+
|
|
105
|
+
The outer schema is read first, so a marker set on the wrapper itself still wins.
|
|
106
|
+
Only a union with exactly one non-null variant is followed: that is the shape an
|
|
107
|
+
optional field has, and a genuine multi-variant union has no single inner schema
|
|
108
|
+
whose semantic could stand for the whole.
|
|
109
|
+
*/
|
|
110
|
+
let rec getFrom = (schema: S.t<unknown>): option<t> =>
|
|
111
|
+
switch S.Metadata.get(schema, ~id=semanticId) {
|
|
112
|
+
| Some(_) as found => found
|
|
113
|
+
| None =>
|
|
114
|
+
switch schema {
|
|
115
|
+
| Union({anyOf}) =>
|
|
116
|
+
switch anyOf->Array.filter(v =>
|
|
117
|
+
switch v {
|
|
118
|
+
| Null(_) | Undefined(_) => false
|
|
119
|
+
| _ => true
|
|
120
|
+
}
|
|
121
|
+
) {
|
|
122
|
+
| [inner] => getFrom(inner)
|
|
123
|
+
| _ => None
|
|
124
|
+
}
|
|
125
|
+
| _ => None
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let get = (fieldSchema: S.t<'a>): option<t> => fieldSchema->S.castToUnknown->getFrom
|
|
61
130
|
|
|
62
131
|
/** Whether a field's schema carries this specific semantic. */
|
|
63
132
|
let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
|
|
@@ -5,7 +5,14 @@ import * as S from "sury/src/S.res.mjs";
|
|
|
5
5
|
let Id = {
|
|
6
6
|
dateTime: "dateTime",
|
|
7
7
|
reference: "reference",
|
|
8
|
-
storageRef: "storageRef"
|
|
8
|
+
storageRef: "storageRef",
|
|
9
|
+
email: "email",
|
|
10
|
+
phone: "phone",
|
|
11
|
+
url: "url",
|
|
12
|
+
percent: "percent",
|
|
13
|
+
bytes: "bytes",
|
|
14
|
+
duration: "duration",
|
|
15
|
+
color: "color"
|
|
9
16
|
};
|
|
10
17
|
|
|
11
18
|
let semanticId = S.Metadata.Id.make("reventless", "semantic");
|
|
@@ -18,12 +25,52 @@ function mark(schema, id, payloadOpt) {
|
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
function
|
|
22
|
-
return S.
|
|
28
|
+
function refined(base, id, check) {
|
|
29
|
+
return mark(S.refine(base, s => (value => {
|
|
30
|
+
let why = check(value);
|
|
31
|
+
if (why.TAG === "Ok") {
|
|
32
|
+
return;
|
|
33
|
+
} else {
|
|
34
|
+
return s.fail(why._0, undefined);
|
|
35
|
+
}
|
|
36
|
+
})), id, undefined);
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
function showString(raw) {
|
|
40
|
+
return JSON.stringify(raw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getFrom(_schema) {
|
|
44
|
+
while (true) {
|
|
45
|
+
let schema = _schema;
|
|
46
|
+
let found = S.Metadata.get(schema, semanticId);
|
|
47
|
+
if (found !== undefined) {
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
if (schema.type !== "union") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let match = schema.anyOf.filter(v => {
|
|
54
|
+
switch (v.type) {
|
|
55
|
+
case "null" :
|
|
56
|
+
case "undefined" :
|
|
57
|
+
return false;
|
|
58
|
+
default:
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (match.length !== 1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
_schema = match[0];
|
|
66
|
+
continue;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let get = getFrom;
|
|
71
|
+
|
|
25
72
|
function has(fieldSchema, id) {
|
|
26
|
-
let s =
|
|
73
|
+
let s = getFrom(fieldSchema);
|
|
27
74
|
if (s !== undefined) {
|
|
28
75
|
return s.id === id;
|
|
29
76
|
} else {
|
|
@@ -35,6 +82,9 @@ export {
|
|
|
35
82
|
Id,
|
|
36
83
|
semanticId,
|
|
37
84
|
mark,
|
|
85
|
+
refined,
|
|
86
|
+
showString,
|
|
87
|
+
getFrom,
|
|
38
88
|
get,
|
|
39
89
|
has,
|
|
40
90
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Marks a `string` field as a web address.
|
|
3
|
+
|
|
4
|
+
## The grammar: parseable, **and** `http`/`https`
|
|
5
|
+
|
|
6
|
+
Sury's `S.url` is the parse — it is `new URL()`, so it settles host, port and
|
|
7
|
+
escaping without this module having an opinion. But `new URL()` accepts every
|
|
8
|
+
scheme, `javascript:alert(1)` included, and that is not an academic gap here: a
|
|
9
|
+
field carrying this semantic renders as an anchor whose `href` is the stored
|
|
10
|
+
value. An append-only log plus a scheme nobody checked is a stored XSS that
|
|
11
|
+
cannot be deleted afterwards — the same shape of hole `StorageRef` exists to
|
|
12
|
+
close, arriving through a different field.
|
|
13
|
+
|
|
14
|
+
So the grammar is sury's parse plus a two-scheme allowlist. `mailto:` and `tel:`
|
|
15
|
+
are deliberately outside it: those are `Email` and `Phone`, which validate what
|
|
16
|
+
they actually hold and render correctly on their own.
|
|
17
|
+
|
|
18
|
+
@example
|
|
19
|
+
```rescript
|
|
20
|
+
@schema type command =
|
|
21
|
+
| SetSupplierSite({
|
|
22
|
+
supplierId: @s.matches(DcbTag.string) string,
|
|
23
|
+
website: @s.matches(Reventless.Url.schema) string,
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The address's representation. Transparent `string`; see `Email.t`. */
|
|
29
|
+
type t = string
|
|
30
|
+
|
|
31
|
+
external unsafe: string => t = "%identity"
|
|
32
|
+
external toString: t => string = "%identity"
|
|
33
|
+
|
|
34
|
+
// Sury's parse, held once — `fromString` runs it and adds the scheme check, and
|
|
35
|
+
// `schema` derives from `fromString`. One grammar, in one place.
|
|
36
|
+
let grammar: S.t<string> = S.string->S.url
|
|
37
|
+
|
|
38
|
+
// Schemes are case-insensitive per RFC 3986, and `HTTPS://x` parses fine, so the
|
|
39
|
+
// allowlist has to fold case or it rejects a valid address on a technicality.
|
|
40
|
+
let hasWebScheme = (raw: string): bool => {
|
|
41
|
+
let lower = String.toLowerCase(raw)
|
|
42
|
+
String.startsWith(lower, "http://") || String.startsWith(lower, "https://")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Validate a raw string as an `http`/`https` URL, saying why when it is not one. */
|
|
46
|
+
let fromString = (raw: string): result<t, string> =>
|
|
47
|
+
switch raw->S.parseOrThrow(grammar) {
|
|
48
|
+
| value =>
|
|
49
|
+
if hasWebScheme(value) {
|
|
50
|
+
Ok(value)
|
|
51
|
+
} else {
|
|
52
|
+
Error(
|
|
53
|
+
`a URL field takes an http:// or https:// address, got ${Semantic.showString(raw)}. ` ++
|
|
54
|
+
`Use Email or Phone for mailto:/tel:, and a storage ref for an uploaded object.`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
| exception _ =>
|
|
58
|
+
Error(
|
|
59
|
+
`expected an absolute URL, got ${Semantic.showString(
|
|
60
|
+
raw,
|
|
61
|
+
)}. A relative path is not a URL — include the scheme and host.`,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The sury schema for a URL field. Use with `@s.matches(Reventless.Url.schema)`. */
|
|
66
|
+
let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.url, ~check=fromString)
|
|
@@ -0,0 +1,48 @@
|
|
|
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.url(S.string, undefined);
|
|
7
|
+
|
|
8
|
+
function hasWebScheme(raw) {
|
|
9
|
+
let lower = raw.toLowerCase();
|
|
10
|
+
if (lower.startsWith("http://")) {
|
|
11
|
+
return true;
|
|
12
|
+
} else {
|
|
13
|
+
return lower.startsWith("https://");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromString(raw) {
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = S.parseOrThrow(raw, grammar);
|
|
21
|
+
} catch (exn) {
|
|
22
|
+
return {
|
|
23
|
+
TAG: "Error",
|
|
24
|
+
_0: `expected an absolute URL, got ` + Semantic$Reventless.showString(raw) + `. A relative path is not a URL — include the scheme and host.`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (hasWebScheme(value)) {
|
|
28
|
+
return {
|
|
29
|
+
TAG: "Ok",
|
|
30
|
+
_0: value
|
|
31
|
+
};
|
|
32
|
+
} else {
|
|
33
|
+
return {
|
|
34
|
+
TAG: "Error",
|
|
35
|
+
_0: `a URL field takes an http:// or https:// address, got ` + Semantic$Reventless.showString(raw) + `. Use Email or Phone for mailto:/tel:, and a storage ref for an uploaded object.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let schema = Semantic$Reventless.refined(S.string, Semantic$Reventless.Id.url, fromString);
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
grammar,
|
|
44
|
+
hasWebScheme,
|
|
45
|
+
fromString,
|
|
46
|
+
schema,
|
|
47
|
+
}
|
|
48
|
+
/* grammar Not a pure module */
|