schema-model 0.6.11 → 0.7.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/.github/workflows/ci.yml +93 -0
- data/.github/workflows/release.yml +93 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +70 -8
- data/.ruby-version +1 -1
- data/ARCHITECTURE.md +135 -0
- data/CHANGELOG.md +9 -0
- data/CLAUDE.md +83 -0
- data/Gemfile +1 -2
- data/Gemfile.lock +79 -47
- data/README.md +361 -85
- 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 +2 -4
- data/lib/schema/associations/has_one.rb +3 -5
- data/lib/schema/associations/schema_creator.rb +2 -2
- data/lib/schema/csv_parser.rb +1 -1
- data/lib/schema/model.rb +26 -21
- 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/version.rb +6 -0
- data/lib/schema-model.rb +1 -0
- data/release-please-config.json +12 -0
- data/schema-model.gemspec +8 -4
- data/script/coverage_badge.rb +49 -0
- metadata +17 -12
- data/.cursor-rules +0 -39
- data/.travis.yml +0 -14
- data/Rakefile +0 -11
- data/docs/ARCHITECTURE.md +0 -171
- data/script/bundle_install_all_versions +0 -8
- data/script/rspec_all_versions +0 -5
data/README.md
CHANGED
|
@@ -2,19 +2,9 @@
|
|
|
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://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
18
8
|
|
|
19
9
|
## Installation
|
|
20
10
|
|
|
@@ -32,8 +22,6 @@ $ bundle install
|
|
|
32
22
|
|
|
33
23
|
## Quick Start
|
|
34
24
|
|
|
35
|
-
Here's a simple example to get you started:
|
|
36
|
-
|
|
37
25
|
```ruby
|
|
38
26
|
class UserSchema
|
|
39
27
|
include Schema::All
|
|
@@ -41,140 +29,429 @@ class UserSchema
|
|
|
41
29
|
attribute :name, :string
|
|
42
30
|
attribute :age, :integer
|
|
43
31
|
attribute :email, :string
|
|
32
|
+
attribute :active, :boolean, default: false
|
|
44
33
|
attribute :tags, :array, separator: ',', data_type: :string
|
|
45
34
|
|
|
46
35
|
validates :name, presence: true
|
|
47
36
|
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
48
37
|
end
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
user_data = {
|
|
39
|
+
user = UserSchema.from_hash(
|
|
52
40
|
name: 'John Doe',
|
|
53
41
|
age: '30',
|
|
54
42
|
email: 'john@example.com',
|
|
43
|
+
active: 'yes',
|
|
55
44
|
tags: 'ruby,rails,developer'
|
|
56
|
-
|
|
45
|
+
)
|
|
57
46
|
|
|
58
|
-
user
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
user.valid? # => true
|
|
48
|
+
user.name # => "John Doe"
|
|
49
|
+
user.age # => 30 (parsed to integer)
|
|
50
|
+
user.active # => true (parsed from "yes")
|
|
51
|
+
user.tags # => ["ruby", "rails", "developer"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Data Types
|
|
55
|
+
|
|
56
|
+
### Basic Types
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
attribute :name, :string # String values
|
|
60
|
+
attribute :count, :integer # Integer values (parses "123" to 123)
|
|
61
|
+
attribute :price, :float # Float values (parses "9.99" to 9.99)
|
|
62
|
+
attribute :active, :boolean # Boolean (accepts: 1, t, true, on, y, yes)
|
|
63
|
+
attribute :notes, :string_or_nil # String, but returns nil if empty
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Date and Time Types
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
attribute :created_at, :time # ISO 8601 format (Time.xmlschema)
|
|
70
|
+
attribute :birth_date, :date # Date.parse format
|
|
71
|
+
attribute :us_date, :american_date # MM/DD/YYYY format
|
|
72
|
+
attribute :us_time, :american_time # MM/DD/YYYY HH:MM:SS format
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
### Complex Types
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
```ruby
|
|
78
|
+
attribute :tags, :array # Array of values
|
|
79
|
+
attribute :tags, :array, separator: ',' # Parse "a,b,c" into ["a","b","c"]
|
|
80
|
+
attribute :tags, :array, separator: ',', data_type: :integer # Parse and convert elements
|
|
81
|
+
attribute :metadata, :hash # Hash/dictionary values
|
|
82
|
+
attribute :config, :json # Parse JSON strings
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Attribute Options
|
|
69
86
|
|
|
70
|
-
|
|
71
|
-
- A name
|
|
72
|
-
- A type
|
|
73
|
-
- Optional aliases
|
|
74
|
-
- Custom parsing rules
|
|
87
|
+
### Aliases
|
|
75
88
|
|
|
76
89
|
```ruby
|
|
90
|
+
# Single alias
|
|
77
91
|
attribute :name, :string, alias: 'FullName'
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
|
|
93
|
+
# Multiple aliases
|
|
94
|
+
attribute :name, :string, aliases: [:full_name, :display_name]
|
|
80
95
|
```
|
|
81
96
|
|
|
82
|
-
###
|
|
97
|
+
### Default Values
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
```ruby
|
|
100
|
+
attribute :status, :string, default: 'pending'
|
|
101
|
+
attribute :count, :integer, default: 0
|
|
102
|
+
attribute :tags, :array, default: []
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Checking If Attribute Was Set
|
|
85
106
|
|
|
86
|
-
|
|
87
|
-
2. **Has Many**: Multiple nested models
|
|
88
|
-
3. **Dynamic Associations**: Type-based model creation
|
|
107
|
+
Every attribute generates a `_was_set?` predicate method:
|
|
89
108
|
|
|
90
109
|
```ruby
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end
|
|
110
|
+
user = UserSchema.from_hash(name: 'John')
|
|
111
|
+
user.name_was_set? # => true
|
|
112
|
+
user.email_was_set? # => false (not provided)
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
end
|
|
114
|
+
# Useful for distinguishing "not provided" vs "provided as nil"
|
|
115
|
+
user = UserSchema.from_hash(name: nil)
|
|
116
|
+
user.name_was_set? # => true (explicitly set to nil)
|
|
100
117
|
```
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
## Associations
|
|
103
120
|
|
|
104
|
-
|
|
121
|
+
### Has One
|
|
105
122
|
|
|
106
123
|
```ruby
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
class OrderSchema
|
|
125
|
+
include Schema::All
|
|
126
|
+
|
|
127
|
+
attribute :id, :integer
|
|
110
128
|
|
|
111
|
-
|
|
112
|
-
attribute :
|
|
129
|
+
has_one :customer do
|
|
130
|
+
attribute :name, :string
|
|
131
|
+
attribute :email, :string
|
|
113
132
|
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
order = OrderSchema.from_hash(
|
|
136
|
+
id: 1,
|
|
137
|
+
customer: { name: 'Alice', email: 'alice@example.com' }
|
|
138
|
+
)
|
|
139
|
+
order.customer.name # => "Alice"
|
|
140
|
+
```
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
### Has Many
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class OrderSchema
|
|
146
|
+
include Schema::All
|
|
147
|
+
|
|
148
|
+
attribute :id, :integer
|
|
149
|
+
|
|
150
|
+
has_many :items do
|
|
151
|
+
attribute :sku, :string
|
|
152
|
+
attribute :quantity, :integer
|
|
117
153
|
end
|
|
118
154
|
end
|
|
155
|
+
|
|
156
|
+
order = OrderSchema.from_hash(
|
|
157
|
+
id: 1,
|
|
158
|
+
items: [
|
|
159
|
+
{ sku: 'ABC', quantity: 2 },
|
|
160
|
+
{ sku: 'XYZ', quantity: 1 }
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
order.items.length # => 2
|
|
164
|
+
order.items.first.sku # => "ABC"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Association Options
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Default values - creates empty model/array if not provided
|
|
171
|
+
has_one :profile, default: true
|
|
172
|
+
has_many :tags, default: true
|
|
173
|
+
|
|
174
|
+
# Reuse existing schema class
|
|
175
|
+
has_one :shipping_address, base_class: AddressSchema
|
|
176
|
+
has_one :billing_address, base_class: AddressSchema
|
|
177
|
+
|
|
178
|
+
# Has many from hash (keyed by field)
|
|
179
|
+
has_many :items, from: :hash, hash_key_field: :id do
|
|
180
|
+
attribute :id, :string
|
|
181
|
+
attribute :name, :string
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Input: { items: { 'abc' => { name: 'Item 1' }, 'xyz' => { name: 'Item 2' } } }
|
|
185
|
+
# Result: items[0].id => 'abc', items[1].id => 'xyz'
|
|
119
186
|
```
|
|
120
187
|
|
|
121
|
-
|
|
188
|
+
### Appending to Has Many
|
|
122
189
|
|
|
123
|
-
|
|
190
|
+
```ruby
|
|
191
|
+
order = OrderSchema.from_hash(id: 1, items: [])
|
|
192
|
+
order.append_to_items(sku: 'NEW', quantity: 5)
|
|
193
|
+
order.items.length # => 1
|
|
194
|
+
```
|
|
124
195
|
|
|
125
|
-
|
|
196
|
+
## Dynamic Types
|
|
197
|
+
|
|
198
|
+
Create different model structures based on a type field:
|
|
126
199
|
|
|
127
200
|
```ruby
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
201
|
+
class CompanySchema
|
|
202
|
+
include Schema::All
|
|
203
|
+
|
|
204
|
+
has_many :employees, type_field: :type do
|
|
205
|
+
attribute :type, :string
|
|
206
|
+
attribute :name, :string
|
|
207
|
+
|
|
208
|
+
add_type('engineer') do
|
|
209
|
+
attribute :programming_languages, :array, separator: ','
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
add_type('manager') do
|
|
213
|
+
attribute :department, :string
|
|
214
|
+
attribute :team_size, :integer
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
default_type do
|
|
218
|
+
# Fallback for unknown types
|
|
219
|
+
end
|
|
131
220
|
end
|
|
132
221
|
end
|
|
222
|
+
|
|
223
|
+
company = CompanySchema.from_hash(
|
|
224
|
+
employees: [
|
|
225
|
+
{ type: 'engineer', name: 'Alice', programming_languages: 'ruby,python' },
|
|
226
|
+
{ type: 'manager', name: 'Bob', department: 'Engineering', team_size: 5 }
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
company.employees[0].programming_languages # => ["ruby", "python"]
|
|
231
|
+
company.employees[1].team_size # => 5
|
|
133
232
|
```
|
|
134
233
|
|
|
135
|
-
###
|
|
234
|
+
### Dynamic Type Options
|
|
136
235
|
|
|
137
|
-
|
|
236
|
+
```ruby
|
|
237
|
+
# Type field within nested data (default)
|
|
238
|
+
has_many :items, type_field: :kind do
|
|
239
|
+
# looks for :kind in each item's data
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Type determined by parent field
|
|
243
|
+
has_one :details, external_type_field: :category do
|
|
244
|
+
# uses parent's :category field to determine type
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Case-insensitive type matching
|
|
248
|
+
has_many :items, type_field: :type, type_ignorecase: true do
|
|
249
|
+
add_type('widget') { } # matches "Widget", "WIDGET", etc.
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Validation and Error Handling
|
|
254
|
+
|
|
255
|
+
### ActiveModel Validations
|
|
138
256
|
|
|
139
257
|
```ruby
|
|
140
|
-
class
|
|
141
|
-
include Schema::
|
|
142
|
-
|
|
258
|
+
class UserSchema
|
|
259
|
+
include Schema::All
|
|
260
|
+
|
|
143
261
|
attribute :name, :string
|
|
144
262
|
attribute :email, :string
|
|
263
|
+
attribute :age, :integer
|
|
264
|
+
|
|
265
|
+
validates :name, presence: true
|
|
266
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
267
|
+
validates :age, numericality: { greater_than: 0 }, allow_nil: true
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Parsing Errors vs Validation Errors
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
user = UserSchema.from_hash(name: 'John', age: 'not-a-number')
|
|
275
|
+
|
|
276
|
+
# Parsing errors (type conversion failures)
|
|
277
|
+
user.parsing_errors.empty? # => false
|
|
278
|
+
user.parsed? # => false
|
|
279
|
+
|
|
280
|
+
# Validation errors (business rules)
|
|
281
|
+
user.valid? # => false (also runs validations)
|
|
282
|
+
user.errors.full_messages # => ["Age is invalid", ...]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Raising Exceptions
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
user = UserSchema.from_hash(age: 'invalid')
|
|
289
|
+
|
|
290
|
+
user.parsed! # raises Schema::ParsingException if parsing errors
|
|
291
|
+
user.valid_model! # raises Schema::ValidationException if validation errors
|
|
292
|
+
user.valid! # raises either (checks both)
|
|
293
|
+
|
|
294
|
+
# Exception includes the model and errors
|
|
295
|
+
begin
|
|
296
|
+
user.valid!
|
|
297
|
+
rescue Schema::ParsingException => e
|
|
298
|
+
e.schema # => the model instance
|
|
299
|
+
e.errors # => the errors object
|
|
145
300
|
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Unknown Attributes
|
|
304
|
+
|
|
305
|
+
By default, unknown attributes are captured as parsing errors:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
user = UserSchema.from_hash(name: 'John', unknown_field: 'value')
|
|
309
|
+
user.parsing_errors[:unknown_field] # => ["unknown_attribute"]
|
|
146
310
|
|
|
147
|
-
|
|
311
|
+
# Disable this behavior
|
|
312
|
+
UserSchema.capture_unknown_attributes = false
|
|
148
313
|
```
|
|
149
314
|
|
|
150
|
-
##
|
|
315
|
+
## Serialization
|
|
316
|
+
|
|
317
|
+
### to_hash / as_json
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
user = UserSchema.from_hash(name: 'John', email: nil)
|
|
321
|
+
|
|
322
|
+
user.to_hash # => { name: "John", email: nil }
|
|
323
|
+
user.as_json # => { name: "John" } (excludes nils)
|
|
324
|
+
user.as_json(include_nils: true) # => { name: "John", email: nil }
|
|
151
325
|
|
|
152
|
-
|
|
326
|
+
# Filter fields
|
|
327
|
+
user.as_json(select_filter: ->(name, value, opts) { name == :name })
|
|
328
|
+
user.as_json(reject_filter: ->(name, value, opts) { value.nil? })
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Protecting Fields with skip_fields
|
|
153
332
|
|
|
154
|
-
|
|
333
|
+
Prevent certain fields from being set by user input:
|
|
155
334
|
|
|
156
335
|
```ruby
|
|
157
336
|
user_data = {
|
|
158
|
-
id: 123,
|
|
337
|
+
id: 123,
|
|
159
338
|
name: 'John Doe',
|
|
160
|
-
|
|
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
|
|
339
|
+
created_at: '2024-01-01'
|
|
163
340
|
}
|
|
164
341
|
|
|
165
|
-
# Skip
|
|
166
|
-
user = UserSchema.from_hash(user_data, [:id, :created_at
|
|
342
|
+
# Skip database-managed fields
|
|
343
|
+
user = UserSchema.from_hash(user_data, [:id, :created_at])
|
|
344
|
+
|
|
345
|
+
user.id # => nil (not set)
|
|
346
|
+
user.name # => "John Doe"
|
|
347
|
+
user.created_at # => nil (not set)
|
|
348
|
+
|
|
349
|
+
# Nested skip_fields for associations
|
|
350
|
+
order = OrderSchema.from_hash(data, [:id, { items: [:id] }])
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Array and CSV Support
|
|
354
|
+
|
|
355
|
+
### Schema::Arrays Module
|
|
356
|
+
|
|
357
|
+
Convert models to/from flat arrays (useful for CSV/spreadsheet data):
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
class UserSchema
|
|
361
|
+
include Schema::All
|
|
362
|
+
schema_include Schema::Arrays
|
|
363
|
+
|
|
364
|
+
attribute :name, :string
|
|
365
|
+
attribute :email, :string
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Get headers
|
|
369
|
+
UserSchema.to_headers # => ["name", "email"]
|
|
370
|
+
|
|
371
|
+
# Convert to array
|
|
372
|
+
user = UserSchema.from_hash(name: 'John', email: 'john@example.com')
|
|
373
|
+
user.to_a # => ["John", "john@example.com"]
|
|
374
|
+
|
|
375
|
+
# Create from array
|
|
376
|
+
headers = ['name', 'email']
|
|
377
|
+
mapped = UserSchema.map_headers_to_attributes(headers)
|
|
378
|
+
user = UserSchema.from_array(['Jane', 'jane@example.com'], mapped)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Schema::CSVParser Module
|
|
382
|
+
|
|
383
|
+
Parse CSV data directly into models:
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
class UserCSVSchema
|
|
387
|
+
include Schema::Model
|
|
388
|
+
include Schema::CSVParser
|
|
389
|
+
|
|
390
|
+
attribute :name, :string
|
|
391
|
+
attribute :email, :string
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
csv_data = CSV.parse("name,email\nJohn,john@example.com", headers: true)
|
|
395
|
+
parser = Schema::CSVParser.new(csv_data, UserCSVSchema)
|
|
167
396
|
|
|
168
|
-
|
|
169
|
-
puts user.
|
|
170
|
-
|
|
171
|
-
puts user.name # => 'John Doe'
|
|
397
|
+
parser.each do |user|
|
|
398
|
+
puts user.name
|
|
399
|
+
end
|
|
172
400
|
```
|
|
173
401
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
402
|
+
### Schema::ArrayHeaders Module
|
|
403
|
+
|
|
404
|
+
Map CSV/array headers to schema attributes:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
class UserSchema
|
|
408
|
+
include Schema::All
|
|
409
|
+
schema_include Schema::ArrayHeaders
|
|
410
|
+
|
|
411
|
+
attribute :name, :string, alias: 'FullName'
|
|
412
|
+
attribute :email, :string
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
headers = ['FullName', 'email', 'unknown_column']
|
|
416
|
+
mapped = UserSchema.map_headers_to_attributes(headers)
|
|
417
|
+
# => { name: { index: 0 }, email: { index: 1 } }
|
|
418
|
+
|
|
419
|
+
UserSchema.get_mapped_field_names(mapped) # => ["name", "email"]
|
|
420
|
+
UserSchema.get_unmapped_field_names(mapped) # => []
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Extending Schemas
|
|
424
|
+
|
|
425
|
+
### schema_include
|
|
426
|
+
|
|
427
|
+
Add modules to a schema and all its nested associations:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
class OrderSchema
|
|
431
|
+
include Schema::All
|
|
432
|
+
|
|
433
|
+
has_many :items do
|
|
434
|
+
attribute :name, :string
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Add Arrays support to OrderSchema and OrderSchema::SchemaHasManyItems
|
|
439
|
+
OrderSchema.schema_include Schema::Arrays
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## CLI Tools
|
|
443
|
+
|
|
444
|
+
### schema-json2csv
|
|
445
|
+
|
|
446
|
+
Convert JSON data to CSV using a schema:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
# Basic usage
|
|
450
|
+
schema-json2csv --require ./my_schema.rb --schema MySchema --json data.json --csv output.csv
|
|
451
|
+
|
|
452
|
+
# From stdin
|
|
453
|
+
cat data.json | schema-json2csv --require ./my_schema.rb --schema MySchema - --csv output.csv
|
|
454
|
+
```
|
|
178
455
|
|
|
179
456
|
## Contributing
|
|
180
457
|
|
|
@@ -187,4 +464,3 @@ puts user.name # => 'John Doe'
|
|
|
187
464
|
## License
|
|
188
465
|
|
|
189
466
|
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
|
-
|
|
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
|