odin-foundation 1.2.0 → 1.2.1
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/lib/odin/types/schema.rb +10 -4
- data/lib/odin/validation/schema_parser.rb +70 -3
- data/lib/odin/validation/validator.rb +64 -2
- data/lib/odin/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: fd44363f49452d2888d4c6adee66875fc13d6cd77e0dac6650028aabbcace2af
|
|
4
|
+
data.tar.gz: fa3f640fe709656ffa5f3436ca7f79d287c2d7a2cf5d9470c4fb7d8d89659b4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8d134498b241ad33fa0936e95b4f5cefcb3642e97ec099509e23d75fcdb2e11df55c8d072e8989557944c0e7ae2ae38abf64e8cff85a76d8be311406fd22dfc
|
|
7
|
+
data.tar.gz: dfc5f7a283b5b8714bbb8a9ba09535a49a368dc178dfc8a7b678fee2a8a88d70e7ff8e3347d41ce310f14bccfb083c25e4ca8e3277be7abe20681932d3abbcec
|
data/lib/odin/types/schema.rb
CHANGED
|
@@ -154,11 +154,12 @@ module Odin
|
|
|
154
154
|
# Schema type definition (named object structure)
|
|
155
155
|
class SchemaType
|
|
156
156
|
attr_reader :name, :fields, :namespace, :composition,
|
|
157
|
-
:base_type, :constraints, :intersection_types, :parent_types
|
|
157
|
+
:base_type, :constraints, :intersection_types, :parent_types,
|
|
158
|
+
:arrays
|
|
158
159
|
|
|
159
160
|
def initialize(name:, fields: {}, namespace: nil, composition: nil,
|
|
160
161
|
base_type: nil, constraints: nil, intersection_types: nil,
|
|
161
|
-
parent_types: nil)
|
|
162
|
+
parent_types: nil, arrays: {})
|
|
162
163
|
@name = name.freeze
|
|
163
164
|
@fields = fields.freeze
|
|
164
165
|
@namespace = namespace&.freeze
|
|
@@ -167,22 +168,27 @@ module Odin
|
|
|
167
168
|
@constraints = (constraints || {}).freeze
|
|
168
169
|
@intersection_types = intersection_types&.freeze
|
|
169
170
|
@parent_types = parent_types&.freeze
|
|
171
|
+
# Array-of-object entries declared inside the type, keyed by array name.
|
|
172
|
+
@arrays = (arrays || {}).freeze
|
|
170
173
|
freeze
|
|
171
174
|
end
|
|
172
175
|
end
|
|
173
176
|
|
|
174
177
|
# Schema array definition
|
|
175
178
|
class SchemaArray
|
|
176
|
-
attr_reader :path, :item_fields, :min_items, :max_items, :unique, :columns
|
|
179
|
+
attr_reader :path, :item_fields, :min_items, :max_items, :unique, :columns,
|
|
180
|
+
:item_type_ref
|
|
177
181
|
|
|
178
182
|
def initialize(path:, item_fields: {}, min_items: nil, max_items: nil,
|
|
179
|
-
unique: false, columns: nil)
|
|
183
|
+
unique: false, columns: nil, item_type_ref: nil)
|
|
180
184
|
@path = path.freeze
|
|
181
185
|
@item_fields = item_fields.freeze
|
|
182
186
|
@min_items = min_items
|
|
183
187
|
@max_items = max_items
|
|
184
188
|
@unique = unique
|
|
185
189
|
@columns = columns&.freeze
|
|
190
|
+
# For an `arr[] = @type` declaration, the entry fields come from this type.
|
|
191
|
+
@item_type_ref = item_type_ref&.freeze
|
|
186
192
|
freeze
|
|
187
193
|
end
|
|
188
194
|
end
|
|
@@ -73,6 +73,8 @@ module Odin
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
extract_type_arrays
|
|
77
|
+
|
|
76
78
|
Types::OdinSchema.new(
|
|
77
79
|
metadata: @metadata,
|
|
78
80
|
types: @types,
|
|
@@ -83,6 +85,62 @@ module Odin
|
|
|
83
85
|
)
|
|
84
86
|
end
|
|
85
87
|
|
|
88
|
+
# Pull array-of-object entry fields out of each type's flat field map. A key
|
|
89
|
+
# `arr[]` (whose value is a type reference) becomes an array with an entry
|
|
90
|
+
# type ref; keys `arr[].field` become an array whose item fields are those
|
|
91
|
+
# fields. Matching keys are removed from the type's fields.
|
|
92
|
+
def extract_type_arrays
|
|
93
|
+
@types.each do |type_name, type|
|
|
94
|
+
builders = {}
|
|
95
|
+
remaining_fields = {}
|
|
96
|
+
|
|
97
|
+
type.fields.each do |key, field|
|
|
98
|
+
m = key.match(/\A([^\[\]]+)\[\](?:\.(.+))?\z/)
|
|
99
|
+
unless m
|
|
100
|
+
remaining_fields[key] = field
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
arr_name = m[1]
|
|
105
|
+
item_path = m[2]
|
|
106
|
+
b = builders[arr_name] ||= { item_fields: {}, item_type_ref: nil }
|
|
107
|
+
|
|
108
|
+
if item_path.nil?
|
|
109
|
+
# arr[] = @type -> entry fields come from the referenced type
|
|
110
|
+
ref = field.type_ref
|
|
111
|
+
b[:item_type_ref] = ref if ref && ref != "array"
|
|
112
|
+
else
|
|
113
|
+
# arr[].item_path = field
|
|
114
|
+
b[:item_fields][item_path] = field
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
next if builders.empty?
|
|
119
|
+
|
|
120
|
+
arrays = {}
|
|
121
|
+
builders.each do |arr_name, b|
|
|
122
|
+
arrays[arr_name] = Types::SchemaArray.new(
|
|
123
|
+
path: arr_name,
|
|
124
|
+
item_fields: b[:item_fields],
|
|
125
|
+
unique: false,
|
|
126
|
+
item_type_ref: b[:item_type_ref]
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@types[type_name] = Types::SchemaType.new(
|
|
131
|
+
name: type.name,
|
|
132
|
+
fields: remaining_fields,
|
|
133
|
+
namespace: type.namespace,
|
|
134
|
+
composition: type.composition,
|
|
135
|
+
base_type: type.base_type,
|
|
136
|
+
constraints: type.constraints,
|
|
137
|
+
intersection_types: type.intersection_types,
|
|
138
|
+
parent_types: type.parent_types,
|
|
139
|
+
arrays: arrays
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
86
144
|
private
|
|
87
145
|
|
|
88
146
|
def parse_import(line)
|
|
@@ -453,6 +511,8 @@ module Odin
|
|
|
453
511
|
end
|
|
454
512
|
|
|
455
513
|
field_name = left
|
|
514
|
+
# Keep the raw left side so a type can retain its `[]` array markers.
|
|
515
|
+
raw_field_name = left
|
|
456
516
|
# Strip array indicator from field names
|
|
457
517
|
is_array_field = field_name.end_with?("[]")
|
|
458
518
|
field_name = field_name[0...-2] if is_array_field
|
|
@@ -481,15 +541,20 @@ module Odin
|
|
|
481
541
|
# Parse the field spec from the right side
|
|
482
542
|
schema_field = parse_field_spec(full_path, right)
|
|
483
543
|
|
|
484
|
-
# Override type_ref for array fields
|
|
544
|
+
# Override type_ref for array fields. Inside a type, keep an entry type
|
|
545
|
+
# ref (arr[] = @entry) so it can be extracted into the type's arrays map.
|
|
485
546
|
if is_array_field
|
|
547
|
+
array_type_ref = "array"
|
|
548
|
+
if @current_header_kind == :type && schema_field.type_ref
|
|
549
|
+
array_type_ref = schema_field.type_ref
|
|
550
|
+
end
|
|
486
551
|
schema_field = Types::SchemaField.new(
|
|
487
552
|
name: schema_field.name, field_type: schema_field.field_type,
|
|
488
553
|
required: schema_field.required, nullable: schema_field.nullable,
|
|
489
554
|
redacted: schema_field.redacted, deprecated: schema_field.deprecated,
|
|
490
555
|
constraints: schema_field.constraints, conditionals: schema_field.conditionals,
|
|
491
556
|
computed: schema_field.computed, immutable: schema_field.immutable,
|
|
492
|
-
type_ref:
|
|
557
|
+
type_ref: array_type_ref
|
|
493
558
|
)
|
|
494
559
|
end
|
|
495
560
|
|
|
@@ -499,8 +564,10 @@ module Odin
|
|
|
499
564
|
if @current_type_name && @types[@current_type_name]
|
|
500
565
|
old_type = @types[@current_type_name]
|
|
501
566
|
new_fields = old_type.fields.dup
|
|
567
|
+
# Keep `[]` markers so array-of-object entries can be extracted later.
|
|
568
|
+
key_name = raw_field_name
|
|
502
569
|
# Relative sub-section ({.term}) prefixes the field key (e.g. term.effective)
|
|
503
|
-
type_key = @current_type_sub_path.empty? ?
|
|
570
|
+
type_key = @current_type_sub_path.empty? ? key_name : "#{@current_type_sub_path}.#{key_name}"
|
|
504
571
|
new_fields[type_key] = schema_field
|
|
505
572
|
@types[@current_type_name] = Types::SchemaType.new(
|
|
506
573
|
name: old_type.name,
|
|
@@ -201,10 +201,14 @@ module Odin
|
|
|
201
201
|
# A field typed @SomeType enforces that type's required fields under the field
|
|
202
202
|
# path, but only when the sub-object is present (or the field itself required).
|
|
203
203
|
def check_field_typeref_required_fields
|
|
204
|
-
@schema.fields.each do |
|
|
205
|
-
next if
|
|
204
|
+
@schema.fields.each do |raw_path, field|
|
|
205
|
+
next if raw_path.end_with?("._composition")
|
|
206
206
|
next unless field.type_ref
|
|
207
207
|
|
|
208
|
+
# A field under the root {} header keys as ".field"; the document path
|
|
209
|
+
# has no leading dot.
|
|
210
|
+
path = raw_path.sub(/\A\./, "")
|
|
211
|
+
|
|
208
212
|
member_names(field.type_ref).each do |member|
|
|
209
213
|
type = lookup_type(member)
|
|
210
214
|
next unless type
|
|
@@ -226,8 +230,66 @@ module Odin
|
|
|
226
230
|
expected: "present"
|
|
227
231
|
)
|
|
228
232
|
end
|
|
233
|
+
|
|
234
|
+
check_type_array_required_fields(path, type) if present
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Expand a referenced type's array-of-object entries under each present
|
|
240
|
+
# index so entry-level required markers fire only when the entry is present.
|
|
241
|
+
def check_type_array_required_fields(path, type)
|
|
242
|
+
return if type.arrays.nil? || type.arrays.empty?
|
|
243
|
+
|
|
244
|
+
type.arrays.each do |arr_name, schema_array|
|
|
245
|
+
item_fields = resolve_array_item_fields(schema_array)
|
|
246
|
+
next if item_fields.empty?
|
|
247
|
+
|
|
248
|
+
array_path = "#{path}.#{arr_name}"
|
|
249
|
+
present_array_indices(array_path).each do |idx|
|
|
250
|
+
item_fields.each do |fname, tfield|
|
|
251
|
+
next if fname == "_composition"
|
|
252
|
+
next unless tfield.required
|
|
253
|
+
next if tfield.computed
|
|
254
|
+
|
|
255
|
+
full = "#{array_path}[#{idx}].#{fname}"
|
|
256
|
+
next if doc_has_value?(full)
|
|
257
|
+
|
|
258
|
+
add_error(
|
|
259
|
+
code: Errors::ValidationErrorCode::REQUIRED_FIELD_MISSING,
|
|
260
|
+
path: full,
|
|
261
|
+
message: "Required field '#{full}' is missing",
|
|
262
|
+
expected: "present"
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Entry fields for an array: inline item fields, or the fields of the type
|
|
270
|
+
# named by item_type_ref (for the arr[] = @type form).
|
|
271
|
+
def resolve_array_item_fields(schema_array)
|
|
272
|
+
return schema_array.item_fields unless schema_array.item_fields.empty?
|
|
273
|
+
|
|
274
|
+
if schema_array.item_type_ref
|
|
275
|
+
member_names(schema_array.item_type_ref).each do |member|
|
|
276
|
+
type = lookup_type(member)
|
|
277
|
+
return type.fields if type
|
|
229
278
|
end
|
|
230
279
|
end
|
|
280
|
+
schema_array.item_fields
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Distinct indices present in the document under the given array path.
|
|
284
|
+
def present_array_indices(array_path)
|
|
285
|
+
seen = {}
|
|
286
|
+
prefix = "#{array_path}["
|
|
287
|
+
@doc.paths.each do |p|
|
|
288
|
+
next unless p.start_with?(prefix)
|
|
289
|
+
m = p[array_path.length..].match(/\A\[(\d+)\]/)
|
|
290
|
+
seen[m[1].to_i] = true if m
|
|
291
|
+
end
|
|
292
|
+
seen.keys.sort
|
|
231
293
|
end
|
|
232
294
|
|
|
233
295
|
# Split an &-joined type_ref (e.g. "@hasName&hasAge") into bare member names.
|
data/lib/odin/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: odin-foundation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ODIN Foundation
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|