@json-layout/vocabulary 0.1.0 → 0.2.0

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/src/normalize.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { validateLayoutKeyword, isComponentName, isPartialCompObject, isPartialChildren, isPartialSwitch, isPartialGetItemsExpr, isPartialGetItemsObj, isPartialSlotMarkdown, isPartialGetItemsFetch } from './layout-keyword/index.js'
2
- import { validateNormalizedLayout, normalizedLayoutSchema, isSectionLayout, isCompositeLayout } from './index.js'
2
+ import { validateNormalizedLayout, compositeCompNames } from './normalized-layout/index.js'
3
3
 
4
4
  /**
5
5
  * @typedef {import('./index.js').Child} Child
@@ -13,11 +13,6 @@ import { validateNormalizedLayout, normalizedLayoutSchema, isSectionLayout, isCo
13
13
  * @typedef {import("./types.js").SchemaFragment} SchemaFragment
14
14
  */
15
15
 
16
- // export type Markdown = (src: string) => string
17
-
18
- // this navice implementation is sufficient here, not used very often
19
- const clone = (/** @type {any} */ obj) => JSON.parse(JSON.stringify(obj))
20
-
21
16
  /**
22
17
  * @param {import('./types.js').SchemaFragment} schemaFragment
23
18
  * @returns {Children}
@@ -85,81 +80,68 @@ function getChildren (defaultChildren, partialChildren) {
85
80
  }
86
81
 
87
82
  /**
83
+ * @param {PartialCompObject} partial
88
84
  * @param {SchemaFragment} schemaFragment
89
- * @param {string} schemaPath
90
- * @returns {import('./index.js').CompObject}
85
+ * @param {'oneOf'} [arrayChild]
86
+ * @returns {import('./index.js').ComponentName}
91
87
  */
92
- function getDefaultCompObject (schemaFragment, schemaPath) {
93
- const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
94
- if ('const' in schemaFragment) return { comp: 'none' }
95
- if (!schemaFragment.type) return { comp: 'none' }
96
- if (schemaFragment.type === 'object') {
97
- return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) }
88
+ function getDefaultComp (partial, schemaFragment, arrayChild) {
89
+ const hasSimpleType = ['string', 'integer', 'number'].includes(schemaFragment.type)
90
+ if (arrayChild === 'oneOf') return 'one-of-select'
91
+ if (hasSimpleType && schemaFragment.enum) return schemaFragment.enum.length > 20 ? 'autocomplete' : 'select'
92
+ if (hasSimpleType && schemaFragment.oneOf) return schemaFragment.oneOf.length > 20 ? 'autocomplete' : 'select'
93
+ if (hasSimpleType && schemaFragment.examples) return schemaFragment.type === 'string' ? 'combobox' : 'number-combobox'
94
+ if (hasSimpleType && schemaFragment.anyOf && schemaFragment.anyOf.length && Object.keys(schemaFragment.anyOf[schemaFragment.anyOf.length - 1]).length === 0) {
95
+ return schemaFragment.type === 'string' ? 'combobox' : 'number-combobox'
98
96
  }
99
- if (schemaFragment.type === 'array') {
100
- if (!schemaFragment.items) return { comp: 'none' }
101
- if (Array.isArray(schemaFragment.items)) {
102
- return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) } // tuples
103
- }
104
- return {
105
- comp: 'list',
106
- title: schemaFragment.title ?? key,
107
- listEditMode: schemaFragment.items.type === 'object' ? 'inline-single' : 'inline',
108
- listActions: ['add', 'edit', 'delete', 'duplicate', 'sort']
97
+ if (partial.items) return partial.items.length > 20 ? 'autocomplete' : 'select'
98
+ if (partial.getItems) {
99
+ if (isPartialGetItemsFetch(partial.getItems)) {
100
+ if (partial.getItems.qSearchParam) return 'autocomplete'
101
+ if (typeof partial.getItems.url === 'string' && partial.getItems.url.includes('{q}')) return 'autocomplete'
109
102
  }
103
+ return 'select'
110
104
  }
111
- const hasSimpleType = ['string', 'integer', 'number'].includes(schemaFragment.type)
112
- if (schemaFragment.enum && hasSimpleType) {
113
- return {
114
- comp: 'select',
115
- label: schemaFragment.title ?? key,
116
- items: schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
105
+ if (schemaFragment.type === 'array' && schemaFragment.items) {
106
+ const hasSimpleTypeItems = ['string', 'integer', 'number'].includes(schemaFragment.items.type)
107
+ if (hasSimpleTypeItems && (schemaFragment.items.enum || schemaFragment.items.oneOf)) {
108
+ return (schemaFragment.items.enum || schemaFragment.items.oneOf).length > 20 ? 'autocomplete' : 'select'
117
109
  }
118
- }
119
- if (schemaFragment.oneOf && !(schemaFragment.oneOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem))) && hasSimpleType) {
120
- return {
121
- comp: 'select',
122
- label: schemaFragment.title ?? key,
123
- items: schemaFragment.oneOf.map((/** @type {{ const: string; title: any; }} */ oneOfItem) => ({ key: oneOfItem.const + '', title: (oneOfItem.title ?? oneOfItem.const) + '', value: oneOfItem.const }))
110
+ if (hasSimpleTypeItems && schemaFragment.items.examples) {
111
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
124
112
  }
125
- }
126
- if (schemaFragment.type === 'string') {
127
- if (schemaFragment.format) {
128
- /** @type {CompObject | null} */
129
- let formatCompObject = null
130
- if (schemaFragment.format === 'date') formatCompObject = { comp: 'date-picker', label: schemaFragment.title ?? key, format: 'date' }
131
- if (schemaFragment.format === 'date-time') formatCompObject = { comp: 'date-time-picker', label: schemaFragment.title ?? key }
132
- if (schemaFragment.format === 'time') formatCompObject = { comp: 'time-picker', label: schemaFragment.title ?? key }
133
- if (formatCompObject) {
134
- if ('formatMinimum' in schemaFragment) formatCompObject.min = schemaFragment.formatMinimum
135
- if ('formatMaximum' in schemaFragment) formatCompObject.max = schemaFragment.formatMaximum
136
- return formatCompObject
137
- }
113
+ if (hasSimpleTypeItems && schemaFragment.items.anyOf && schemaFragment.items.anyOf.length && Object.keys(schemaFragment.items.anyOf[schemaFragment.items.anyOf.length - 1]).length === 0) {
114
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
115
+ }
116
+ if (hasSimpleTypeItems && !schemaFragment.items.layout && !['date', 'date-time', 'time'].includes(schemaFragment.items.format)) {
117
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
138
118
  }
139
- return { comp: 'text-field', label: schemaFragment.title ?? key }
140
119
  }
141
- if (schemaFragment.type === 'integer' || schemaFragment.type === 'number') {
142
- /** @type {import('./index.js').NumberField} */
143
- const compObject = { comp: 'number-field', label: schemaFragment.title ?? key }
144
- if (schemaFragment.type === 'integer') compObject.step = 1
145
- if ('minimum' in schemaFragment) compObject.min = schemaFragment.minimum
146
- if ('maximum' in schemaFragment) compObject.max = schemaFragment.maximum
147
- return compObject
120
+ if (schemaFragment.type === 'object') return 'section'
121
+ if (schemaFragment.type === 'array') {
122
+ if (Array.isArray(schemaFragment.items)) return 'section'
123
+ else return 'list'
148
124
  }
149
- if (schemaFragment.type === 'boolean') return { comp: 'checkbox', label: schemaFragment.title ?? key }
150
- console.warn(`failed to calculate default layout for schema ${schemaPath}`, schemaFragment)
151
- return { comp: 'none' }
125
+ if (schemaFragment.type === 'string') {
126
+ if (schemaFragment.format === 'date') return 'date-picker'
127
+ if (schemaFragment.format === 'date-time') return 'date-time-picker'
128
+ if (schemaFragment.format === 'time') return 'time-picker'
129
+ return 'text-field'
130
+ }
131
+ if (schemaFragment.type === 'integer' || schemaFragment.type === 'number') return 'number-field'
132
+ if (schemaFragment.type === 'boolean') return 'checkbox'
133
+ throw new Error('failed to calculate default component for schema fragment')
152
134
  }
153
135
 
154
136
  /**
155
137
  * @param {LayoutKeyword} layoutKeyword
156
- * @returns {PartialCompObject | null}
138
+ * @returns {PartialCompObject}
157
139
  */
158
140
  function getPartialCompObject (layoutKeyword) {
159
141
  if (isPartialCompObject(layoutKeyword)) return { ...layoutKeyword }
160
142
  else if (isComponentName(layoutKeyword)) return { comp: layoutKeyword }
161
143
  else if (isPartialChildren(layoutKeyword)) return { children: layoutKeyword }
162
- return null
144
+ return {}
163
145
  }
164
146
 
165
147
  /**
@@ -172,16 +154,121 @@ function normalizeExpression (expression, defaultType = 'js-eval') {
172
154
  else return { ...expression, type: expression.type ?? defaultType }
173
155
  }
174
156
 
157
+ /**
158
+ * @param {SchemaFragment} schemaFragment
159
+ * @returns {import('./index.js').PartialSelectItem[] | null}
160
+ */
161
+ function getItemsFromSchema (schemaFragment) {
162
+ if (!schemaFragment) return null
163
+ const hasSimpleType = ['string', 'integer', 'number'].includes(schemaFragment.type)
164
+ if (schemaFragment.enum && hasSimpleType) {
165
+ return schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
166
+ }
167
+ if (schemaFragment.examples && hasSimpleType) {
168
+ return schemaFragment.examples.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
169
+ }
170
+ if (schemaFragment.anyOf && hasSimpleType && schemaFragment.anyOf.length && Object.keys(schemaFragment.anyOf[schemaFragment.anyOf.length - 1]).length === 0) {
171
+ const nonEmptyAnyOf = schemaFragment.anyOf.slice(0, -1)
172
+ if (!(nonEmptyAnyOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem)))) {
173
+ return nonEmptyAnyOf.map((/** @type {{ const: string; title: any; }} */ anyOfItem) => ({
174
+ ...anyOfItem,
175
+ key: anyOfItem.const + '',
176
+ title: (anyOfItem.title ?? anyOfItem.const) + '',
177
+ value: anyOfItem.const
178
+ }))
179
+ }
180
+ }
181
+ if (schemaFragment.oneOf && hasSimpleType && !(schemaFragment.oneOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem)))) {
182
+ return schemaFragment.oneOf.map((/** @type {{ const: string; title: any; }} */ oneOfItem) => ({
183
+ ...oneOfItem,
184
+ key: oneOfItem.const + '',
185
+ title: (oneOfItem.title ?? oneOfItem.const) + '',
186
+ value: oneOfItem.const
187
+ }))
188
+ }
189
+ return null
190
+ }
191
+
175
192
  /**
176
193
  * @param {LayoutKeyword} layoutKeyword
177
- * @param {CompObject} defaultCompObject
178
194
  * @param {SchemaFragment} schemaFragment
195
+ * @param {string} schemaPath
179
196
  * @param {(text: string) => string} markdown
180
- * @returns {CompObject}
197
+ * @param {'oneOf'} [arrayChild]
198
+ * @returns {{normalized: CompObject, errors: string[]}}
181
199
  */
182
- function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
200
+ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
201
+ /** @type {string[]} */
202
+ const errors = []
203
+ const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
204
+
205
+ if ('const' in schemaFragment) return { normalized: { comp: 'none' }, errors }
206
+ if (!schemaFragment.type) return { normalized: { comp: 'none' }, errors }
207
+ if (schemaFragment.type === 'array' && !schemaFragment.items) return { normalized: { comp: 'none' }, errors }
208
+
183
209
  const partial = getPartialCompObject(layoutKeyword)
184
- if (!partial) return defaultCompObject
210
+
211
+ // chose the default component for a schema fragment
212
+ if (!partial.comp) {
213
+ try {
214
+ partial.comp = getDefaultComp(partial, schemaFragment, arrayChild)
215
+ } catch (/** @type {any} */err) {
216
+ errors.push(err.message)
217
+ partial.comp = 'none'
218
+ }
219
+ }
220
+ if (partial.comp === 'none') return { normalized: { comp: 'none' }, errors }
221
+
222
+ // @ts-ignore
223
+ if (compositeCompNames.includes(partial.comp)) {
224
+ partial.title = partial.title ?? schemaFragment.title ?? null
225
+ partial.children = getChildren(getDefaultChildren(schemaFragment), partial.children)
226
+ } else if (partial.comp === 'list') {
227
+ partial.title = partial.title ?? schemaFragment.title ?? key
228
+ partial.listEditMode = partial.listEditMode ?? (schemaFragment.items.type === 'object' ? 'inline-single' : 'inline')
229
+ partial.listActions = partial.listActions ?? ['add', 'edit', 'delete', 'duplicate', 'sort']
230
+ } else {
231
+ partial.label = partial.label ?? schemaFragment.title ?? key
232
+ }
233
+
234
+ if (['select', 'autocomplete', 'combobox'].includes(partial.comp) && !partial.items) {
235
+ let items
236
+ if (schemaFragment.type === 'array') {
237
+ items = getItemsFromSchema(schemaFragment.items)
238
+ partial.multiple = true
239
+ } else {
240
+ items = getItemsFromSchema(schemaFragment)
241
+ }
242
+ if (items) {
243
+ if (partial.getItems && isPartialGetItemsObj(partial.getItems)) {
244
+ partial.getItems.expr = JSON.stringify(items)
245
+ } else {
246
+ partial.getItems = JSON.stringify(items)
247
+ }
248
+ }
249
+ }
250
+
251
+ if (['combobox', 'number-combobox'].includes(partial.comp)) {
252
+ if (schemaFragment.type === 'array') {
253
+ partial.multiple = true
254
+ }
255
+ }
256
+
257
+ if (partial.comp === 'date-picker') {
258
+ if (schemaFragment.format === 'date') partial.format = 'date'
259
+ if (schemaFragment.format === 'date-time') partial.format = 'date-time'
260
+ }
261
+
262
+ if (['date-picker', 'date-time-picker', 'time-picker'].includes(partial.comp)) {
263
+ if ('formatMinimum' in schemaFragment) partial.min = partial.min ?? schemaFragment.formatMinimum
264
+ if ('formatMaximum' in schemaFragment) partial.max = partial.max ?? schemaFragment.formatMaximum
265
+ }
266
+
267
+ if (['number-field', 'slider'].includes(partial.comp)) {
268
+ if (schemaFragment.type === 'integer') partial.step = partial.step ?? 1
269
+ if ('minimum' in schemaFragment) partial.min = partial.min ?? schemaFragment.minimum
270
+ if ('maximum' in schemaFragment) partial.max = partial.max ?? schemaFragment.maximum
271
+ }
185
272
 
186
273
  if (partial.if) partial.if = normalizeExpression(partial.if)
187
274
  if (partial.getItems && isPartialGetItemsExpr(partial.getItems)) partial.getItems = normalizeExpression(partial.getItems)
@@ -190,6 +277,7 @@ function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdo
190
277
  if (partial.getItems.itemTitle) partial.getItems.itemTitle = normalizeExpression(partial.getItems.itemTitle)
191
278
  if (partial.getItems.itemKey) partial.getItems.itemKey = normalizeExpression(partial.getItems.itemKey)
192
279
  if (partial.getItems.itemValue) partial.getItems.itemValue = normalizeExpression(partial.getItems.itemValue)
280
+ if (partial.getItems.itemIcon) partial.getItems.itemIcon = normalizeExpression(partial.getItems.itemIcon)
193
281
  if (partial.getItems.itemsResults) partial.getItems.itemsResults = normalizeExpression(partial.getItems.itemsResults)
194
282
  }
195
283
  if (partial.getItems && isPartialGetItemsFetch(partial.getItems)) {
@@ -236,48 +324,70 @@ function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdo
236
324
  }
237
325
  }
238
326
 
239
- /** @type {any} */
240
- const compObject = {}
241
- if (partial.comp && defaultCompObject.comp !== partial.comp) {
242
- const compProperties = normalizedLayoutSchema.$defs[partial.comp]?.properties
243
- if (typeof compProperties === 'object') {
244
- for (const key of Object.keys(compProperties).concat(['if', 'help', 'cols', 'props', 'slots', 'options'])) {
245
- if (key in defaultCompObject) compObject[key] = defaultCompObject[key]
246
- if (key in partial) compObject[key] = partial[key]
247
- }
248
- }
249
- } else {
250
- Object.assign(compObject, defaultCompObject, partial)
251
- }
252
-
253
- if (isSectionLayout(defaultCompObject) && isCompositeLayout(compObject)) {
254
- compObject.children = getChildren((defaultCompObject).children, partial.children)
255
- }
256
-
257
- if (compObject.description && !compObject.help) compObject.help = compObject.description
258
- if (compObject.help) compObject.help = markdown(compObject.help).trim()
327
+ if (schemaFragment.description && !partial.help) partial.help = schemaFragment.description
328
+ if (partial.help) partial.help = markdown(partial.help).trim()
259
329
 
260
- if (typeof compObject.cols === 'number') compObject.cols = { xs: compObject.cols }
261
- if (typeof compObject.cols === 'object' && compObject.cols.xs === undefined) compObject.cols.xs = 12
330
+ if (typeof partial.cols === 'number') partial.cols = { xs: partial.cols }
331
+ if (typeof partial.cols === 'object' && partial.cols.xs === undefined) partial.cols.xs = 12
262
332
 
263
- return compObject
333
+ return { normalized: /** @type {CompObject} */(partial), errors }
264
334
  }
265
335
 
266
336
  /**
267
337
  * @param {LayoutKeyword} layoutKeyword
268
- * @param {CompObject} defaultCompObject
269
338
  * @param {SchemaFragment} schemaFragment
339
+ * @param {string} schemaPath
270
340
  * @param {(text: string) => string} markdown
271
- * @returns {NormalizedLayout}
341
+ * @param {'oneOf'} [arrayChild]
342
+ * @returns {{normalized: NormalizedLayout, errors: string[]}}
272
343
  */
273
- function getNormalizedLayout (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
344
+ function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
274
345
  if (isPartialSwitch(layoutKeyword)) {
275
- const switchCases = layoutKeyword.switch.map(layout => getCompObject(layout, clone(defaultCompObject), schemaFragment, markdown))
276
- if (!switchCases.find(switchCase => !switchCase.if)) switchCases.push(clone(defaultCompObject))
277
- return { switch: switchCases }
346
+ /** @type {CompObject[]} */
347
+ const normalizedSwitchCases = []
348
+ const errors = []
349
+ const switchCases = [...layoutKeyword.switch]
350
+ if (!switchCases.find(s => !s.if)) {
351
+ switchCases.push({})
352
+ }
353
+ for (let i = 0; i < switchCases.length; i++) {
354
+ const switchCase = switchCases[i]
355
+ const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, markdown, arrayChild)
356
+ normalizedSwitchCases.push(compObjectResult.normalized)
357
+ for (const error of compObjectResult.errors) errors.push(`switch ${i} - ${error}`)
358
+ }
359
+ return { normalized: { switch: normalizedSwitchCases }, errors: [] }
278
360
  } else {
279
- return getCompObject(layoutKeyword, defaultCompObject, schemaFragment, markdown)
361
+ return getCompObject(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
362
+ }
363
+ }
364
+
365
+ /**
366
+ * @param {import('ajv').ErrorObject} error
367
+ * @param {(error: import('ajv').ErrorObject) => boolean} fn
368
+ * @returns {boolean}
369
+ */
370
+ function matchValidationError (error, fn) {
371
+ if (error.keyword === 'errorMessage') {
372
+ error = error.params.errors[0]
280
373
  }
374
+ return fn(error)
375
+ }
376
+
377
+ /**
378
+ * @param {import('ajv').ErrorObject[]} errors
379
+ * @returns {string[]}
380
+ */
381
+ function lighterValidationErrors (errors) {
382
+ const compositeErrors = errors.filter(e => matchValidationError(e, (e) => e.keyword === 'anyOf' || e.keyword === 'oneOf'))
383
+ // in case of a anyOf/oneOf error there are some subschemas errors that mostly prevent readability
384
+ for (const compositeError of compositeErrors) {
385
+ const explicitError = errors.find(e => matchValidationError(e, (e) => e.instancePath === compositeError.instancePath && e.keyword !== 'type'))
386
+ if (explicitError) {
387
+ errors = errors.filter(e => matchValidationError(e, (e) => e.instancePath !== compositeError.instancePath || e.keyword !== 'type'))
388
+ }
389
+ }
390
+ return errors.map(e => e.message ?? e.keyword)
281
391
  }
282
392
 
283
393
  /**
@@ -285,29 +395,30 @@ function getNormalizedLayout (layoutKeyword, defaultCompObject, schemaFragment,
285
395
  * @param {string} schemaPath
286
396
  * @param {(text: string) => string} markdown
287
397
  * @param {'oneOf'} [arrayChild]
288
- * @returns {NormalizedLayout}
398
+ * @returns {{layout: NormalizedLayout, errors: string[]}}
289
399
  */
290
400
  export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, arrayChild) {
291
401
  let layoutKeyword
292
- /** @type {CompObject} */
293
- let defaultCompObject
294
402
  if (arrayChild === 'oneOf') {
295
403
  layoutKeyword = schemaFragment.oneOfLayout ?? {}
296
- defaultCompObject = { comp: 'one-of-select' } // TODO: default label ?
297
404
  } else {
298
405
  layoutKeyword = schemaFragment.layout ?? {}
299
- defaultCompObject = getDefaultCompObject(schemaFragment, schemaPath)
300
406
  }
301
407
  if (!validateLayoutKeyword(layoutKeyword)) {
302
408
  console.error(`layout keyword validation errors at path ${schemaPath}`, layoutKeyword, validateLayoutKeyword.errors)
303
- return defaultCompObject
304
- // throw new Error(`invalid layout keyword at path ${schemaPath}`, { cause: validateLayoutKeyword.errors })
409
+ return {
410
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
411
+ errors: lighterValidationErrors(validateLayoutKeyword.errors)
412
+ }
305
413
  }
306
- const normalizedLayout = getNormalizedLayout(layoutKeyword, defaultCompObject, schemaFragment, markdown)
307
- if (!validateNormalizedLayout(normalizedLayout)) {
414
+ const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
415
+ if (!validateNormalizedLayout(normalizedLayout.normalized)) {
308
416
  console.error(`normalized layout validation errors at path ${schemaPath}`, normalizedLayout, validateNormalizedLayout.errors)
309
- return defaultCompObject
417
+ return {
418
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
419
+ errors: lighterValidationErrors(validateNormalizedLayout.errors)
420
+ }
310
421
  // throw new Error(`invalid layout at path ${schemaPath}`, { cause: validateNormalizedLayout.errors })
311
422
  }
312
- return normalizedLayout
423
+ return { layout: normalizedLayout.normalized, errors: [] }
313
424
  }
@@ -9,6 +9,7 @@ import schema from './schema.js'
9
9
  * @typedef {import('./types.js').GetItems} GetItems
10
10
  * @typedef {import('./types.js').Expression} Expression
11
11
  * @typedef {import('./types.js').Select} Select
12
+ * @typedef {import('./types.js').Autocomplete} Autocomplete
12
13
  * @typedef {import('./types.js').SelectItem} SelectItem
13
14
  * @typedef {import('./types.js').SelectItems} SelectItems
14
15
  * @typedef {import('./types.js').GetItemsFetch} GetItemsFetch
@@ -30,6 +31,7 @@ import schema from './schema.js'
30
31
  * @typedef {import('./types.js').VerticalTabs} VerticalTabs
31
32
  * @typedef {import('./types.js').ExpansionPanels} ExpansionPanels
32
33
  * @typedef {import('./types.js').List} List
34
+ * @typedef {import('./types.js').Combobox} Combobox
33
35
  * @typedef {import('./types.js').Cols} Cols
34
36
  * @typedef {import('./types.js').ColsObj} ColsObj
35
37
  * @typedef {import('./types.js').StateNodeOptions} StateNodeOptions
@@ -61,9 +63,11 @@ export function isSectionLayout (layout) {
61
63
  return layout.comp === 'section'
62
64
  }
63
65
 
66
+ export const compositeCompNames = ['section', 'tabs', 'vertical-tabs', 'expansion-panels']
67
+
64
68
  /** @type {(layout: CompObject) => layout is CompositeCompObject} */
65
69
  export function isCompositeLayout (layout) {
66
- return ['section', 'tabs', 'vertical-tabs', 'expansion-panels'].includes(layout.comp)
70
+ return compositeCompNames.includes(layout.comp)
67
71
  }
68
72
 
69
73
  /** @type {(layout: CompObject) => layout is TextField} */
@@ -76,6 +80,11 @@ export function isSelectLayout (layout) {
76
80
  return layout.comp === 'select'
77
81
  }
78
82
 
83
+ /** @type {(layout: CompObject) => layout is Select | Combobox | Autocomplete} */
84
+ export function isItemsLayout (layout) {
85
+ return layout.comp === 'select' || layout.comp === 'combobox' || layout.comp === 'autocomplete'
86
+ }
87
+
79
88
  /** @type {(getItems: GetItems) => getItems is Expression} */
80
89
  export function isGetItemsExpression (getItems) {
81
90
  return !!getItems.expr