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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7489a2b5deaa74c751691afdf57939c046e29d6a853774462ce5587e05dcbcf3
4
- data.tar.gz: fca882189c10df105bc30df80c5fe34cc0108ca82b87420e638170ec7a62d1e3
3
+ metadata.gz: 31a527e5dd1e038e5c5fc36559c1d9a169581eb57f48ecafd87b55e941202154
4
+ data.tar.gz: 39f1bc49c2a3143fd73b156a9e560c83597540d3707906c4e1dd128cbd1f640d
5
5
  SHA512:
6
- metadata.gz: b27dd39b382a08e4fc1aa58ae1b9803ffbeee75fc3db6e3df0261871ce43d3aabfbe7cc863207b190c5a88e52212fca6f43acd40506d4f82088094a0ffd57ebf
7
- data.tar.gz: d3af571ef80959e85a9c3e7ccaebb0a5e721d94348616a873edd25f6c488c596542ca0a8a836b94917a34bfdbc675cc2dc50f844995d7c5a055fc44348987926
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-07 10:44:46 UTC using RuboCop version 1.86.1.
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
- <span v-else-if="isObjectProperty(field.prop)" class="ctrl-static">{"..."}</span>
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
- if (t === 'array' && prop.itemsType) return `array of ${prop.itemsType}`
20
- if (resolvedTitle && (t === 'object' || t === 'any') && prop.ref) return resolvedTitle
21
- if (prop.format) return `${t} (${prop.format})`
22
- return t
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) return {}
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' | 'definition'>('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' | 'definition') { activePanelTab.value = tab }
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
 
@@ -77,6 +77,7 @@ export interface SpaSchema {
77
77
  export interface SpaSearchEntry {
78
78
  name: string
79
79
  title?: string
80
+ description?: string
80
81
  type: string
81
82
  schemaName: string
82
83
  }
@@ -166,7 +166,7 @@
166
166
  class="btn btn-outline btn-sm"
167
167
  @click="downloadAllSchemas"
168
168
  >
169
- Download All Schemas (.zip)
169
+ Download All Schemas
170
170
  </button>
171
171
  </div>
172
172
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Jsonschema
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.7"
6
6
  end
7
7
  end
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.6
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-07 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json