lutaml-jsonschema 0.1.2 → 0.1.3
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/SchemaBuilder.vue +41 -14
- data/frontend/src/composables/useSchemaTypes.ts +48 -1
- data/frontend/src/types.ts +5 -0
- data/lib/lutaml/jsonschema/spa/spa_builder.rb +6 -1
- data/lib/lutaml/jsonschema/spa/spa_property.rb +10 -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: fcb6ad4f8e4b98d7610fb58a9556af739f9c3c1ba348de4faf9dfdd83b39f8fc
|
|
4
|
+
data.tar.gz: f772f3eb0c0e204d3217ff5c72377ae4a6568daf29015f143eafd7e22d7ed6a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b8238500b470d461a05119853f1aad9049c2dd92ced3afb7c2c072ccca9e890f0c08a1ac86600dbdd42df47bc21cc2c5893eecf4de4bb0f59e2cdac62897c79
|
|
7
|
+
data.tar.gz: 7ec354115ae70763afff045959ba6a2fce37d9f14c1221f254c653a241fc0bae9f2696e3490eb2e5f23b491f140c554b6ec83615d00481529130e9f1af7f636e
|
|
@@ -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', () => {
|
|
@@ -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 -->
|
|
@@ -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,
|
|
@@ -236,6 +227,23 @@ const outputObj = computed(() => {
|
|
|
236
227
|
|
|
237
228
|
watch(outputObj, (v) => { emit('update:json', v) }, { immediate: true, deep: true })
|
|
238
229
|
|
|
230
|
+
const highlightedJson = computed(() => syntaxHighlight(outputJson.value))
|
|
231
|
+
|
|
232
|
+
function syntaxHighlight(json: string): string {
|
|
233
|
+
return json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
234
|
+
.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
|
|
235
|
+
let cls = 'json-number'
|
|
236
|
+
if (/^"/.test(match)) {
|
|
237
|
+
cls = /:$/.test(match) ? 'json-key' : 'json-string'
|
|
238
|
+
} else if (/true|false/.test(match)) {
|
|
239
|
+
cls = 'json-boolean'
|
|
240
|
+
} else if (/null/.test(match)) {
|
|
241
|
+
cls = 'json-null'
|
|
242
|
+
}
|
|
243
|
+
return `<span class="${cls}">${match}</span>`
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
239
247
|
async function copyJson() {
|
|
240
248
|
try {
|
|
241
249
|
await navigator.clipboard.writeText(outputJson.value)
|
|
@@ -463,6 +471,16 @@ async function copyJson() {
|
|
|
463
471
|
border-radius: var(--radius-sm);
|
|
464
472
|
}
|
|
465
473
|
|
|
474
|
+
.constraint-chip.chip-pattern {
|
|
475
|
+
font-family: var(--font-mono);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.constraint-chip.chip-locked {
|
|
479
|
+
color: var(--color-orange);
|
|
480
|
+
background: var(--color-orange-alpha);
|
|
481
|
+
font-weight: 500;
|
|
482
|
+
}
|
|
483
|
+
|
|
466
484
|
.nested-section {
|
|
467
485
|
margin-left: 22px;
|
|
468
486
|
margin-top: var(--space-2);
|
|
@@ -535,6 +553,15 @@ async function copyJson() {
|
|
|
535
553
|
color: var(--text-primary);
|
|
536
554
|
}
|
|
537
555
|
|
|
556
|
+
.json-block :deep(.json-key) { color: var(--color-primary-dark); }
|
|
557
|
+
.json-block :deep(.json-string) { color: var(--color-green); }
|
|
558
|
+
.json-block :deep(.json-number) { color: var(--color-orange); }
|
|
559
|
+
.json-block :deep(.json-boolean) { color: var(--color-accent); }
|
|
560
|
+
.json-block :deep(.json-null) { color: var(--text-muted); }
|
|
561
|
+
|
|
562
|
+
:root[data-theme="dark"] .json-block :deep(.json-key) { color: var(--color-primary-light); }
|
|
563
|
+
:root[data-theme="dark"] .json-block :deep(.json-string) { color: var(--color-teal); }
|
|
564
|
+
|
|
538
565
|
.empty-hint {
|
|
539
566
|
padding: var(--space-8);
|
|
540
567
|
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 {
|
|
@@ -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
|
|
@@ -154,6 +154,11 @@ all_required = root_schema.required)
|
|
|
154
154
|
unique_items: resolved.unique_items,
|
|
155
155
|
multiple_of: resolved.multiple_of,
|
|
156
156
|
const_value: resolved.const,
|
|
157
|
+
exclusive_minimum: resolved.exclusive_minimum,
|
|
158
|
+
exclusive_maximum: resolved.exclusive_maximum,
|
|
159
|
+
additional_properties: resolved.additional_properties,
|
|
160
|
+
content_type: resolved.content_type,
|
|
161
|
+
content_encoding: resolved.content_encoding,
|
|
157
162
|
)
|
|
158
163
|
end
|
|
159
164
|
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
|