@reventlessdev/reventless-spec 3.0.0-alpha.103 → 3.0.0-alpha.105

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,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.105 (2026-08-09)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-spec
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.104 (2026-08-09)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **spec:** see payload-less variants inside a mixed union, and say what differs ([e82e742](https://github.com/ReventlessDev/reventless-core/commit/e82e742201941f978bfde809eeb2581fdda453c5))
19
+
20
+
6
21
  # 3.0.0-alpha.103 (2026-08-09)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.103",
3
+ "version": "3.0.0-alpha.105",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "sury": "11.0.0-alpha.4",
22
22
  "sury-ppx": "11.0.0-alpha.2",
23
23
  "yaml": "^2.8.3",
24
- "@reventlessdev/rescript-node": "2.0.0-alpha.2"
24
+ "@reventlessdev/rescript-node": "2.0.0-alpha.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "rescript": "12.3.0",
@@ -26,22 +26,28 @@ let extractVariantInfo = (variantSchema: S.t<unknown>): option<variantInfo> =>
26
26
  )
27
27
  {tagName, fields: properties, taggedFields}
28
28
  })
29
+ // Payload-less variant — sury compiles a constructor with no payload to
30
+ // String({const}), whether it stands alone (`type t = Foo`) or sits beside
31
+ // payload-carrying constructors in one union (`Foo | Bar(string)`). Matching it
32
+ // here rather than only at the top level is what makes it visible inside a
33
+ // mixed union: the Union branch below filters members through this function, so
34
+ // while this case lived only in `extractAllVariants` every payload-less arm of a
35
+ // mixed union was silently dropped — `NoPriorType | PriorType(string)` extracted
36
+ // as just `[PriorType]`, and a tag-set comparison could not tell it apart from a
37
+ // union that genuinely has only that one arm.
38
+ | String({const: ?Some(name)}) => Some({tagName: name, fields: Dict.make(), taggedFields: []})
29
39
  | _ => None
30
40
  }
31
41
 
32
- // Extract all variant infos from a schema (handles Union and single Object)
42
+ // Extract all variant infos from a schema (handles Union and single variants)
33
43
  let extractAllVariants = (schema: S.t<unknown>): array<variantInfo> =>
34
44
  switch schema {
35
45
  | Union({anyOf}) => anyOf->Array.filterMap(extractVariantInfo)
36
- | Object(_) =>
46
+ | _ =>
37
47
  switch extractVariantInfo(schema) {
38
48
  | Some(info) => [info]
39
49
  | None => []
40
50
  }
41
- | String({const: ?Some(name)}) =>
42
- // Single payload-less variant — sury compiles `type t = Foo` to String({const: "Foo"})
43
- [{tagName: name, fields: Dict.make(), taggedFields: []}]
44
- | _ => []
45
51
  }
46
52
 
47
53
  // Check if a variant has no payload fields (only TAG)
@@ -105,6 +111,31 @@ let schemaTypeName = (schema: S.t<unknown>): string =>
105
111
  | _ => "unknown"
106
112
  }
107
113
 
114
+ // One level deeper than `schemaTypeName`, for the mismatch messages.
115
+ //
116
+ // `schemaTypeName` alone makes an incompatibility unreadable whenever both sides
117
+ // share a top-level kind: `schemasAreCompatible` rejects two unions on their
118
+ // variant tags or a variant's payload, but both stringify to "union", so the
119
+ // message read "consumes X as union but producer declares it as union" and named
120
+ // nothing a reader could act on. Listing the members turns that into the actual
121
+ // difference — e.g. `union[string | unknown]` against
122
+ // `union[NoPriorType | PriorType]`, which names the drift outright.
123
+ //
124
+ // A union of *tagged* variants is described by its tag names; one whose members
125
+ // carry no TAG (what `option<t>` compiles to) falls back to the member kinds,
126
+ // since that is all there is to say about it.
127
+ let describeSchema = (schema: S.t<unknown>): string =>
128
+ switch schema {
129
+ | Union({anyOf}) =>
130
+ let tags = extractAllVariants(schema)->Array.map(v => v.tagName)
131
+ let members = tags->Array.length > 0 ? tags : anyOf->Array.map(schemaTypeName)
132
+ `union[${members->Array.join(" | ")}]`
133
+ | Object({properties}) =>
134
+ `object{${properties->Dict.keysToArray->Array.toSorted(String.compare)->Array.join(", ")}}`
135
+ | Array({additionalItems: Schema(item)}) => `array<${schemaTypeName(item)}>`
136
+ | _ => schemaTypeName(schema)
137
+ }
138
+
108
139
  type producerEntry = {
109
140
  sliceName: string,
110
141
  variant: variantInfo,
@@ -314,7 +345,7 @@ let validateProducedAndConsumed = (
314
345
  let _ =
315
346
  errors->Array.push({
316
347
  sliceName,
317
- message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' as ${schemaTypeName(consumedFieldSchema)} but producer declares it as ${schemaTypeName(producedFieldSchema)}`,
348
+ message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' as ${describeSchema(consumedFieldSchema)} but producer '${producer.sliceName}' declares it as ${describeSchema(producedFieldSchema)}`,
318
349
  })
319
350
  }
320
351
  }
@@ -7,48 +7,46 @@ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js
7
7
  import * as DcbTag$Reventless from "./DcbTag.res.mjs";
8
8
 
9
9
  function extractVariantInfo(variantSchema) {
10
- if (variantSchema.type !== "object") {
11
- return;
12
- }
13
- let properties = variantSchema.properties;
14
- return Stdlib_Option.map(DcbTag$Reventless.variantTagName(variantSchema.items), tagName => {
15
- let taggedFields = Stdlib_Array.filterMap(Object.entries(properties), param => {
16
- if (DcbTag$Reventless.isTagged(param[1])) {
17
- return param[0];
18
- }
19
- });
20
- return {
21
- tagName: tagName,
22
- fields: properties,
23
- taggedFields: taggedFields
24
- };
25
- });
26
- }
27
-
28
- function extractAllVariants(schema) {
29
- switch (schema.type) {
10
+ switch (variantSchema.type) {
30
11
  case "string" :
31
- let name = schema.const;
12
+ let name = variantSchema.const;
32
13
  if (name !== undefined) {
33
- return [{
34
- tagName: name,
35
- fields: {},
36
- taggedFields: []
37
- }];
14
+ return {
15
+ tagName: name,
16
+ fields: {},
17
+ taggedFields: []
18
+ };
38
19
  } else {
39
- return [];
20
+ return;
40
21
  }
41
22
  case "object" :
42
- let info = extractVariantInfo(schema);
43
- if (info !== undefined) {
44
- return [info];
45
- } else {
46
- return [];
47
- }
48
- case "union" :
49
- return Stdlib_Array.filterMap(schema.anyOf, extractVariantInfo);
23
+ let properties = variantSchema.properties;
24
+ return Stdlib_Option.map(DcbTag$Reventless.variantTagName(variantSchema.items), tagName => {
25
+ let taggedFields = Stdlib_Array.filterMap(Object.entries(properties), param => {
26
+ if (DcbTag$Reventless.isTagged(param[1])) {
27
+ return param[0];
28
+ }
29
+ });
30
+ return {
31
+ tagName: tagName,
32
+ fields: properties,
33
+ taggedFields: taggedFields
34
+ };
35
+ });
50
36
  default:
51
- return [];
37
+ return;
38
+ }
39
+ }
40
+
41
+ function extractAllVariants(schema) {
42
+ if (schema.type === "union") {
43
+ return Stdlib_Array.filterMap(schema.anyOf, extractVariantInfo);
44
+ }
45
+ let info = extractVariantInfo(schema);
46
+ if (info !== undefined) {
47
+ return [info];
48
+ } else {
49
+ return [];
52
50
  }
53
51
  }
54
52
 
@@ -160,6 +158,26 @@ function schemaTypeName(schema) {
160
158
  }
161
159
  }
162
160
 
161
+ function describeSchema(schema) {
162
+ switch (schema.type) {
163
+ case "array" :
164
+ let item = schema.additionalItems;
165
+ if (item === "strip" || item === "strict") {
166
+ return schemaTypeName(schema);
167
+ } else {
168
+ return `array<` + schemaTypeName(item) + `>`;
169
+ }
170
+ case "object" :
171
+ return `object{` + Object.keys(schema.properties).toSorted(Primitive_string.compare).join(", ") + `}`;
172
+ case "union" :
173
+ let tags = extractAllVariants(schema).map(v => v.tagName);
174
+ let members = tags.length !== 0 ? tags : schema.anyOf.map(schemaTypeName);
175
+ return `union[` + members.join(" | ") + `]`;
176
+ default:
177
+ return schemaTypeName(schema);
178
+ }
179
+ }
180
+
163
181
  function dedupeKeys(keys) {
164
182
  let seen = new Set();
165
183
  return keys.filter(k => {
@@ -298,7 +316,7 @@ function validateProducedAndConsumed(produced, consumed) {
298
316
  if (!schemasAreCompatible(consumedFieldSchema, producedFieldSchema)) {
299
317
  errors.push({
300
318
  sliceName: sliceName,
301
- message: `Slice '` + sliceName + `' consumes '` + variant.tagName + `.` + fieldName + `' as ` + schemaTypeName(consumedFieldSchema) + ` but producer declares it as ` + schemaTypeName(producedFieldSchema)
319
+ message: `Slice '` + sliceName + `' consumes '` + variant.tagName + `.` + fieldName + `' as ` + describeSchema(consumedFieldSchema) + ` but producer '` + producer.sliceName + `' declares it as ` + describeSchema(producedFieldSchema)
302
320
  });
303
321
  return;
304
322
  } else {
@@ -414,6 +432,7 @@ export {
414
432
  schemasAreCompatible,
415
433
  propsCompatible,
416
434
  schemaTypeName,
435
+ describeSchema,
417
436
  dedupeKeys,
418
437
  validateCompositeReads,
419
438
  validateProducedAndConsumed,