lutaml-jsonschema 0.1.5 → 0.1.6

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: a480735cb35e5d992a010e0f3f51d030a53041d67be3e04b748dfca2b79e2cf2
4
- data.tar.gz: cf7332baaff5e9ceda9171e8baf56a40443f8946527542c8a18d40141899d68e
3
+ metadata.gz: 7489a2b5deaa74c751691afdf57939c046e29d6a853774462ce5587e05dcbcf3
4
+ data.tar.gz: fca882189c10df105bc30df80c5fe34cc0108ca82b87420e638170ec7a62d1e3
5
5
  SHA512:
6
- metadata.gz: 8511a93b9362c07dbd1403f65a00c2bfeccd0f23b50b35c52ec257a4c22d4911f3578e67bfc7ae01c49f37ee2ed927257995d8ebfb7ec505484cfee61b0d8de2
7
- data.tar.gz: d5f8b8b703ae21083dfd3b566825be66efc3901f94711d7188a78e3967754d4f6ef185067e967426dd69dfdedc617baee17ff69752ad7faf4ee0d11c8f55b5a6
6
+ metadata.gz: b27dd39b382a08e4fc1aa58ae1b9803ffbeee75fc3db6e3df0261871ce43d3aabfbe7cc863207b190c5a88e52212fca6f43acd40506d4f82088094a0ffd57ebf
7
+ data.tar.gz: d3af571ef80959e85a9c3e7ccaebb0a5e721d94348616a873edd25f6c488c596542ca0a8a836b94917a34bfdbc675cc2dc50f844995d7c5a055fc44348987926
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 03:03:38 UTC using RuboCop version 1.86.1.
3
+ # on 2026-05-07 10:44:46 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
@@ -11,15 +11,7 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'lutaml-jsonschema.gemspec'
13
13
 
14
- # Offense count: 2
15
- # This cop supports safe autocorrection (--autocorrect).
16
- # Configuration parameters: EnforcedStyle, IndentationWidth.
17
- # SupportedStyles: with_first_argument, with_fixed_indentation
18
- Layout/ArgumentAlignment:
19
- Exclude:
20
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
21
-
22
- # Offense count: 2
14
+ # Offense count: 1
23
15
  # This cop supports safe autocorrection (--autocorrect).
24
16
  # Configuration parameters: EnforcedStyleAlignWith.
25
17
  # SupportedStylesAlignWith: either, start_of_block, start_of_line
@@ -27,21 +19,7 @@ Layout/BlockAlignment:
27
19
  Exclude:
28
20
  - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
29
21
 
30
- # Offense count: 1
31
- # This cop supports safe autocorrection (--autocorrect).
32
- Layout/BlockEndNewline:
33
- Exclude:
34
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
35
-
36
- # Offense count: 2
37
- # This cop supports safe autocorrection (--autocorrect).
38
- # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
39
- # SupportedStylesAlignWith: start_of_line, relative_to_receiver
40
- Layout/IndentationWidth:
41
- Exclude:
42
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
43
-
44
- # Offense count: 31
22
+ # Offense count: 28
45
23
  # This cop supports safe autocorrection (--autocorrect).
46
24
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
47
25
  # URISchemes: http, https
@@ -61,13 +39,6 @@ Layout/LineLength:
61
39
  - 'spec/lutaml/schema_set_resolution_spec.rb'
62
40
  - 'spec/lutaml/spa_builder_spec.rb'
63
41
 
64
- # Offense count: 2
65
- # This cop supports safe autocorrection (--autocorrect).
66
- # Configuration parameters: AllowInHeredoc.
67
- Layout/TrailingWhitespace:
68
- Exclude:
69
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
70
-
71
42
  # Offense count: 2
72
43
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
73
44
  Lint/DuplicateBranch:
@@ -136,25 +107,8 @@ Naming/PredicateMethod:
136
107
  Exclude:
137
108
  - 'lib/lutaml/jsonschema/schema_set.rb'
138
109
 
139
- # Offense count: 2
140
- # This cop supports safe autocorrection (--autocorrect).
141
- # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
142
- # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
143
- # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
144
- # FunctionalMethods: let, let!, subject, watch
145
- # AllowedMethods: lambda, proc, it
146
- Style/BlockDelimiters:
147
- Exclude:
148
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
149
-
150
110
  # Offense count: 2
151
111
  # This cop supports unsafe autocorrection (--autocorrect-all).
152
112
  Style/IdenticalConditionalBranches:
153
113
  Exclude:
154
114
  - 'spec/lutaml/spa_builder_spec.rb'
155
-
156
- # Offense count: 1
157
- # This cop supports unsafe autocorrection (--autocorrect-all).
158
- Style/MapIntoArray:
159
- Exclude:
160
- - 'lib/lutaml/jsonschema/spa/spa_builder.rb'
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { renderInlineMarkdown } from '../composables/useMarkdownLite'
3
+
4
+ describe('renderInlineMarkdown', () => {
5
+ it('returns empty for empty string', () => {
6
+ expect(renderInlineMarkdown('')).toBe('')
7
+ })
8
+
9
+ it('renders bold text', () => {
10
+ expect(renderInlineMarkdown('**bold**')).toBe('<strong>bold</strong>')
11
+ })
12
+
13
+ it('renders italic text', () => {
14
+ expect(renderInlineMarkdown('*italic*')).toBe('<em>italic</em>')
15
+ })
16
+
17
+ it('renders inline code', () => {
18
+ expect(renderInlineMarkdown('`code`')).toBe('<code class="md-code">code</code>')
19
+ })
20
+
21
+ it('renders links', () => {
22
+ const result = renderInlineMarkdown('[click](https://example.com)')
23
+ expect(result).toContain('<a href="https://example.com"')
24
+ expect(result).toContain('class="md-link"')
25
+ expect(result).toContain('>click</a>')
26
+ })
27
+
28
+ it('escapes HTML', () => {
29
+ expect(renderInlineMarkdown('<b>no</b>')).toBe('&lt;b&gt;no&lt;/b&gt;')
30
+ })
31
+
32
+ it('returns plain text unchanged', () => {
33
+ expect(renderInlineMarkdown('hello world')).toBe('hello world')
34
+ })
35
+
36
+ it('handles mixed formatting', () => {
37
+ const result = renderInlineMarkdown('Use `method` for **special** cases.')
38
+ expect(result).toContain('<code class="md-code">method</code>')
39
+ expect(result).toContain('<strong>special</strong>')
40
+ })
41
+ })
@@ -38,8 +38,16 @@ describe('primaryType', () => {
38
38
  })
39
39
 
40
40
  describe('displayType', () => {
41
- it('shows array<itemsType> for arrays', () => {
42
- expect(displayType(prop({ type: 'array', itemsType: 'string' }))).toBe('array<string>')
41
+ it('shows "array of X" for arrays', () => {
42
+ expect(displayType(prop({ type: 'array', itemsType: 'string' }))).toBe('array of string')
43
+ })
44
+
45
+ it('shows resolved title for $ref props', () => {
46
+ expect(displayType(prop({ type: 'object', ref: '#/definitions/Addr' }), 'Address')).toBe('Address')
47
+ })
48
+
49
+ it('ignores resolved title when no ref', () => {
50
+ expect(displayType(prop({ type: 'object' }), 'Address')).toBe('object')
43
51
  })
44
52
 
45
53
  it('shows type (format) when format is set', () => {
@@ -225,24 +233,34 @@ describe('hasConstraints', () => {
225
233
  })
226
234
 
227
235
  describe('humanizeConstraints', () => {
228
- it('returns string range constraint', () => {
236
+ it('returns string range constraint with bracket notation', () => {
229
237
  const chips = humanizeConstraints(prop({ type: 'string', minLength: 1, maxLength: 100 }))
230
- expect(chips).toEqual([{ label: '1..100 characters' }])
238
+ expect(chips).toEqual([{ label: '[ 1 .. 100 ] characters' }])
231
239
  })
232
240
 
233
- it('returns numeric >= and <= constraints', () => {
241
+ it('returns non-empty for minLength === 1', () => {
242
+ const chips = humanizeConstraints(prop({ type: 'string', minLength: 1 }))
243
+ expect(chips).toEqual([{ label: 'non-empty' }])
244
+ })
245
+
246
+ it('returns numeric range constraint with bracket notation', () => {
234
247
  const chips = humanizeConstraints(prop({ type: 'integer', minimum: 0, maximum: 100 }))
235
- expect(chips.map(c => c.label)).toEqual(['>= 0', '<= 100'])
248
+ expect(chips.map(c => c.label)).toEqual(['[ 0 .. 100 ]'])
236
249
  })
237
250
 
238
- it('returns exclusive bounds with > and <', () => {
251
+ it('returns exclusive bounds combined as range', () => {
239
252
  const chips = humanizeConstraints(prop({ type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 100 }))
240
- expect(chips.map(c => c.label)).toEqual(['> 0', '< 100'])
253
+ expect(chips.map(c => c.label)).toEqual(['( 0 .. 100 )'])
254
+ })
255
+
256
+ it('returns inclusive range combined with exclusive modifier', () => {
257
+ const chips = humanizeConstraints(prop({ type: 'integer', minimum: 0, maximum: 100, exclusiveMinimum: 0 }))
258
+ expect(chips.map(c => c.label)).toEqual(['( 0 .. 100 ]'])
241
259
  })
242
260
 
243
- it('returns array range constraint', () => {
261
+ it('returns array range constraint with bracket notation', () => {
244
262
  const chips = humanizeConstraints(prop({ type: 'array', minItems: 1, maxItems: 10 }))
245
- expect(chips).toEqual([{ label: '1..10 items' }])
263
+ expect(chips).toEqual([{ label: '[ 1 .. 10 ] items' }])
246
264
  })
247
265
 
248
266
  it('returns unique for uniqueItems', () => {
@@ -275,6 +293,26 @@ describe('humanizeConstraints', () => {
275
293
  expect(chips).toEqual([])
276
294
  })
277
295
 
296
+ it('returns "non-empty" for minLength 1', () => {
297
+ const chips = humanizeConstraints(prop({ type: 'string', minLength: 1 }))
298
+ expect(chips.map(c => c.label)).toEqual(['non-empty'])
299
+ })
300
+
301
+ it('returns "= N characters" when min equals max', () => {
302
+ const chips = humanizeConstraints(prop({ type: 'string', minLength: 10, maxLength: 10 }))
303
+ expect(chips.map(c => c.label)).toEqual(['= 10 characters'])
304
+ })
305
+
306
+ it('returns "= N items" when minItems equals maxItems', () => {
307
+ const chips = humanizeConstraints(prop({ type: 'array', minItems: 3, maxItems: 3 }))
308
+ expect(chips.map(c => c.label)).toEqual(['= 3 items'])
309
+ })
310
+
311
+ it('returns decimal places for small multipleOf', () => {
312
+ const chips = humanizeConstraints(prop({ type: 'number', multipleOf: 0.01 }))
313
+ expect(chips.map(c => c.label)).toEqual(['decimal places <= 2'])
314
+ })
315
+
278
316
  it('returns contentMediaType and contentEncoding', () => {
279
317
  const chips = humanizeConstraints(prop({ contentMediaType: 'text/html', contentEncoding: 'base64' }))
280
318
  expect(chips.map(c => c.label)).toEqual(['content-type: text/html', 'encoding: base64'])
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <aside class="sidebar" :class="{ collapsed: uiStore.sidebarCollapsed }">
2
+ <aside class="sidebar" :class="{ collapsed: uiStore.sidebarCollapsed }" :aria-label="`${schemaStore.metadata?.title || 'JSON Schema Docs'} navigation`">
3
3
  <div class="sidebar-content">
4
4
  <!-- Branding -->
5
5
  <div class="sidebar-branding">
@@ -44,18 +44,21 @@
44
44
  type="text"
45
45
  class="search-input"
46
46
  placeholder="Search schemas, definitions..."
47
+ aria-label="Search schemas and definitions"
48
+ @keydown="handleSearchKey"
47
49
  />
48
- <button v-if="searchQuery" class="search-clear" @click="searchQuery = ''">
50
+ <button v-if="searchQuery" class="search-clear" aria-label="Clear search" @click="searchQuery = ''">
49
51
  <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
50
52
  <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
51
53
  </svg>
52
54
  </button>
53
55
  </div>
54
- <div v-if="searchQuery && searchResults.length" class="search-results">
56
+ <div v-if="debouncedQuery && searchResults.length" class="search-results">
55
57
  <button
56
- v-for="result in searchResults"
58
+ v-for="(result, idx) in searchResults"
57
59
  :key="`${result.type}:${result.name}@${result.schemaName}`"
58
60
  class="search-result-item"
61
+ :class="{ active: idx === activeResultIdx }"
59
62
  @click="goToSearchResult(result)"
60
63
  >
61
64
  <span class="badge" :class="resultBadgeClass(result.type)">{{ resultTypeLabel(result.type) }}</span>
@@ -63,6 +66,9 @@
63
66
  <span class="search-result-schema text-muted">{{ result.schemaName }}</span>
64
67
  </button>
65
68
  </div>
69
+ <div v-else-if="debouncedQuery && !searchResults.length && searchQuery" class="search-no-results text-muted">
70
+ No results found
71
+ </div>
66
72
  <div v-else class="schema-tree">
67
73
  <div
68
74
  v-for="schema in schemaStore.schemas"
@@ -72,7 +78,12 @@
72
78
  <div
73
79
  class="schema-node-header"
74
80
  :class="{ active: schema.name === schemaStore.selectedSchemaName }"
81
+ role="treeitem"
82
+ :aria-expanded="uiStore.isSchemaExpanded(schema.name)"
83
+ tabindex="0"
75
84
  @click="toggleAndSelect(schema.name)"
85
+ @keydown.enter="toggleAndSelect(schema.name)"
86
+ @keydown.space.prevent="toggleAndSelect(schema.name)"
76
87
  >
77
88
  <svg width="12" height="12" viewBox="0 0 12 12" fill="none" :class="{ expanded: uiStore.isSchemaExpanded(schema.name) }">
78
89
  <path d="M4 3l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -139,7 +150,7 @@
139
150
  </template>
140
151
 
141
152
  <script setup lang="ts">
142
- import { ref, computed } from 'vue'
153
+ import { ref, computed, watch } from 'vue'
143
154
  import { useSchemaStore } from '../stores/schemaStore'
144
155
  import { useUiStore } from '../stores/uiStore'
145
156
  import type { SpaSearchEntry } from '../types'
@@ -148,9 +159,23 @@ const schemaStore = useSchemaStore()
148
159
  const uiStore = useUiStore()
149
160
 
150
161
  const searchQuery = ref('')
162
+ const debouncedQuery = ref('')
163
+ const activeResultIdx = ref(-1)
164
+
165
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
166
+
167
+ watch(searchQuery, (q) => {
168
+ if (debounceTimer) clearTimeout(debounceTimer)
169
+ if (!q.trim()) {
170
+ debouncedQuery.value = ''
171
+ activeResultIdx.value = -1
172
+ return
173
+ }
174
+ debounceTimer = setTimeout(() => { debouncedQuery.value = q }, 300)
175
+ })
151
176
 
152
177
  const searchResults = computed(() => {
153
- const q = searchQuery.value.trim().toLowerCase()
178
+ const q = debouncedQuery.value.trim().toLowerCase()
154
179
  if (!q) return []
155
180
  return schemaStore.searchIndex.filter(entry =>
156
181
  entry.name.toLowerCase().includes(q) ||
@@ -159,6 +184,25 @@ const searchResults = computed(() => {
159
184
  ).slice(0, 15)
160
185
  })
161
186
 
187
+ watch(searchResults, () => { activeResultIdx.value = -1 })
188
+
189
+ function handleSearchKey(event: KeyboardEvent) {
190
+ if (event.key === 'ArrowDown') {
191
+ activeResultIdx.value = Math.min(activeResultIdx.value + 1, searchResults.value.length - 1)
192
+ event.preventDefault()
193
+ } else if (event.key === 'ArrowUp') {
194
+ activeResultIdx.value = Math.max(0, activeResultIdx.value - 1)
195
+ event.preventDefault()
196
+ } else if (event.key === 'Enter') {
197
+ const result = searchResults.value[activeResultIdx.value]
198
+ if (result) goToSearchResult(result)
199
+ activeResultIdx.value = -1
200
+ } else if (event.key === 'Escape') {
201
+ searchQuery.value = ''
202
+ activeResultIdx.value = -1
203
+ }
204
+ }
205
+
162
206
  function goToSearchResult(result: SpaSearchEntry) {
163
207
  schemaStore.selectSchema(result.schemaName)
164
208
  if (result.type === 'definition') {
@@ -167,6 +211,7 @@ function goToSearchResult(result: SpaSearchEntry) {
167
211
  schemaStore.selectProperty(result.name)
168
212
  }
169
213
  searchQuery.value = ''
214
+ debouncedQuery.value = ''
170
215
  uiStore.closeDetailPanel()
171
216
  }
172
217
 
@@ -544,10 +589,17 @@ function selectDefinition(schemaName: string, defName: string) {
544
589
  transition: background var(--transition-fast);
545
590
  }
546
591
 
547
- .search-result-item:hover {
592
+ .search-result-item:hover,
593
+ .search-result-item.active {
548
594
  background: var(--bg-hover);
549
595
  }
550
596
 
597
+ .search-no-results {
598
+ padding: var(--space-3) var(--space-2);
599
+ font-size: var(--text-xs);
600
+ text-align: center;
601
+ }
602
+
551
603
  .search-result-name {
552
604
  flex: 1;
553
605
  color: var(--text-primary);
@@ -629,4 +681,19 @@ function selectDefinition(schemaName: string, defName: string) {
629
681
  .footer-text a:hover {
630
682
  text-decoration: underline;
631
683
  }
684
+
685
+ @media (max-width: 768px) {
686
+ .sidebar {
687
+ position: fixed;
688
+ top: 0;
689
+ left: 0;
690
+ bottom: 0;
691
+ z-index: 40;
692
+ box-shadow: var(--shadow-lg);
693
+ }
694
+ .sidebar.collapsed {
695
+ transform: translateX(-100%);
696
+ width: 280px;
697
+ }
698
+ }
632
699
  </style>