schema-model 0.6.11 → 0.7.0

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.
data/README.md CHANGED
@@ -2,19 +2,8 @@
2
2
 
3
3
  A powerful Ruby gem for data transformation, validation, and type safety. Schema provides a flexible and intuitive way to define data models with support for complex nested structures, dynamic associations, and robust validation.
4
4
 
5
- [![Build Status](https://travis-ci.org/dougyouch/schema.svg?branch=master)](https://travis-ci.org/dougyouch/schema)
6
- [![Maintainability](https://api.codeclimate.com/v1/badges/c142d46a7a37d4a8c2e5/maintainability)](https://codeclimate.com/github/dougyouch/schema/maintainability)
7
- [![Test Coverage](https://api.codeclimate.com/v1/badges/c142d46a7a37d4a8c2e5/test_coverage)](https://codeclimate.com/github/dougyouch/schema/test_coverage)
8
-
9
- ## Features
10
-
11
- - **Type Safety**: Strong typing with automatic parsing and validation
12
- - **Flexible Attributes**: Support for aliases and custom data types
13
- - **Nested Models**: Complex data structures with nested associations
14
- - **Dynamic Associations**: Runtime type-based model creation
15
- - **ActiveModel Integration**: Seamless integration with ActiveModel validations
16
- - **Error Handling**: Comprehensive error collection and reporting
17
- - **CSV Support**: Built-in CSV parsing capabilities
5
+ [![CI](https://github.com/dougyouch/schema/actions/workflows/ci.yml/badge.svg)](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/dougyouch/schema/graph/badge.svg)](https://codecov.io/gh/dougyouch/schema)
18
7
 
19
8
  ## Installation
20
9
 
@@ -32,8 +21,6 @@ $ bundle install
32
21
 
33
22
  ## Quick Start
34
23
 
35
- Here's a simple example to get you started:
36
-
37
24
  ```ruby
38
25
  class UserSchema
39
26
  include Schema::All
@@ -41,140 +28,429 @@ class UserSchema
41
28
  attribute :name, :string
42
29
  attribute :age, :integer
43
30
  attribute :email, :string
31
+ attribute :active, :boolean, default: false
44
32
  attribute :tags, :array, separator: ',', data_type: :string
45
33
 
46
34
  validates :name, presence: true
47
35
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
48
36
  end
49
37
 
50
- # Usage
51
- user_data = {
38
+ user = UserSchema.from_hash(
52
39
  name: 'John Doe',
53
40
  age: '30',
54
41
  email: 'john@example.com',
42
+ active: 'yes',
55
43
  tags: 'ruby,rails,developer'
56
- }
44
+ )
57
45
 
58
- user = UserSchema.from_hash(user_data)
59
- if user.valid?
60
- puts "User created: #{user.name}"
61
- else
62
- puts "Validation errors: #{user.errors.full_messages}"
63
- end
46
+ user.valid? # => true
47
+ user.name # => "John Doe"
48
+ user.age # => 30 (parsed to integer)
49
+ user.active # => true (parsed from "yes")
50
+ user.tags # => ["ruby", "rails", "developer"]
51
+ ```
52
+
53
+ ## Data Types
54
+
55
+ ### Basic Types
56
+
57
+ ```ruby
58
+ attribute :name, :string # String values
59
+ attribute :count, :integer # Integer values (parses "123" to 123)
60
+ attribute :price, :float # Float values (parses "9.99" to 9.99)
61
+ attribute :active, :boolean # Boolean (accepts: 1, t, true, on, y, yes)
62
+ attribute :notes, :string_or_nil # String, but returns nil if empty
63
+ ```
64
+
65
+ ### Date and Time Types
66
+
67
+ ```ruby
68
+ attribute :created_at, :time # ISO 8601 format (Time.xmlschema)
69
+ attribute :birth_date, :date # Date.parse format
70
+ attribute :us_date, :american_date # MM/DD/YYYY format
71
+ attribute :us_time, :american_time # MM/DD/YYYY HH:MM:SS format
64
72
  ```
65
73
 
66
- ## Core Concepts
74
+ ### Complex Types
67
75
 
68
- ### Attributes
76
+ ```ruby
77
+ attribute :tags, :array # Array of values
78
+ attribute :tags, :array, separator: ',' # Parse "a,b,c" into ["a","b","c"]
79
+ attribute :tags, :array, separator: ',', data_type: :integer # Parse and convert elements
80
+ attribute :metadata, :hash # Hash/dictionary values
81
+ attribute :config, :json # Parse JSON strings
82
+ ```
83
+
84
+ ## Attribute Options
69
85
 
70
- Attributes define the structure of your data model. Each attribute has:
71
- - A name
72
- - A type
73
- - Optional aliases
74
- - Custom parsing rules
86
+ ### Aliases
75
87
 
76
88
  ```ruby
89
+ # Single alias
77
90
  attribute :name, :string, alias: 'FullName'
78
- attribute :age, :integer
79
- attribute :tags, :array, separator: ',', data_type: :string
91
+
92
+ # Multiple aliases
93
+ attribute :name, :string, aliases: [:full_name, :display_name]
80
94
  ```
81
95
 
82
- ### Associations
96
+ ### Default Values
83
97
 
84
- Schema supports various types of associations:
98
+ ```ruby
99
+ attribute :status, :string, default: 'pending'
100
+ attribute :count, :integer, default: 0
101
+ attribute :tags, :array, default: []
102
+ ```
103
+
104
+ ### Checking If Attribute Was Set
85
105
 
86
- 1. **Has One**: Single nested model
87
- 2. **Has Many**: Multiple nested models
88
- 3. **Dynamic Associations**: Type-based model creation
106
+ Every attribute generates a `_was_set?` predicate method:
89
107
 
90
108
  ```ruby
91
- has_one(:profile) do
92
- attribute :bio, :string
93
- attribute :website, :string
94
- end
109
+ user = UserSchema.from_hash(name: 'John')
110
+ user.name_was_set? # => true
111
+ user.email_was_set? # => false (not provided)
95
112
 
96
- has_many(:posts) do
97
- attribute :title, :string
98
- attribute :content, :string
99
- end
113
+ # Useful for distinguishing "not provided" vs "provided as nil"
114
+ user = UserSchema.from_hash(name: nil)
115
+ user.name_was_set? # => true (explicitly set to nil)
100
116
  ```
101
117
 
102
- ### Dynamic Types
118
+ ## Associations
103
119
 
104
- Create different model structures based on a type field:
120
+ ### Has One
105
121
 
106
122
  ```ruby
107
- has_many(:vehicles, type_field: :type) do
108
- attribute :type, :string
109
- attribute :color, :string
123
+ class OrderSchema
124
+ include Schema::All
125
+
126
+ attribute :id, :integer
110
127
 
111
- add_type('car') do
112
- attribute :doors, :integer
128
+ has_one :customer do
129
+ attribute :name, :string
130
+ attribute :email, :string
113
131
  end
132
+ end
133
+
134
+ order = OrderSchema.from_hash(
135
+ id: 1,
136
+ customer: { name: 'Alice', email: 'alice@example.com' }
137
+ )
138
+ order.customer.name # => "Alice"
139
+ ```
114
140
 
115
- add_type('truck') do
116
- attribute :bed_length, :float
141
+ ### Has Many
142
+
143
+ ```ruby
144
+ class OrderSchema
145
+ include Schema::All
146
+
147
+ attribute :id, :integer
148
+
149
+ has_many :items do
150
+ attribute :sku, :string
151
+ attribute :quantity, :integer
117
152
  end
118
153
  end
154
+
155
+ order = OrderSchema.from_hash(
156
+ id: 1,
157
+ items: [
158
+ { sku: 'ABC', quantity: 2 },
159
+ { sku: 'XYZ', quantity: 1 }
160
+ ]
161
+ )
162
+ order.items.length # => 2
163
+ order.items.first.sku # => "ABC"
164
+ ```
165
+
166
+ ### Association Options
167
+
168
+ ```ruby
169
+ # Default values - creates empty model/array if not provided
170
+ has_one :profile, default: true
171
+ has_many :tags, default: true
172
+
173
+ # Reuse existing schema class
174
+ has_one :shipping_address, base_class: AddressSchema
175
+ has_one :billing_address, base_class: AddressSchema
176
+
177
+ # Has many from hash (keyed by field)
178
+ has_many :items, from: :hash, hash_key_field: :id do
179
+ attribute :id, :string
180
+ attribute :name, :string
181
+ end
182
+
183
+ # Input: { items: { 'abc' => { name: 'Item 1' }, 'xyz' => { name: 'Item 2' } } }
184
+ # Result: items[0].id => 'abc', items[1].id => 'xyz'
119
185
  ```
120
186
 
121
- ## Advanced Features
187
+ ### Appending to Has Many
122
188
 
123
- ### Custom Parsers
189
+ ```ruby
190
+ order = OrderSchema.from_hash(id: 1, items: [])
191
+ order.append_to_items(sku: 'NEW', quantity: 5)
192
+ order.items.length # => 1
193
+ ```
124
194
 
125
- Define custom parsing logic for your attributes:
195
+ ## Dynamic Types
196
+
197
+ Create different model structures based on a type field:
126
198
 
127
199
  ```ruby
128
- attribute :custom_field, :custom_type do
129
- def parse_custom_type(field_name, errors, value)
130
- # Custom parsing logic
200
+ class CompanySchema
201
+ include Schema::All
202
+
203
+ has_many :employees, type_field: :type do
204
+ attribute :type, :string
205
+ attribute :name, :string
206
+
207
+ add_type('engineer') do
208
+ attribute :programming_languages, :array, separator: ','
209
+ end
210
+
211
+ add_type('manager') do
212
+ attribute :department, :string
213
+ attribute :team_size, :integer
214
+ end
215
+
216
+ default_type do
217
+ # Fallback for unknown types
218
+ end
131
219
  end
132
220
  end
221
+
222
+ company = CompanySchema.from_hash(
223
+ employees: [
224
+ { type: 'engineer', name: 'Alice', programming_languages: 'ruby,python' },
225
+ { type: 'manager', name: 'Bob', department: 'Engineering', team_size: 5 }
226
+ ]
227
+ )
228
+
229
+ company.employees[0].programming_languages # => ["ruby", "python"]
230
+ company.employees[1].team_size # => 5
133
231
  ```
134
232
 
135
- ### CSV Integration
233
+ ### Dynamic Type Options
136
234
 
137
- Parse CSV data directly into your models:
235
+ ```ruby
236
+ # Type field within nested data (default)
237
+ has_many :items, type_field: :kind do
238
+ # looks for :kind in each item's data
239
+ end
240
+
241
+ # Type determined by parent field
242
+ has_one :details, external_type_field: :category do
243
+ # uses parent's :category field to determine type
244
+ end
245
+
246
+ # Case-insensitive type matching
247
+ has_many :items, type_field: :type, type_ignorecase: true do
248
+ add_type('widget') { } # matches "Widget", "WIDGET", etc.
249
+ end
250
+ ```
251
+
252
+ ## Validation and Error Handling
253
+
254
+ ### ActiveModel Validations
138
255
 
139
256
  ```ruby
140
- class UserCSVSchema
141
- include Schema::CSVParser
142
-
257
+ class UserSchema
258
+ include Schema::All
259
+
143
260
  attribute :name, :string
144
261
  attribute :email, :string
262
+ attribute :age, :integer
263
+
264
+ validates :name, presence: true
265
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
266
+ validates :age, numericality: { greater_than: 0 }, allow_nil: true
267
+ end
268
+ ```
269
+
270
+ ### Parsing Errors vs Validation Errors
271
+
272
+ ```ruby
273
+ user = UserSchema.from_hash(name: 'John', age: 'not-a-number')
274
+
275
+ # Parsing errors (type conversion failures)
276
+ user.parsing_errors.empty? # => false
277
+ user.parsed? # => false
278
+
279
+ # Validation errors (business rules)
280
+ user.valid? # => false (also runs validations)
281
+ user.errors.full_messages # => ["Age is invalid", ...]
282
+ ```
283
+
284
+ ### Raising Exceptions
285
+
286
+ ```ruby
287
+ user = UserSchema.from_hash(age: 'invalid')
288
+
289
+ user.parsed! # raises Schema::ParsingException if parsing errors
290
+ user.valid_model! # raises Schema::ValidationException if validation errors
291
+ user.valid! # raises either (checks both)
292
+
293
+ # Exception includes the model and errors
294
+ begin
295
+ user.valid!
296
+ rescue Schema::ParsingException => e
297
+ e.schema # => the model instance
298
+ e.errors # => the errors object
145
299
  end
300
+ ```
301
+
302
+ ### Unknown Attributes
303
+
304
+ By default, unknown attributes are captured as parsing errors:
305
+
306
+ ```ruby
307
+ user = UserSchema.from_hash(name: 'John', unknown_field: 'value')
308
+ user.parsing_errors[:unknown_field] # => ["unknown_attribute"]
146
309
 
147
- users = UserCSVSchema.parse_csv(csv_data)
310
+ # Disable this behavior
311
+ UserSchema.capture_unknown_attributes = false
148
312
  ```
149
313
 
150
- ## Using `skip_fields` to Protect Internal Fields
314
+ ## Serialization
315
+
316
+ ### to_hash / as_json
317
+
318
+ ```ruby
319
+ user = UserSchema.from_hash(name: 'John', email: nil)
320
+
321
+ user.to_hash # => { name: "John", email: nil }
322
+ user.as_json # => { name: "John" } (excludes nils)
323
+ user.as_json(include_nils: true) # => { name: "John", email: nil }
151
324
 
152
- When instantiating a schema with `from_hash`, you can use the `skip_fields` argument to prevent certain fields (such as `id`, `created_at`, `updated_at`) from being set by user input. This is especially useful for fields managed by the database or internal logic, ensuring end users cannot override these values.
325
+ # Filter fields
326
+ user.as_json(select_filter: ->(name, value, opts) { name == :name })
327
+ user.as_json(reject_filter: ->(name, value, opts) { value.nil? })
328
+ ```
329
+
330
+ ## Protecting Fields with skip_fields
153
331
 
154
- **Example:**
332
+ Prevent certain fields from being set by user input:
155
333
 
156
334
  ```ruby
157
335
  user_data = {
158
- id: 123, # Should be managed by DB
336
+ id: 123,
159
337
  name: 'John Doe',
160
- email: 'john@example.com',
161
- created_at: '2024-06-01T12:00:00Z', # Should be managed by DB
162
- updated_at: '2024-06-01T12:00:00Z' # Should be managed by DB
338
+ created_at: '2024-01-01'
163
339
  }
164
340
 
165
- # Skip DB-managed fields
166
- user = UserSchema.from_hash(user_data, [:id, :created_at, :updated_at])
341
+ # Skip database-managed fields
342
+ user = UserSchema.from_hash(user_data, [:id, :created_at])
343
+
344
+ user.id # => nil (not set)
345
+ user.name # => "John Doe"
346
+ user.created_at # => nil (not set)
347
+
348
+ # Nested skip_fields for associations
349
+ order = OrderSchema.from_hash(data, [:id, { items: [:id] }])
350
+ ```
351
+
352
+ ## Array and CSV Support
353
+
354
+ ### Schema::Arrays Module
355
+
356
+ Convert models to/from flat arrays (useful for CSV/spreadsheet data):
357
+
358
+ ```ruby
359
+ class UserSchema
360
+ include Schema::All
361
+ schema_include Schema::Arrays
362
+
363
+ attribute :name, :string
364
+ attribute :email, :string
365
+ end
366
+
367
+ # Get headers
368
+ UserSchema.to_headers # => ["name", "email"]
369
+
370
+ # Convert to array
371
+ user = UserSchema.from_hash(name: 'John', email: 'john@example.com')
372
+ user.to_a # => ["John", "john@example.com"]
373
+
374
+ # Create from array
375
+ headers = ['name', 'email']
376
+ mapped = UserSchema.map_headers_to_attributes(headers)
377
+ user = UserSchema.from_array(['Jane', 'jane@example.com'], mapped)
378
+ ```
379
+
380
+ ### Schema::CSVParser Module
381
+
382
+ Parse CSV data directly into models:
383
+
384
+ ```ruby
385
+ class UserCSVSchema
386
+ include Schema::Model
387
+ include Schema::CSVParser
388
+
389
+ attribute :name, :string
390
+ attribute :email, :string
391
+ end
392
+
393
+ csv_data = CSV.parse("name,email\nJohn,john@example.com", headers: true)
394
+ parser = Schema::CSVParser.new(csv_data, UserCSVSchema)
167
395
 
168
- puts user.id # => nil (not set from user input)
169
- puts user.created_at # => nil (not set from user input)
170
- puts user.updated_at # => nil (not set from user input)
171
- puts user.name # => 'John Doe'
396
+ parser.each do |user|
397
+ puts user.name
398
+ end
172
399
  ```
173
400
 
174
- **Benefit:**
175
- - Prevents end users from setting or changing internal DB values.
176
- - Ensures only safe, intended fields are settable from external input.
177
- - Helps maintain data integrity and security in your application.
401
+ ### Schema::ArrayHeaders Module
402
+
403
+ Map CSV/array headers to schema attributes:
404
+
405
+ ```ruby
406
+ class UserSchema
407
+ include Schema::All
408
+ schema_include Schema::ArrayHeaders
409
+
410
+ attribute :name, :string, alias: 'FullName'
411
+ attribute :email, :string
412
+ end
413
+
414
+ headers = ['FullName', 'email', 'unknown_column']
415
+ mapped = UserSchema.map_headers_to_attributes(headers)
416
+ # => { name: { index: 0 }, email: { index: 1 } }
417
+
418
+ UserSchema.get_mapped_field_names(mapped) # => ["name", "email"]
419
+ UserSchema.get_unmapped_field_names(mapped) # => []
420
+ ```
421
+
422
+ ## Extending Schemas
423
+
424
+ ### schema_include
425
+
426
+ Add modules to a schema and all its nested associations:
427
+
428
+ ```ruby
429
+ class OrderSchema
430
+ include Schema::All
431
+
432
+ has_many :items do
433
+ attribute :name, :string
434
+ end
435
+ end
436
+
437
+ # Add Arrays support to OrderSchema and OrderSchema::SchemaHasManyItems
438
+ OrderSchema.schema_include Schema::Arrays
439
+ ```
440
+
441
+ ## CLI Tools
442
+
443
+ ### schema-json2csv
444
+
445
+ Convert JSON data to CSV using a schema:
446
+
447
+ ```bash
448
+ # Basic usage
449
+ schema-json2csv --require ./my_schema.rb --schema MySchema --json data.json --csv output.csv
450
+
451
+ # From stdin
452
+ cat data.json | schema-json2csv --require ./my_schema.rb --schema MySchema - --csv output.csv
453
+ ```
178
454
 
179
455
  ## Contributing
180
456
 
@@ -187,4 +463,3 @@ puts user.name # => 'John Doe'
187
463
  ## License
188
464
 
189
465
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
190
-
@@ -11,13 +11,13 @@ module Schema
11
11
  end
12
12
 
13
13
  def valid_model!
14
- unless valid?
15
- raise ValidationException.new(
16
- "invalid values for attributes #{errors.map(&:attribute).join(', ')}",
17
- self,
18
- errors
19
- )
20
- end
14
+ return if valid?
15
+
16
+ raise ValidationException.new(
17
+ "invalid values for attributes #{errors.map(&:attribute).join(', ')}",
18
+ self,
19
+ errors
20
+ )
21
21
  end
22
22
 
23
23
  def valid!
@@ -36,13 +36,13 @@ module Schema
36
36
  end
37
37
 
38
38
  def parsed!
39
- unless parsed?
40
- raise ParsingException.new(
41
- "schema parsing failed for attributes #{parsing_errors.errors.map(&:attribute).join(', ')}",
42
- self,
43
- parsing_errors
44
- )
45
- end
39
+ return if parsed?
40
+
41
+ raise ParsingException.new(
42
+ "schema parsing failed for attributes #{parsing_errors.errors.map(&:attribute).join(', ')}",
43
+ self,
44
+ parsing_errors
45
+ )
46
46
  end
47
47
  end
48
48
  end
@@ -41,13 +41,15 @@ module Schema
41
41
  fields
42
42
  end
43
43
 
44
+ MAX_ARRAY_INDEX = 10_000
45
+
44
46
  private
45
47
 
46
48
  def get_model_field_names(field_name, field_options, mapped_headers, header_prefix, mapped)
47
49
  mapped_model = mapped_headers[field_name] || {}
48
50
  const_get(field_options[:class_name]).get_field_names(
49
51
  mapped_model,
50
- header_prefix || (field_options[:aliases] && field_options[:aliases].first),
52
+ header_prefix || field_options[:aliases]&.first,
51
53
  mapped
52
54
  )
53
55
  end
@@ -62,7 +64,7 @@ module Schema
62
64
 
63
65
  def generate_field_name(field_name, field_options, header_prefix)
64
66
  field_name = field_options[:aliases].first if field_options[:aliases]
65
- field_name = header_prefix + 'X' + field_name.to_s if header_prefix
67
+ field_name = "#{header_prefix}X#{field_name}" if header_prefix
66
68
  field_name.to_s
67
69
  end
68
70
 
@@ -114,9 +116,9 @@ module Schema
114
116
  cnt = field_options[:starting_index] || 1
115
117
  indexes = []
116
118
  # finding all headers that look like Company1Name through CompanyXName
117
- while (index = headers.index(header_prefix + cnt.to_s + field_options[:key]))
118
- cnt += 1
119
+ while cnt <= MAX_ARRAY_INDEX && (index = headers.index(header_prefix + cnt.to_s + field_options[:key]))
119
120
  indexes << index
121
+ cnt += 1
120
122
  end
121
123
  indexes
122
124
  end
data/lib/schema/arrays.rb CHANGED
@@ -15,7 +15,7 @@ module Schema
15
15
 
16
16
  def to_empty_array
17
17
  data = []
18
- schema.each do |_, field_options|
18
+ schema.each_value do |field_options|
19
19
  next if field_options[:alias_of]
20
20
 
21
21
  data <<
@@ -24,8 +24,6 @@ module Schema
24
24
  const_get(field_options[:class_name]).to_empty_array
25
25
  when :has_many
26
26
  field_options[:size].times.map { const_get(field_options[:class_name]).to_empty_array }
27
- else
28
- nil
29
27
  end
30
28
  end
31
29
  data
@@ -33,16 +31,16 @@ module Schema
33
31
 
34
32
  def to_headers(prefix = nil)
35
33
  headers = []
36
- schema.each do |_, field_options|
34
+ schema.each_value do |field_options|
37
35
  next if field_options[:alias_of]
38
36
 
39
37
  headers <<
40
38
  case field_options[:type]
41
39
  when :has_one
42
- const_get(field_options[:class_name]).to_headers(prefix.to_s + field_options[:key] + '.')
40
+ const_get(field_options[:class_name]).to_headers("#{prefix}#{field_options[:key]}.")
43
41
  when :has_many
44
42
  field_options[:size].times.map do |i|
45
- const_get(field_options[:class_name]).to_headers(prefix.to_s + field_options[:key] + "[#{i +1}].")
43
+ const_get(field_options[:class_name]).to_headers(prefix.to_s + field_options[:key] + "[#{i + 1}].")
46
44
  end
47
45
  else
48
46
  prefix.to_s + field_options[:key]
@@ -54,7 +52,7 @@ module Schema
54
52
 
55
53
  def to_a
56
54
  data = []
57
- self.class.schema.each do |_, field_options|
55
+ self.class.schema.each_value do |field_options|
58
56
  next if field_options[:alias_of]
59
57
 
60
58
  value = public_send(field_options[:getter])
@@ -76,7 +74,7 @@ module Schema
76
74
  end
77
75
 
78
76
  def update_attributes_with_array(array, mapped_headers, offset = nil)
79
- self.class.schema.each do |_, field_options|
77
+ self.class.schema.each_value do |field_options|
80
78
  next unless (mapped_field = mapped_headers[field_options[:name]])
81
79
 
82
80
  if offset
@@ -103,7 +101,7 @@ module Schema
103
101
  end
104
102
 
105
103
  def update_nested_has_one_associations_from_array(array, mapped_headers, current_offset = nil)
106
- self.class.schema.each do |_, field_options|
104
+ self.class.schema.each_value do |field_options|
107
105
  next unless field_options[:type] == :has_one
108
106
  next unless (mapped_model = mapped_headers[field_options[:name]])
109
107
 
@@ -115,7 +113,7 @@ module Schema
115
113
  end
116
114
 
117
115
  def update_nested_has_many_associations_from_array(array, mapped_headers)
118
- self.class.schema.each do |_, field_options|
116
+ self.class.schema.each_value do |field_options|
119
117
  next unless field_options[:type] == :has_many
120
118
  next unless (mapped_model = mapped_headers[field_options[:name]])
121
119
 
@@ -136,10 +134,10 @@ module Schema
136
134
 
137
135
  def largest_number_of_indexes_from_map(mapped_model)
138
136
  size = 0
139
- mapped_model.each do |_, info|
137
+ mapped_model.each_value do |info|
140
138
  if info[:indexes]
141
- size = info[:indexes].size if info[:indexes] && info[:indexes].size > size
142
- else
139
+ size = info[:indexes].size if info[:indexes].size > size
140
+ elsif info.is_a?(Hash)
143
141
  new_size = largest_number_of_indexes_from_map(info)
144
142
  size = new_size if new_size > size
145
143
  end