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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80d56fbc558fd39c49bc397b01a2cd3cdf5d19b475483fbe50968b6cc7c8e6f6
4
- data.tar.gz: 6cee07bff101bcbd7ed60639af85d99376c2b899c23be949a73401ecda5d6cb8
3
+ metadata.gz: a31e1dc445c583474f10cf52d8468991193679ab428af9d46a0f760492373a2d
4
+ data.tar.gz: 7dfe7fd154e030c64a9670b4d0c694b83e397fa8cff9d08d83222869bbfc1917
5
5
  SHA512:
6
- metadata.gz: fd8be87137f96a96c1cfc5ebc608a8cdc8a6726541ab1812e99e25f502db980efb83a464baa3e43b92520b2e25bacbb396c91792778cca6235efeb021ba3f9e3
7
- data.tar.gz: 479d21b5e4df3626fe75c6c4c88ee61f16db6467b9774e024c0f4c6e120c6aa1e86a599819b3ff766896d2a867593aa7ca0b080142e4307aeae224da245f2d55
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 fields" :key="field.prop.name" class="field-row">
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-if="field.prop.pattern" class="constraint-chip font-mono">/{{ field.prop.pattern }}/</span>
119
- <span v-if="field.prop.minimum != null" class="constraint-chip">min: {{ field.prop.minimum }}</span>
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="!fields.length" class="empty-hint">
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>{{ outputJson }}</code></pre>
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
+ }
@@ -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
- <button
10
- v-if="schemaStore.selectedSchema.sourceJson"
11
- class="btn btn-outline btn-sm"
12
- @click="downloadSchema(schemaStore.selectedSchema)"
13
- >
14
- Download JSON
15
- </button>
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">&middot; {{ schemaStore.selectedSchema.required.length }} required</span>
23
29
  <span v-if="schemaStore.selectedSchema.definitions.length" class="text-muted">&middot; {{ 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
- <h2 class="section-heading">Definitions</h2>
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 }} properties</span>
136
- <span>{{ schema.definitions.length }} definitions</span>
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Jsonschema
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-jsonschema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.