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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80d56fbc558fd39c49bc397b01a2cd3cdf5d19b475483fbe50968b6cc7c8e6f6
4
- data.tar.gz: 6cee07bff101bcbd7ed60639af85d99376c2b899c23be949a73401ecda5d6cb8
3
+ metadata.gz: fcb6ad4f8e4b98d7610fb58a9556af739f9c3c1ba348de4faf9dfdd83b39f8fc
4
+ data.tar.gz: f772f3eb0c0e204d3217ff5c72377ae4a6568daf29015f143eafd7e22d7ed6a6
5
5
  SHA512:
6
- metadata.gz: fd8be87137f96a96c1cfc5ebc608a8cdc8a6726541ab1812e99e25f502db980efb83a464baa3e43b92520b2e25bacbb396c91792778cca6235efeb021ba3f9e3
7
- data.tar.gz: 479d21b5e4df3626fe75c6c4c88ee61f16db6467b9774e024c0f4c6e120c6aa1e86a599819b3ff766896d2a867593aa7ca0b080142e4307aeae224da245f2d55
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-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 -->
@@ -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,
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
+ }
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Jsonschema
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.3"
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.