lutaml-jsonschema 0.1.6 → 0.1.7
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/.rubocop_todo.yml +1 -1
- data/frontend/src/components/AppSidebar.vue +2 -1
- data/frontend/src/components/SchemaBuilder.vue +77 -2
- data/frontend/src/composables/useSchemaTypes.ts +59 -5
- data/frontend/src/composables/useSearch.ts +4 -1
- data/frontend/src/stores/uiStore.ts +2 -2
- data/frontend/src/types.ts +1 -0
- data/frontend/src/views/HomeView.vue +1 -1
- data/lib/lutaml/jsonschema/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31a527e5dd1e038e5c5fc36559c1d9a169581eb57f48ecafd87b55e941202154
|
|
4
|
+
data.tar.gz: 39f1bc49c2a3143fd73b156a9e560c83597540d3707906c4e1dd128cbd1f640d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0f41867a7d7220534d7e0f4301aa5b624fde7f1a60b6ce152a9ea3fad666e8fe76201de9256b9eb4209eb515927512b39346001a21975177227076953be0616
|
|
7
|
+
data.tar.gz: 672ea77bd554cd8a288cdd0b54a7d196efaf141861177af25d9225372b56fe4832caf3773b21b9e6131b72950aff899bdd732ce63847c74a8e9b23af160b18bd
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-05-
|
|
3
|
+
# on 2026-05-08 02:21:36 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -180,7 +180,8 @@ const searchResults = computed(() => {
|
|
|
180
180
|
return schemaStore.searchIndex.filter(entry =>
|
|
181
181
|
entry.name.toLowerCase().includes(q) ||
|
|
182
182
|
(entry.title && entry.title.toLowerCase().includes(q)) ||
|
|
183
|
-
entry.schemaName.toLowerCase().includes(q)
|
|
183
|
+
entry.schemaName.toLowerCase().includes(q) ||
|
|
184
|
+
(entry.description && entry.description.toLowerCase().includes(q)),
|
|
184
185
|
).slice(0, 15)
|
|
185
186
|
})
|
|
186
187
|
|
|
@@ -66,7 +66,9 @@
|
|
|
66
66
|
:value="field.rawValue"
|
|
67
67
|
:disabled="!field.included"
|
|
68
68
|
class="ctrl-number"
|
|
69
|
+
:class="{ 'ctrl-error': fieldError(field.prop.name) }"
|
|
69
70
|
@input="field.rawValue = ($event.target as HTMLInputElement).value"
|
|
71
|
+
@blur="validate(field.prop.name, field.rawValue, field.prop)"
|
|
70
72
|
/>
|
|
71
73
|
|
|
72
74
|
<!-- Number -->
|
|
@@ -76,11 +78,22 @@
|
|
|
76
78
|
:value="field.rawValue"
|
|
77
79
|
:disabled="!field.included"
|
|
78
80
|
class="ctrl-number"
|
|
81
|
+
:class="{ 'ctrl-error': fieldError(field.prop.name) }"
|
|
79
82
|
@input="field.rawValue = ($event.target as HTMLInputElement).value"
|
|
83
|
+
@blur="validate(field.prop.name, field.rawValue, field.prop)"
|
|
80
84
|
/>
|
|
81
85
|
|
|
82
|
-
<!-- Object without $ref -->
|
|
83
|
-
<
|
|
86
|
+
<!-- Object without $ref: editable JSON textarea -->
|
|
87
|
+
<textarea
|
|
88
|
+
v-else-if="isObjectProperty(field.prop)"
|
|
89
|
+
v-model="field.rawValue"
|
|
90
|
+
:disabled="!field.included"
|
|
91
|
+
class="ctrl-textarea"
|
|
92
|
+
:class="{ 'ctrl-error': fieldError(field.prop.name) }"
|
|
93
|
+
rows="2"
|
|
94
|
+
placeholder='{"key": "value"}'
|
|
95
|
+
@blur="validate(field.prop.name, field.rawValue, field.prop)"
|
|
96
|
+
/>
|
|
84
97
|
|
|
85
98
|
<!-- Array with itemsType -->
|
|
86
99
|
<div v-else-if="primaryType(field.prop.type) === 'array'" class="ctrl-array">
|
|
@@ -113,11 +126,17 @@
|
|
|
113
126
|
:value="field.rawValue"
|
|
114
127
|
:disabled="!field.included"
|
|
115
128
|
class="ctrl-text"
|
|
129
|
+
:class="{ 'ctrl-error': fieldError(field.prop.name) }"
|
|
116
130
|
@input="field.rawValue = ($event.target as HTMLInputElement).value"
|
|
131
|
+
@blur="validate(field.prop.name, field.rawValue, field.prop)"
|
|
117
132
|
/>
|
|
118
133
|
</div>
|
|
119
134
|
</div>
|
|
120
135
|
|
|
136
|
+
<div v-if="fieldError(field.prop.name)" class="field-error-hint">
|
|
137
|
+
{{ fieldError(field.prop.name) }}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
121
140
|
<div v-if="field.prop.description" class="field-desc-wrap">
|
|
122
141
|
<div class="field-desc text-secondary" :class="{ 'desc-collapsed': !descExpanded.has(field.prop.name) && field.prop.description.length > 200 }" v-html="renderInlineMarkdown(field.prop.description)"></div>
|
|
123
142
|
<button v-if="field.prop.description.length > 200" class="btn-see-more" @click="toggleDesc(field.prop.name)">
|
|
@@ -215,6 +234,7 @@ import {
|
|
|
215
234
|
isObjectProperty,
|
|
216
235
|
hasConstraints,
|
|
217
236
|
humanizeConstraints,
|
|
237
|
+
validateFieldValue,
|
|
218
238
|
} from '../composables/useSchemaTypes'
|
|
219
239
|
import {
|
|
220
240
|
createField,
|
|
@@ -256,6 +276,7 @@ const descExpanded = ref(new Set<string>())
|
|
|
256
276
|
const enumExpanded = ref(new Set<string>())
|
|
257
277
|
|
|
258
278
|
const MAX_ENUM_SHOW = 8
|
|
279
|
+
const validationErrors = ref<Map<string, string>>(new Map())
|
|
259
280
|
|
|
260
281
|
function toggleDesc(name: string) {
|
|
261
282
|
const s = new Set(descExpanded.value)
|
|
@@ -276,6 +297,18 @@ function toggleEnum(name: string) {
|
|
|
276
297
|
enumExpanded.value = s
|
|
277
298
|
}
|
|
278
299
|
|
|
300
|
+
function fieldError(name: string): string | undefined {
|
|
301
|
+
return validationErrors.value.get(name)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function validate(name: string, rawValue: string, prop: SpaProperty) {
|
|
305
|
+
const err = validateFieldValue(rawValue, prop)
|
|
306
|
+
const m = new Map(validationErrors.value)
|
|
307
|
+
if (err) m.set(name, err)
|
|
308
|
+
else m.delete(name)
|
|
309
|
+
validationErrors.value = m
|
|
310
|
+
}
|
|
311
|
+
|
|
279
312
|
const fields = ref<BuilderField[]>(props.properties.map(p => createField(p, props.required, props.schema, props.allSchemas)))
|
|
280
313
|
|
|
281
314
|
const sortedFields = computed(() => {
|
|
@@ -618,6 +651,21 @@ async function copyJson() {
|
|
|
618
651
|
border-color: var(--color-primary);
|
|
619
652
|
}
|
|
620
653
|
|
|
654
|
+
.ctrl-error {
|
|
655
|
+
border-color: var(--color-red) !important;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.ctrl-error:focus {
|
|
659
|
+
box-shadow: 0 0 0 2px rgba(179, 31, 36, 0.15);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.field-error-hint {
|
|
663
|
+
font-size: var(--text-xs);
|
|
664
|
+
color: var(--color-red);
|
|
665
|
+
margin-top: 2px;
|
|
666
|
+
margin-left: 22px;
|
|
667
|
+
}
|
|
668
|
+
|
|
621
669
|
.ctrl-toggle {
|
|
622
670
|
display: flex;
|
|
623
671
|
align-items: center;
|
|
@@ -649,6 +697,28 @@ async function copyJson() {
|
|
|
649
697
|
color: var(--text-muted);
|
|
650
698
|
}
|
|
651
699
|
|
|
700
|
+
.ctrl-textarea {
|
|
701
|
+
width: 100%;
|
|
702
|
+
padding: 3px 8px;
|
|
703
|
+
font-size: var(--text-sm);
|
|
704
|
+
font-family: var(--font-mono);
|
|
705
|
+
background: var(--bg-primary);
|
|
706
|
+
border: 1px solid var(--border-light);
|
|
707
|
+
border-radius: var(--radius-sm);
|
|
708
|
+
color: var(--text-primary);
|
|
709
|
+
resize: vertical;
|
|
710
|
+
min-height: 2em;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.ctrl-textarea:disabled {
|
|
714
|
+
opacity: 0.35;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.ctrl-textarea:focus {
|
|
718
|
+
outline: none;
|
|
719
|
+
border-color: var(--color-primary);
|
|
720
|
+
}
|
|
721
|
+
|
|
652
722
|
.chevron {
|
|
653
723
|
font-size: 10px;
|
|
654
724
|
transition: transform var(--transition-fast);
|
|
@@ -986,6 +1056,11 @@ async function copyJson() {
|
|
|
986
1056
|
text-decoration: underline;
|
|
987
1057
|
}
|
|
988
1058
|
|
|
1059
|
+
.json-block :deep(.jv-row:hover) {
|
|
1060
|
+
background: var(--bg-hover);
|
|
1061
|
+
border-radius: 2px;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
989
1064
|
:root[data-theme="dark"] .json-block :deep(.jv-key) { color: var(--color-primary-light); }
|
|
990
1065
|
:root[data-theme="dark"] .json-block :deep(.jv-string) { color: var(--color-teal); }
|
|
991
1066
|
|
|
@@ -16,10 +16,12 @@ export function primaryType(type?: string): string {
|
|
|
16
16
|
*/
|
|
17
17
|
export function displayType(prop: SpaProperty, resolvedTitle?: string): string {
|
|
18
18
|
const t = primaryType(prop.type)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (prop.
|
|
22
|
-
return
|
|
19
|
+
const isNullable = (prop.type || '').split(',').map(s => s.trim()).includes('null')
|
|
20
|
+
const suffix = isNullable ? ' | null' : ''
|
|
21
|
+
if (t === 'array' && prop.itemsType) return `array of ${prop.itemsType}${suffix}`
|
|
22
|
+
if (resolvedTitle && (t === 'object' || t === 'any') && prop.ref) return resolvedTitle + suffix
|
|
23
|
+
if (prop.format) return `${t} (${prop.format})${suffix}`
|
|
24
|
+
return t + suffix
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -69,6 +71,7 @@ export function initialValue(prop: SpaProperty): string {
|
|
|
69
71
|
case 'number': return '0.0'
|
|
70
72
|
case 'boolean': return 'false'
|
|
71
73
|
case 'string': return formatDefault(prop.format)
|
|
74
|
+
case 'object': return '{}'
|
|
72
75
|
default: return ''
|
|
73
76
|
}
|
|
74
77
|
}
|
|
@@ -158,7 +161,9 @@ export function parsePropertyValue(rawValue: string, prop: SpaProperty): unknown
|
|
|
158
161
|
const n = parseFloat(rawValue)
|
|
159
162
|
return isNaN(n) ? 0 : n
|
|
160
163
|
}
|
|
161
|
-
if (t === 'object' && !prop.ref)
|
|
164
|
+
if (t === 'object' && !prop.ref) {
|
|
165
|
+
try { return JSON.parse(rawValue) } catch { return {} }
|
|
166
|
+
}
|
|
162
167
|
return rawValue
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -261,3 +266,52 @@ export function humanizeConstraints(prop: SpaProperty): ConstraintChip[] {
|
|
|
261
266
|
|
|
262
267
|
return chips
|
|
263
268
|
}
|
|
269
|
+
|
|
270
|
+
export type ValidationError = string
|
|
271
|
+
|
|
272
|
+
export function validateFieldValue(rawValue: string, prop: SpaProperty): ValidationError | null {
|
|
273
|
+
const t = primaryType(prop.type)
|
|
274
|
+
|
|
275
|
+
if (t === 'string' || t === 'any' || !t) {
|
|
276
|
+
if (prop.minLength != null && rawValue.length < prop.minLength && rawValue.length > 0) {
|
|
277
|
+
return `Min ${prop.minLength} characters`
|
|
278
|
+
}
|
|
279
|
+
if (prop.maxLength != null && rawValue.length > prop.maxLength) {
|
|
280
|
+
return `Max ${prop.maxLength} characters`
|
|
281
|
+
}
|
|
282
|
+
if (prop.pattern && rawValue.length > 0) {
|
|
283
|
+
try {
|
|
284
|
+
if (!new RegExp(prop.pattern).test(rawValue)) {
|
|
285
|
+
return `Must match /${prop.pattern.length > 30 ? prop.pattern.slice(0, 30) + '…' : prop.pattern}/`
|
|
286
|
+
}
|
|
287
|
+
} catch { /* invalid regex, skip */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (t === 'integer' || t === 'number') {
|
|
292
|
+
const n = t === 'integer' ? parseInt(rawValue, 10) : parseFloat(rawValue)
|
|
293
|
+
if (rawValue !== '' && isNaN(n)) return 'Invalid number'
|
|
294
|
+
if (!isNaN(n)) {
|
|
295
|
+
if (prop.minimum != null && n < prop.minimum) return `Must be >= ${prop.minimum}`
|
|
296
|
+
if (prop.maximum != null && n > prop.maximum) return `Must be <= ${prop.maximum}`
|
|
297
|
+
if (prop.exclusiveMinimum != null && n <= prop.exclusiveMinimum) return `Must be > ${prop.exclusiveMinimum}`
|
|
298
|
+
if (prop.exclusiveMaximum != null && n >= prop.exclusiveMaximum) return `Must be < ${prop.exclusiveMaximum}`
|
|
299
|
+
if (prop.multipleOf != null && n % prop.multipleOf !== 0) return `Must be multiple of ${prop.multipleOf}`
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (t === 'array') {
|
|
304
|
+
if (rawValue.length > 0) {
|
|
305
|
+
try {
|
|
306
|
+
const arr = JSON.parse(rawValue)
|
|
307
|
+
if (Array.isArray(arr)) {
|
|
308
|
+
if (prop.minItems != null && arr.length < prop.minItems) return `Min ${prop.minItems} items`
|
|
309
|
+
if (prop.maxItems != null && arr.length > prop.maxItems) return `Max ${prop.maxItems} items`
|
|
310
|
+
if (prop.uniqueItems && new Set(arr).size !== arr.length) return 'Items must be unique'
|
|
311
|
+
}
|
|
312
|
+
} catch { /* not JSON, skip */ }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
@@ -8,6 +8,7 @@ interface SearchResult {
|
|
|
8
8
|
name: string
|
|
9
9
|
rawName: string
|
|
10
10
|
schemaName: string
|
|
11
|
+
description?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function useSearch() {
|
|
@@ -26,6 +27,7 @@ export function useSearch() {
|
|
|
26
27
|
name: entry.title || entry.name,
|
|
27
28
|
rawName: entry.name,
|
|
28
29
|
schemaName: entry.schemaName,
|
|
30
|
+
description: entry.description,
|
|
29
31
|
}))
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -41,7 +43,8 @@ export function useSearch() {
|
|
|
41
43
|
|
|
42
44
|
results.value = entries.filter(e =>
|
|
43
45
|
e.name.toLowerCase().includes(q) ||
|
|
44
|
-
e.schemaName.toLowerCase().includes(q)
|
|
46
|
+
e.schemaName.toLowerCase().includes(q) ||
|
|
47
|
+
(e.description && e.description.toLowerCase().includes(q))
|
|
45
48
|
).slice(0, 50)
|
|
46
49
|
|
|
47
50
|
isSearching.value = false
|
|
@@ -8,7 +8,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
8
8
|
const resolvedTheme = ref<'light' | 'dark'>('light')
|
|
9
9
|
const sidebarCollapsed = ref(false)
|
|
10
10
|
const detailPanelOpen = ref(false)
|
|
11
|
-
const activePanelTab = ref<'overview' | '
|
|
11
|
+
const activePanelTab = ref<'overview' | 'properties' | 'examples'>('overview')
|
|
12
12
|
const searchOpen = ref(false)
|
|
13
13
|
const expandedSchemaNames = ref<Set<string>>(new Set())
|
|
14
14
|
|
|
@@ -47,7 +47,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
47
47
|
function toggleSidebar() { sidebarCollapsed.value = !sidebarCollapsed.value }
|
|
48
48
|
function openDetailPanel() { detailPanelOpen.value = true }
|
|
49
49
|
function closeDetailPanel() { detailPanelOpen.value = false }
|
|
50
|
-
function setPanelTab(tab: 'overview' | '
|
|
50
|
+
function setPanelTab(tab: 'overview' | 'properties' | 'examples') { activePanelTab.value = tab }
|
|
51
51
|
function openSearch() { searchOpen.value = true }
|
|
52
52
|
function closeSearch() { searchOpen.value = false }
|
|
53
53
|
|
data/frontend/src/types.ts
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-jsonschema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|