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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a42b39245e4ab2f8821e0e12100d16418316b22758071d7acf28db0b7a754f20
4
- data.tar.gz: 99f7bc6624af35e90864f3ccc1239eb2c3c8ab1cd1a50bcd5daffdca36655e98
3
+ metadata.gz: fd44363f49452d2888d4c6adee66875fc13d6cd77e0dac6650028aabbcace2af
4
+ data.tar.gz: fa3f640fe709656ffa5f3436ca7f79d287c2d7a2cf5d9470c4fb7d8d89659b4e
5
5
  SHA512:
6
- metadata.gz: 00027a7160bb7e6605bdbb3e77e2ea54232147902027cdc98d3dccc6b6ce06635563d4ca84698a12a3325d29b1310e88355911dcd0c75099ad13ba35eef84a64
7
- data.tar.gz: 4ad5366860defb9e19affa8f666eccd4d3d6bd3a9cba539131bb189987c9b878a6217cb60260bb58ff03660e71d4bb3865eb77fed4bb4ff47a3e01124f4621fb
6
+ metadata.gz: d8d134498b241ad33fa0936e95b4f5cefcb3642e97ec099509e23d75fcdb2e11df55c8d072e8989557944c0e7ae2ae38abf64e8cff85a76d8be311406fd22dfc
7
+ data.tar.gz: dfc5f7a283b5b8714bbb8a9ba09535a49a368dc178dfc8a7b678fee2a8a88d70e7ff8e3347d41ce310f14bccfb083c25e4ca8e3277be7abe20681932d3abbcec
@@ -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: "array"
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? ? field_name : "#{@current_type_sub_path}.#{field_name}"
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 |path, field|
205
- next if path.end_with?("._composition")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Odin
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.1"
5
5
  end
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.0
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-03 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal