strong_json 1.0.1 → 2.1.2
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/ruby.yml +29 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +3 -1
- data/CHANGELOG.md +20 -0
- data/Gemfile +4 -0
- data/README.md +16 -15
- data/Rakefile +2 -2
- data/Steepfile +7 -0
- data/example/Steepfile +8 -0
- data/example/{example.rb → lib/example.rb} +1 -1
- data/example/sig/example.rbs +11 -0
- data/lib/strong_json/error_reporter.rb +2 -4
- data/lib/strong_json/type.rb +82 -52
- data/lib/strong_json/types.rb +18 -6
- data/lib/strong_json/version.rb +2 -2
- data/pp.rb +27 -0
- data/sig/polyfill.rbs +13 -0
- data/sig/strong_json.rbs +67 -0
- data/sig/type.rbs +132 -0
- data/spec/basetype_spec.rb +12 -0
- data/spec/enum_spec.rb +8 -8
- data/spec/hash_spec.rb +32 -0
- data/spec/json_spec.rb +10 -10
- data/spec/object_spec.rb +108 -80
- data/strong_json.gemspec +0 -5
- metadata +18 -68
- data/example/example.rbi +0 -11
- data/sig/strong_json.rbi +0 -64
- data/sig/type.rbi +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3024051238c907ceb915ad211936b82d0c4422f6980f2abeff76a667017f6a0a
|
4
|
+
data.tar.gz: 9f9d1efa03cb0d35155837928577062ae696066841d50a6495c33f40f7fee6ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99a431a8d8f686604b8b0a44a1827e0440c61144d9d1b5f1253fbf79b6707dfb10f60a54de337b4dc87b7d27f31b6bd5f3c20f0a6a8a2dff2f48c2a144020827
|
7
|
+
data.tar.gz: 38953830ba84e56e13ee9ab8ff91b97f305bdea2cc81ca12655f2d474932f1ea36bc4c9b1971dd17b123af18ff1036df317ea69bdc93c60002d4fe73cc1654f3
|
@@ -0,0 +1,29 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
pull_request: {}
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: "ubuntu-latest"
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
container_tag:
|
15
|
+
- master-nightly-bionic
|
16
|
+
- 2.6.5-bionic
|
17
|
+
- 2.7.0-bionic
|
18
|
+
container:
|
19
|
+
image: rubylang/ruby:${{ matrix.container_tag }}
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v1
|
22
|
+
- name: Install
|
23
|
+
run: |
|
24
|
+
ruby -v
|
25
|
+
gem install bundler
|
26
|
+
bundle install
|
27
|
+
- name: Run test
|
28
|
+
run: |
|
29
|
+
bundle exec rake
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.6
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,26 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 2.1.2
|
6
|
+
|
7
|
+
* Update Steep
|
8
|
+
|
9
|
+
## 2.1.1 (2020-05-16)
|
10
|
+
|
11
|
+
* [24](https://github.com/soutaro/strong_json/pull/24): Ship with RBS
|
12
|
+
|
13
|
+
## 2.1.0 (2019-07-19)
|
14
|
+
|
15
|
+
* [20](https://github.com/soutaro/strong_json/pull/20): Add `integer` type
|
16
|
+
|
17
|
+
## 2.0.0 (2019-06-28)
|
18
|
+
|
19
|
+
* [17](https://github.com/soutaro/strong_json/pull/17): Revise `Object` type _ignore_/_reject_ API
|
20
|
+
|
21
|
+
## 1.1.0 (2019-06-09)
|
22
|
+
|
23
|
+
* [#15](https://github.com/soutaro/strong_json/pull/15): Add `hash` type
|
24
|
+
|
5
25
|
## 1.0.1 (2019-05-29)
|
6
26
|
|
7
27
|
* Improve enum error reporting
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -47,7 +47,7 @@ If the input JSON data conforms to `order`'s structure, the `json` will be that
|
|
47
47
|
|
48
48
|
If the input JSON contains attributes which is not white-listed in the definition, it will raise an exception.
|
49
49
|
|
50
|
-
If an attribute has a value which does not match with given type, the `coerce` method call will raise an exception `StrongJSON::Type::
|
50
|
+
If an attribute has a value which does not match with given type, the `coerce` method call will raise an exception `StrongJSON::Type::TypeError`.
|
51
51
|
|
52
52
|
## Catalogue of Types
|
53
53
|
|
@@ -59,26 +59,20 @@ If an attribute has a value which does not match with given type, the `coerce` m
|
|
59
59
|
|
60
60
|
#### Ignoring unknown attributes
|
61
61
|
|
62
|
-
`
|
63
|
-
You can reject the unknown attributes.
|
62
|
+
You can use `ignore` method to ignore unknown attributes.
|
64
63
|
|
65
64
|
```
|
66
|
-
object(attrs).ignore(
|
67
|
-
object(attrs).ignore
|
65
|
+
object(attrs).ignore() # Ignores all unknown attributes.
|
66
|
+
object(attrs).ignore(:x, :y) # Ignores :x and :y, but rejects other unknown attributes.
|
67
|
+
object(attrs).ignore(except: Set[:x, :y]) # Rejects :x and :y, but ignores other unknown attributes.
|
68
68
|
```
|
69
69
|
|
70
|
-
|
70
|
+
`Object` also provides `reject` method to do the opposite.
|
71
71
|
|
72
72
|
```
|
73
|
-
object(attrs).
|
74
|
-
object(attrs).
|
75
|
-
|
76
|
-
|
77
|
-
`Object` also has `prohibit` method to specify attributes to make the type check failed.
|
78
|
-
|
79
|
-
```
|
80
|
-
object(attrs).prohibit(Set.new([:created_at, :updated_at])) # Make type check failed if :created_at or :updated_at included
|
81
|
-
object(attrs).prohibit!(Set.new([:created_at, :updated_at])) # Destructive version
|
73
|
+
object(attrs).reject() # Rejects all unknown attributes. (default)
|
74
|
+
object(attrs).reject(:x, :y) # Rejects :x and :y, but ignores other unknown attributes.
|
75
|
+
object(attrs).reject(except: Set[:x, :y]) # Ignores :x and :y, but rejects other unknown attributes.
|
82
76
|
```
|
83
77
|
|
84
78
|
### array(type)
|
@@ -86,6 +80,11 @@ object(attrs).prohibit!(Set.new([:created_at, :updated_at])) # Destructive vers
|
|
86
80
|
* The value must be an array
|
87
81
|
* All elements in the array must be value of given `type`
|
88
82
|
|
83
|
+
### hash(type)
|
84
|
+
|
85
|
+
* The value must be an object
|
86
|
+
* All values in the object must be value of given `type`
|
87
|
+
|
89
88
|
### optional(type)
|
90
89
|
|
91
90
|
* The value can be `nil` (or not contained in an object)
|
@@ -118,6 +117,7 @@ enum(person,
|
|
118
117
|
### Base types
|
119
118
|
|
120
119
|
* `number` The value must be an instance of `Numeric`
|
120
|
+
* `integer` The value must be an instance of `Integer`
|
121
121
|
* `string` The value must be an instance of `String`
|
122
122
|
* `boolean` The value must be `true` or `false`
|
123
123
|
* `numeric` The value must be an instance of `Numeric` or a string which represents a number
|
@@ -133,6 +133,7 @@ enum(person,
|
|
133
133
|
There are some alias for `optional(base)`, where base is base types, as the following:
|
134
134
|
|
135
135
|
* `number?`
|
136
|
+
* `integer?`
|
136
137
|
* `string?`
|
137
138
|
* `boolean?`
|
138
139
|
* `numeric?`
|
data/Rakefile
CHANGED
@@ -6,11 +6,11 @@ RSpec::Core::RakeTask.new(:spec)
|
|
6
6
|
task :default => [:spec, :typecheck, :"example:typecheck"]
|
7
7
|
|
8
8
|
task :typecheck do
|
9
|
-
sh "bundle exec steep check
|
9
|
+
sh "bundle exec steep check"
|
10
10
|
end
|
11
11
|
|
12
12
|
namespace :example do
|
13
13
|
task :typecheck do
|
14
|
-
sh "bundle exec steep check --
|
14
|
+
sh "bundle exec steep check --steepfile=example/Steepfile"
|
15
15
|
end
|
16
16
|
end
|
data/Steepfile
ADDED
data/example/Steepfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
type address = { address: String, country: Symbol? }
|
2
|
+
type email = { email: String }
|
3
|
+
|
4
|
+
class AddressSchema < StrongJSON
|
5
|
+
def address: -> StrongJSON::Type::Object[address]
|
6
|
+
def email: -> StrongJSON::Type::Object[email]
|
7
|
+
def contact: -> StrongJSON::Type::Object[email | address]
|
8
|
+
def person: -> StrongJSON::Type::Object[{ name: String, contacts: Array[email | address] }]
|
9
|
+
end
|
10
|
+
|
11
|
+
Schema: AddressSchema
|
@@ -8,7 +8,7 @@ class StrongJSON
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def to_s
|
11
|
-
format() unless @string
|
11
|
+
format() unless defined?(@string)
|
12
12
|
@string
|
13
13
|
end
|
14
14
|
|
@@ -18,7 +18,7 @@ class StrongJSON
|
|
18
18
|
format_trace(path: path)
|
19
19
|
where = format_aliases(path: path, where: [])
|
20
20
|
|
21
|
-
# @type var ty: Type::Enum
|
21
|
+
# @type var ty: Type::Enum[untyped]
|
22
22
|
if (ty = _ = path.type).is_a?(Type::Enum)
|
23
23
|
ty.types.each do |t|
|
24
24
|
if (a = t.alias)
|
@@ -67,7 +67,6 @@ class StrongJSON
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def format_single_alias(name, type)
|
70
|
-
# @type const PrettyPrint: any
|
71
70
|
PrettyPrint.format do |pp|
|
72
71
|
pp.text(name.to_s)
|
73
72
|
pp.text(" = ")
|
@@ -78,7 +77,6 @@ class StrongJSON
|
|
78
77
|
end
|
79
78
|
|
80
79
|
def pretty_str(type, expand_alias: false)
|
81
|
-
# @type const PrettyPrint: any
|
82
80
|
PrettyPrint.singleline_format do |pp|
|
83
81
|
pretty(type, pp, expand_alias: expand_alias)
|
84
82
|
end
|
data/lib/strong_json/type.rb
CHANGED
@@ -15,12 +15,12 @@ class StrongJSON
|
|
15
15
|
|
16
16
|
module WithAlias
|
17
17
|
def alias
|
18
|
-
@alias
|
18
|
+
defined?(@alias) ? @alias : nil
|
19
19
|
end
|
20
20
|
|
21
21
|
def with_alias(name)
|
22
22
|
_ = dup.tap do |copy|
|
23
|
-
copy.instance_eval do
|
23
|
+
copy.instance_eval do |x|
|
24
24
|
@alias = name
|
25
25
|
end
|
26
26
|
end
|
@@ -44,6 +44,8 @@ class StrongJSON
|
|
44
44
|
true
|
45
45
|
when :number
|
46
46
|
value.is_a?(Numeric)
|
47
|
+
when :integer
|
48
|
+
value.is_a?(Integer)
|
47
49
|
when :string
|
48
50
|
value.is_a?(String)
|
49
51
|
when :boolean
|
@@ -74,7 +76,6 @@ class StrongJSON
|
|
74
76
|
|
75
77
|
def ==(other)
|
76
78
|
if other.is_a?(Base)
|
77
|
-
# @type var other: Base<any>
|
78
79
|
other.type == type
|
79
80
|
end
|
80
81
|
end
|
@@ -109,7 +110,6 @@ class StrongJSON
|
|
109
110
|
|
110
111
|
def ==(other)
|
111
112
|
if other.is_a?(Optional)
|
112
|
-
# @type var other: Optional<any>
|
113
113
|
other.type == type
|
114
114
|
end
|
115
115
|
end
|
@@ -141,7 +141,6 @@ class StrongJSON
|
|
141
141
|
|
142
142
|
def ==(other)
|
143
143
|
if other.is_a?(Literal)
|
144
|
-
# @type var other: Literal<any>
|
145
144
|
other.value == value
|
146
145
|
end
|
147
146
|
end
|
@@ -178,7 +177,6 @@ class StrongJSON
|
|
178
177
|
|
179
178
|
def ==(other)
|
180
179
|
if other.is_a?(Array)
|
181
|
-
# @type var other: Array<any>
|
182
180
|
other.type == type
|
183
181
|
end
|
184
182
|
end
|
@@ -192,47 +190,45 @@ class StrongJSON
|
|
192
190
|
include Match
|
193
191
|
include WithAlias
|
194
192
|
|
195
|
-
# @dynamic fields,
|
196
|
-
attr_reader :fields, :
|
193
|
+
# @dynamic fields, on_unknown, exceptions
|
194
|
+
attr_reader :fields, :on_unknown, :exceptions
|
197
195
|
|
198
|
-
def initialize(fields,
|
196
|
+
def initialize(fields, on_unknown:, exceptions:)
|
199
197
|
@fields = fields
|
200
|
-
@
|
201
|
-
@
|
198
|
+
@on_unknown = on_unknown
|
199
|
+
@exceptions = exceptions
|
202
200
|
end
|
203
201
|
|
204
202
|
def coerce(object, path: ErrorPath.root(self))
|
205
|
-
unless object.is_a?(Hash)
|
203
|
+
unless object.is_a?(::Hash)
|
206
204
|
raise TypeError.new(path: path, value: object)
|
207
205
|
end
|
208
206
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
207
|
+
object = object.dup
|
208
|
+
unknown_attributes = Set.new(object.keys) - fields.keys
|
209
|
+
|
210
|
+
case on_unknown
|
211
|
+
when :reject
|
212
|
+
unknown_attributes.each do |attr|
|
213
|
+
if exceptions.member?(attr)
|
214
|
+
object.delete(attr)
|
215
|
+
else
|
216
|
+
raise UnexpectedAttributeError.new(path: path, attribute: attr)
|
217
|
+
end
|
219
218
|
end
|
220
|
-
when
|
221
|
-
|
222
|
-
|
223
|
-
|
219
|
+
when :ignore
|
220
|
+
unknown_attributes.each do |attr|
|
221
|
+
if exceptions.member?(attr)
|
222
|
+
raise UnexpectedAttributeError.new(path: path, attribute: attr)
|
223
|
+
else
|
224
|
+
object.delete(attr)
|
225
|
+
end
|
224
226
|
end
|
225
227
|
end
|
226
228
|
|
227
|
-
# @type var result: ::Hash
|
229
|
+
# @type var result: ::Hash[Symbol, untyped]
|
228
230
|
result = {}
|
229
231
|
|
230
|
-
object.each do |key, _|
|
231
|
-
unless fields.key?(key)
|
232
|
-
raise UnexpectedAttributeError.new(path: path, attribute: key)
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
232
|
fields.each do |key, type|
|
237
233
|
result[key] = type.coerce(object[key], path: path.dig(key: key, type: type))
|
238
234
|
end
|
@@ -240,29 +236,35 @@ class StrongJSON
|
|
240
236
|
_ = result
|
241
237
|
end
|
242
238
|
|
243
|
-
def ignore(
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
Object.new(fields, ignored_attributes: ignored_attributes, prohibited_attributes: attrs)
|
239
|
+
def ignore(*ignores, except: nil)
|
240
|
+
if ignores.empty? && !except
|
241
|
+
Object.new(fields, on_unknown: :ignore, exceptions: Set[])
|
242
|
+
else
|
243
|
+
if except
|
244
|
+
Object.new(fields, on_unknown: :ignore, exceptions: except)
|
245
|
+
else
|
246
|
+
Object.new(fields, on_unknown: :reject, exceptions: Set.new(ignores))
|
247
|
+
end
|
248
|
+
end
|
254
249
|
end
|
255
250
|
|
256
|
-
def
|
257
|
-
|
258
|
-
|
251
|
+
def reject(*rejecteds, except: nil)
|
252
|
+
if rejecteds.empty? && !except
|
253
|
+
Object.new(fields, on_unknown: :reject, exceptions: Set[])
|
254
|
+
else
|
255
|
+
if except
|
256
|
+
Object.new(fields, on_unknown: :reject, exceptions: except)
|
257
|
+
else
|
258
|
+
Object.new(fields, on_unknown: :ignore, exceptions: Set.new(rejecteds))
|
259
|
+
end
|
260
|
+
end
|
259
261
|
end
|
260
262
|
|
261
263
|
def update_fields
|
262
264
|
fields.dup.yield_self do |fields|
|
263
265
|
yield fields
|
264
266
|
|
265
|
-
Object.new(fields,
|
267
|
+
Object.new(fields, on_unknown: on_unknown, exceptions: exceptions)
|
266
268
|
end
|
267
269
|
end
|
268
270
|
|
@@ -276,10 +278,9 @@ class StrongJSON
|
|
276
278
|
|
277
279
|
def ==(other)
|
278
280
|
if other.is_a?(Object)
|
279
|
-
# @type var other: Object<any>
|
280
281
|
other.fields == fields &&
|
281
|
-
other.
|
282
|
-
other.
|
282
|
+
other.on_unknown == on_unknown &&
|
283
|
+
other.exceptions == exceptions
|
283
284
|
end
|
284
285
|
end
|
285
286
|
|
@@ -325,7 +326,6 @@ class StrongJSON
|
|
325
326
|
|
326
327
|
def ==(other)
|
327
328
|
if other.is_a?(Enum)
|
328
|
-
# @type var other: Enum<any>
|
329
329
|
other.types == types &&
|
330
330
|
other.detector == detector
|
331
331
|
end
|
@@ -336,6 +336,36 @@ class StrongJSON
|
|
336
336
|
end
|
337
337
|
end
|
338
338
|
|
339
|
+
class Hash
|
340
|
+
include Match
|
341
|
+
include WithAlias
|
342
|
+
|
343
|
+
# @dynamic type
|
344
|
+
attr_reader :type
|
345
|
+
|
346
|
+
def initialize(type)
|
347
|
+
@type = type
|
348
|
+
end
|
349
|
+
|
350
|
+
def coerce(value, path: ErrorPath.root(self))
|
351
|
+
if value.is_a?(::Hash)
|
352
|
+
(_ = {}).tap do |result|
|
353
|
+
value.each do |k, v|
|
354
|
+
result[k] = type.coerce(v, path: path.dig(key: k, type: type))
|
355
|
+
end
|
356
|
+
end
|
357
|
+
else
|
358
|
+
raise TypeError.new(path: path, value: value)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def ==(other)
|
363
|
+
if other.is_a?(Hash)
|
364
|
+
other.type == type
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
339
369
|
class UnexpectedAttributeError < StandardError
|
340
370
|
# @dynamic path, attribute
|
341
371
|
attr_reader :path, :attribute
|