schema-model 0.6.10 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +53 -0
- data/.rubocop.yml +70 -8
- data/.ruby-version +1 -1
- data/ARCHITECTURE.md +135 -0
- data/CLAUDE.md +74 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +75 -43
- data/README.md +426 -124
- data/lib/schema/active_model_validations.rb +14 -14
- data/lib/schema/array_headers.rb +6 -4
- data/lib/schema/arrays.rb +11 -13
- data/lib/schema/associations/dynamic_types.rb +3 -5
- data/lib/schema/associations/has_many.rb +6 -8
- data/lib/schema/associations/has_one.rb +5 -7
- data/lib/schema/associations/schema_creator.rb +7 -7
- data/lib/schema/csv_parser.rb +1 -1
- data/lib/schema/model.rb +37 -29
- data/lib/schema/parsers/array.rb +2 -2
- data/lib/schema/parsers/common.rb +6 -8
- data/lib/schema/parsers/hash.rb +1 -1
- data/lib/schema/parsers/json.rb +1 -1
- data/lib/schema/parsing_errors.rb +1 -1
- data/lib/schema/utils.rb +5 -5
- data/lib/schema-model.rb +1 -0
- data/schema-model.gemspec +6 -4
- metadata +11 -13
- data/.travis.yml +0 -14
- data/Rakefile +0 -11
- data/script/bundle_install_all_versions +0 -8
- data/script/rspec_all_versions +0 -5
data/README.md
CHANGED
|
@@ -1,163 +1,465 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Schema
|
|
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
|
-
[](https://codeclimate.com/github/dougyouch/schema/test_coverage)
|
|
5
|
+
[](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/dougyouch/schema)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
## Installation
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
```ruby
|
|
13
|
+
gem 'schema-model'
|
|
14
|
+
```
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
And then execute:
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
```bash
|
|
19
|
+
$ bundle install
|
|
20
|
+
```
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class UserSchema
|
|
24
26
|
include Schema::All
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
attribute :
|
|
29
|
-
attribute :
|
|
28
|
+
attribute :name, :string
|
|
29
|
+
attribute :age, :integer
|
|
30
|
+
attribute :email, :string
|
|
31
|
+
attribute :active, :boolean, default: false
|
|
32
|
+
attribute :tags, :array, separator: ',', data_type: :string
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
attribute :number_list, :array, separator: ',', data_type: :integer
|
|
34
|
+
validates :name, presence: true
|
|
35
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
36
|
+
end
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
user = UserSchema.from_hash(
|
|
39
|
+
name: 'John Doe',
|
|
40
|
+
age: '30',
|
|
41
|
+
email: 'john@example.com',
|
|
42
|
+
active: 'yes',
|
|
43
|
+
tags: 'ruby,rails,developer'
|
|
44
|
+
)
|
|
39
45
|
|
|
40
|
-
|
|
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
|
+
```
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
attribute :custom_description, :string
|
|
44
|
-
end
|
|
53
|
+
## Data Types
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
attribute :number_of_locations, :integer
|
|
55
|
+
### Basic Types
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Complex Types
|
|
75
|
+
|
|
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
|
|
85
|
+
|
|
86
|
+
### Aliases
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Single alias
|
|
90
|
+
attribute :name, :string, alias: 'FullName'
|
|
91
|
+
|
|
92
|
+
# Multiple aliases
|
|
93
|
+
attribute :name, :string, aliases: [:full_name, :display_name]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Default Values
|
|
97
|
+
|
|
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
|
|
105
|
+
|
|
106
|
+
Every attribute generates a `_was_set?` predicate method:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
user = UserSchema.from_hash(name: 'John')
|
|
110
|
+
user.name_was_set? # => true
|
|
111
|
+
user.email_was_set? # => false (not provided)
|
|
112
|
+
|
|
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)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Associations
|
|
119
|
+
|
|
120
|
+
### Has One
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class OrderSchema
|
|
124
|
+
include Schema::All
|
|
125
|
+
|
|
126
|
+
attribute :id, :integer
|
|
127
|
+
|
|
128
|
+
has_one :customer do
|
|
129
|
+
attribute :name, :string
|
|
130
|
+
attribute :email, :string
|
|
52
131
|
end
|
|
132
|
+
end
|
|
53
133
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
attribute :zip, :string
|
|
134
|
+
order = OrderSchema.from_hash(
|
|
135
|
+
id: 1,
|
|
136
|
+
customer: { name: 'Alice', email: 'alice@example.com' }
|
|
137
|
+
)
|
|
138
|
+
order.customer.name # => "Alice"
|
|
139
|
+
```
|
|
61
140
|
|
|
62
|
-
|
|
63
|
-
attribute :main_floor, :integer
|
|
141
|
+
### Has Many
|
|
64
142
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
143
|
+
```ruby
|
|
144
|
+
class OrderSchema
|
|
145
|
+
include Schema::All
|
|
68
146
|
|
|
69
|
-
|
|
70
|
-
attribute :main_entrance, :string
|
|
147
|
+
attribute :id, :integer
|
|
71
148
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
has_many :items do
|
|
150
|
+
attribute :sku, :string
|
|
151
|
+
attribute :quantity, :integer
|
|
75
152
|
end
|
|
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'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Appending to Has Many
|
|
188
|
+
|
|
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
|
+
```
|
|
76
194
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
195
|
+
## Dynamic Types
|
|
196
|
+
|
|
197
|
+
Create different model structures based on a type field:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class CompanySchema
|
|
201
|
+
include Schema::All
|
|
202
|
+
|
|
203
|
+
has_many :employees, type_field: :type do
|
|
204
|
+
attribute :type, :string
|
|
80
205
|
attribute :name, :string
|
|
81
|
-
|
|
82
|
-
add_type(
|
|
83
|
-
attribute :
|
|
206
|
+
|
|
207
|
+
add_type('engineer') do
|
|
208
|
+
attribute :programming_languages, :array, separator: ','
|
|
84
209
|
end
|
|
85
|
-
|
|
86
|
-
|
|
210
|
+
|
|
211
|
+
add_type('manager') do
|
|
212
|
+
attribute :department, :string
|
|
213
|
+
attribute :team_size, :integer
|
|
87
214
|
end
|
|
88
|
-
# if no or an invalid type is specified, create a default employee schema object
|
|
89
|
-
# useful for communicating errors in an API
|
|
90
|
-
default_type
|
|
91
215
|
|
|
92
|
-
|
|
93
|
-
|
|
216
|
+
default_type do
|
|
217
|
+
# Fallback for unknown types
|
|
218
|
+
end
|
|
94
219
|
end
|
|
220
|
+
end
|
|
95
221
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
)
|
|
100
228
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
229
|
+
company.employees[0].programming_languages # => ["ruby", "python"]
|
|
230
|
+
company.employees[1].team_size # => 5
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Dynamic Type Options
|
|
234
|
+
|
|
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
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
class UserSchema
|
|
258
|
+
include Schema::All
|
|
259
|
+
|
|
260
|
+
attribute :name, :string
|
|
261
|
+
attribute :email, :string
|
|
262
|
+
attribute :age, :integer
|
|
104
263
|
|
|
105
264
|
validates :name, presence: true
|
|
106
|
-
validates :
|
|
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')
|
|
107
274
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
299
|
end
|
|
114
300
|
```
|
|
115
301
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"captain": {
|
|
154
|
-
"email": "captain@example.com",
|
|
155
|
-
"name": "Captain Kurk"
|
|
156
|
-
},
|
|
157
|
-
"joe": {
|
|
158
|
-
"email": "joe.smith@example.com",
|
|
159
|
-
"name": "Joe Smith"
|
|
160
|
-
}
|
|
161
|
-
}
|
|
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"]
|
|
309
|
+
|
|
310
|
+
# Disable this behavior
|
|
311
|
+
UserSchema.capture_unknown_attributes = false
|
|
312
|
+
```
|
|
313
|
+
|
|
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 }
|
|
324
|
+
|
|
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
|
|
331
|
+
|
|
332
|
+
Prevent certain fields from being set by user input:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
user_data = {
|
|
336
|
+
id: 123,
|
|
337
|
+
name: 'John Doe',
|
|
338
|
+
created_at: '2024-01-01'
|
|
162
339
|
}
|
|
340
|
+
|
|
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)
|
|
395
|
+
|
|
396
|
+
parser.each do |user|
|
|
397
|
+
puts user.name
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
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) # => []
|
|
163
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
|
+
```
|
|
454
|
+
|
|
455
|
+
## Contributing
|
|
456
|
+
|
|
457
|
+
1. Fork the repository
|
|
458
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
459
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
460
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
461
|
+
5. Create a new Pull Request
|
|
462
|
+
|
|
463
|
+
## License
|
|
464
|
+
|
|
465
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -11,13 +11,13 @@ module Schema
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def valid_model!
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
data/lib/schema/array_headers.rb
CHANGED
|
@@ -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 ||
|
|
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
|
|
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
|