@reventlessdev/reventless-spec 3.0.0-alpha.105 → 3.0.0-alpha.107
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
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.107 (2026-08-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** collect [@ref](https://github.com/ref) declared on an array field ([6f9e2fe](https://github.com/ReventlessDev/reventless-core/commit/6f9e2fef2568ad57a8bf2efbaa9cd4830a947f26))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.106 (2026-08-09)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **ppx,spec,core:** [@live](https://github.com/live) on state declarations → top-level x-reventless-live schema key ([0f38f38](https://github.com/ReventlessDev/reventless-core/commit/0f38f38dc83c38da2fde6615b89a98cd7fda6fed))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.105 (2026-08-09)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
package/package.json
CHANGED
|
@@ -46,6 +46,35 @@ let getTarget = (schema: S.t<unknown>): option<target> =>
|
|
|
46
46
|
| _ => None
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
The entity a *field* references, wherever inside the field's type the marker sits.
|
|
51
|
+
|
|
52
|
+
`to_` returns an element schema — a `S.t<string>` — so on `@ref("E") ids:
|
|
53
|
+
array<string>` the ppx annotates the `string` *inside* the array and the field's
|
|
54
|
+
own schema carries nothing. `getTarget` answers `None` there, which is not the
|
|
55
|
+
same as the field declaring no reference: a consumer that finds no declared
|
|
56
|
+
reference falls back to a naming heuristic and resolves the field to whatever
|
|
57
|
+
entity the name suggests, so a dropped `@ref` is a *different* resolution rather
|
|
58
|
+
than a missing one. Any walk collecting a command's or event's references must
|
|
59
|
+
ask this question, not `getTarget`.
|
|
60
|
+
|
|
61
|
+
Only wrappers around the field's own value are followed — the optional union and
|
|
62
|
+
the array element, to any depth. Object properties are not: a reference declared
|
|
63
|
+
on a nested record's field belongs to that field, and attributing it to the
|
|
64
|
+
enclosing one would name the wrong field.
|
|
65
|
+
*/
|
|
66
|
+
let rec getFieldTarget = (schema: S.t<unknown>): option<target> =>
|
|
67
|
+
switch getTarget(schema) {
|
|
68
|
+
| Some(_) as found => found
|
|
69
|
+
| None =>
|
|
70
|
+
// `getTarget` already reads through the optional wrapper; the *shape* inside
|
|
71
|
+
// it still has to be unwrapped here to reach an optional array's element.
|
|
72
|
+
switch schema->Semantic.unwrapOptional->Option.getOr(schema) {
|
|
73
|
+
| Array({additionalItems: Schema(item)}) => getFieldTarget(item)
|
|
74
|
+
| _ => None
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/**
|
|
50
79
|
Like `to_` but does not imply DCB tag semantics.
|
|
51
80
|
Use with `@ref("Entity") @noDcbTag` when the field references another entity
|
|
@@ -1,6 +1,7 @@
|
|
|
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 Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as DcbTag$Reventless from "./DcbTag.res.mjs";
|
|
5
6
|
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
6
7
|
|
|
@@ -32,6 +33,26 @@ function getTarget(schema) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function getFieldTarget(_schema) {
|
|
37
|
+
while (true) {
|
|
38
|
+
let schema = _schema;
|
|
39
|
+
let found = getTarget(schema);
|
|
40
|
+
if (found !== undefined) {
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
let match = Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(schema), schema);
|
|
44
|
+
if (match.type !== "array") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let item = match.additionalItems;
|
|
48
|
+
if (item === "strip" || item === "strict") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
_schema = item;
|
|
52
|
+
continue;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
function toWithoutDcbTag(plugin, entity) {
|
|
36
57
|
return Semantic$Reventless.mark(S.string, Semantic$Reventless.Id.reference, {
|
|
37
58
|
TAG: "ReferenceTo",
|
|
@@ -45,6 +66,7 @@ function toWithoutDcbTag(plugin, entity) {
|
|
|
45
66
|
export {
|
|
46
67
|
to_,
|
|
47
68
|
getTarget,
|
|
69
|
+
getFieldTarget,
|
|
48
70
|
toWithoutDcbTag,
|
|
49
71
|
}
|
|
50
72
|
/* S Not a pure module */
|
|
@@ -5,7 +5,8 @@ Spec describing the structural annotations declared on the fields of an
|
|
|
5
5
|
`@compositeId`, `@subId`, `@compositeSubId`, `@index`), a visibility
|
|
6
6
|
annotation (`@hidden`, `@summary`), a hierarchical-rendering annotation
|
|
7
7
|
(`@drillTarget`, `@collapsed`), a server-query opt-in annotation
|
|
8
|
-
(`@scan`, `@scanSort`),
|
|
8
|
+
(`@scan`, `@scanSort`), a UI-list annotation (`@status`, `@groupBy`), or a
|
|
9
|
+
type-level live-updates annotation (`@live` on the `state` declaration).
|
|
9
10
|
Downstream consumers (UI, MCP, codegen) read the
|
|
10
11
|
spec to surface field roles in JSON Schema as `x-reventless-*` extension
|
|
11
12
|
properties.
|
|
@@ -86,6 +87,15 @@ type stateAnnotationSpec = {
|
|
|
86
87
|
the default case is omitted to keep schemas compact.
|
|
87
88
|
*/
|
|
88
89
|
visibility: option<string>,
|
|
90
|
+
/**
|
|
91
|
+
Component-level live-updates hint from `@live(true | false)` on the
|
|
92
|
+
`@schema type state` declaration (PPX-emitted). `SuryToJsonSchema.deriveObjectSchema`
|
|
93
|
+
emits top-level `x-reventless-live: bool` when present; absent annotation ⇒
|
|
94
|
+
key absent ⇒ the consumer's own default applies. The framework only
|
|
95
|
+
transports the declaration — UI consumers decide whether a live-updates
|
|
96
|
+
control is offered (`true`) or hidden (`false`) for the view.
|
|
97
|
+
*/
|
|
98
|
+
live: option<bool>,
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
/** Sury metadata ID used to attach a `stateAnnotationSpec` to a state schema. */
|
|
@@ -117,39 +117,49 @@ let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.
|
|
|
117
117
|
let showString = (raw: string): string => raw->JSON.Encode.string->JSON.stringify
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
The
|
|
120
|
+
The schema an optional field's wrapper stands for, if it is one.
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
uncollected, the branded scalar loses its brand. Every reader converges here, so
|
|
128
|
-
following the wrapper once here is what keeps "optional" a statement about
|
|
129
|
-
presence rather than a way to lose the field's type.
|
|
122
|
+
sury-ppx compiles `f?: X` to a union of `X`'s schema with `Undefined`/`Null`, and
|
|
123
|
+
that wrapper is a new schema carrying no metadata of its own. Every reader that
|
|
124
|
+
looks *through* a field — for its semantic, or for the element type inside it —
|
|
125
|
+
needs the same one-level unwrap, so it lives here once rather than once per
|
|
126
|
+
reader.
|
|
130
127
|
|
|
131
|
-
The outer schema is read first, so a marker set on the wrapper itself still wins.
|
|
132
128
|
Only a union with exactly one non-null variant is followed: that is the shape an
|
|
133
129
|
optional field has, and a genuine multi-variant union has no single inner schema
|
|
134
|
-
|
|
130
|
+
that could stand for the whole.
|
|
135
131
|
*/
|
|
136
|
-
let
|
|
137
|
-
switch
|
|
138
|
-
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
switch v {
|
|
144
|
-
| Null(_) | Undefined(_) => false
|
|
145
|
-
| _ => true
|
|
146
|
-
}
|
|
147
|
-
) {
|
|
148
|
-
| [inner] => getFrom(inner)
|
|
149
|
-
| _ => None
|
|
132
|
+
let unwrapOptional = (schema: S.t<unknown>): option<S.t<unknown>> =>
|
|
133
|
+
switch schema {
|
|
134
|
+
| Union({anyOf}) =>
|
|
135
|
+
switch anyOf->Array.filter(v =>
|
|
136
|
+
switch v {
|
|
137
|
+
| Null(_) | Undefined(_) => false
|
|
138
|
+
| _ => true
|
|
150
139
|
}
|
|
140
|
+
) {
|
|
141
|
+
| [inner] => Some(inner)
|
|
151
142
|
| _ => None
|
|
152
143
|
}
|
|
144
|
+
| _ => None
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
The semantic a field's schema carries, if any.
|
|
149
|
+
|
|
150
|
+
An **optional** field keeps its marker one level down, inside the wrapper
|
|
151
|
+
`unwrapOptional` describes. So a walk that reads only the outer schema sees
|
|
152
|
+
`imageUrl?: string` as carrying no semantic at all — the store goes undeclared,
|
|
153
|
+
the reference goes uncollected, the branded scalar loses its brand. Every reader
|
|
154
|
+
converges here, so following the wrapper once here is what keeps "optional" a
|
|
155
|
+
statement about presence rather than a way to lose the field's type.
|
|
156
|
+
|
|
157
|
+
The outer schema is read first, so a marker set on the wrapper itself still wins.
|
|
158
|
+
*/
|
|
159
|
+
let rec getFrom = (schema: S.t<unknown>): option<t> =>
|
|
160
|
+
switch S.Metadata.get(schema, ~id=semanticId) {
|
|
161
|
+
| Some(_) as found => found
|
|
162
|
+
| None => schema->unwrapOptional->Option.flatMap(getFrom)
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
let get = (fieldSchema: S.t<'a>): option<t> => fieldSchema->S.castToUnknown->getFrom
|
|
@@ -1,6 +1,7 @@
|
|
|
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 Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
|
|
5
6
|
let Id = {
|
|
6
7
|
dateTime: "dateTime",
|
|
@@ -44,31 +45,33 @@ function showString(raw) {
|
|
|
44
45
|
return JSON.stringify(raw);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
switch (v.type) {
|
|
59
|
-
case "null" :
|
|
60
|
-
case "undefined" :
|
|
61
|
-
return false;
|
|
62
|
-
default:
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
if (match.length !== 1) {
|
|
67
|
-
return;
|
|
48
|
+
function unwrapOptional(schema) {
|
|
49
|
+
if (schema.type !== "union") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let match = schema.anyOf.filter(v => {
|
|
53
|
+
switch (v.type) {
|
|
54
|
+
case "null" :
|
|
55
|
+
case "undefined" :
|
|
56
|
+
return false;
|
|
57
|
+
default:
|
|
58
|
+
return true;
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
});
|
|
61
|
+
if (match.length !== 1) {
|
|
62
|
+
return;
|
|
63
|
+
} else {
|
|
64
|
+
return match[0];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getFrom(schema) {
|
|
69
|
+
let found = S.Metadata.get(schema, semanticId);
|
|
70
|
+
if (found !== undefined) {
|
|
71
|
+
return found;
|
|
72
|
+
} else {
|
|
73
|
+
return Stdlib_Option.flatMap(unwrapOptional(schema), getFrom);
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
let get = getFrom;
|
|
@@ -88,6 +91,7 @@ export {
|
|
|
88
91
|
mark,
|
|
89
92
|
refined,
|
|
90
93
|
showString,
|
|
94
|
+
unwrapOptional,
|
|
91
95
|
getFrom,
|
|
92
96
|
get,
|
|
93
97
|
has,
|