@reventlessdev/reventless-spec 3.0.0-alpha.81 → 3.0.0-alpha.83
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 +15 -0
- package/package.json +1 -1
- package/src/components/Plugin.res +15 -0
- package/src/components/Plugin.res.mjs +2 -1
- package/src/components/Reference.res +11 -7
- package/src/components/Reference.res.mjs +24 -11
- package/src/semantic/Semantic.res +67 -0
- package/src/semantic/Semantic.res.mjs +41 -0
- package/src/semantic/StorageRef.res +124 -0
- package/src/semantic/StorageRef.res.mjs +85 -0
- package/src/types/DateTime.res +2 -6
- package/src/types/DateTime.res.mjs +4 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.83 (2026-07-28)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **platform:** provision object stores from the fields that declare them ([b5e2a1e](https://github.com/ReventlessDev/reventless-core/commit/b5e2a1ec88099941c113e4963f9c4b346b96b0d6))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.82 (2026-07-28)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **spec:** one semantic marker every typed semantic marks itself with ([aa18afc](https://github.com/ReventlessDev/reventless-core/commit/aa18afcf04c8edad9afe27e6fa4261d01e184da7))
|
|
18
|
+
* **spec:** StorageRef — the first semantic type, declared on the field's type ([44f15c3](https://github.com/ReventlessDev/reventless-core/commit/44f15c37de71261d701d18a9f1ada6f481c4a8dc))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.81 (2026-07-26)
|
|
7
22
|
|
|
8
23
|
### Features
|
package/package.json
CHANGED
|
@@ -374,6 +374,21 @@ type pluginStructure = {
|
|
|
374
374
|
// read as []). js_nullable keeps it JSON-safe inside union variant payloads.
|
|
375
375
|
extensionPoints: @s.matches(extensionPointDefArrayOptionSchema)
|
|
376
376
|
option<array<extensionPointDef>>,
|
|
377
|
+
/**
|
|
378
|
+
The object stores this plugin's fields declare they need, deduplicated and
|
|
379
|
+
fully qualified as `{plugin}.{store}`.
|
|
380
|
+
|
|
381
|
+
A field typed as a storage ref states a *requirement*: the deployment needs
|
|
382
|
+
that store to exist. Collecting the requirement here is what lets it be read
|
|
383
|
+
without re-walking every component's schema — the same reason
|
|
384
|
+
`producedEventTypes` is carried rather than recomputed.
|
|
385
|
+
|
|
386
|
+
Qualified even for the common same-plugin case, so one entry has one shape
|
|
387
|
+
and the string is directly the store's identity. Optional and js_nullable for
|
|
388
|
+
the same reason as `extensionPoints`: definitions persisted before this field
|
|
389
|
+
existed still decode (absent → None, read as []).
|
|
390
|
+
*/
|
|
391
|
+
requiredStores: @s.matches(stringArrayOptionSchema) option<array<string>>,
|
|
377
392
|
}
|
|
378
393
|
|
|
379
394
|
let pluginStructureOptionSchema = _jsNullable(pluginStructureSchema, ())
|
|
@@ -186,7 +186,8 @@ let pluginStructureSchema = S.schema(s => ({
|
|
|
186
186
|
outboundTranslationSlices: s.m(S.array(outboundTranslationSliceDefSchema)),
|
|
187
187
|
inboundTranslationSlices: s.m(S.array(inboundTranslationSliceDefSchema)),
|
|
188
188
|
extensions: s.m(S.array(extensionDefSchema)),
|
|
189
|
-
extensionPoints: s.m(extensionPointDefArrayOptionSchema)
|
|
189
|
+
extensionPoints: s.m(extensionPointDefArrayOptionSchema),
|
|
190
|
+
requiredStores: s.m(stringArrayOptionSchema)
|
|
190
191
|
}));
|
|
191
192
|
|
|
192
193
|
let pluginStructureOptionSchema = SuryResMjs.js_nullable(pluginStructureSchema);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/** Identifies which entity a reference field points to. */
|
|
2
|
-
type target =
|
|
3
|
-
|
|
4
|
-
/** Sury metadata ID for entity reference annotation. */
|
|
5
|
-
let referenceId: S.Metadata.Id.t<target> = S.Metadata.Id.make(~namespace="reventless", ~name="reference")
|
|
2
|
+
type target = Semantic.referenceTarget
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
A sury string schema annotated as an entity reference field.
|
|
@@ -28,10 +25,14 @@ Prefer the `@ref("EntityName")` ppx shorthand over writing `@s.matches(...)` by
|
|
|
28
25
|
```
|
|
29
26
|
*/
|
|
30
27
|
let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
|
|
28
|
+
// Reference-ness and DCB-tagged-ness are two independent facts that happen to
|
|
29
|
+
// co-occur here: this constructor declares both. `toWithoutDcbTag` declares
|
|
30
|
+
// only the first, and plain `DcbTag.string` only the second — so no consumer
|
|
31
|
+
// may infer either one from the other.
|
|
31
32
|
let base =
|
|
32
33
|
S.string
|
|
33
34
|
->S.Metadata.set(~id=DcbTag.dcbTagId, true)
|
|
34
|
-
->
|
|
35
|
+
->Semantic.mark(~id=Semantic.Id.reference, ~payload=ReferenceTo({entity, plugin}))
|
|
35
36
|
switch key {
|
|
36
37
|
| Some(k) => base->S.Metadata.set(~id=DcbTag.dcbTagKeyOverrideId, k)
|
|
37
38
|
| None => base
|
|
@@ -40,7 +41,10 @@ let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
|
|
|
40
41
|
|
|
41
42
|
/** Returns the reference target if the schema carries `Reference.to_(...)` metadata. */
|
|
42
43
|
let getTarget = (schema: S.t<unknown>): option<target> =>
|
|
43
|
-
|
|
44
|
+
switch Semantic.get(schema) {
|
|
45
|
+
| Some({payload: ReferenceTo(target)}) => Some(target)
|
|
46
|
+
| _ => None
|
|
47
|
+
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
Like `to_` but does not imply DCB tag semantics.
|
|
@@ -48,4 +52,4 @@ Use with `@ref("Entity") @noDcbTag` when the field references another entity
|
|
|
48
52
|
but should not participate in content-based event routing.
|
|
49
53
|
*/
|
|
50
54
|
let toWithoutDcbTag = (~plugin=?, entity: string): S.t<string> =>
|
|
51
|
-
S.string->
|
|
55
|
+
S.string->Semantic.mark(~id=Semantic.Id.reference, ~payload=ReferenceTo({entity, plugin}))
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as DcbTag$Reventless from "./DcbTag.res.mjs";
|
|
5
|
-
|
|
6
|
-
let referenceId = S.Metadata.Id.make("reventless", "reference");
|
|
5
|
+
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
7
6
|
|
|
8
7
|
function to_(plugin, key, entity) {
|
|
9
|
-
let base =
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
let base = Semantic$Reventless.mark(S.Metadata.set(S.string, DcbTag$Reventless.dcbTagId, true), Semantic$Reventless.Id.reference, {
|
|
9
|
+
TAG: "ReferenceTo",
|
|
10
|
+
_0: {
|
|
11
|
+
entity: entity,
|
|
12
|
+
plugin: plugin
|
|
13
|
+
}
|
|
12
14
|
});
|
|
13
15
|
if (key !== undefined) {
|
|
14
16
|
return S.Metadata.set(base, DcbTag$Reventless.dcbTagKeyOverrideId, key);
|
|
@@ -18,20 +20,31 @@ function to_(plugin, key, entity) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function getTarget(schema) {
|
|
21
|
-
|
|
23
|
+
let match = Semantic$Reventless.get(schema);
|
|
24
|
+
if (match === undefined) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let target = match.payload;
|
|
28
|
+
if (typeof target !== "object" || target.TAG !== "ReferenceTo") {
|
|
29
|
+
return;
|
|
30
|
+
} else {
|
|
31
|
+
return target._0;
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
function toWithoutDcbTag(plugin, entity) {
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
return Semantic$Reventless.mark(S.string, Semantic$Reventless.Id.reference, {
|
|
37
|
+
TAG: "ReferenceTo",
|
|
38
|
+
_0: {
|
|
39
|
+
entity: entity,
|
|
40
|
+
plugin: plugin
|
|
41
|
+
}
|
|
28
42
|
});
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
export {
|
|
32
|
-
referenceId,
|
|
33
46
|
to_,
|
|
34
47
|
getTarget,
|
|
35
48
|
toWithoutDcbTag,
|
|
36
49
|
}
|
|
37
|
-
/*
|
|
50
|
+
/* S Not a pure module */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The one marker every typed semantic marks itself with.
|
|
3
|
+
|
|
4
|
+
A semantic type says what a field's value *is* — a date-time, a reference to
|
|
5
|
+
another entity, a ref into an object store — as a property of the field's
|
|
6
|
+
**type**, not as a string stapled beside it. Every layer downstream then derives
|
|
7
|
+
from that single declaration: validation, the wire contract, the UI widget it
|
|
8
|
+
gets rendered with, and eventually the infrastructure provisioned for it.
|
|
9
|
+
|
|
10
|
+
Typed markers predate this module, and each was bespoke: `DateTime` carried its
|
|
11
|
+
own metadata id, `Reference` carried another, and the schema walk detected both
|
|
12
|
+
by hardcoded special case. That made every new typed marker new detection code.
|
|
13
|
+
One shared marker means the walk reads a semantic generically and a new semantic
|
|
14
|
+
type is a new *value*, not a new branch.
|
|
15
|
+
|
|
16
|
+
The payload is a real variant rather than free-form JSON. The semantic
|
|
17
|
+
vocabulary is framework-owned — an application declares a field *is* a storage
|
|
18
|
+
ref, it does not invent what a storage ref means — so the set is closed, and a
|
|
19
|
+
closed set typed here is one the compiler checks at every producer and consumer.
|
|
20
|
+
It also keeps `Reference.getTarget` a total typed function instead of a decode
|
|
21
|
+
that can fail at runtime.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Which entity a reference field points to. */
|
|
25
|
+
type referenceTarget = {entity: string, plugin: option<string>}
|
|
26
|
+
|
|
27
|
+
/** Which object store a storage-ref field's value lives in. `plugin` is absent
|
|
28
|
+
when the store belongs to the declaring plugin, which is the common case. */
|
|
29
|
+
type storeTarget = {plugin: option<string>, store: string}
|
|
30
|
+
|
|
31
|
+
/** Per-semantic detail, for the semantics that carry any. */
|
|
32
|
+
type payload =
|
|
33
|
+
| Plain
|
|
34
|
+
| ReferenceTo(referenceTarget)
|
|
35
|
+
| StoredIn(storeTarget)
|
|
36
|
+
|
|
37
|
+
/** A field's semantic: the vocabulary id, plus its detail. */
|
|
38
|
+
type t = {id: string, payload: payload}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
The semantic ids the framework itself defines.
|
|
42
|
+
|
|
43
|
+
These strings are the wire vocabulary — they are what `x-reventless-semantic`
|
|
44
|
+
carries, and the same vocabulary the string annotation path already uses, so the
|
|
45
|
+
type path and the annotation path converge on one wire format rather than two.
|
|
46
|
+
*/
|
|
47
|
+
module Id = {
|
|
48
|
+
let dateTime = "dateTime"
|
|
49
|
+
let reference = "reference"
|
|
50
|
+
let storageRef = "storageRef"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless", ~name="semantic")
|
|
54
|
+
|
|
55
|
+
/** Mark a schema as carrying a semantic. */
|
|
56
|
+
let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
|
|
57
|
+
schema->S.Metadata.set(~id=semanticId, {id, payload})
|
|
58
|
+
|
|
59
|
+
/** The semantic a field's schema carries, if any. */
|
|
60
|
+
let get = (fieldSchema: S.t<'a>): option<t> => S.Metadata.get(fieldSchema, ~id=semanticId)
|
|
61
|
+
|
|
62
|
+
/** Whether a field's schema carries this specific semantic. */
|
|
63
|
+
let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
|
|
64
|
+
switch get(fieldSchema) {
|
|
65
|
+
| Some(s) => s.id === id
|
|
66
|
+
| None => false
|
|
67
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
|
|
5
|
+
let Id = {
|
|
6
|
+
dateTime: "dateTime",
|
|
7
|
+
reference: "reference",
|
|
8
|
+
storageRef: "storageRef"
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let semanticId = S.Metadata.Id.make("reventless", "semantic");
|
|
12
|
+
|
|
13
|
+
function mark(schema, id, payloadOpt) {
|
|
14
|
+
let payload = payloadOpt !== undefined ? payloadOpt : "Plain";
|
|
15
|
+
return S.Metadata.set(schema, semanticId, {
|
|
16
|
+
id: id,
|
|
17
|
+
payload: payload
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function get(fieldSchema) {
|
|
22
|
+
return S.Metadata.get(fieldSchema, semanticId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function has(fieldSchema, id) {
|
|
26
|
+
let s = S.Metadata.get(fieldSchema, semanticId);
|
|
27
|
+
if (s !== undefined) {
|
|
28
|
+
return s.id === id;
|
|
29
|
+
} else {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
Id,
|
|
36
|
+
semanticId,
|
|
37
|
+
mark,
|
|
38
|
+
get,
|
|
39
|
+
has,
|
|
40
|
+
}
|
|
41
|
+
/* semanticId Not a pure module */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A reference to an object living in one of the platform's object stores.
|
|
3
|
+
|
|
4
|
+
The value is the ref string a store's presign service minted — an origin-relative
|
|
5
|
+
path rooted at the store's served prefix, which the UI renders directly because
|
|
6
|
+
the store is fronted read-only on the app's own origin.
|
|
7
|
+
|
|
8
|
+
## Why this is a type and not a convention
|
|
9
|
+
|
|
10
|
+
An event log is append-only, so whatever a command accepts into it is permanent.
|
|
11
|
+
Before this type, an `imageUrl: string` field accepted *anything* — including an
|
|
12
|
+
`https://` URL pointing at somebody else's server, or a multi-megabyte `data:`
|
|
13
|
+
URI inlined into the event itself. Both deploy green, both are unfixable after
|
|
14
|
+
the fact, and neither is what the field means. Declaring the field's type makes
|
|
15
|
+
the wrong values unrepresentable at the boundary, before `decide` ever runs.
|
|
16
|
+
|
|
17
|
+
The declaration also states a *requirement*: a field of this type says the
|
|
18
|
+
deployment needs a store called `store` to exist. Nothing provisions that store
|
|
19
|
+
yet, and that is a deliberate resting point — the validation hole is closed and
|
|
20
|
+
the requirement is written down, which is strictly better than the status quo
|
|
21
|
+
even if automatic provisioning never lands.
|
|
22
|
+
|
|
23
|
+
## The grammar
|
|
24
|
+
|
|
25
|
+
A ref is an absolute, origin-relative path of at least two non-empty segments:
|
|
26
|
+
|
|
27
|
+
/uploads/2f8c1e94-.../photo.jpg
|
|
28
|
+
/uploads/user-42/2f8c1e94-.../photo.jpg
|
|
29
|
+
|
|
30
|
+
Rejected: anything with a scheme (`https://…`, `data:…`), protocol-relative
|
|
31
|
+
`//host/path`, relative paths, empty segments, and `.`/`..` traversal.
|
|
32
|
+
|
|
33
|
+
The framework mints refs in exactly this form — the presign service builds the
|
|
34
|
+
object key as `{servedPrefix}/{identity}{uuid}/{fileName}` and returns `/{key}` —
|
|
35
|
+
so the grammar is the framework's to define, not an application's.
|
|
36
|
+
|
|
37
|
+
Note what is *not* checked: that the ref's prefix belongs to this specific store.
|
|
38
|
+
Today every store shares one served prefix, so there is nothing store-specific to
|
|
39
|
+
check against; the store identity is carried in the field's semantic payload,
|
|
40
|
+
where provisioning and the UI read it. When stores gain per-store prefixes, this
|
|
41
|
+
check tightens from a structural one to a per-store one without the type, the
|
|
42
|
+
wire format, or any stored value changing.
|
|
43
|
+
|
|
44
|
+
@example
|
|
45
|
+
```rescript
|
|
46
|
+
@schema type command =
|
|
47
|
+
| ChangeProductImage({
|
|
48
|
+
productId: @s.matches(DcbTag.string) string,
|
|
49
|
+
imageUrl: @storageRef("productImages") string,
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/** The ref's representation. Transparent `string` on purpose: the marker refines
|
|
55
|
+
an existing `string` field rather than replacing it, so the field's runtime
|
|
56
|
+
representation — and therefore every stored event — is unchanged. A sealed
|
|
57
|
+
type here would defeat that, and would also be unattachable via `@s.matches`,
|
|
58
|
+
which requires the schema's type to match the field's. */
|
|
59
|
+
type t = string
|
|
60
|
+
|
|
61
|
+
external unsafe: string => t = "%identity"
|
|
62
|
+
external toString: t => string = "%identity"
|
|
63
|
+
|
|
64
|
+
let segmentIsSafe = (segment: string) =>
|
|
65
|
+
segment !== "" && segment !== "." && segment !== ".."
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
Validate a raw string as a storage ref, saying why when it is not one.
|
|
69
|
+
|
|
70
|
+
This is the single definition of the grammar. `forStore`'s sury schema is derived
|
|
71
|
+
from it rather than hand-rolling a second check, so the constructor and the
|
|
72
|
+
schema validation cannot drift apart.
|
|
73
|
+
*/
|
|
74
|
+
let fromString = (raw: string): result<t, string> =>
|
|
75
|
+
if !String.startsWith(raw, "/") {
|
|
76
|
+
Error(
|
|
77
|
+
`expected an origin-relative storage ref starting with "/", got ${raw->JSON.Encode.string->JSON.stringify}. External URLs and data: URIs are not storage refs.`,
|
|
78
|
+
)
|
|
79
|
+
} else if String.startsWith(raw, "//") {
|
|
80
|
+
Error(`protocol-relative refs are not storage refs: ${raw}`)
|
|
81
|
+
} else {
|
|
82
|
+
let segments = raw->String.slice(~start=1, ~end=String.length(raw))->String.split("/")
|
|
83
|
+
if segments->Array.length < 2 {
|
|
84
|
+
Error(`a storage ref needs a prefix and an object path, got ${raw}`)
|
|
85
|
+
} else if !(segments->Array.every(segmentIsSafe)) {
|
|
86
|
+
Error(`a storage ref may not contain empty or traversal segments, got ${raw}`)
|
|
87
|
+
} else {
|
|
88
|
+
Ok(raw)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
The sury schema for a field holding a ref into a named store.
|
|
94
|
+
|
|
95
|
+
Prefer the `@storageRef("<store>")` ppx shorthand over writing this by hand.
|
|
96
|
+
Qualify the store as `"<plugin>.<store>"` to point at another plugin's store.
|
|
97
|
+
*/
|
|
98
|
+
let forStore = (~plugin: option<string>=?, ~store: string): S.t<t> =>
|
|
99
|
+
S.string
|
|
100
|
+
->S.refine(s => value =>
|
|
101
|
+
// The empty string is admitted as the "no object" sentinel. The fields this
|
|
102
|
+
// marks are non-optional today, and a producer with nothing to reference —
|
|
103
|
+
// a supplier feed carrying no image, say — already writes `""` to mean
|
|
104
|
+
// absence. Rejecting it here would break a legitimate existing value and
|
|
105
|
+
// force an event-schema change, which this marker exists to avoid: it
|
|
106
|
+
// refines an existing `string` field without altering what is stored.
|
|
107
|
+
//
|
|
108
|
+
// Note this is a strictly weaker guarantee than `fromString`, which stays
|
|
109
|
+
// exact. Making these fields properly optional would let the sentinel go.
|
|
110
|
+
if value !== "" {
|
|
111
|
+
switch fromString(value) {
|
|
112
|
+
| Ok(_) => ()
|
|
113
|
+
| Error(why) => s.fail(why)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
->Semantic.mark(~id=Semantic.Id.storageRef, ~payload=StoredIn({plugin, store}))
|
|
118
|
+
|
|
119
|
+
/** The store a field's schema declares its refs live in, if any. */
|
|
120
|
+
let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
|
|
121
|
+
switch Semantic.get(schema) {
|
|
122
|
+
| Some({payload: StoredIn(target)}) => Some(target)
|
|
123
|
+
| _ => None
|
|
124
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 segmentIsSafe(segment) {
|
|
7
|
+
if (segment !== "" && segment !== ".") {
|
|
8
|
+
return segment !== "..";
|
|
9
|
+
} else {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fromString(raw) {
|
|
15
|
+
if (!raw.startsWith("/")) {
|
|
16
|
+
return {
|
|
17
|
+
TAG: "Error",
|
|
18
|
+
_0: `expected an origin-relative storage ref starting with "/", got ` + JSON.stringify(raw) + `. External URLs and data: URIs are not storage refs.`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (raw.startsWith("//")) {
|
|
22
|
+
return {
|
|
23
|
+
TAG: "Error",
|
|
24
|
+
_0: `protocol-relative refs are not storage refs: ` + raw
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let segments = raw.slice(1, raw.length).split("/");
|
|
28
|
+
if (segments.length < 2) {
|
|
29
|
+
return {
|
|
30
|
+
TAG: "Error",
|
|
31
|
+
_0: `a storage ref needs a prefix and an object path, got ` + raw
|
|
32
|
+
};
|
|
33
|
+
} else if (segments.every(segmentIsSafe)) {
|
|
34
|
+
return {
|
|
35
|
+
TAG: "Ok",
|
|
36
|
+
_0: raw
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
return {
|
|
40
|
+
TAG: "Error",
|
|
41
|
+
_0: `a storage ref may not contain empty or traversal segments, got ` + raw
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function forStore(plugin, store) {
|
|
47
|
+
return Semantic$Reventless.mark(S.refine(S.string, s => (value => {
|
|
48
|
+
if (value === "") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let why = fromString(value);
|
|
52
|
+
if (why.TAG === "Ok") {
|
|
53
|
+
return;
|
|
54
|
+
} else {
|
|
55
|
+
return s.fail(why._0, undefined);
|
|
56
|
+
}
|
|
57
|
+
})), Semantic$Reventless.Id.storageRef, {
|
|
58
|
+
TAG: "StoredIn",
|
|
59
|
+
_0: {
|
|
60
|
+
plugin: plugin,
|
|
61
|
+
store: store
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getStore(schema) {
|
|
67
|
+
let match = Semantic$Reventless.get(schema);
|
|
68
|
+
if (match === undefined) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let target = match.payload;
|
|
72
|
+
if (typeof target !== "object" || target.TAG === "ReferenceTo") {
|
|
73
|
+
return;
|
|
74
|
+
} else {
|
|
75
|
+
return target._0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
segmentIsSafe,
|
|
81
|
+
fromString,
|
|
82
|
+
forStore,
|
|
83
|
+
getStore,
|
|
84
|
+
}
|
|
85
|
+
/* S Not a pure module */
|
package/src/types/DateTime.res
CHANGED
|
@@ -21,13 +21,9 @@ type state = {
|
|
|
21
21
|
```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
/** Internal sury metadata ID used to mark a date-time string field. */
|
|
25
|
-
let dateTimeId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(~namespace="reventless", ~name="dateTime")
|
|
26
|
-
|
|
27
24
|
/** A sury string schema annotated as an ISO-8601 date-time field.
|
|
28
25
|
Use with `@s.matches(Reventless.DateTime.string)`. */
|
|
29
|
-
let string: S.t<string> = S.string->
|
|
26
|
+
let string: S.t<string> = S.string->Semantic.mark(~id=Semantic.Id.dateTime)
|
|
30
27
|
|
|
31
28
|
/** Whether a field schema carries the date-time marker. */
|
|
32
|
-
let isDateTime = (fieldSchema: S.t<unknown>) =>
|
|
33
|
-
S.Metadata.get(fieldSchema, ~id=dateTimeId)->Option.isSome
|
|
29
|
+
let isDateTime = (fieldSchema: S.t<unknown>) => fieldSchema->Semantic.has(~id=Semantic.Id.dateTime)
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
-
import * as
|
|
4
|
+
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
5
5
|
|
|
6
|
-
let
|
|
7
|
-
|
|
8
|
-
let string = S.Metadata.set(S.string, dateTimeId, true);
|
|
6
|
+
let string = Semantic$Reventless.mark(S.string, Semantic$Reventless.Id.dateTime, undefined);
|
|
9
7
|
|
|
10
8
|
function isDateTime(fieldSchema) {
|
|
11
|
-
return
|
|
9
|
+
return Semantic$Reventless.has(fieldSchema, Semantic$Reventless.Id.dateTime);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export {
|
|
15
|
-
dateTimeId,
|
|
16
13
|
string,
|
|
17
14
|
isDateTime,
|
|
18
15
|
}
|
|
19
|
-
/*
|
|
16
|
+
/* string Not a pure module */
|