strong_json 1.0.1 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|