schema-model 0.9.1 → 0.11.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/CHANGELOG.md +30 -0
- data/README.md +19 -4
- data/bin/schema-json2csv +5 -1
- data/lib/schema/active_model_parsing_errors.rb +1 -0
- data/lib/schema/active_model_validations.rb +5 -0
- data/lib/schema/array_headers.rb +15 -0
- data/lib/schema/arrays.rb +37 -3
- data/lib/schema/associations/base.rb +2 -0
- data/lib/schema/associations/dynamic_types.rb +11 -0
- data/lib/schema/associations/has_many.rb +7 -0
- data/lib/schema/associations/has_one.rb +6 -0
- data/lib/schema/associations/schema_creator.rb +16 -0
- data/lib/schema/attribute_values.rb +2 -0
- data/lib/schema/csv_parser.rb +19 -2
- data/lib/schema/deep_copy.rb +3 -0
- data/lib/schema/errors.rb +8 -0
- data/lib/schema/model.rb +58 -3
- data/lib/schema/parsers/american.rb +6 -0
- data/lib/schema/parsers/array.rb +7 -0
- data/lib/schema/parsers/common.rb +17 -2
- data/lib/schema/parsers/hash.rb +2 -0
- data/lib/schema/parsers/json.rb +2 -0
- data/lib/schema/parsing_errors.rb +5 -0
- data/lib/schema/utils.rb +16 -0
- data/lib/schema/version.rb +1 -1
- data/lib/schema-model.rb +3 -0
- data/lib/schema_validator.rb +2 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f4f7d2224dda7f2a0ced1da41e8597e6374df27a22fd739c1bd86a0b02441c3
|
|
4
|
+
data.tar.gz: 4ff7d3bcf5f8e80d241d7e85b25c84f0a7f651c1e43beeafe923537eeb3aa880
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c113bc5af055eae0d038c42113fade624505848db7945576fd642845e7071791bf041db388e8ef8ff8b076a27a82b2de94beb8cf91ec80d8f02b478152d7ce4d
|
|
7
|
+
data.tar.gz: 77f0728d85424f45a5cf5c034f4b7a7510a54357a0e55bebfdd95a4f1c3da7a32517e4bc5d33d86d776d0d5245581fea71c2d163ff419da53d41d12b1ae8c6a2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/dougyouch/schema/compare/v0.10.0...v0.11.0) (2026-10-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **gem:** Ruby 3.2 reached end of life in March 2026 and is no longer supported. CI now tests Ruby 3.3 and the .ruby-version Ruby, both with Gemfile.lock and the coverage gate.
|
|
9
|
+
|
|
10
|
+
### Build System
|
|
11
|
+
|
|
12
|
+
* **gem:** require ruby 3.3 ([412cb95](https://github.com/dougyouch/schema/commit/412cb95c84f26623fb9b3d67257f3339390e952f))
|
|
13
|
+
|
|
14
|
+
## [0.10.0](https://github.com/dougyouch/schema/compare/v0.9.1...v0.10.0) (2026-10-04)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### ⚠ BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
* **parsers:** string_or_nil now returns nil for whitespace-only strings such as " ", matching how blank strings parse for the other types. Other strings are still kept as is.
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **model:** add as_json only_set and handle input that is not a hash ([bdda922](https://github.com/dougyouch/schema/commit/bdda922bd6e4b7fbb2c31e0ada74596b67a3d5e7))
|
|
24
|
+
* **parsers:** treat whitespace-only strings as nil for string_or_nil ([7313c99](https://github.com/dougyouch/schema/commit/7313c99bb0de5e340ad92cc958a1b601a5e3e15c))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **arrays:** raise a clear error for has_many without size ([617cb89](https://github.com/dougyouch/schema/commit/617cb89d9cbb028560d2148d4450e7d77689b877))
|
|
30
|
+
* **cli:** report malformed json without a backtrace ([fc2f1f9](https://github.com/dougyouch/schema/commit/fc2f1f95df5681184cc80850a18d40cffeede121))
|
|
31
|
+
* **csv:** strip the utf-8 byte order mark and handle empty header cells ([ad0216d](https://github.com/dougyouch/schema/commit/ad0216d429c881582d949ee2785e2e729d28bd23))
|
|
32
|
+
|
|
3
33
|
## [0.9.1](https://github.com/dougyouch/schema/compare/v0.9.0...v0.9.1) (2026-10-04)
|
|
4
34
|
|
|
5
35
|
|
data/README.md
CHANGED
|
@@ -6,9 +6,11 @@ A powerful Ruby gem for data transformation, validation, and type safety. Schema
|
|
|
6
6
|
[](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/dougyouch/schema/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
|
+
[API reference](https://rubydoc.info/gems/schema-model) · [Changelog](CHANGELOG.md) · [Architecture](ARCHITECTURE.md)
|
|
10
|
+
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
11
|
-
Requires Ruby 3.
|
|
13
|
+
Requires Ruby 3.3 or newer. Add this line to your application's Gemfile:
|
|
12
14
|
|
|
13
15
|
```ruby
|
|
14
16
|
gem 'schema-model'
|
|
@@ -181,6 +183,16 @@ end
|
|
|
181
183
|
|
|
182
184
|
Associations in `set_attribute_values` are schema models; call `as_json` on them if the record expects hashes. For a single attribute, `input.name_was_set?` answers the same question.
|
|
183
185
|
|
|
186
|
+
For nested input, `as_json(only_set: true)` keeps only the fields that were sent, at every level, so it can be merged into a JSON column without wiping fields the client left out:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# body {"address": {"zip": null}}
|
|
190
|
+
input.as_json(only_set: true) # => { address: { zip: nil } }
|
|
191
|
+
record.data = record.data.deep_merge(input.as_json(only_set: true).deep_stringify_keys)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`JSON.parse` returns an array, string or number for bodies that aren't JSON objects; `from_hash` records an `incompatible` error on `:base` for those instead of raising, so `parsed_and_valid?` handles them like any other bad input.
|
|
195
|
+
|
|
184
196
|
## Data Types
|
|
185
197
|
|
|
186
198
|
### Basic Types
|
|
@@ -190,7 +202,7 @@ attribute :name, :string # String values
|
|
|
190
202
|
attribute :count, :integer # Integer values (parses "123", " 123 " and "0123" to 123)
|
|
191
203
|
attribute :price, :float # Float values (parses "9.99" to 9.99, also "1e+5")
|
|
192
204
|
attribute :active, :boolean # see below
|
|
193
|
-
attribute :notes, :string_or_nil # String, but
|
|
205
|
+
attribute :notes, :string_or_nil # String, but nil when empty or whitespace-only
|
|
194
206
|
```
|
|
195
207
|
|
|
196
208
|
Booleans accept `1, t, true, on, y, yes` as `true` and `0, f, false, off, n, no` as `false` (any case). Any other string is an `invalid` parsing error, and numbers are `true` unless they're 0.
|
|
@@ -533,6 +545,9 @@ user.as_json(include_nils: true) # => { name: "John", email: nil }
|
|
|
533
545
|
# Filter fields
|
|
534
546
|
user.as_json(select_filter: ->(name, value, opts) { name == :name })
|
|
535
547
|
user.as_json(reject_filter: ->(name, value, opts) { value.nil? })
|
|
548
|
+
|
|
549
|
+
# Only the fields present in the input, at every level (nils that were sent are kept)
|
|
550
|
+
user.as_json(only_set: true) # => { name: "John", email: nil }
|
|
536
551
|
```
|
|
537
552
|
|
|
538
553
|
### Comparing, Inspecting and Copying
|
|
@@ -619,11 +634,11 @@ end
|
|
|
619
634
|
UserSchema.to_headers # => [..., "phones[1].number", "phones[2].number", "phones[3].number"]
|
|
620
635
|
```
|
|
621
636
|
|
|
622
|
-
`to_a` writes at most `size` entries: a model with more entries than that has the extra ones left out of the array, with no error, so pick a `size` large enough for your data.
|
|
637
|
+
Without `size:`, these methods raise an `ArgumentError` naming the association. `to_a` writes at most `size` entries: a model with more entries than that has the extra ones left out of the array, with no error, so pick a `size` large enough for your data.
|
|
623
638
|
|
|
624
639
|
### Schema::CSVParser
|
|
625
640
|
|
|
626
|
-
`Schema::CSVParser` is a class that reads rows from a `CSV` object into models. The model needs `Schema::ArrayHeaders` and `Schema::Arrays`. Pass a plain `CSV` (not one created with `headers: true`); the first row is used as the headers unless you pass them in:
|
|
641
|
+
`Schema::CSVParser` is a class that reads rows from a `CSV` object into models. The model needs `Schema::ArrayHeaders` and `Schema::Arrays`. Pass a plain `CSV` (not one created with `headers: true`); the first row is used as the headers unless you pass them in. Headers are stripped, and the byte order mark Excel puts at the start of UTF-8 files is removed:
|
|
627
642
|
|
|
628
643
|
```ruby
|
|
629
644
|
require 'csv'
|
data/bin/schema-json2csv
CHANGED
|
@@ -61,7 +61,11 @@ io = csv_file ? File.open(csv_file, 'ab') : $stdout
|
|
|
61
61
|
|
|
62
62
|
io.print CSV.generate_line(schema_class.to_headers) if write_headers
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
begin
|
|
65
|
+
json_data = JSON.parse(json_data)
|
|
66
|
+
rescue JSON::ParserError => e
|
|
67
|
+
abort("invalid json: #{e.message.lines.first.strip}")
|
|
68
|
+
end
|
|
65
69
|
json_data = [json_data] unless json_data.is_a?(Array)
|
|
66
70
|
|
|
67
71
|
json_data.each do |data|
|
|
@@ -6,6 +6,7 @@ module Schema
|
|
|
6
6
|
# Schema::ActiveModelParsingErrors turns parsing error codes into readable messages,
|
|
7
7
|
# e.g. "Age is invalid" instead of "Age invalid". Override them with I18n keys under schema.parsing_errors.
|
|
8
8
|
class ActiveModelParsingErrors < ActiveModel::Errors
|
|
9
|
+
# Default English message for each parsing error code.
|
|
9
10
|
MESSAGES = {
|
|
10
11
|
::Schema::ParsingErrors::INVALID => 'is invalid',
|
|
11
12
|
::Schema::ParsingErrors::INCOMPATIBLE => 'is an incompatible type',
|
|
@@ -20,6 +20,10 @@ module Schema
|
|
|
20
20
|
)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Raises unless the model parsed cleanly and passes its validations.
|
|
24
|
+
# @raise [ParsingException] when there are parsing errors
|
|
25
|
+
# @raise [ValidationException] when validations fail
|
|
26
|
+
# @return [void]
|
|
23
27
|
def valid!
|
|
24
28
|
parsed!
|
|
25
29
|
valid_model!
|
|
@@ -38,6 +42,7 @@ module Schema
|
|
|
38
42
|
|
|
39
43
|
# no-doc
|
|
40
44
|
module OverrideParsingErrors
|
|
45
|
+
# @return [ActiveModelParsingErrors] parsing errors with readable messages
|
|
41
46
|
def parsing_errors
|
|
42
47
|
@parsing_errors ||= ActiveModelParsingErrors.new(self)
|
|
43
48
|
end
|
data/lib/schema/array_headers.rb
CHANGED
|
@@ -9,6 +9,10 @@ module Schema
|
|
|
9
9
|
|
|
10
10
|
# adds methods to the class
|
|
11
11
|
module ClassMethods
|
|
12
|
+
# Maps header names to the attributes they fill.
|
|
13
|
+
# @param headers [Array<String>] the header row
|
|
14
|
+
# @param header_prefix [String, nil] used internally for has_many columns
|
|
15
|
+
# @return [Hash] e.g. `{ name: { index: 0 }, phones: { number: { indexes: [1, 2] } } }`
|
|
12
16
|
def map_headers_to_attributes(headers, header_prefix = nil)
|
|
13
17
|
mapped_headers = {}
|
|
14
18
|
map_headers_to_fields(headers, mapped_headers, header_prefix)
|
|
@@ -17,14 +21,24 @@ module Schema
|
|
|
17
21
|
mapped_headers
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# Names of the fields no header was mapped to.
|
|
25
|
+
# @param mapped_headers [Hash] result of {#map_headers_to_attributes}
|
|
26
|
+
# @param header_prefix [String, nil] used internally for nested associations
|
|
27
|
+
# @return [Array<String>]
|
|
20
28
|
def get_unmapped_field_names(mapped_headers, header_prefix = nil)
|
|
21
29
|
get_field_names(mapped_headers, header_prefix, false)
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
# Names of the fields the headers were mapped to, as they'd appear in a header row.
|
|
33
|
+
# @param mapped_headers [Hash] result of {#map_headers_to_attributes}
|
|
34
|
+
# @param header_prefix [String, nil] used internally for nested associations
|
|
35
|
+
# @return [Array<String>]
|
|
24
36
|
def get_mapped_field_names(mapped_headers, header_prefix = nil)
|
|
25
37
|
get_field_names(mapped_headers, header_prefix, true)
|
|
26
38
|
end
|
|
27
39
|
|
|
40
|
+
# Field names (first alias preferred) that are, or with mapped false are not, in the mapping.
|
|
41
|
+
# @api private
|
|
28
42
|
def get_field_names(mapped_headers, header_prefix = nil, mapped = true)
|
|
29
43
|
fields = []
|
|
30
44
|
schema.each do |field_name, field_options|
|
|
@@ -41,6 +55,7 @@ module Schema
|
|
|
41
55
|
fields
|
|
42
56
|
end
|
|
43
57
|
|
|
58
|
+
# Highest has_many index looked for in headers such as Phones1Number.
|
|
44
59
|
MAX_ARRAY_INDEX = 10_000
|
|
45
60
|
|
|
46
61
|
private
|
data/lib/schema/arrays.rb
CHANGED
|
@@ -7,12 +7,26 @@ module Schema
|
|
|
7
7
|
base.extend ClassMethods
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# the fixed number of entries a has_many takes up in an array
|
|
11
|
+
def self.association_size(field_options)
|
|
12
|
+
field_options[:size] || raise(
|
|
13
|
+
ArgumentError,
|
|
14
|
+
"has_many #{field_options[:name].inspect} needs a size: option to be converted to and from arrays"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
# adds from_array method to the class
|
|
11
19
|
module ClassMethods
|
|
20
|
+
# Builds a model from a row.
|
|
21
|
+
# @param array [Array] row values
|
|
22
|
+
# @param mapped_headers [Hash] result of ArrayHeaders::ClassMethods#map_headers_to_attributes
|
|
23
|
+
# @return [Schema::Model]
|
|
12
24
|
def from_array(array, mapped_headers)
|
|
13
25
|
new.update_attributes_with_array(array, mapped_headers)
|
|
14
26
|
end
|
|
15
27
|
|
|
28
|
+
# An array of nils shaped like {Schema::Arrays#to_a}'s output.
|
|
29
|
+
# @return [Array]
|
|
16
30
|
def to_empty_array
|
|
17
31
|
data = []
|
|
18
32
|
schema.each_value do |field_options|
|
|
@@ -23,12 +37,15 @@ module Schema
|
|
|
23
37
|
when :has_one
|
|
24
38
|
const_get(field_options[:class_name]).to_empty_array
|
|
25
39
|
when :has_many
|
|
26
|
-
field_options
|
|
40
|
+
Arrays.association_size(field_options).times.map { const_get(field_options[:class_name]).to_empty_array }
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
data
|
|
30
44
|
end
|
|
31
45
|
|
|
46
|
+
# Header names in array order, e.g. `phones[1].number`.
|
|
47
|
+
# @param prefix [String, nil] used internally for nested associations
|
|
48
|
+
# @return [Array<String>]
|
|
32
49
|
def to_headers(prefix = nil)
|
|
33
50
|
headers = []
|
|
34
51
|
schema.each_value do |field_options|
|
|
@@ -39,7 +56,7 @@ module Schema
|
|
|
39
56
|
when :has_one
|
|
40
57
|
const_get(field_options[:class_name]).to_headers("#{prefix}#{field_options[:key]}.")
|
|
41
58
|
when :has_many
|
|
42
|
-
field_options
|
|
59
|
+
Arrays.association_size(field_options).times.map do |i|
|
|
43
60
|
const_get(field_options[:class_name]).to_headers(prefix.to_s + field_options[:key] + "[#{i + 1}].")
|
|
44
61
|
end
|
|
45
62
|
else
|
|
@@ -50,6 +67,8 @@ module Schema
|
|
|
50
67
|
end
|
|
51
68
|
end
|
|
52
69
|
|
|
70
|
+
# The model as a nested array, in {ClassMethods#to_headers} order; has_many entries beyond size: are left out.
|
|
71
|
+
# @return [Array]
|
|
53
72
|
def to_a
|
|
54
73
|
data = []
|
|
55
74
|
self.class.schema.each_value do |field_options|
|
|
@@ -62,7 +81,7 @@ module Schema
|
|
|
62
81
|
value ? value.to_a : self.class.const_get(field_options[:class_name]).to_empty_array
|
|
63
82
|
when :has_many
|
|
64
83
|
values = value || []
|
|
65
|
-
field_options
|
|
84
|
+
Arrays.association_size(field_options).times.map do |idx|
|
|
66
85
|
value = values[idx]
|
|
67
86
|
value ? value.to_a : self.class.const_get(field_options[:class_name]).to_empty_array
|
|
68
87
|
end
|
|
@@ -73,6 +92,11 @@ module Schema
|
|
|
73
92
|
data
|
|
74
93
|
end
|
|
75
94
|
|
|
95
|
+
# Sets attributes from a row using a header mapping.
|
|
96
|
+
# @param array [Array] row values
|
|
97
|
+
# @param mapped_headers [Hash] result of ArrayHeaders::ClassMethods#map_headers_to_attributes
|
|
98
|
+
# @param offset [Integer, nil] has_many entry index, used internally
|
|
99
|
+
# @return [self]
|
|
76
100
|
def update_attributes_with_array(array, mapped_headers, offset = nil)
|
|
77
101
|
self.class.schema.each_value do |field_options|
|
|
78
102
|
next unless (mapped_field = mapped_headers[field_options[:name]])
|
|
@@ -95,11 +119,15 @@ module Schema
|
|
|
95
119
|
self
|
|
96
120
|
end
|
|
97
121
|
|
|
122
|
+
# Builds nested models from a row.
|
|
123
|
+
# @api private
|
|
98
124
|
def update_nested_schemas_from_array(array, mapped_headers, current_offset = nil)
|
|
99
125
|
update_nested_has_one_associations_from_array(array, mapped_headers, current_offset)
|
|
100
126
|
update_nested_has_many_associations_from_array(array, mapped_headers)
|
|
101
127
|
end
|
|
102
128
|
|
|
129
|
+
# Builds has_one models from a row.
|
|
130
|
+
# @api private
|
|
103
131
|
def update_nested_has_one_associations_from_array(array, mapped_headers, current_offset = nil)
|
|
104
132
|
self.class.schema.each_value do |field_options|
|
|
105
133
|
next unless field_options[:type] == :has_one
|
|
@@ -112,6 +140,8 @@ module Schema
|
|
|
112
140
|
end
|
|
113
141
|
end
|
|
114
142
|
|
|
143
|
+
# Builds has_many models from a row.
|
|
144
|
+
# @api private
|
|
115
145
|
def update_nested_has_many_associations_from_array(array, mapped_headers)
|
|
116
146
|
self.class.schema.each_value do |field_options|
|
|
117
147
|
next unless field_options[:type] == :has_many
|
|
@@ -128,10 +158,14 @@ module Schema
|
|
|
128
158
|
end
|
|
129
159
|
end
|
|
130
160
|
|
|
161
|
+
# Builds one nested model from a row.
|
|
162
|
+
# @api private
|
|
131
163
|
def create_schema_with_array(field_options, array, mapped_model, offset)
|
|
132
164
|
self.class.const_get(field_options[:class_name]).new.update_attributes_with_array(array, mapped_model, offset)
|
|
133
165
|
end
|
|
134
166
|
|
|
167
|
+
# Number of has_many entries a mapping has columns for.
|
|
168
|
+
# @api private
|
|
135
169
|
def largest_number_of_indexes_from_map(mapped_model)
|
|
136
170
|
size = 0
|
|
137
171
|
mapped_model.each_value do |info|
|
|
@@ -19,10 +19,12 @@ module Schema
|
|
|
19
19
|
@base_schema_class_name = kls.name
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# @return [Class] the class that declared this association
|
|
22
23
|
def base_schema_class
|
|
23
24
|
@base_schema_class_name ? Object.const_get(@base_schema_class_name) : @base_schema_class
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# @return [Hash] the association's options in the parent schema
|
|
26
28
|
def schema_options
|
|
27
29
|
base_schema_class.schema[schema_name]
|
|
28
30
|
end
|
|
@@ -10,6 +10,11 @@ module Schema
|
|
|
10
10
|
|
|
11
11
|
# no-doc
|
|
12
12
|
module ClassMethods
|
|
13
|
+
# Declares a subtype, used when the type field equals type.
|
|
14
|
+
# @param type [String, Symbol] type field value
|
|
15
|
+
# @param options [Hash] `class_name:` to name the generated class
|
|
16
|
+
# @yield attribute definitions for this type
|
|
17
|
+
# @return [Class] the subtype class
|
|
13
18
|
def add_type(type, options = {}, &block)
|
|
14
19
|
class_name = options[:class_name] || schema_dynamic_type_class_name(type)
|
|
15
20
|
kls = Class.new(self)
|
|
@@ -19,14 +24,20 @@ module Schema
|
|
|
19
24
|
kls
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
# Declares the subtype used when no other type matches.
|
|
28
|
+
# @param options [Hash] see {#add_type}
|
|
29
|
+
# @yield attribute definitions for this type
|
|
30
|
+
# @return [Class] the subtype class
|
|
22
31
|
def default_type(options = {}, &)
|
|
23
32
|
add_type(:default, options, &)
|
|
24
33
|
end
|
|
25
34
|
|
|
35
|
+
# @return [Hash] type name => class name, including :default
|
|
26
36
|
def dynamic_types
|
|
27
37
|
schema_options[:types]
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
# @return [Array] declared type names, without :default
|
|
30
41
|
def dynamic_type_names
|
|
31
42
|
dynamic_types.keys - [:default]
|
|
32
43
|
end
|
|
@@ -10,6 +10,13 @@ module Schema
|
|
|
10
10
|
|
|
11
11
|
# no-doc
|
|
12
12
|
module ClassMethods
|
|
13
|
+
# Declares a list of nested models, with a getter, a setter and append_to_<name>.
|
|
14
|
+
# @param name [Symbol]
|
|
15
|
+
# @param options [Hash] `alias:`/`aliases:`, `default: true`, `class_name:`, `base_class:`,
|
|
16
|
+
# `from: :hash` with `hash_key_field:`, `size:` (for Schema::Arrays), and
|
|
17
|
+
# `type_field:`/`external_type_field:`/`type_ignorecase:` for dynamic types
|
|
18
|
+
# @yield attribute definitions for the nested model
|
|
19
|
+
# @return [Class] the nested model class
|
|
13
20
|
def has_many(name, options = {}, &block)
|
|
14
21
|
options = ::Schema::Utils.add_association_class(self, name, :has_many, options)
|
|
15
22
|
|
|
@@ -10,6 +10,12 @@ module Schema
|
|
|
10
10
|
|
|
11
11
|
# no-doc
|
|
12
12
|
module ClassMethods
|
|
13
|
+
# Declares a nested model, with a getter and a setter.
|
|
14
|
+
# @param name [Symbol]
|
|
15
|
+
# @param options [Hash] `alias:`/`aliases:`, `default: true`, `class_name:`, `base_class:`, and
|
|
16
|
+
# `type_field:`/`external_type_field:`/`type_ignorecase:` for dynamic types
|
|
17
|
+
# @yield attribute definitions for the nested model
|
|
18
|
+
# @return [Class] the nested model class
|
|
13
19
|
def has_one(name, options = {}, &block)
|
|
14
20
|
options = ::Schema::Utils.add_association_class(self, name, :has_one, options)
|
|
15
21
|
|
|
@@ -16,6 +16,8 @@ module Schema
|
|
|
16
16
|
configure_dynamic_schema_options(options)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Builds one nested model, recording parsing errors on base_schema.
|
|
20
|
+
# @api private
|
|
19
21
|
def create_schema(base_schema, data, error_name = nil, skip_fields = [])
|
|
20
22
|
if data.is_a?(Hash)
|
|
21
23
|
unless (schema_class = get_schema_class(base_schema, data))
|
|
@@ -31,6 +33,8 @@ module Schema
|
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
36
|
+
# Builds a has_many list from an array, or from a hash with `from: :hash`.
|
|
37
|
+
# @api private
|
|
34
38
|
def create_schemas(base_schema, list, skip_fields = [])
|
|
35
39
|
if is_list? && list.is_a?(Array)
|
|
36
40
|
list.each_with_index.map { |data, idx| create_schema(base_schema, data, "#{@schema_name}:#{idx}", skip_fields) }
|
|
@@ -46,6 +50,8 @@ module Schema
|
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
|
|
53
|
+
# The class to build for data, choosing a dynamic type when configured.
|
|
54
|
+
# @api private
|
|
49
55
|
def get_schema_class(base_schema, data)
|
|
50
56
|
if dynamic?
|
|
51
57
|
get_dynamic_schema_class(base_schema, data)
|
|
@@ -62,6 +68,8 @@ module Schema
|
|
|
62
68
|
@is_list
|
|
63
69
|
end
|
|
64
70
|
|
|
71
|
+
# The dynamic type class matching data's type.
|
|
72
|
+
# @api private
|
|
65
73
|
def get_dynamic_schema_class(base_schema, data)
|
|
66
74
|
type = get_dynamic_type(base_schema, data)
|
|
67
75
|
type = type.to_s.downcase if @ignorecase
|
|
@@ -72,12 +80,16 @@ module Schema
|
|
|
72
80
|
get_default_dynamic_schema_class(base_schema)
|
|
73
81
|
end
|
|
74
82
|
|
|
83
|
+
# The default_type class, if one was declared.
|
|
84
|
+
# @api private
|
|
75
85
|
def get_default_dynamic_schema_class(base_schema)
|
|
76
86
|
return unless (class_name = @types[:default])
|
|
77
87
|
|
|
78
88
|
base_schema.class.const_get(class_name)
|
|
79
89
|
end
|
|
80
90
|
|
|
91
|
+
# The type value from the nested data or the parent's external type field.
|
|
92
|
+
# @api private
|
|
81
93
|
def get_dynamic_type(base_schema, data)
|
|
82
94
|
if @type_field
|
|
83
95
|
type_fields.each do |name|
|
|
@@ -98,10 +110,14 @@ module Schema
|
|
|
98
110
|
end
|
|
99
111
|
end
|
|
100
112
|
|
|
113
|
+
# Records a parsing error on the parent model.
|
|
114
|
+
# @api private
|
|
101
115
|
def add_parsing_error(base_schema, error_name, error_msg)
|
|
102
116
|
base_schema.parsing_errors.add(error_name || @schema_name, error_msg)
|
|
103
117
|
end
|
|
104
118
|
|
|
119
|
+
# Reads the dynamic type options.
|
|
120
|
+
# @api private
|
|
105
121
|
def configure_dynamic_schema_options(options)
|
|
106
122
|
@type_field = options[:type_field]
|
|
107
123
|
@external_type_field = options[:external_type_field]
|
|
@@ -18,6 +18,8 @@ module Schema
|
|
|
18
18
|
set_field_options.to_h { |field_options| [field_options[:name], public_send(field_options[:getter])] }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# @param other [Object]
|
|
22
|
+
# @return [Boolean] true for a model of the same class with equal attribute values
|
|
21
23
|
def ==(other)
|
|
22
24
|
other.instance_of?(self.class) && other.attribute_values == attribute_values
|
|
23
25
|
end
|
data/lib/schema/csv_parser.rb
CHANGED
|
@@ -8,28 +8,45 @@ module Schema
|
|
|
8
8
|
def initialize(csv, schema_class, headers = nil)
|
|
9
9
|
@csv = csv
|
|
10
10
|
@schema_class = schema_class
|
|
11
|
-
@headers = (headers || csv.shift) || []
|
|
12
|
-
@headers = @headers.map(&:strip)
|
|
11
|
+
@headers = clean_headers((headers || csv.shift) || [])
|
|
13
12
|
@mapped_headers = schema_class.map_headers_to_attributes(@headers)
|
|
14
13
|
end
|
|
15
14
|
|
|
15
|
+
# strips whitespace, treats empty header cells as "", and drops the UTF-8 byte order mark
|
|
16
|
+
# that Excel puts before the first header
|
|
17
|
+
def clean_headers(headers)
|
|
18
|
+
headers.map.with_index do |header, idx|
|
|
19
|
+
header = header.to_s.strip
|
|
20
|
+
idx.zero? ? header.delete_prefix("\uFEFF") : header
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Required header names the CSV doesn't map.
|
|
25
|
+
# @param required_fields [Array<String>]
|
|
26
|
+
# @return [Array<String>]
|
|
16
27
|
def missing_fields(required_fields)
|
|
17
28
|
required_fields - get_mapped_headers(@mapped_headers)
|
|
18
29
|
end
|
|
19
30
|
alias missing_headers missing_fields
|
|
20
31
|
|
|
32
|
+
# Reads the next row.
|
|
33
|
+
# @return [Schema::Model, nil] nil at the end of the CSV
|
|
21
34
|
def shift
|
|
22
35
|
return unless (row = @csv.shift)
|
|
23
36
|
|
|
24
37
|
@schema_class.from_array(row, @mapped_headers)
|
|
25
38
|
end
|
|
26
39
|
|
|
40
|
+
# Yields a model for each remaining row.
|
|
41
|
+
# @yieldparam model [Schema::Model]
|
|
27
42
|
def each
|
|
28
43
|
while (schema = shift)
|
|
29
44
|
yield schema
|
|
30
45
|
end
|
|
31
46
|
end
|
|
32
47
|
|
|
48
|
+
# Header names covered by a mapping.
|
|
49
|
+
# @api private
|
|
33
50
|
def get_mapped_headers(mapped_headers)
|
|
34
51
|
indexed_headers = []
|
|
35
52
|
mapped_headers.each_value do |info|
|
data/lib/schema/deep_copy.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module Schema
|
|
4
4
|
# Schema::DeepCopy copies a model along with its nested models, collections and parsing errors
|
|
5
5
|
module DeepCopy
|
|
6
|
+
# Copies the model with its nested models, collections, strings and parsing errors,
|
|
7
|
+
# so the copy can be changed without touching the original.
|
|
8
|
+
# @return [Schema::Model]
|
|
6
9
|
def deep_dup
|
|
7
10
|
copy = dup
|
|
8
11
|
copy.copy_attribute_values_from(self)
|
data/lib/schema/errors.rb
CHANGED
|
@@ -5,16 +5,23 @@ module Schema
|
|
|
5
5
|
class Errors
|
|
6
6
|
attr_reader :errors
|
|
7
7
|
|
|
8
|
+
# Returned by {#[]} for names without errors.
|
|
8
9
|
EMPTY_ARRAY = [].freeze
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@errors = {}
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
# @param name [Symbol, String]
|
|
16
|
+
# @return [Array<String>] error codes for name (empty when none)
|
|
14
17
|
def [](name)
|
|
15
18
|
@errors[name] || EMPTY_ARRAY
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
# Records an error code for name.
|
|
22
|
+
# @param name [Symbol, String]
|
|
23
|
+
# @param error [String] a {Schema::ParsingErrors} code
|
|
24
|
+
# @return [Array<String>]
|
|
18
25
|
def add(name, error)
|
|
19
26
|
@errors[name] ||= []
|
|
20
27
|
@errors[name] << error
|
|
@@ -25,6 +32,7 @@ module Schema
|
|
|
25
32
|
@errors.empty?
|
|
26
33
|
end
|
|
27
34
|
|
|
35
|
+
# @return [Array] names that have errors
|
|
28
36
|
def attribute_names
|
|
29
37
|
@errors.keys
|
|
30
38
|
end
|
data/lib/schema/model.rb
CHANGED
|
@@ -14,6 +14,8 @@ module Schema
|
|
|
14
14
|
base.extend ClassMethods
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Options every attribute starts with.
|
|
18
|
+
# @api private
|
|
17
19
|
def self.default_attribute_options(name, type)
|
|
18
20
|
{
|
|
19
21
|
key: name.to_s.freeze,
|
|
@@ -28,6 +30,7 @@ module Schema
|
|
|
28
30
|
|
|
29
31
|
# no-doc
|
|
30
32
|
module ClassMethods
|
|
33
|
+
# @return [Hash{Symbol => Hash}] field name => options, including alias entries
|
|
31
34
|
def schema
|
|
32
35
|
{}.freeze
|
|
33
36
|
end
|
|
@@ -41,6 +44,8 @@ module Schema
|
|
|
41
44
|
@schema_with_string_keys = current_schema.transform_keys(&:to_s).freeze
|
|
42
45
|
end
|
|
43
46
|
|
|
47
|
+
# Modules to include in nested classes, and the unknown attribute setting.
|
|
48
|
+
# @api private
|
|
44
49
|
def schema_config
|
|
45
50
|
{
|
|
46
51
|
schema_includes: [],
|
|
@@ -65,6 +70,12 @@ module Schema
|
|
|
65
70
|
schema_config[:capture_unknown_attributes]
|
|
66
71
|
end
|
|
67
72
|
|
|
73
|
+
# Declares an attribute with a getter, a parsing setter and a `<name>_was_set?` predicate.
|
|
74
|
+
# @param name [Symbol]
|
|
75
|
+
# @param type [Symbol] parsed by `parse_<type>`, e.g. :integer
|
|
76
|
+
# @param options [Hash] `alias:`/`aliases:`, `default:`, and parser options such as
|
|
77
|
+
# `separator:`/`data_type:` for :array
|
|
78
|
+
# @return [void]
|
|
68
79
|
def attribute(name, type, options = {})
|
|
69
80
|
options[:aliases] = [options[:alias]] if options.key?(:alias)
|
|
70
81
|
|
|
@@ -79,10 +90,15 @@ module Schema
|
|
|
79
90
|
add_aliases(name, options)
|
|
80
91
|
end
|
|
81
92
|
|
|
93
|
+
# Builds a model from data. See {Schema::Model#update_attributes}.
|
|
94
|
+
# @return [Schema::Model]
|
|
82
95
|
def from_hash(data = nil, skip_fields = [])
|
|
83
96
|
new.update_attributes(data, skip_fields)
|
|
84
97
|
end
|
|
85
98
|
|
|
99
|
+
# Includes mod here and in every nested association class, current and future.
|
|
100
|
+
# @param mod [Module]
|
|
101
|
+
# @return [void]
|
|
86
102
|
def schema_include(mod)
|
|
87
103
|
config = schema_config.dup
|
|
88
104
|
config[:schema_includes] = config[:schema_includes] + [mod]
|
|
@@ -96,6 +112,8 @@ module Schema
|
|
|
96
112
|
end
|
|
97
113
|
end
|
|
98
114
|
|
|
115
|
+
# Defines an attribute's getter, setter and predicate.
|
|
116
|
+
# @api private
|
|
99
117
|
def add_attribute_methods(name, options)
|
|
100
118
|
parser_check = parser_check_code(name, options)
|
|
101
119
|
class_eval(
|
|
@@ -123,6 +141,8 @@ module Schema
|
|
|
123
141
|
"::Schema::Utils.check_parser!(self, #{options[:parser].inspect}, #{name.inspect}, #{options[:type].inspect})"
|
|
124
142
|
end
|
|
125
143
|
|
|
144
|
+
# Adds schema entries and methods for an attribute's aliases.
|
|
145
|
+
# @api private
|
|
126
146
|
def add_aliases(name, options)
|
|
127
147
|
return unless options[:aliases]
|
|
128
148
|
|
|
@@ -134,32 +154,47 @@ module Schema
|
|
|
134
154
|
end
|
|
135
155
|
end
|
|
136
156
|
|
|
157
|
+
# Sets attributes, then associations, from data.
|
|
158
|
+
# @param data [Hash, Array, nil] a hash with symbol or string keys, or [key, value] pairs;
|
|
159
|
+
# anything else records an incompatible error on :base
|
|
160
|
+
# @param skip_fields [Array] fields to ignore, e.g. `[:id, { items: [:id] }]`
|
|
161
|
+
# @return [self]
|
|
137
162
|
def update_attributes(data = nil, skip_fields = [])
|
|
138
163
|
return self if data.nil?
|
|
164
|
+
return add_incompatible_data_error unless attributes_data?(data)
|
|
139
165
|
|
|
140
166
|
update_model_attributes(data, skip_fields)
|
|
141
167
|
update_associations(data, skip_fields)
|
|
142
168
|
self
|
|
143
169
|
end
|
|
144
170
|
|
|
171
|
+
# The model as a hash; nested models become hashes.
|
|
172
|
+
# @param opts [Hash]
|
|
173
|
+
# @option opts [Boolean] :include_nils include nil values
|
|
174
|
+
# @option opts [Boolean] :only_set include only fields present in the input (nils included), at every level
|
|
175
|
+
# @option opts [Proc] :select_filter keep a field when it returns true; called with (name, value, options)
|
|
176
|
+
# @option opts [Proc] :reject_filter drop a field when it returns true; called with (name, value, options)
|
|
177
|
+
# @return [Hash]
|
|
145
178
|
def as_json(opts = {})
|
|
146
179
|
self.class.schema.each_with_object({}) do |(field_name, field_options), memo|
|
|
147
180
|
next if field_options[:alias_of]
|
|
181
|
+
next if opts[:only_set] && !instance_variable_defined?(field_options[:instance_variable])
|
|
148
182
|
|
|
149
183
|
value = public_send(field_options[:getter])
|
|
150
|
-
next
|
|
151
|
-
next if opts[:select_filter] && !opts[:select_filter].call(field_name, value, field_options)
|
|
152
|
-
next if opts[:reject_filter]&.call(field_name, value, field_options)
|
|
184
|
+
next unless include_in_json?(field_name, value, field_options, opts)
|
|
153
185
|
|
|
154
186
|
memo[field_name] = value.is_a?(Array) ? value.map { |e| value_as_json(e, opts) } : value_as_json(value, opts)
|
|
155
187
|
end
|
|
156
188
|
end
|
|
157
189
|
|
|
190
|
+
# {#as_json} with nil values included.
|
|
191
|
+
# @return [Hash]
|
|
158
192
|
def to_hash
|
|
159
193
|
as_json(include_nils: true)
|
|
160
194
|
end
|
|
161
195
|
alias to_h to_hash
|
|
162
196
|
|
|
197
|
+
# @return [Schema::Errors] parsing error codes by field
|
|
163
198
|
def parsing_errors
|
|
164
199
|
@parsing_errors ||= Errors.new
|
|
165
200
|
end
|
|
@@ -172,6 +207,26 @@ module Schema
|
|
|
172
207
|
|
|
173
208
|
private
|
|
174
209
|
|
|
210
|
+
# a Hash (or anything with each_pair), or an array of [key, value] pairs
|
|
211
|
+
def attributes_data?(data)
|
|
212
|
+
return true if data.respond_to?(:each_pair)
|
|
213
|
+
|
|
214
|
+
data.is_a?(Array) && data.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def add_incompatible_data_error
|
|
218
|
+
parsing_errors.add(:base, ::Schema::ParsingErrors::INCOMPATIBLE)
|
|
219
|
+
self
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# only_set keeps nils that came from the input, since they mean "set to null"
|
|
223
|
+
def include_in_json?(field_name, value, field_options, opts)
|
|
224
|
+
return false if value.nil? && !opts[:include_nils] && !opts[:only_set]
|
|
225
|
+
return false if opts[:select_filter] && !opts[:select_filter].call(field_name, value, field_options)
|
|
226
|
+
|
|
227
|
+
!opts[:reject_filter]&.call(field_name, value, field_options)
|
|
228
|
+
end
|
|
229
|
+
|
|
175
230
|
# values without as_json (no ActiveSupport JSON extension) are returned as is
|
|
176
231
|
def value_as_json(value, opts)
|
|
177
232
|
value.respond_to?(:as_json) ? value.as_json(opts) : value
|
|
@@ -8,9 +8,13 @@ module Schema
|
|
|
8
8
|
module American
|
|
9
9
|
include StringValue
|
|
10
10
|
|
|
11
|
+
# strptime format for :american_date.
|
|
11
12
|
DATE_FORMAT = '%m/%d/%Y'
|
|
13
|
+
# strptime format for :american_time.
|
|
12
14
|
TIME_FORMAT = '%m/%d/%Y %H:%M:%S'
|
|
13
15
|
|
|
16
|
+
# Parses MM/DD/YYYY strings; blank strings are nil.
|
|
17
|
+
# @return [Date, nil]
|
|
14
18
|
def parse_american_date(field_name, parsing_errors, value)
|
|
15
19
|
case value
|
|
16
20
|
when Date
|
|
@@ -27,6 +31,8 @@ module Schema
|
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
|
|
34
|
+
# Parses MM/DD/YYYY HH:MM:SS strings; blank strings are nil.
|
|
35
|
+
# @return [Time, nil]
|
|
30
36
|
def parse_american_time(field_name, parsing_errors, value)
|
|
31
37
|
case value
|
|
32
38
|
when Time
|
data/lib/schema/parsers/array.rb
CHANGED
|
@@ -4,6 +4,9 @@ module Schema
|
|
|
4
4
|
module Parsers
|
|
5
5
|
# Schema::Parsers::Array adds the array type to schemas
|
|
6
6
|
module Array
|
|
7
|
+
# Parses arrays, or strings split on the attribute's `separator:`, converting
|
|
8
|
+
# elements with its `data_type:`.
|
|
9
|
+
# @return [Array, nil]
|
|
7
10
|
def parse_array(field_name, parsing_errors, value)
|
|
8
11
|
case value
|
|
9
12
|
when ::Array
|
|
@@ -25,12 +28,16 @@ module Schema
|
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# Splits a string on the attribute's separator.
|
|
32
|
+
# @api private
|
|
28
33
|
def self.parse_string_array(model, field_name, parsing_errors, value, schema_options)
|
|
29
34
|
return nil unless (separator = schema_options[:separator])
|
|
30
35
|
|
|
31
36
|
convert_array_values(model, field_name, parsing_errors, value.split(separator), schema_options)
|
|
32
37
|
end
|
|
33
38
|
|
|
39
|
+
# Converts elements with the attribute's data_type.
|
|
40
|
+
# @api private
|
|
34
41
|
def self.convert_array_values(model, field_name, parsing_errors, data, schema_options)
|
|
35
42
|
return data unless (data_type = schema_options[:data_type])
|
|
36
43
|
|
|
@@ -10,6 +10,7 @@ module Schema
|
|
|
10
10
|
|
|
11
11
|
# strings are stripped first; a blank string parses to nil without an error
|
|
12
12
|
INTEGER_REGEX = /\A[-+]?\d+(?:\.0+)?\z/
|
|
13
|
+
# Accepted float strings, e.g. "1.5", "007.5", "1e+5".
|
|
13
14
|
FLOAT_REGEX = /\A[-+]?\d+(?:\.\d+)?([Ee][-+]?\d+)?\z/
|
|
14
15
|
# matched case-insensitively; any other string is invalid
|
|
15
16
|
BOOLEAN_STRINGS = {
|
|
@@ -17,6 +18,8 @@ module Schema
|
|
|
17
18
|
'0' => false, 'f' => false, 'false' => false, 'off' => false, 'n' => false, 'no' => false
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
21
|
+
# Parses integers; whole floats are converted, fractional ones are incompatible.
|
|
22
|
+
# @return [Integer, nil]
|
|
20
23
|
def parse_integer(field_name, parsing_errors, value)
|
|
21
24
|
case value
|
|
22
25
|
when Integer
|
|
@@ -35,6 +38,8 @@ module Schema
|
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
|
|
41
|
+
# Converts a float for parse_integer.
|
|
42
|
+
# @api private
|
|
38
43
|
def parse_float_as_integer(field_name, parsing_errors, value)
|
|
39
44
|
unless value.finite?
|
|
40
45
|
parsing_errors.add(field_name, ::Schema::ParsingErrors::INCOMPATIBLE)
|
|
@@ -45,6 +50,8 @@ module Schema
|
|
|
45
50
|
value.to_i
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
# Converts scalars to strings; hashes and arrays are incompatible.
|
|
54
|
+
# @return [String, nil]
|
|
48
55
|
def parse_string(field_name, parsing_errors, value)
|
|
49
56
|
case value
|
|
50
57
|
when String
|
|
@@ -59,11 +66,11 @@ module Schema
|
|
|
59
66
|
end
|
|
60
67
|
end
|
|
61
68
|
|
|
62
|
-
#
|
|
69
|
+
# a blank (empty or whitespace-only) string returns nil; other strings are kept as is
|
|
63
70
|
def parse_string_or_nil(field_name, parsing_errors, value)
|
|
64
71
|
case value
|
|
65
72
|
when String
|
|
66
|
-
value.empty? ? nil : value
|
|
73
|
+
value.strip.empty? ? nil : value
|
|
67
74
|
when ::Hash, ::Array
|
|
68
75
|
parsing_errors.add(field_name, ::Schema::ParsingErrors::INCOMPATIBLE)
|
|
69
76
|
nil
|
|
@@ -74,6 +81,8 @@ module Schema
|
|
|
74
81
|
end
|
|
75
82
|
end
|
|
76
83
|
|
|
84
|
+
# Parses floats.
|
|
85
|
+
# @return [Float, nil]
|
|
77
86
|
def parse_float(field_name, parsing_errors, value)
|
|
78
87
|
case value
|
|
79
88
|
when Float
|
|
@@ -92,6 +101,8 @@ module Schema
|
|
|
92
101
|
end
|
|
93
102
|
end
|
|
94
103
|
|
|
104
|
+
# Parses ISO 8601 date-times (Time.xmlschema).
|
|
105
|
+
# @return [Time, nil]
|
|
95
106
|
def parse_time(field_name, parsing_errors, value)
|
|
96
107
|
case value
|
|
97
108
|
when Time
|
|
@@ -108,6 +119,8 @@ module Schema
|
|
|
108
119
|
end
|
|
109
120
|
end
|
|
110
121
|
|
|
122
|
+
# Parses ISO 8601 dates (Date.iso8601).
|
|
123
|
+
# @return [Date, nil]
|
|
111
124
|
def parse_date(field_name, parsing_errors, value)
|
|
112
125
|
case value
|
|
113
126
|
when Date
|
|
@@ -124,6 +137,8 @@ module Schema
|
|
|
124
137
|
end
|
|
125
138
|
end
|
|
126
139
|
|
|
140
|
+
# Parses booleans; see {BOOLEAN_STRINGS}. Numbers are true unless 0.
|
|
141
|
+
# @return [Boolean, nil]
|
|
127
142
|
def parse_boolean(field_name, parsing_errors, value)
|
|
128
143
|
case value
|
|
129
144
|
when TrueClass, FalseClass
|
data/lib/schema/parsers/hash.rb
CHANGED
data/lib/schema/parsers/json.rb
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
module Schema
|
|
4
4
|
# Schema::ParsingErrors is a collection of parsing error messages
|
|
5
5
|
module ParsingErrors
|
|
6
|
+
# The value couldn't be parsed.
|
|
6
7
|
INVALID = 'invalid'
|
|
8
|
+
# The value is the wrong kind, e.g. a hash for a string attribute.
|
|
7
9
|
INCOMPATIBLE = 'incompatible'
|
|
10
|
+
# No dynamic type matched the type field.
|
|
8
11
|
UNKNOWN = 'unknown'
|
|
12
|
+
# The key isn't in the schema.
|
|
9
13
|
UNKNOWN_ATTRIBUTE = 'unknown_attribute'
|
|
14
|
+
# The parser doesn't handle the value's class.
|
|
10
15
|
UNHANDLED_TYPE = 'unhandled_type'
|
|
11
16
|
end
|
|
12
17
|
end
|
data/lib/schema/utils.rb
CHANGED
|
@@ -13,10 +13,14 @@ module Schema
|
|
|
13
13
|
"no #{parser} method (schema_include the parser module that defines it)"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Turns a name into a constant name.
|
|
17
|
+
# @api private
|
|
16
18
|
def classify_name(name)
|
|
17
19
|
name.gsub(/[^\da-z_-]/, '').gsub(/(^.|[_|-].)/) { |m| m[-1].upcase }
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# Creates and registers an association's model class.
|
|
23
|
+
# @api private
|
|
20
24
|
def create_schema_class(base_schema_class, schema_name, options)
|
|
21
25
|
base_schema_class.add_value_to_class_method(:schema, schema_name => options)
|
|
22
26
|
kls = Class.new(options[:base_class] || Object)
|
|
@@ -26,6 +30,8 @@ module Schema
|
|
|
26
30
|
kls
|
|
27
31
|
end
|
|
28
32
|
|
|
33
|
+
# Includes Schema::Model and the parent's schema_include modules.
|
|
34
|
+
# @api private
|
|
29
35
|
def include_schema_modules(kls, schema_config)
|
|
30
36
|
kls.send(:include, ::Schema::Model)
|
|
31
37
|
schema_config[:schema_includes].each do |mod|
|
|
@@ -33,6 +39,8 @@ module Schema
|
|
|
33
39
|
end
|
|
34
40
|
end
|
|
35
41
|
|
|
42
|
+
# Builds an association's schema options.
|
|
43
|
+
# @api private
|
|
36
44
|
def association_options(schema_name, schema_type, options)
|
|
37
45
|
options[:class_name] ||= "Schema#{classify_name(schema_type.to_s)}#{classify_name(schema_name.to_s)}"
|
|
38
46
|
options[:association] = true
|
|
@@ -41,6 +49,8 @@ module Schema
|
|
|
41
49
|
::Schema::Model.default_attribute_options(schema_name, schema_type).merge(options)
|
|
42
50
|
end
|
|
43
51
|
|
|
52
|
+
# Sets up an association's class and options.
|
|
53
|
+
# @api private
|
|
44
54
|
def add_association_class(base_schema_class, schema_name, schema_type, options)
|
|
45
55
|
options = ::Schema::Utils.association_options(schema_name, schema_type, options)
|
|
46
56
|
kls = ::Schema::Utils.create_schema_class(
|
|
@@ -53,12 +63,16 @@ module Schema
|
|
|
53
63
|
options
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
# Links an association class to its parent.
|
|
67
|
+
# @api private
|
|
56
68
|
def add_association_defaults(kls, base_schema_class, schema_name)
|
|
57
69
|
kls.send(:include, ::Schema::Associations::Base)
|
|
58
70
|
kls.base_schema_class = base_schema_class
|
|
59
71
|
kls.schema_name = schema_name
|
|
60
72
|
end
|
|
61
73
|
|
|
74
|
+
# Adds dynamic type support when configured.
|
|
75
|
+
# @api private
|
|
62
76
|
def add_association_dynamic_types(kls, options)
|
|
63
77
|
return if !options[:type_field] && !options[:external_type_field]
|
|
64
78
|
|
|
@@ -92,6 +106,8 @@ module Schema
|
|
|
92
106
|
value.dup
|
|
93
107
|
end
|
|
94
108
|
|
|
109
|
+
# Defines an attribute's default method and getter.
|
|
110
|
+
# @api private
|
|
95
111
|
def add_attribute_default_methods(kls, options)
|
|
96
112
|
default = options[:default]
|
|
97
113
|
kls.send(:define_method, options[:default_method]) { ::Schema::Utils.copy_default(default) }
|
data/lib/schema/version.rb
CHANGED
data/lib/schema-model.rb
CHANGED
|
@@ -4,6 +4,7 @@ autoload :SchemaValidator, 'schema_validator'
|
|
|
4
4
|
|
|
5
5
|
# Schema is a series of tools for transforming data into models
|
|
6
6
|
module Schema
|
|
7
|
+
# Base error for parsed! and valid_model!; carries the model and its errors.
|
|
7
8
|
class SchemaException < StandardError
|
|
8
9
|
attr_reader :schema,
|
|
9
10
|
:errors
|
|
@@ -15,7 +16,9 @@ module Schema
|
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
# Raised by parsed! (and valid!) when the model has parsing errors.
|
|
18
20
|
class ParsingException < SchemaException; end
|
|
21
|
+
# Raised by valid_model! (and valid!) when the model fails its validations.
|
|
19
22
|
class ValidationException < SchemaException; end
|
|
20
23
|
|
|
21
24
|
# raised when a value is assigned to an attribute whose type has no parse_<type> method
|
data/lib/schema_validator.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# SchemaValidator validates nested schemas: each one must have no parsing errors and pass its validations
|
|
4
4
|
class SchemaValidator < ActiveModel::EachValidator
|
|
5
|
+
# Called by ActiveModel for `validates :x, schema: true`.
|
|
6
|
+
# @api private
|
|
5
7
|
def validate_each(record, attribute, value)
|
|
6
8
|
record.errors.add(attribute, options.fetch(:message, :invalid)) unless valid_schema?(value)
|
|
7
9
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schema-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
@@ -15,14 +15,20 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
18
|
+
version: '0.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '2'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
28
|
+
version: '0.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '2'
|
|
26
32
|
description: Declare typed attributes and Schema turns incoming hashes, JSON and CSV
|
|
27
33
|
rows into Ruby objects. Values are coerced to integers, floats, booleans, dates,
|
|
28
34
|
times, arrays and hashes, and bad input is recorded as parsing errors instead of
|
|
@@ -73,6 +79,7 @@ metadata:
|
|
|
73
79
|
source_code_uri: https://github.com/dougyouch/schema
|
|
74
80
|
changelog_uri: https://github.com/dougyouch/schema/blob/master/CHANGELOG.md
|
|
75
81
|
bug_tracker_uri: https://github.com/dougyouch/schema/issues
|
|
82
|
+
documentation_uri: https://rubydoc.info/gems/schema-model
|
|
76
83
|
rdoc_options: []
|
|
77
84
|
require_paths:
|
|
78
85
|
- lib
|
|
@@ -80,7 +87,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
80
87
|
requirements:
|
|
81
88
|
- - ">="
|
|
82
89
|
- !ruby/object:Gem::Version
|
|
83
|
-
version: '3.
|
|
90
|
+
version: '3.3'
|
|
84
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
92
|
requirements:
|
|
86
93
|
- - ">="
|