lutaml-jsonschema 0.1.2 → 0.1.4
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.
- checksums.yaml +4 -4
- data/frontend/src/__tests__/useSchemaTypes.test.ts +74 -0
- data/frontend/src/components/AppSidebar.vue +10 -0
- data/frontend/src/components/DetailPanel.vue +45 -1
- data/frontend/src/components/SchemaBuilder.vue +59 -16
- data/frontend/src/composables/useSchemaTypes.ts +48 -1
- data/frontend/src/types.ts +8 -0
- data/frontend/src/views/HomeView.vue +184 -10
- data/lib/lutaml/jsonschema/spa/spa_builder.rb +9 -1
- data/lib/lutaml/jsonschema/spa/spa_property.rb +10 -0
- data/lib/lutaml/jsonschema/spa/spa_schema.rb +6 -0
- data/lib/lutaml/jsonschema/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a31e1dc445c583474f10cf52d8468991193679ab428af9d46a0f760492373a2d
|
|
4
|
+
data.tar.gz: 7dfe7fd154e030c64a9670b4d0c694b83e397fa8cff9d08d83222869bbfc1917
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a884595f8021d51474b547a1a06087aec67a5286250ba493f74657f8d9c7e3b8fdfc089c2cad865a90b9fceff984f877f1376c82adceb0928cfc33807b765c7
|
|
7
|
+
data.tar.gz: f01cba83ee80adda6d6bc5ef16494fd23bbee7f0258759f4ea7ddc55305b9c535ab5c2ac4a0be15d8acc5fcbfef8d8f81b5a6655309501d3e2a9cde7f20bb475
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isObjectProperty,
|
|
12
12
|
hasConstraints,
|
|
13
13
|
parsePropertyValue,
|
|
14
|
+
humanizeConstraints,
|
|
14
15
|
} from '../composables/useSchemaTypes'
|
|
15
16
|
import type { SpaProperty } from '../types'
|
|
16
17
|
|
|
@@ -205,6 +206,79 @@ describe('hasConstraints', () => {
|
|
|
205
206
|
it('returns true for const', () => {
|
|
206
207
|
expect(hasConstraints(prop({ const: 'fixed' }))).toBe(true)
|
|
207
208
|
})
|
|
209
|
+
|
|
210
|
+
it('returns true for exclusiveMinimum', () => {
|
|
211
|
+
expect(hasConstraints(prop({ exclusiveMinimum: 0 }))).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('returns true for exclusiveMaximum', () => {
|
|
215
|
+
expect(hasConstraints(prop({ exclusiveMaximum: 100 }))).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('returns true for contentMediaType', () => {
|
|
219
|
+
expect(hasConstraints(prop({ contentMediaType: 'text/html' }))).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('returns true for contentEncoding', () => {
|
|
223
|
+
expect(hasConstraints(prop({ contentEncoding: 'base64' }))).toBe(true)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('humanizeConstraints', () => {
|
|
228
|
+
it('returns string range constraint', () => {
|
|
229
|
+
const chips = humanizeConstraints(prop({ type: 'string', minLength: 1, maxLength: 100 }))
|
|
230
|
+
expect(chips).toEqual([{ label: '1..100 characters' }])
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('returns numeric >= and <= constraints', () => {
|
|
234
|
+
const chips = humanizeConstraints(prop({ type: 'integer', minimum: 0, maximum: 100 }))
|
|
235
|
+
expect(chips.map(c => c.label)).toEqual(['>= 0', '<= 100'])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('returns exclusive bounds with > and <', () => {
|
|
239
|
+
const chips = humanizeConstraints(prop({ type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 100 }))
|
|
240
|
+
expect(chips.map(c => c.label)).toEqual(['> 0', '< 100'])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns array range constraint', () => {
|
|
244
|
+
const chips = humanizeConstraints(prop({ type: 'array', minItems: 1, maxItems: 10 }))
|
|
245
|
+
expect(chips).toEqual([{ label: '1..10 items' }])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('returns unique for uniqueItems', () => {
|
|
249
|
+
const chips = humanizeConstraints(prop({ type: 'array', uniqueItems: true }))
|
|
250
|
+
expect(chips.map(c => c.label)).toEqual(['unique'])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('returns const chip', () => {
|
|
254
|
+
const chips = humanizeConstraints(prop({ const: 'v1' }))
|
|
255
|
+
expect(chips.map(c => c.label)).toEqual(['const: v1'])
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('returns pattern chip', () => {
|
|
259
|
+
const chips = humanizeConstraints(prop({ pattern: '^[a-z]+$' }))
|
|
260
|
+
expect(chips.map(c => c.label)).toEqual(['^[a-z]+$'])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('returns default chip', () => {
|
|
264
|
+
const chips = humanizeConstraints(prop({ default: 'hello' }))
|
|
265
|
+
expect(chips.map(c => c.label)).toEqual(['default: hello'])
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('returns multipleOf chip', () => {
|
|
269
|
+
const chips = humanizeConstraints(prop({ type: 'integer', multipleOf: 5 }))
|
|
270
|
+
expect(chips.map(c => c.label)).toEqual(['multiple of 5'])
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('returns empty array for no constraints', () => {
|
|
274
|
+
const chips = humanizeConstraints(prop({ type: 'string' }))
|
|
275
|
+
expect(chips).toEqual([])
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns contentMediaType and contentEncoding', () => {
|
|
279
|
+
const chips = humanizeConstraints(prop({ contentMediaType: 'text/html', contentEncoding: 'base64' }))
|
|
280
|
+
expect(chips.map(c => c.label)).toEqual(['content-type: text/html', 'encoding: base64'])
|
|
281
|
+
})
|
|
208
282
|
})
|
|
209
283
|
|
|
210
284
|
describe('parsePropertyValue', () => {
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
>
|
|
63
63
|
<span class="badge badge-definition-sm">D</span>
|
|
64
64
|
<span class="tree-item-name">{{ def.title || def.name }}</span>
|
|
65
|
+
<span class="tree-item-count">{{ def.properties.length }}</span>
|
|
65
66
|
</div>
|
|
66
67
|
</div>
|
|
67
68
|
</div>
|
|
@@ -342,6 +343,15 @@ function selectDefinition(schemaName: string, defName: string) {
|
|
|
342
343
|
text-overflow: ellipsis;
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
.tree-item-count {
|
|
347
|
+
font-size: 10px;
|
|
348
|
+
color: var(--text-muted);
|
|
349
|
+
background: var(--bg-primary);
|
|
350
|
+
padding: 0px 4px;
|
|
351
|
+
border-radius: 2px;
|
|
352
|
+
flex-shrink: 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
345
355
|
.badge-definition-sm {
|
|
346
356
|
background: var(--badge-definition-bg);
|
|
347
357
|
color: var(--badge-definition);
|
|
@@ -102,6 +102,46 @@
|
|
|
102
102
|
<td class="constraint-key">Items</td>
|
|
103
103
|
<td>{{ (item as any).property.itemsType }}</td>
|
|
104
104
|
</tr>
|
|
105
|
+
<tr v-if="(item as any).property.exclusiveMinimum != null">
|
|
106
|
+
<td class="constraint-key">Exclusive Min</td>
|
|
107
|
+
<td>{{ (item as any).property.exclusiveMinimum }}</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr v-if="(item as any).property.exclusiveMaximum != null">
|
|
110
|
+
<td class="constraint-key">Exclusive Max</td>
|
|
111
|
+
<td>{{ (item as any).property.exclusiveMaximum }}</td>
|
|
112
|
+
</tr>
|
|
113
|
+
<tr v-if="(item as any).property.minItems != null">
|
|
114
|
+
<td class="constraint-key">Min Items</td>
|
|
115
|
+
<td>{{ (item as any).property.minItems }}</td>
|
|
116
|
+
</tr>
|
|
117
|
+
<tr v-if="(item as any).property.maxItems != null">
|
|
118
|
+
<td class="constraint-key">Max Items</td>
|
|
119
|
+
<td>{{ (item as any).property.maxItems }}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
<tr v-if="(item as any).property.uniqueItems">
|
|
122
|
+
<td class="constraint-key">Unique Items</td>
|
|
123
|
+
<td>Yes</td>
|
|
124
|
+
</tr>
|
|
125
|
+
<tr v-if="(item as any).property.multipleOf != null">
|
|
126
|
+
<td class="constraint-key">Multiple Of</td>
|
|
127
|
+
<td>{{ (item as any).property.multipleOf }}</td>
|
|
128
|
+
</tr>
|
|
129
|
+
<tr v-if="(item as any).property.const != null">
|
|
130
|
+
<td class="constraint-key">Const</td>
|
|
131
|
+
<td class="font-mono">{{ (item as any).property.const }}</td>
|
|
132
|
+
</tr>
|
|
133
|
+
<tr v-if="(item as any).property.contentMediaType">
|
|
134
|
+
<td class="constraint-key">Content Type</td>
|
|
135
|
+
<td>{{ (item as any).property.contentMediaType }}</td>
|
|
136
|
+
</tr>
|
|
137
|
+
<tr v-if="(item as any).property.contentEncoding">
|
|
138
|
+
<td class="constraint-key">Content Encoding</td>
|
|
139
|
+
<td>{{ (item as any).property.contentEncoding }}</td>
|
|
140
|
+
</tr>
|
|
141
|
+
<tr v-if="(item as any).property.additionalProperties === false">
|
|
142
|
+
<td class="constraint-key">Additional Props</td>
|
|
143
|
+
<td>Denied</td>
|
|
144
|
+
</tr>
|
|
105
145
|
</tbody>
|
|
106
146
|
</table>
|
|
107
147
|
</div>
|
|
@@ -204,7 +244,11 @@ const hasConstraints = computed(() => {
|
|
|
204
244
|
const p = item.value.property
|
|
205
245
|
return p.minimum != null || p.maximum != null ||
|
|
206
246
|
p.minLength != null || p.maxLength != null ||
|
|
207
|
-
p.pattern || p.enum?.length || p.itemsType
|
|
247
|
+
p.pattern || p.enum?.length || p.itemsType ||
|
|
248
|
+
p.exclusiveMinimum != null || p.exclusiveMaximum != null ||
|
|
249
|
+
p.minItems != null || p.maxItems != null || p.uniqueItems ||
|
|
250
|
+
p.multipleOf != null || p.const != null ||
|
|
251
|
+
p.contentMediaType || p.contentEncoding
|
|
208
252
|
})
|
|
209
253
|
|
|
210
254
|
type TabId = 'overview' | 'properties'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="builder-layout">
|
|
3
3
|
<div class="builder-fields">
|
|
4
|
-
<div v-for="field in
|
|
4
|
+
<div v-for="(field, idx) in sortedFields" :key="field.prop.name" class="field-row" :class="{ 'field-row-alt': idx % 2 === 1 }">
|
|
5
5
|
<div class="field-main">
|
|
6
6
|
<input
|
|
7
7
|
type="checkbox"
|
|
@@ -113,20 +113,10 @@
|
|
|
113
113
|
<div v-if="field.prop.description" class="field-desc text-secondary">{{ field.prop.description }}</div>
|
|
114
114
|
|
|
115
115
|
<div v-if="hasConstraints(field.prop) || field.prop.ref" class="field-constraints">
|
|
116
|
-
<span v-if="field.prop.ref" class="constraint-chip">ref → {{ field.resolvedDef?.title || field.resolvedDef?.name || field.prop.ref }}</span>
|
|
116
|
+
<span v-if="field.prop.ref" class="constraint-chip chip-ref">ref → {{ field.resolvedDef?.title || field.resolvedDef?.name || field.prop.ref }}</span>
|
|
117
117
|
<span v-if="field.prop.enum?.length && isObjectProperty(field.prop)" class="constraint-chip">enum: {{ field.prop.enum.join(' | ') }}</span>
|
|
118
|
-
<span v-
|
|
119
|
-
<span v-if="field.prop.
|
|
120
|
-
<span v-if="field.prop.maximum != null" class="constraint-chip">max: {{ field.prop.maximum }}</span>
|
|
121
|
-
<span v-if="field.prop.minLength != null" class="constraint-chip">minLength: {{ field.prop.minLength }}</span>
|
|
122
|
-
<span v-if="field.prop.maxLength != null" class="constraint-chip">maxLength: {{ field.prop.maxLength }}</span>
|
|
123
|
-
<span v-if="field.prop.default != null" class="constraint-chip">default: {{ field.prop.default }}</span>
|
|
124
|
-
<span v-if="field.prop.const != null" class="constraint-chip">const: {{ field.prop.const }}</span>
|
|
125
|
-
<span v-if="field.prop.minItems != null" class="constraint-chip">minItems: {{ field.prop.minItems }}</span>
|
|
126
|
-
<span v-if="field.prop.maxItems != null" class="constraint-chip">maxItems: {{ field.prop.maxItems }}</span>
|
|
127
|
-
<span v-if="field.prop.uniqueItems" class="constraint-chip">unique</span>
|
|
128
|
-
<span v-if="field.prop.multipleOf != null" class="constraint-chip">multipleOf: {{ field.prop.multipleOf }}</span>
|
|
129
|
-
<span v-if="field.prop.examples?.length" class="constraint-chip">e.g. {{ field.prop.examples.join(', ') }}</span>
|
|
118
|
+
<span v-for="(chip, idx) in humanizeConstraints(field.prop)" :key="idx" class="constraint-chip" :class="chip.class">{{ chip.label }}</span>
|
|
119
|
+
<span v-if="field.prop.additionalProperties === false" class="constraint-chip chip-locked">no additional properties</span>
|
|
130
120
|
</div>
|
|
131
121
|
|
|
132
122
|
<!-- Nested object builder -->
|
|
@@ -156,7 +146,7 @@
|
|
|
156
146
|
</div>
|
|
157
147
|
</div>
|
|
158
148
|
|
|
159
|
-
<div v-if="!
|
|
149
|
+
<div v-if="!sortedFields.length" class="empty-hint">
|
|
160
150
|
<p class="text-muted">No properties defined.</p>
|
|
161
151
|
</div>
|
|
162
152
|
</div>
|
|
@@ -169,7 +159,7 @@
|
|
|
169
159
|
{{ copied ? 'Copied!' : 'Copy' }}
|
|
170
160
|
</button>
|
|
171
161
|
</div>
|
|
172
|
-
<pre class="json-block"><code
|
|
162
|
+
<pre class="json-block"><code v-html="highlightedJson"></code></pre>
|
|
173
163
|
</div>
|
|
174
164
|
</div>
|
|
175
165
|
</div>
|
|
@@ -186,6 +176,7 @@ import {
|
|
|
186
176
|
arrayDefaultValue,
|
|
187
177
|
isObjectProperty,
|
|
188
178
|
hasConstraints,
|
|
179
|
+
humanizeConstraints,
|
|
189
180
|
} from '../composables/useSchemaTypes'
|
|
190
181
|
import {
|
|
191
182
|
createField,
|
|
@@ -212,6 +203,14 @@ const copied = ref(false)
|
|
|
212
203
|
|
|
213
204
|
const fields = ref<BuilderField[]>(props.properties.map(p => createField(p, props.required, props.schema, props.allSchemas)))
|
|
214
205
|
|
|
206
|
+
const sortedFields = computed(() => {
|
|
207
|
+
return [...fields.value].sort((a, b) => {
|
|
208
|
+
if (a.isRequired && !b.isRequired) return -1
|
|
209
|
+
if (!a.isRequired && b.isRequired) return 1
|
|
210
|
+
return 0
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
215
214
|
function toggleField(field: BuilderField, checked: boolean) {
|
|
216
215
|
field.included = checked
|
|
217
216
|
if (!checked) field.expanded = false
|
|
@@ -236,6 +235,23 @@ const outputObj = computed(() => {
|
|
|
236
235
|
|
|
237
236
|
watch(outputObj, (v) => { emit('update:json', v) }, { immediate: true, deep: true })
|
|
238
237
|
|
|
238
|
+
const highlightedJson = computed(() => syntaxHighlight(outputJson.value))
|
|
239
|
+
|
|
240
|
+
function syntaxHighlight(json: string): string {
|
|
241
|
+
return json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
242
|
+
.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
|
|
243
|
+
let cls = 'json-number'
|
|
244
|
+
if (/^"/.test(match)) {
|
|
245
|
+
cls = /:$/.test(match) ? 'json-key' : 'json-string'
|
|
246
|
+
} else if (/true|false/.test(match)) {
|
|
247
|
+
cls = 'json-boolean'
|
|
248
|
+
} else if (/null/.test(match)) {
|
|
249
|
+
cls = 'json-null'
|
|
250
|
+
}
|
|
251
|
+
return `<span class="${cls}">${match}</span>`
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
async function copyJson() {
|
|
240
256
|
try {
|
|
241
257
|
await navigator.clipboard.writeText(outputJson.value)
|
|
@@ -268,6 +284,14 @@ async function copyJson() {
|
|
|
268
284
|
background: var(--bg-hover);
|
|
269
285
|
}
|
|
270
286
|
|
|
287
|
+
.field-row-alt {
|
|
288
|
+
background: var(--bg-secondary);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.field-row-alt:hover {
|
|
292
|
+
background: var(--bg-hover);
|
|
293
|
+
}
|
|
294
|
+
|
|
271
295
|
.field-main {
|
|
272
296
|
display: flex;
|
|
273
297
|
align-items: center;
|
|
@@ -463,6 +487,16 @@ async function copyJson() {
|
|
|
463
487
|
border-radius: var(--radius-sm);
|
|
464
488
|
}
|
|
465
489
|
|
|
490
|
+
.constraint-chip.chip-pattern {
|
|
491
|
+
font-family: var(--font-mono);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.constraint-chip.chip-locked {
|
|
495
|
+
color: var(--color-orange);
|
|
496
|
+
background: var(--color-orange-alpha);
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
}
|
|
499
|
+
|
|
466
500
|
.nested-section {
|
|
467
501
|
margin-left: 22px;
|
|
468
502
|
margin-top: var(--space-2);
|
|
@@ -535,6 +569,15 @@ async function copyJson() {
|
|
|
535
569
|
color: var(--text-primary);
|
|
536
570
|
}
|
|
537
571
|
|
|
572
|
+
.json-block :deep(.json-key) { color: var(--color-primary-dark); }
|
|
573
|
+
.json-block :deep(.json-string) { color: var(--color-green); }
|
|
574
|
+
.json-block :deep(.json-number) { color: var(--color-orange); }
|
|
575
|
+
.json-block :deep(.json-boolean) { color: var(--color-accent); }
|
|
576
|
+
.json-block :deep(.json-null) { color: var(--text-muted); }
|
|
577
|
+
|
|
578
|
+
:root[data-theme="dark"] .json-block :deep(.json-key) { color: var(--color-primary-light); }
|
|
579
|
+
:root[data-theme="dark"] .json-block :deep(.json-string) { color: var(--color-teal); }
|
|
580
|
+
|
|
538
581
|
.empty-hint {
|
|
539
582
|
padding: var(--space-8);
|
|
540
583
|
text-align: center;
|
|
@@ -132,7 +132,11 @@ export function hasConstraints(prop: SpaProperty): boolean {
|
|
|
132
132
|
prop.maxItems != null ||
|
|
133
133
|
prop.uniqueItems ||
|
|
134
134
|
prop.multipleOf != null ||
|
|
135
|
-
prop.const != null
|
|
135
|
+
prop.const != null ||
|
|
136
|
+
prop.exclusiveMinimum != null ||
|
|
137
|
+
prop.exclusiveMaximum != null ||
|
|
138
|
+
prop.contentMediaType ||
|
|
139
|
+
prop.contentEncoding
|
|
136
140
|
)
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -155,3 +159,46 @@ export function parsePropertyValue(rawValue: string, prop: SpaProperty): unknown
|
|
|
155
159
|
if (t === 'object' && !prop.ref) return {}
|
|
156
160
|
return rawValue
|
|
157
161
|
}
|
|
162
|
+
|
|
163
|
+
export interface ConstraintChip {
|
|
164
|
+
label: string
|
|
165
|
+
class?: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function humanizeConstraints(prop: SpaProperty): ConstraintChip[] {
|
|
169
|
+
const chips: ConstraintChip[] = []
|
|
170
|
+
const t = primaryType(prop.type)
|
|
171
|
+
|
|
172
|
+
if (prop.const != null) chips.push({ label: `const: ${prop.const}`, class: 'chip-const' })
|
|
173
|
+
|
|
174
|
+
if (t === 'string' || !t || t === 'any') {
|
|
175
|
+
if (prop.minLength != null && prop.maxLength != null)
|
|
176
|
+
chips.push({ label: `${prop.minLength}..${prop.maxLength} characters` })
|
|
177
|
+
else if (prop.minLength != null) chips.push({ label: `>= ${prop.minLength} characters` })
|
|
178
|
+
else if (prop.maxLength != null) chips.push({ label: `<= ${prop.maxLength} characters` })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (t === 'integer' || t === 'number') {
|
|
182
|
+
if (prop.minimum != null) chips.push({ label: `>= ${prop.minimum}` })
|
|
183
|
+
if (prop.exclusiveMinimum != null) chips.push({ label: `> ${prop.exclusiveMinimum}` })
|
|
184
|
+
if (prop.maximum != null) chips.push({ label: `<= ${prop.maximum}` })
|
|
185
|
+
if (prop.exclusiveMaximum != null) chips.push({ label: `< ${prop.exclusiveMaximum}` })
|
|
186
|
+
if (prop.multipleOf != null) chips.push({ label: `multiple of ${prop.multipleOf}` })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (t === 'array') {
|
|
190
|
+
if (prop.minItems != null && prop.maxItems != null)
|
|
191
|
+
chips.push({ label: `${prop.minItems}..${prop.maxItems} items` })
|
|
192
|
+
else if (prop.minItems != null) chips.push({ label: `>= ${prop.minItems} items` })
|
|
193
|
+
else if (prop.maxItems != null) chips.push({ label: `<= ${prop.maxItems} items` })
|
|
194
|
+
if (prop.uniqueItems) chips.push({ label: 'unique', class: 'chip-unique' })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (prop.pattern) chips.push({ label: `${prop.pattern}`, class: 'chip-pattern' })
|
|
198
|
+
if (prop.default != null) chips.push({ label: `default: ${prop.default}`, class: 'chip-default' })
|
|
199
|
+
if (prop.examples?.length) chips.push({ label: `e.g. ${prop.examples.join(', ')}`, class: 'chip-example' })
|
|
200
|
+
if (prop.contentMediaType) chips.push({ label: `content-type: ${prop.contentMediaType}` })
|
|
201
|
+
if (prop.contentEncoding) chips.push({ label: `encoding: ${prop.contentEncoding}` })
|
|
202
|
+
|
|
203
|
+
return chips
|
|
204
|
+
}
|
data/frontend/src/types.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface SpaProperty {
|
|
|
34
34
|
uniqueItems?: boolean
|
|
35
35
|
multipleOf?: number
|
|
36
36
|
const?: string
|
|
37
|
+
exclusiveMinimum?: number
|
|
38
|
+
exclusiveMaximum?: number
|
|
39
|
+
additionalProperties?: boolean
|
|
40
|
+
contentMediaType?: string
|
|
41
|
+
contentEncoding?: string
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface SpaDefinition {
|
|
@@ -56,6 +61,9 @@ export interface SpaSchema {
|
|
|
56
61
|
required: string[]
|
|
57
62
|
examples?: string[]
|
|
58
63
|
sourceJson?: string
|
|
64
|
+
$schema?: string
|
|
65
|
+
$id?: string
|
|
66
|
+
additionalProperties?: boolean
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
export interface SpaSearchEntry {
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
<div class="schema-header-info">
|
|
7
7
|
<div class="schema-title-row">
|
|
8
8
|
<h1>{{ schemaStore.selectedSchema.title || schemaStore.selectedSchema.name }}</h1>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<div class="schema-actions">
|
|
10
|
+
<div class="view-toggle">
|
|
11
|
+
<button class="toggle-btn" :class="{ active: viewMode === 'builder' }" @click="viewMode = 'builder'">Builder</button>
|
|
12
|
+
<button class="toggle-btn" :class="{ active: viewMode === 'source' }" @click="viewMode = 'source'">Source</button>
|
|
13
|
+
</div>
|
|
14
|
+
<button
|
|
15
|
+
v-if="schemaStore.selectedSchema.sourceJson"
|
|
16
|
+
class="btn btn-outline btn-sm"
|
|
17
|
+
@click="downloadSchema(schemaStore.selectedSchema)"
|
|
18
|
+
>
|
|
19
|
+
Download JSON
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
16
22
|
</div>
|
|
17
23
|
<span v-if="schemaStore.selectedSchema.title && schemaStore.selectedSchema.title !== schemaStore.selectedSchema.name" class="schema-name-hint font-mono text-muted">{{ schemaStore.selectedSchema.name }}</span>
|
|
18
24
|
<p v-if="schemaStore.selectedSchema.description" class="schema-desc text-secondary">{{ schemaStore.selectedSchema.description }}</p>
|
|
@@ -21,6 +27,11 @@
|
|
|
21
27
|
<span class="text-muted">{{ schemaStore.selectedSchema.properties.length }} properties</span>
|
|
22
28
|
<span v-if="schemaStore.selectedSchema.required.length" class="text-muted">· {{ schemaStore.selectedSchema.required.length }} required</span>
|
|
23
29
|
<span v-if="schemaStore.selectedSchema.definitions.length" class="text-muted">· {{ schemaStore.selectedSchema.definitions.length }} definitions</span>
|
|
30
|
+
<span v-if="schemaStore.selectedSchema.additionalProperties === false" class="badge badge-locked">no additional properties</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div v-if="schemaStore.selectedSchema.$schema || schemaStore.selectedSchema.$id" class="schema-id-row">
|
|
33
|
+
<span v-if="schemaStore.selectedSchema.$schema" class="meta-id-chip font-mono text-muted" title="JSON Schema version">{{ schemaStore.selectedSchema.$schema }}</span>
|
|
34
|
+
<span v-if="schemaStore.selectedSchema.$id" class="meta-id-chip font-mono text-muted" title="Schema $id">{{ schemaStore.selectedSchema.$id }}</span>
|
|
24
35
|
</div>
|
|
25
36
|
<div v-if="schemaStore.selectedSchema.required.length" class="schema-required">
|
|
26
37
|
<span class="required-label text-muted">Required:</span>
|
|
@@ -35,6 +46,8 @@
|
|
|
35
46
|
</div>
|
|
36
47
|
</div>
|
|
37
48
|
|
|
49
|
+
<!-- Builder View -->
|
|
50
|
+
<template v-if="viewMode === 'builder'">
|
|
38
51
|
<!-- Properties -->
|
|
39
52
|
<div class="schema-section">
|
|
40
53
|
<SchemaBuilder
|
|
@@ -47,7 +60,13 @@
|
|
|
47
60
|
|
|
48
61
|
<!-- Definitions -->
|
|
49
62
|
<div v-if="schemaStore.selectedSchema.definitions.length" class="schema-section">
|
|
50
|
-
<
|
|
63
|
+
<div class="section-heading-row">
|
|
64
|
+
<h2 class="section-heading">Definitions</h2>
|
|
65
|
+
<div class="section-actions">
|
|
66
|
+
<button class="btn btn-ghost btn-sm" @click="expandAllDefs">Expand All</button>
|
|
67
|
+
<button class="btn btn-ghost btn-sm" @click="collapseAllDefs">Collapse All</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
51
70
|
<div class="def-list">
|
|
52
71
|
<div
|
|
53
72
|
v-for="def in schemaStore.selectedSchema.definitions"
|
|
@@ -86,6 +105,17 @@
|
|
|
86
105
|
</div>
|
|
87
106
|
</div>
|
|
88
107
|
</div>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<!-- Source JSON View -->
|
|
111
|
+
<div v-if="viewMode === 'source'" class="schema-section">
|
|
112
|
+
<div v-if="schemaStore.selectedSchema.sourceJson" class="source-viewer">
|
|
113
|
+
<pre class="source-pre"><code v-html="highlightedSource"></code></pre>
|
|
114
|
+
</div>
|
|
115
|
+
<div v-else class="source-empty">
|
|
116
|
+
<p class="text-muted">Source JSON Schema not available for this schema.</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
89
119
|
</div>
|
|
90
120
|
|
|
91
121
|
<!-- Landing Page -->
|
|
@@ -132,9 +162,10 @@
|
|
|
132
162
|
<span class="badge badge-type-sm">{{ schema.type || 'any' }}</span>
|
|
133
163
|
</div>
|
|
134
164
|
<div class="schema-card-stats">
|
|
135
|
-
<span>{{ schema.properties.length }}
|
|
136
|
-
<span>{{ schema.definitions.length }}
|
|
165
|
+
<span>{{ schema.properties.length }} props</span>
|
|
166
|
+
<span>{{ schema.definitions.length }} defs</span>
|
|
137
167
|
<span v-if="schema.required.length">{{ schema.required.length }} required</span>
|
|
168
|
+
<span v-if="schema.examples?.length">{{ schema.examples.length }} examples</span>
|
|
138
169
|
</div>
|
|
139
170
|
</div>
|
|
140
171
|
</div>
|
|
@@ -152,6 +183,38 @@ import type { SpaSchema } from '../types'
|
|
|
152
183
|
|
|
153
184
|
const schemaStore = useSchemaStore()
|
|
154
185
|
const expandedDefs = reactive(new Set<string>())
|
|
186
|
+
const viewMode = ref<'builder' | 'source'>('builder')
|
|
187
|
+
|
|
188
|
+
function expandAllDefs() {
|
|
189
|
+
for (const def of schemaStore.selectedSchema?.definitions ?? []) {
|
|
190
|
+
expandedDefs.add(def.name)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collapseAllDefs() {
|
|
195
|
+
expandedDefs.clear()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const highlightedSource = computed(() => {
|
|
199
|
+
const src = schemaStore.selectedSchema?.sourceJson
|
|
200
|
+
if (!src) return ''
|
|
201
|
+
return syntaxHighlight(src)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
function syntaxHighlight(json: string): string {
|
|
205
|
+
return json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
206
|
+
.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
|
|
207
|
+
let cls = 'json-number'
|
|
208
|
+
if (/^"/.test(match)) {
|
|
209
|
+
cls = /:$/.test(match) ? 'json-key' : 'json-string'
|
|
210
|
+
} else if (/true|false/.test(match)) {
|
|
211
|
+
cls = 'json-boolean'
|
|
212
|
+
} else if (/null/.test(match)) {
|
|
213
|
+
cls = 'json-null'
|
|
214
|
+
}
|
|
215
|
+
return `<span class="${cls}">${match}</span>`
|
|
216
|
+
})
|
|
217
|
+
}
|
|
155
218
|
|
|
156
219
|
function selectSchema(name: string) {
|
|
157
220
|
schemaStore.selectSchema(name)
|
|
@@ -228,6 +291,44 @@ watch(() => schemaStore.selectedDefinitionName, (name) => {
|
|
|
228
291
|
margin: 0;
|
|
229
292
|
}
|
|
230
293
|
|
|
294
|
+
.schema-actions {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: var(--space-2);
|
|
298
|
+
flex-shrink: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.view-toggle {
|
|
302
|
+
display: flex;
|
|
303
|
+
border: 1px solid var(--border-medium);
|
|
304
|
+
border-radius: var(--radius-md);
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.toggle-btn {
|
|
309
|
+
padding: var(--space-1) var(--space-3);
|
|
310
|
+
font-size: var(--text-xs);
|
|
311
|
+
font-weight: 500;
|
|
312
|
+
color: var(--text-muted);
|
|
313
|
+
background: transparent;
|
|
314
|
+
transition: all var(--transition-fast);
|
|
315
|
+
border: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.toggle-btn:not(:last-child) {
|
|
319
|
+
border-right: 1px solid var(--border-medium);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.toggle-btn.active {
|
|
323
|
+
background: var(--color-primary-alpha);
|
|
324
|
+
color: var(--color-primary);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.toggle-btn:hover:not(.active) {
|
|
328
|
+
background: var(--bg-hover);
|
|
329
|
+
color: var(--text-primary);
|
|
330
|
+
}
|
|
331
|
+
|
|
231
332
|
.schema-name-hint {
|
|
232
333
|
font-size: var(--text-xs);
|
|
233
334
|
display: block;
|
|
@@ -245,6 +346,34 @@ watch(() => schemaStore.selectedDefinitionName, (name) => {
|
|
|
245
346
|
align-items: center;
|
|
246
347
|
gap: var(--space-2);
|
|
247
348
|
margin-top: var(--space-2);
|
|
349
|
+
flex-wrap: wrap;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.schema-id-row {
|
|
353
|
+
display: flex;
|
|
354
|
+
gap: var(--space-2);
|
|
355
|
+
margin-top: var(--space-2);
|
|
356
|
+
flex-wrap: wrap;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.meta-id-chip {
|
|
360
|
+
font-size: 10px;
|
|
361
|
+
background: var(--bg-secondary);
|
|
362
|
+
padding: 2px 6px;
|
|
363
|
+
border-radius: var(--radius-sm);
|
|
364
|
+
max-width: 400px;
|
|
365
|
+
overflow: hidden;
|
|
366
|
+
text-overflow: ellipsis;
|
|
367
|
+
white-space: nowrap;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.badge-locked {
|
|
371
|
+
font-size: 10px;
|
|
372
|
+
color: var(--color-orange);
|
|
373
|
+
background: var(--color-orange-alpha);
|
|
374
|
+
padding: 2px 6px;
|
|
375
|
+
border-radius: var(--radius-sm);
|
|
376
|
+
font-weight: 500;
|
|
248
377
|
}
|
|
249
378
|
|
|
250
379
|
.schema-required {
|
|
@@ -331,6 +460,51 @@ watch(() => schemaStore.selectedDefinitionName, (name) => {
|
|
|
331
460
|
color: var(--text-primary);
|
|
332
461
|
}
|
|
333
462
|
|
|
463
|
+
.section-heading-row {
|
|
464
|
+
display: flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
justify-content: space-between;
|
|
467
|
+
margin-bottom: var(--space-4);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.section-actions {
|
|
471
|
+
display: flex;
|
|
472
|
+
gap: var(--space-2);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Source viewer */
|
|
476
|
+
.source-viewer {
|
|
477
|
+
border-radius: var(--radius-lg);
|
|
478
|
+
overflow: hidden;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.source-pre {
|
|
482
|
+
background: var(--bg-secondary);
|
|
483
|
+
border: 1px solid var(--border-light);
|
|
484
|
+
border-radius: var(--radius-lg);
|
|
485
|
+
padding: var(--space-4);
|
|
486
|
+
font-size: var(--text-sm);
|
|
487
|
+
line-height: var(--leading-relaxed);
|
|
488
|
+
overflow-x: auto;
|
|
489
|
+
max-height: 70vh;
|
|
490
|
+
overflow-y: auto;
|
|
491
|
+
margin: 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.source-pre :deep(.json-key) { color: var(--color-primary-dark); }
|
|
495
|
+
.source-pre :deep(.json-string) { color: var(--color-green); }
|
|
496
|
+
.source-pre :deep(.json-number) { color: var(--color-orange); }
|
|
497
|
+
.source-pre :deep(.json-boolean) { color: var(--color-accent); }
|
|
498
|
+
.source-pre :deep(.json-null) { color: var(--text-muted); }
|
|
499
|
+
|
|
500
|
+
:root[data-theme="dark"] .source-pre :deep(.json-key) { color: var(--color-primary-light); }
|
|
501
|
+
:root[data-theme="dark"] .source-pre :deep(.json-string) { color: var(--color-teal); }
|
|
502
|
+
|
|
503
|
+
.source-empty {
|
|
504
|
+
padding: var(--space-8);
|
|
505
|
+
text-align: center;
|
|
506
|
+
}
|
|
507
|
+
|
|
334
508
|
/* Definitions */
|
|
335
509
|
.def-list {
|
|
336
510
|
display: flex;
|
|
@@ -30,7 +30,7 @@ module Lutaml
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def build_schemas
|
|
33
|
-
@schema_set.schemas.map do |name, schema|
|
|
33
|
+
@schema_set.schemas.to_a.map do |name, schema|
|
|
34
34
|
build_spa_schema(name, schema)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -53,6 +53,9 @@ module Lutaml
|
|
|
53
53
|
required: all_required,
|
|
54
54
|
examples: schema.examples,
|
|
55
55
|
source_json: @schema_set.source_json(name) || "",
|
|
56
|
+
dollar_schema: schema.dollar_schema,
|
|
57
|
+
dollar_id: schema.dollar_id,
|
|
58
|
+
additional_properties: schema.additional_properties,
|
|
56
59
|
)
|
|
57
60
|
end
|
|
58
61
|
|
|
@@ -154,6 +157,11 @@ all_required = root_schema.required)
|
|
|
154
157
|
unique_items: resolved.unique_items,
|
|
155
158
|
multiple_of: resolved.multiple_of,
|
|
156
159
|
const_value: resolved.const,
|
|
160
|
+
exclusive_minimum: resolved.exclusive_minimum,
|
|
161
|
+
exclusive_maximum: resolved.exclusive_maximum,
|
|
162
|
+
additional_properties: resolved.additional_properties,
|
|
163
|
+
content_type: resolved.content_type,
|
|
164
|
+
content_encoding: resolved.content_encoding,
|
|
157
165
|
)
|
|
158
166
|
end
|
|
159
167
|
end
|
|
@@ -28,6 +28,11 @@ module Lutaml
|
|
|
28
28
|
attribute :unique_items, :boolean
|
|
29
29
|
attribute :multiple_of, :float
|
|
30
30
|
attribute :const_value, :string
|
|
31
|
+
attribute :exclusive_minimum, :float
|
|
32
|
+
attribute :exclusive_maximum, :float
|
|
33
|
+
attribute :additional_properties, :boolean
|
|
34
|
+
attribute :content_type, :string
|
|
35
|
+
attribute :content_encoding, :string
|
|
31
36
|
|
|
32
37
|
json do
|
|
33
38
|
map "name", to: :name
|
|
@@ -54,6 +59,11 @@ module Lutaml
|
|
|
54
59
|
map "uniqueItems", to: :unique_items
|
|
55
60
|
map "multipleOf", to: :multiple_of
|
|
56
61
|
map "const", to: :const_value
|
|
62
|
+
map "exclusiveMinimum", to: :exclusive_minimum
|
|
63
|
+
map "exclusiveMaximum", to: :exclusive_maximum
|
|
64
|
+
map "additionalProperties", to: :additional_properties
|
|
65
|
+
map "contentMediaType", to: :content_type
|
|
66
|
+
map "contentEncoding", to: :content_encoding
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
end
|
|
@@ -15,6 +15,9 @@ module Lutaml
|
|
|
15
15
|
attribute :required, :string, collection: true
|
|
16
16
|
attribute :examples, :string, collection: true
|
|
17
17
|
attribute :source_json, :string, default: -> { "" }
|
|
18
|
+
attribute :dollar_schema, :string
|
|
19
|
+
attribute :dollar_id, :string
|
|
20
|
+
attribute :additional_properties, :boolean
|
|
18
21
|
|
|
19
22
|
json do
|
|
20
23
|
map "name", to: :name
|
|
@@ -26,6 +29,9 @@ module Lutaml
|
|
|
26
29
|
map "required", to: :required
|
|
27
30
|
map "examples", to: :examples
|
|
28
31
|
map "sourceJson", to: :source_json
|
|
32
|
+
map "$schema", to: :dollar_schema
|
|
33
|
+
map "$id", to: :dollar_id
|
|
34
|
+
map "additionalProperties", to: :additional_properties
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
end
|