@builder-builder/builder 0.0.13 → 0.0.15

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.
Files changed (96) hide show
  1. package/dist/bb.d.ts +3 -1
  2. package/dist/bb.js +6 -1
  3. package/dist/check.d.ts +2 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +53 -0
  6. package/dist/codegen/index.d.ts +7 -0
  7. package/dist/codegen/index.js +212 -0
  8. package/dist/codegen/template.d.ts +5 -0
  9. package/dist/codegen/template.js +17 -0
  10. package/dist/entities/builder/builder.d.ts +11 -1
  11. package/dist/entities/builder/builder.js +22 -6
  12. package/dist/entities/collection/collection.d.ts +7 -1
  13. package/dist/entities/collection/collection.js +9 -2
  14. package/dist/entities/collection/config.d.ts +6 -1
  15. package/dist/entities/collection/config.js +9 -2
  16. package/dist/entities/component/component.d.ts +41 -1
  17. package/dist/entities/component/component.js +9 -2
  18. package/dist/entities/component/details.d.ts +11 -2
  19. package/dist/entities/component/details.js +9 -2
  20. package/dist/entities/component/field.d.ts +10 -1
  21. package/dist/entities/component/field.js +13 -3
  22. package/dist/entities/index.d.ts +7 -3
  23. package/dist/entities/index.js +3 -1
  24. package/dist/entities/kind.d.ts +1 -1
  25. package/dist/entities/model/model.d.ts +5 -1
  26. package/dist/entities/model/model.js +10 -3
  27. package/dist/entities/model/models.js +9 -5
  28. package/dist/entities/option/option.d.ts +41 -1
  29. package/dist/entities/option/option.js +9 -2
  30. package/dist/entities/option/select.d.ts +7 -1
  31. package/dist/entities/option/select.js +17 -6
  32. package/dist/entities/option/toggle.d.ts +7 -1
  33. package/dist/entities/option/toggle.js +16 -4
  34. package/dist/entities/option/values.d.ts +6 -0
  35. package/dist/entities/pricing.d.ts +64 -0
  36. package/dist/entities/pricing.js +48 -0
  37. package/dist/entities/serialise.d.ts +632 -8
  38. package/dist/entities/serialise.js +33 -12
  39. package/dist/entities/tags.d.ts +3 -0
  40. package/dist/entities/tags.js +2 -0
  41. package/dist/entities/ui/describe.d.ts +231 -8
  42. package/dist/entities/ui/describe.js +12 -5
  43. package/dist/entities/ui/index.d.ts +2 -0
  44. package/dist/entities/ui/index.js +1 -0
  45. package/dist/entities/ui/input.d.ts +343 -0
  46. package/dist/entities/ui/input.js +51 -0
  47. package/dist/entities/ui/page.d.ts +231 -8
  48. package/dist/entities/ui/page.js +12 -5
  49. package/dist/entities/ui/pages.d.ts +5 -1
  50. package/dist/entities/ui/pages.js +9 -2
  51. package/dist/entities/ui/ui.d.ts +9 -4
  52. package/dist/entities/ui/ui.js +29 -11
  53. package/dist/entities/validated.d.ts +7 -3
  54. package/dist/exception.d.ts +2 -2
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +1 -1
  57. package/dist/instance.d.ts +9 -0
  58. package/dist/instance.js +3 -1
  59. package/dist/mappers/index.d.ts +6 -4
  60. package/dist/mappers/index.js +3 -2
  61. package/dist/mappers/pricing.d.ts +3 -0
  62. package/dist/mappers/pricing.js +101 -0
  63. package/dist/mappers/render/index.d.ts +1 -1
  64. package/dist/mappers/render/pages.d.ts +8 -0
  65. package/dist/mappers/render/pages.js +2 -1
  66. package/dist/mappers/render/render.js +19 -3
  67. package/dist/mappers/resolve.d.ts +4 -4
  68. package/dist/mappers/variants/index.d.ts +2 -1
  69. package/dist/mappers/variants/index.js +2 -1
  70. package/dist/mappers/variants/variants.d.ts +5 -2
  71. package/dist/mappers/variants/variants.js +2 -2
  72. package/dist/validate/brand.d.ts +0 -7
  73. package/dist/validate/brand.js +5 -5
  74. package/dist/validate/builder.d.ts +2 -1
  75. package/dist/validate/builder.js +19 -13
  76. package/dist/validate/errors.d.ts +138 -0
  77. package/dist/validate/errors.js +148 -0
  78. package/dist/validate/expectations.d.ts +3 -10
  79. package/dist/validate/expectations.js +8 -10
  80. package/dist/validate/index.d.ts +10 -14
  81. package/dist/validate/index.js +5 -7
  82. package/dist/validate/instance.d.ts +2 -15
  83. package/dist/validate/instance.js +41 -40
  84. package/dist/validate/model.d.ts +4 -31
  85. package/dist/validate/model.js +157 -176
  86. package/dist/validate/pricing.d.ts +6 -0
  87. package/dist/validate/pricing.js +71 -0
  88. package/dist/validate/resolve.d.ts +3 -15
  89. package/dist/validate/resolve.js +66 -69
  90. package/dist/validate/result.d.ts +1 -7
  91. package/dist/validate/result.js +1 -4
  92. package/dist/validate/ui.d.ts +4 -4
  93. package/dist/validate/ui.js +80 -62
  94. package/dist/validate/variants.d.ts +2 -53
  95. package/dist/validate/variants.js +83 -86
  96. package/package.json +12 -3
@@ -1,223 +1,204 @@
1
1
  import { check } from '../check.js';
2
2
  import { BuilderCollectionConfigSerialisedSchema, BuilderModelSerialisedSchema, BuilderWhenSerialisedSchema, model, modelsMerge, serialise } from '../entities/index.js';
3
- import { builderError } from '../exception.js';
4
3
  import { validate } from './brand.js';
4
+ import { BuilderValidateErrors } from './errors.js';
5
5
  import { checkExpectations } from './expectations.js';
6
6
  import { resolver } from './resolve.js';
7
- import { builderErrorInvalidInput } from './result.js';
8
- export function builderErrorDuplicateName(name, location = []) {
9
- return { ...builderError('duplicate-name', location), name };
10
- }
11
- export function builderErrorInvalidCollectionBounds(name, min, max, location = []) {
12
- return { ...builderError('invalid-collection-bounds', location), name, min, max };
13
- }
14
- export function builderErrorInvalidPath(reason, location = []) {
15
- return { ...builderError('invalid-path', location), reason };
16
- }
17
- export function builderErrorInvalidSelectMapKey(key, location = []) {
18
- return { ...builderError('invalid-select-map-key', location), key };
19
- }
20
7
  const EMPTY_MODEL = validate(serialise.model(model()));
21
- export function validateModel(input, references = []) {
8
+ export function validateModel(input, references = [], errors = new BuilderValidateErrors()) {
22
9
  if (!check.is(BuilderModelSerialisedSchema, input)) {
23
- return [EMPTY_MODEL, [builderErrorInvalidInput('model')]];
10
+ errors.invalidInput('model');
11
+ return [EMPTY_MODEL, errors.errors];
24
12
  }
25
- const resolve = resolver(references);
26
- const [structure, structureErrors] = validateModelStructure(input, resolve, []);
13
+ const resolve = resolver(errors, references);
14
+ const structure = validateModelStructure(input, resolve, errors);
27
15
  const data = validate(modelsMerge(structure));
28
- const expectationErrors = checkModelExpectations(data, structure, []);
29
- return [data, [...expectationErrors, ...structureErrors]];
16
+ checkModelExpectations(data, structure, errors);
17
+ return [data, errors.errors];
30
18
  }
31
- export function checkModelExpectations(mergedModel, model, location) {
32
- return [
33
- ...checkExpectations(mergedModel, model.expectations, [...location, 'expectations']),
34
- ...model.models.flatMap((nested, nestedIndex) => checkModelExpectations(mergedModel, nested, [
35
- ...location,
36
- 'models',
37
- nestedIndex
38
- ]))
39
- ];
19
+ export function checkModelExpectations(mergedModel, rootModel, errors) {
20
+ recurse(rootModel);
21
+ function recurse(node) {
22
+ errors.scope('expectations', () => {
23
+ checkExpectations(mergedModel, node.expectations, errors);
24
+ });
25
+ errors.scope('models', () => {
26
+ node.models.forEach((nested, nestedIndex) => {
27
+ errors.scope(nestedIndex, () => {
28
+ recurse(nested);
29
+ });
30
+ });
31
+ });
32
+ }
40
33
  }
41
- export function validateModelStructure(input, resolve, location) {
42
- const [resolved, resolveErrors] = resolve(input, location);
34
+ export function validateModelStructure(input, resolve, errors) {
35
+ const resolved = resolve(input);
43
36
  if (!check.is(BuilderModelSerialisedSchema, resolved)) {
44
- return [validate(serialise.model(model())), resolveErrors];
37
+ return validate(serialise.model(model()));
45
38
  }
46
- const errors = [...resolveErrors];
47
39
  const modelInput = resolved;
48
- const childModels = modelInput.models.flatMap((part, partIndex) => {
49
- const partLocation = [...location, 'models', partIndex];
50
- const [child, childErrors] = validateModelStructure(part, resolve, partLocation);
51
- errors.push(...childErrors);
52
- return [child];
53
- });
54
- const [options, optionErrors] = walkEntries(modelInput.options, 'option', resolve, location);
55
- const [components, componentErrors] = walkEntries(modelInput.components, 'component', resolve, location);
56
- const [collections, collectionErrors] = walkEntries(modelInput.collections, 'collection', resolve, location);
57
- errors.push(...optionErrors, ...componentErrors, ...collectionErrors);
58
40
  const skipPathValidation = modelInput.expectations.length > 0;
59
- const checkPaths = (entry, entryLocation) => (skipPathValidation ? [] : checkEntryPaths(modelInput, entry, entryLocation));
60
- options.forEach((entry, entryIndex) => errors.push(...checkPaths(entry, [...location, 'options', entryIndex])));
61
- components.forEach((entry, entryIndex) => errors.push(...checkPaths(entry, [...location, 'components', entryIndex])));
62
- const resolvedCollections = collections.map((entry, entryIndex) => {
63
- const collectionLocation = [...location, 'collections', entryIndex];
64
- errors.push(...checkPaths(entry, collectionLocation), ...checkCollectionBounds(entry, collectionLocation));
65
- const [resolvedEntry, entryErrors] = resolveCollectionInnerModels(entry, resolve, [
66
- ...collectionLocation,
67
- 'payload'
68
- ]);
69
- errors.push(...entryErrors);
70
- return resolvedEntry;
71
- });
72
- const data = {
41
+ const childModels = errors.scope('models', () => modelInput.models.flatMap((part, partIndex) => errors.scope(partIndex, () => [validateModelStructure(part, resolve, errors)])));
42
+ const options = walkEntries(modelInput.options, 'option');
43
+ const components = walkEntries(modelInput.components, 'component');
44
+ const collections = walkCollections();
45
+ return {
73
46
  ...modelInput,
74
47
  models: childModels,
75
48
  options,
76
49
  components,
77
- collections: resolvedCollections
50
+ collections
78
51
  };
79
- return [data, errors];
80
- }
81
- function resolveCollectionInnerModels(entry, resolve, payloadLocation) {
82
- const errors = [];
83
- const newPayload = mapCollectionConfigs(entry.payload, payloadLocation, (config, configLocation) => {
84
- if (!check.is(BuilderModelSerialisedSchema, config.model)) {
85
- return config;
86
- }
87
- const [validated, modelErrors] = validateModelStructure(config.model, resolve, [...configLocation, 'model']);
88
- errors.push(...modelErrors);
89
- return { ...config, model: validated };
90
- });
91
- return [{ ...entry, payload: newPayload }, errors];
92
- }
93
- function mapCollectionConfigs(payload, location, visit) {
94
- if (check.is(BuilderCollectionConfigSerialisedSchema, payload)) {
95
- return visit(payload, location);
52
+ function walkEntries(entries, entryKind) {
53
+ const seen = new Set();
54
+ const items = [];
55
+ errors.scope(`${entryKind}s`, () => {
56
+ entries.forEach((entry, entryIndex) => {
57
+ errors.scope(entryIndex, () => {
58
+ const resolved = resolveEntry(entry, seen);
59
+ if (resolved != null) {
60
+ items.push(resolved);
61
+ }
62
+ });
63
+ });
64
+ });
65
+ return items;
96
66
  }
97
- if (!check.is(BuilderWhenSerialisedSchema, payload)) {
98
- return payload;
99
- }
100
- const when = payload;
101
- if (when.type === 'match') {
102
- const selectMap = when.selectMap;
103
- const newSelectMap = Object.fromEntries(Object.entries(selectMap).map(([key, value]) => [
104
- key,
105
- mapCollectionConfigs(value, [...location, 'selectMap', key], visit)
106
- ]));
107
- return { ...when, selectMap: newSelectMap };
67
+ function walkCollections() {
68
+ const seen = new Set();
69
+ const items = [];
70
+ errors.scope('collections', () => {
71
+ modelInput.collections.forEach((entry, entryIndex) => {
72
+ errors.scope(entryIndex, () => {
73
+ const resolved = resolveEntry(entry, seen);
74
+ if (resolved == null) {
75
+ return;
76
+ }
77
+ checkCollectionBounds(resolved);
78
+ items.push(resolved);
79
+ });
80
+ });
81
+ });
82
+ return items;
108
83
  }
109
- return { ...when, payload: mapCollectionConfigs(when.payload, [...location, 'payload'], visit) };
110
- }
111
- function walkEntries(entries, entryKind, resolve, location) {
112
- const seen = new Set();
113
- const items = [];
114
- const errors = [];
115
- const arrayKey = `${entryKind}s`;
116
- entries.forEach((entry, entryIndex) => {
117
- const entryLocation = [...location, arrayKey, entryIndex];
84
+ function resolveEntry(entry, seen) {
118
85
  let paths = entry.paths;
119
86
  if (entry.paths != null) {
120
- const [resolvedPaths, pathErrors] = resolve(entry.paths, [...entryLocation, 'paths']);
87
+ const resolvedPaths = errors.scope('paths', () => resolve(entry.paths));
121
88
  paths = resolvedPaths ?? entry.paths;
122
- errors.push(...pathErrors);
123
89
  }
124
- const [payload, payloadErrors] = resolve(entry.payload, [...entryLocation, 'payload']);
125
- errors.push(...payloadErrors);
126
- const resolved = { ...entry, paths, payload: payload ?? entry.payload };
127
- if (seen.has(resolved.name)) {
128
- errors.push(builderErrorDuplicateName(resolved.name, entryLocation));
90
+ const payload = errors.scope('payload', () => resolve(entry.payload));
91
+ const resolvedEntry = { ...entry, paths, payload: payload ?? entry.payload };
92
+ if (seen.has(resolvedEntry.name)) {
93
+ errors.duplicateName(resolvedEntry.name);
94
+ return null;
95
+ }
96
+ seen.add(resolvedEntry.name);
97
+ checkPaths(resolvedEntry);
98
+ return resolvedEntry;
99
+ }
100
+ function checkPaths(entry) {
101
+ if (skipPathValidation) {
129
102
  return;
130
103
  }
131
- seen.add(resolved.name);
132
- items.push(resolved);
133
- });
134
- return [items, errors];
135
- }
136
- function checkEntryPaths(modelInput, entry, location) {
137
- const { name, paths, payload } = entry;
138
- const directPaths = Array.isArray(paths)
139
- ? paths.map((path, pathIndex) => [path, [...location, 'paths', pathIndex]])
140
- : [];
141
- const extractWhenPathsList = extractWhenPaths(payload, location);
142
- return [...directPaths, ...extractWhenPathsList].flatMap(([path, pathLocation]) => {
143
- const [reached, walkErrors] = walkPath([modelInput], path.slice(0, -1), name, path, pathLocation);
144
- if (walkErrors.length > 0) {
145
- return walkErrors;
104
+ const { paths, payload } = entry;
105
+ if (Array.isArray(paths)) {
106
+ errors.scope('paths', () => {
107
+ paths.forEach((path, pathIndex) => {
108
+ errors.scope(pathIndex, () => {
109
+ checkPath(path);
110
+ });
111
+ });
112
+ });
113
+ }
114
+ checkWhenPaths(payload);
115
+ }
116
+ function checkPath(path) {
117
+ const reached = walkPath([modelInput], path.slice(0, -1));
118
+ if (reached == null) {
119
+ return;
146
120
  }
147
121
  const optionName = path.at(-1);
148
122
  if (!reached.some((candidate) => candidate.options.some((option) => option.name === optionName))) {
149
- return [builderErrorInvalidPath('option-not-found', pathLocation)];
123
+ errors.invalidPath('option-not-found');
150
124
  }
151
- return [];
152
- });
153
- }
154
- function checkCollectionBounds(collection, location) {
155
- return collectionConfigs(collection, location).flatMap(([{ min, max }]) => {
156
- if (typeof min !== 'number' || typeof max !== 'number') {
157
- return [];
125
+ }
126
+ function checkWhenPaths(payload) {
127
+ if (!check.is(BuilderWhenSerialisedSchema, payload)) {
128
+ return;
158
129
  }
159
- if (min < 0 || max <= 0 || max < min) {
160
- return [builderErrorInvalidCollectionBounds(collection.name, min, max, location)];
130
+ if (payload.type === 'match' && Array.isArray(payload.matchPath)) {
131
+ errors.scope('payload', () => {
132
+ errors.scope('matchPath', () => {
133
+ checkPath(payload.matchPath);
134
+ });
135
+ });
136
+ return;
161
137
  }
162
- return [];
163
- });
138
+ if (payload.type === 'unless' && Array.isArray(payload.unlessPath)) {
139
+ errors.scope('payload', () => {
140
+ errors.scope('unlessPath', () => {
141
+ checkPath(payload.unlessPath);
142
+ });
143
+ });
144
+ }
145
+ }
146
+ function walkPath(candidates, remaining) {
147
+ if (remaining.length === 0) {
148
+ return candidates;
149
+ }
150
+ const [collectionName, index, ...rest] = remaining;
151
+ if (typeof collectionName !== 'string' || typeof index !== 'number') {
152
+ errors.invalidPath('shape');
153
+ return null;
154
+ }
155
+ const matchedConfigs = candidates.flatMap((current) => {
156
+ const collection = current.collections.find((entry) => entry.name === collectionName);
157
+ return collection == null ? [] : [collectionConfigs(collection)];
158
+ });
159
+ if (matchedConfigs.length === 0) {
160
+ errors.invalidPath('missing-collection');
161
+ return null;
162
+ }
163
+ const anyOutOfBounds = matchedConfigs.some((configs) => configs.length > 0 && configs.every(({ max }) => typeof max === 'number' && index >= max));
164
+ if (anyOutOfBounds) {
165
+ errors.invalidPath('out-of-bounds');
166
+ return null;
167
+ }
168
+ const next = matchedConfigs.flatMap((configs) => configs
169
+ .filter(({ max }) => typeof max !== 'number' || index < max)
170
+ .flatMap(({ model: configModel }) => check.is(BuilderModelSerialisedSchema, configModel) ? [configModel] : []));
171
+ if (next.length === 0) {
172
+ errors.invalidPath('missing-collection');
173
+ return null;
174
+ }
175
+ return walkPath(next, rest);
176
+ }
177
+ function checkCollectionBounds(collection) {
178
+ collectionConfigs(collection).forEach(({ min, max }) => {
179
+ if (typeof min !== 'number' || typeof max !== 'number') {
180
+ return;
181
+ }
182
+ if (min < 0 || max <= 0 || max < min) {
183
+ errors.invalidCollectionBounds(collection.name, min, max);
184
+ }
185
+ });
186
+ }
164
187
  }
165
- function collectionConfigs(collection, location) {
188
+ function collectionConfigs(collection) {
166
189
  const { payload } = collection;
167
- const payloadLocation = [...location, 'payload'];
168
190
  if (check.is(BuilderCollectionConfigSerialisedSchema, payload)) {
169
- return [[payload, payloadLocation]];
191
+ return [payload];
170
192
  }
171
193
  if (!check.is(BuilderWhenSerialisedSchema, payload)) {
172
194
  return [];
173
195
  }
174
196
  if (payload.type === 'match') {
175
- return Object.entries(payload.selectMap).flatMap(([key, value]) => check.is(BuilderCollectionConfigSerialisedSchema, value)
176
- ? [[value, [...payloadLocation, 'selectMap', key]]]
197
+ return Object.entries(payload.selectMap).flatMap(([, value]) => check.is(BuilderCollectionConfigSerialisedSchema, value)
198
+ ? [value]
177
199
  : []);
178
200
  }
179
201
  return check.is(BuilderCollectionConfigSerialisedSchema, payload.payload)
180
- ? [[payload.payload, [...payloadLocation, 'payload']]]
202
+ ? [payload.payload]
181
203
  : [];
182
204
  }
183
- function extractWhenPaths(payload, location) {
184
- if (!check.is(BuilderWhenSerialisedSchema, payload)) {
185
- return [];
186
- }
187
- if (payload.type === 'match' && Array.isArray(payload.matchPath)) {
188
- return [[payload.matchPath, [...location, 'payload', 'matchPath']]];
189
- }
190
- if (payload.type === 'unless' && Array.isArray(payload.unlessPath)) {
191
- return [[payload.unlessPath, [...location, 'payload', 'unlessPath']]];
192
- }
193
- return [];
194
- }
195
- function walkPath(candidates, remaining, entryName, fullPath, location) {
196
- if (remaining.length === 0) {
197
- return [candidates, []];
198
- }
199
- const [collectionName, indexValue, ...rest] = remaining;
200
- if (typeof collectionName !== 'string' || typeof indexValue !== 'number') {
201
- return [[], [builderErrorInvalidPath('shape', location)]];
202
- }
203
- const [next, anyOutOfBounds] = candidates.reduce(([accumulator, anyOutOfBoundsAccumulator], current) => {
204
- const collection = current.collections.find(({ name }) => name === collectionName);
205
- if (collection == null) {
206
- return [accumulator, anyOutOfBoundsAccumulator];
207
- }
208
- const configs = collectionConfigs(collection, []);
209
- const withinBounds = configs.filter(([{ max }]) => typeof max !== 'number' || indexValue < max);
210
- if (withinBounds.length === 0 && configs.length > 0) {
211
- return [accumulator, true];
212
- }
213
- const reached = withinBounds.flatMap(([{ model: configModel }]) => check.is(BuilderModelSerialisedSchema, configModel) ? [configModel] : []);
214
- return [[...accumulator, ...reached], anyOutOfBoundsAccumulator];
215
- }, [[], false]);
216
- if (anyOutOfBounds) {
217
- return [[], [builderErrorInvalidPath('out-of-bounds', location)]];
218
- }
219
- if (next.length === 0) {
220
- return [[], [builderErrorInvalidPath('missing-collection', location)]];
221
- }
222
- return walkPath(next, rest, entryName, fullPath, location);
223
- }
@@ -0,0 +1,6 @@
1
+ import type { BuilderPricingValidated } from '../entities/index';
2
+ import type { ValidationResult } from './result';
3
+ import { BuilderValidateErrors } from './errors.js';
4
+ export type BuilderPricingValidationResult = ValidationResult<BuilderPricingValidated>;
5
+ export declare function validatePricing(input: unknown, errors?: BuilderValidateErrors): BuilderPricingValidationResult;
6
+ export declare function validatePricingStructure(pricingInput: unknown, errors: BuilderValidateErrors): void;
@@ -0,0 +1,71 @@
1
+ import { check } from '../check.js';
2
+ import { BuilderPricingSchema } from '../entities/index.js';
3
+ import { validate } from './brand.js';
4
+ import { BuilderValidateErrors } from './errors.js';
5
+ const EMPTY_PRICING = validate({
6
+ rates: {},
7
+ formula: { kind: 'num', value: 0 }
8
+ });
9
+ export function validatePricing(input, errors = new BuilderValidateErrors()) {
10
+ if (!check.is(BuilderPricingSchema, input)) {
11
+ errors.invalidInput('pricing');
12
+ return [EMPTY_PRICING, errors.errors];
13
+ }
14
+ validatePricingStructure(input, errors);
15
+ return [validate(input), errors.errors];
16
+ }
17
+ export function validatePricingStructure(pricingInput, errors) {
18
+ if (!check.is(BuilderPricingSchema, pricingInput)) {
19
+ return;
20
+ }
21
+ const pricing = pricingInput;
22
+ walkExpr(pricing.formula, false);
23
+ function walkExpr(expression, insideVariants) {
24
+ switch (expression.kind) {
25
+ case 'num':
26
+ return;
27
+ case 'variantPrice':
28
+ if (!insideVariants) {
29
+ errors.invalidPricing('scope', 'variantPrice may only be used inside a variants.expression');
30
+ }
31
+ return;
32
+ case 'rate':
33
+ errors.scope('rate', () => {
34
+ if (pricing.rates[expression.rate] == null) {
35
+ errors.invalidPricing('missing-rate', `rate '${expression.rate}' is not defined`);
36
+ }
37
+ checkTagExpr(expression.tag, insideVariants);
38
+ });
39
+ return;
40
+ case 'variants':
41
+ errors.scope('variants', () => {
42
+ if (insideVariants) {
43
+ errors.invalidPricing('nested-variants', 'variants cannot nest inside variants');
44
+ }
45
+ const { tag } = expression;
46
+ if (tag) {
47
+ errors.scope('tag', () => checkFilterTagExpr(tag));
48
+ }
49
+ errors.scope('expression', () => walkExpr(expression.expression, true));
50
+ });
51
+ return;
52
+ case 'add':
53
+ case 'sub':
54
+ case 'mul':
55
+ case 'div':
56
+ errors.scope('left', () => walkExpr(expression.left, insideVariants));
57
+ errors.scope('right', () => walkExpr(expression.right, insideVariants));
58
+ return;
59
+ }
60
+ }
61
+ function checkTagExpr(tag, insideVariants) {
62
+ if (tag.kind === 'variantTags' && !insideVariants) {
63
+ errors.invalidPricing('scope', 'variantTags may only be used inside a variants.expression');
64
+ }
65
+ }
66
+ function checkFilterTagExpr(tag) {
67
+ if (tag.kind === 'variantTags') {
68
+ errors.invalidPricing('invalid-filter', 'variantTags is not a valid variants.tag filter');
69
+ }
70
+ }
71
+ }
@@ -1,16 +1,4 @@
1
1
  import type { BuilderBindings, BuilderRefEntities } from '../entities/index';
2
- import type { BuilderErrorLocation, BuilderErrors } from '../exception';
3
- export declare function builderErrorUnboundParameter(name: string, location?: BuilderErrorLocation): {
4
- name: string;
5
- kind: "unbound-parameter";
6
- location: BuilderErrorLocation;
7
- };
8
- export type BuilderErrorUnboundParameter = ReturnType<typeof builderErrorUnboundParameter>;
9
- export declare function builderErrorMissingReference(id: string, location?: BuilderErrorLocation): {
10
- id: string;
11
- kind: "missing-reference";
12
- location: BuilderErrorLocation;
13
- };
14
- export type BuilderErrorMissingReference = ReturnType<typeof builderErrorMissingReference>;
15
- export type BuilderResolve = (value: unknown, location: BuilderErrorLocation) => readonly [unknown, BuilderErrors];
16
- export declare function resolver(references?: BuilderRefEntities, bindings?: BuilderBindings): BuilderResolve;
2
+ import type { BuilderValidateErrors } from './errors';
3
+ export type BuilderResolve = (value: unknown) => unknown;
4
+ export declare function resolver(errors: BuilderValidateErrors, references?: BuilderRefEntities, bindings?: BuilderBindings): BuilderResolve;
@@ -1,89 +1,86 @@
1
1
  import { check } from '../check.js';
2
- import { BuilderCollectionConfigSerialisedSchema, BuilderWhenSerialisedSchema } from '../entities/index.js';
3
- import { builderError } from '../exception.js';
2
+ import { BuilderCollectionConfigSerialisedSchema, BuilderModelSerialisedSchema, BuilderWhenSerialisedSchema } from '../entities/index.js';
4
3
  import { BuilderParameterSerialisedSchema, BuilderRefSerialisedSchema } from '../references.js';
5
- export function builderErrorUnboundParameter(name, location = []) {
6
- return { ...builderError('unbound-parameter', location), name };
7
- }
8
- export function builderErrorMissingReference(id, location = []) {
9
- return { ...builderError('missing-reference', location), id };
10
- }
11
- export function resolver(references = [], bindings = {}) {
12
- function resolve(value, location) {
4
+ import { validateModelStructure } from './model.js';
5
+ export function resolver(errors, references = [], bindings = {}) {
6
+ function resolve(value) {
13
7
  if (check.is(BuilderParameterSerialisedSchema, value)) {
14
- return resolveParameter(value, location);
8
+ return resolveParameter(value);
15
9
  }
16
10
  if (check.is(BuilderRefSerialisedSchema, value)) {
17
- return resolveReference(value, location);
11
+ return resolveReference(value);
18
12
  }
19
13
  if (check.is(BuilderWhenSerialisedSchema, value)) {
20
- return resolveWhen(value, location);
14
+ return resolveWhen(value);
21
15
  }
22
16
  if (check.is(BuilderCollectionConfigSerialisedSchema, value)) {
23
- return resolveCollectionConfig(value, location);
24
- }
25
- return [value, []];
26
- }
27
- function resolveReference(reference, location) {
28
- const found = references.find((entry) => entry.id === reference.id);
29
- if (found == null) {
30
- return [reference, [builderErrorMissingReference(reference.id, location)]];
31
- }
32
- return [found.serialised, []];
33
- }
34
- function resolveParameter(parameter, location) {
35
- if (!(parameter.name in bindings)) {
36
- return [null, [builderErrorUnboundParameter(parameter.name, location)]];
37
- }
38
- const binding = bindings[parameter.name];
39
- if (!check.is(BuilderRefSerialisedSchema, binding)) {
40
- return [binding, []];
41
- }
42
- return resolveReference(binding, location);
43
- }
44
- function resolveWhen(when, location) {
45
- if (when.type === 'enable') {
46
- const [payload, errors] = resolve(when.payload, [...location, 'payload']);
47
- return [{ ...when, payload: payload ?? when.payload }, errors];
17
+ return resolveCollectionConfig(value);
48
18
  }
49
- if (when.type === 'unless') {
50
- const [unlessPath, unlessErrors] = resolve(when.unlessPath, [...location, 'unlessPath']);
51
- const [payload, payloadErrors] = resolve(when.payload, [...location, 'payload']);
52
- return [
53
- { ...when, unlessPath: unlessPath ?? when.unlessPath, payload: payload ?? when.payload },
54
- [...unlessErrors, ...payloadErrors]
55
- ];
19
+ return value;
20
+ function resolveReference(reference) {
21
+ const found = references.find((entry) => entry.id === reference.id);
22
+ if (found == null) {
23
+ errors.missingReference(reference.id);
24
+ return reference;
25
+ }
26
+ return found.serialised;
56
27
  }
57
- const [matchPath, matchErrors] = resolve(when.matchPath, [...location, 'matchPath']);
58
- const [map, mapErrors] = resolve(when.selectMap, [...location, 'selectMap']);
59
- if (map == null || typeof map !== 'object') {
60
- return [{ ...when, matchPath: matchPath ?? when.matchPath }, [...matchErrors, ...mapErrors]];
28
+ function resolveParameter(parameter) {
29
+ if (!(parameter.name in bindings)) {
30
+ errors.unboundParameter(parameter.name);
31
+ return null;
32
+ }
33
+ const binding = bindings[parameter.name];
34
+ if (!check.is(BuilderRefSerialisedSchema, binding)) {
35
+ return binding;
36
+ }
37
+ return resolveReference(binding);
61
38
  }
62
- const [selectMap, entryErrors] = Object.entries(map).reduce(([values, errors], [key, entryValue]) => {
63
- if (entryValue == null) {
64
- return [{ ...values, [key]: null }, errors];
39
+ function resolveWhen(when) {
40
+ if (when.type === 'enable') {
41
+ const payload = errors.scope('payload', () => resolve(when.payload));
42
+ return { ...when, payload: payload ?? when.payload };
65
43
  }
66
- const [resolved, resolvedErrors] = resolve(entryValue, [...location, 'selectMap', key]);
67
- return [{ ...values, [key]: resolved ?? entryValue }, [...errors, ...resolvedErrors]];
68
- }, [{}, []]);
69
- return [
70
- { ...when, matchPath: matchPath ?? when.matchPath, selectMap },
71
- [...matchErrors, ...mapErrors, ...entryErrors]
72
- ];
73
- }
74
- function resolveCollectionConfig(config, location) {
75
- const [model, modelErrors] = resolve(config.model, [...location, 'model']);
76
- const [min, minErrors] = resolve(config.min, [...location, 'min']);
77
- const [max, maxErrors] = resolve(config.max, [...location, 'max']);
78
- return [
79
- {
44
+ if (when.type === 'unless') {
45
+ const unlessPath = errors.scope('unlessPath', () => resolve(when.unlessPath));
46
+ const payload = errors.scope('payload', () => resolve(when.payload));
47
+ return {
48
+ ...when,
49
+ unlessPath: unlessPath ?? when.unlessPath,
50
+ payload: payload ?? when.payload
51
+ };
52
+ }
53
+ const matchPath = errors.scope('matchPath', () => resolve(when.matchPath));
54
+ const map = errors.scope('selectMap', () => resolve(when.selectMap));
55
+ if (map == null || typeof map !== 'object') {
56
+ return { ...when, matchPath: matchPath ?? when.matchPath };
57
+ }
58
+ const selectMap = errors.scope('selectMap', () => Object.entries(map).reduce((values, [key, entryValue]) => {
59
+ if (entryValue == null) {
60
+ return { ...values, [key]: null };
61
+ }
62
+ const resolved = errors.scope(key, () => resolve(entryValue));
63
+ return { ...values, [key]: resolved ?? entryValue };
64
+ }, {}));
65
+ return { ...when, matchPath: matchPath ?? when.matchPath, selectMap };
66
+ }
67
+ function resolveCollectionConfig(config) {
68
+ const model = errors.scope('model', () => {
69
+ const resolved = resolve(config.model);
70
+ if (check.is(BuilderModelSerialisedSchema, resolved)) {
71
+ return validateModelStructure(resolved, resolve, errors);
72
+ }
73
+ return resolved;
74
+ });
75
+ const min = errors.scope('min', () => resolve(config.min));
76
+ const max = errors.scope('max', () => resolve(config.max));
77
+ return {
80
78
  ...config,
81
79
  model: model ?? config.model,
82
80
  min: min ?? config.min,
83
81
  max: max ?? config.max
84
- },
85
- [...modelErrors, ...minErrors, ...maxErrors]
86
- ];
82
+ };
83
+ }
87
84
  }
88
85
  return resolve;
89
86
  }
@@ -1,8 +1,2 @@
1
- import type { BuilderErrorLocation, BuilderErrors } from '../exception';
1
+ import type { BuilderErrors } from '../exception';
2
2
  export type ValidationResult<Entity> = readonly [Entity, BuilderErrors];
3
- export declare function builderErrorInvalidInput(target: 'builder' | 'model' | 'ui' | 'variants', location?: BuilderErrorLocation): {
4
- target: "builder" | "model" | "ui" | "variants";
5
- kind: "invalid-input";
6
- location: BuilderErrorLocation;
7
- };
8
- export type BuilderErrorInvalidInput = ReturnType<typeof builderErrorInvalidInput>;