strong_json 0.9.0 → 1.0.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.
data/sig/type.rbi CHANGED
@@ -1,89 +1,116 @@
1
1
  module StrongJSON::Type
2
2
  end
3
3
 
4
- StrongJSON::Type::NONE: any
5
-
6
4
  module StrongJSON::Type::Match: _Schema<any>
7
5
  def =~: (any) -> bool
8
6
  def ===: (any) -> bool
9
7
  end
10
8
 
11
- type StrongJSON::base_type_name = :ignored | :any | :number | :string | :boolean | :numeric | :symbol | :prohibited
9
+ module StrongJSON::Type::WithAlias: ::Object
10
+ @alias: Symbol?
11
+ def alias: -> Symbol?
12
+ def with_alias: (Symbol) -> self
13
+ end
14
+
15
+ type StrongJSON::base_type_name = :any | :number | :string | :boolean | :numeric | :symbol
12
16
 
13
17
  class StrongJSON::Type::Base<'a>
14
18
  include Match
19
+ include WithAlias
15
20
 
16
21
  attr_reader type: base_type_name
17
22
 
18
23
  def initialize: (base_type_name) -> any
19
24
  def test: (any) -> bool
20
- def coerce: (any, ?path: ::Array<Symbol>) -> 'a
25
+ def coerce: (any, ?path: ErrorPath) -> 'a
21
26
  end
22
27
 
23
28
  class StrongJSON::Type::Optional<'t>
24
29
  include Match
30
+ include WithAlias
25
31
 
26
- @type: _Schema<'t>
32
+ attr_reader type: _Schema<'t>
27
33
 
28
34
  def initialize: (_Schema<'t>) -> any
29
- def coerce: (any, ?path: ::Array<Symbol>) -> ('t | nil)
35
+ def coerce: (any, ?path: ErrorPath) -> ('t | nil)
30
36
  end
31
37
 
32
38
  class StrongJSON::Type::Literal<'t>
33
39
  include Match
40
+ include WithAlias
34
41
 
35
42
  attr_reader value: 't
36
43
 
37
44
  def initialize: ('t) -> any
38
- def coerce: (any, ?path: ::Array<Symbol>) -> 't
45
+ def coerce: (any, ?path: ErrorPath) -> 't
39
46
  end
40
47
 
41
48
  class StrongJSON::Type::Array<'t>
42
49
  include Match
50
+ include WithAlias
43
51
 
44
- @type: _Schema<'t>
52
+ attr_reader type: _Schema<'t>
45
53
 
46
54
  def initialize: (_Schema<'t>) -> any
47
- def coerce: (any, ?path: ::Array<Symbol>) -> ::Array<'t>
55
+ def coerce: (any, ?path: ErrorPath) -> ::Array<'t>
48
56
  end
49
57
 
50
58
  class StrongJSON::Type::Object<'t>
51
59
  include Match
60
+ include WithAlias
52
61
 
53
- @fields: Hash<Symbol, _Schema<'t>>
62
+ attr_reader fields: Hash<Symbol, _Schema<any>>
63
+ attr_reader ignored_attributes: :any | Set<Symbol> | nil
64
+ attr_reader prohibited_attributes: Set<Symbol>
54
65
 
55
- def initialize: (Hash<Symbol, _Schema<'t>>) -> any
56
- def coerce: (any, ?path: ::Array<Symbol>) -> 't
57
- def test_value_type: <'x, 'y> (::Array<Symbol>, _Schema<'x>, any) { ('x) -> 'y } -> 'y
58
- def merge: (Object<any> | Hash<Symbol, _Schema<any>>) -> Object<any>
59
- def except: (*Symbol) -> Object<any>
66
+ def initialize: (Hash<Symbol, _Schema<'t>>, ignored_attributes: :any | Set<Symbol> | nil, prohibited_attributes: Set<Symbol>) -> any
67
+ def coerce: (any, ?path: ErrorPath) -> 't
68
+
69
+ def ignore: (:any | Set<Symbol> | nil) -> self
70
+ def ignore!: (:any | Set<Symbol> | nil) -> self
71
+ def prohibit: (Set<Symbol>) -> self
72
+ def prohibit!: (Set<Symbol>) -> self
73
+ def update_fields: <'x> { (Hash<Symbol, _Schema<any>>) -> void } -> Object<'x>
60
74
  end
61
75
 
76
+ type StrongJSON::Type::detector = ^(any) -> _Schema<any>?
77
+
62
78
  class StrongJSON::Type::Enum<'t>
63
79
  include Match
80
+ include WithAlias
64
81
 
65
82
  attr_reader types: ::Array<_Schema<any>>
83
+ attr_reader detector: detector?
66
84
 
67
- def initialize: (::Array<_Schema<any>>) -> any
68
- def coerce: (any, ?path: ::Array<Symbol>) -> 't
85
+ def initialize: (::Array<_Schema<any>>, ?detector?) -> any
86
+ def coerce: (any, ?path: ErrorPath) -> 't
69
87
  end
70
88
 
71
- class StrongJSON::Type::Error
72
- attr_reader path: ::Array<Symbol>
73
- attr_reader type: ty
74
- attr_reader value: any
89
+ class StrongJSON::Type::ErrorPath
90
+ attr_reader type: _Schema<any>
91
+ attr_reader parent: [Symbol | Integer | nil, instance]?
92
+
93
+ def initialize: (type: _Schema<any>, parent: [Symbol | Integer | nil, instance]?) -> any
94
+ def (constructor) dig: (key: Symbol | Integer, type: _Schema<any>) -> self
95
+ def (constructor) expand: (type: _Schema<any>) -> self
75
96
 
76
- def initialize: (path: ::Array<Symbol>, type: ty, value: any) -> any
97
+ def self.root: (_Schema<any>) -> instance
98
+ def root?: -> bool
77
99
  end
78
100
 
79
- class StrongJSON::Type::UnexpectedFieldError
80
- attr_reader path: ::Array<Symbol>
101
+ class StrongJSON::Type::TypeError < StandardError
102
+ attr_reader path: ErrorPath
81
103
  attr_reader value: any
82
104
 
83
- def initialize: (path: ::Array<Symbol>, value: any) -> any
105
+ def initialize: (path: ErrorPath, value: any) -> any
106
+ def type: -> _Schema<any>
84
107
  end
85
108
 
86
- class StrongJSON::Type::IllegalTypeError
87
- attr_reader type: ty
88
- def initialize: (type: ty) -> any
109
+ class StrongJSON::Type::UnexpectedAttributeError < StandardError
110
+ attr_reader path: ErrorPath
111
+ attr_reader attribute: Symbol
112
+
113
+ def initialize: (path: ErrorPath, attribute: Symbol) -> any
114
+ def type: -> _Schema<any>
89
115
  end
116
+
data/spec/array_spec.rb CHANGED
@@ -21,19 +21,19 @@ describe StrongJSON::Type::Array, "#coerce" do
21
21
  it "reject non array" do
22
22
  type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
23
23
 
24
- expect { type.coerce({}) }.to raise_error(StrongJSON::Type::Error)
24
+ expect { type.coerce({}) }.to raise_error(StrongJSON::Type::TypeError)
25
25
  end
26
26
 
27
27
  it "reject membership" do
28
28
  type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
29
29
 
30
- expect { type.coerce(["a"]) }.to raise_error(StrongJSON::Type::Error)
30
+ expect { type.coerce(["a"]) }.to raise_error(StrongJSON::Type::TypeError)
31
31
  end
32
32
 
33
33
  it "rejects nil" do
34
34
  type = StrongJSON::Type::Array.new(StrongJSON::Type::Base.new(:number))
35
35
 
36
- expect { type.coerce(nil) }.to raise_error(StrongJSON::Type::Error)
36
+ expect { type.coerce(nil) }.to raise_error(StrongJSON::Type::TypeError)
37
37
  end
38
38
 
39
39
  describe "=~" do
@@ -2,14 +2,6 @@ require "strong_json"
2
2
 
3
3
  describe StrongJSON::Type::Base do
4
4
  describe "#test" do
5
- context ":ignored" do
6
- let (:type) { StrongJSON::Type::Base.new(:ignored) }
7
-
8
- it "can not be placed on toplevel" do
9
- expect { type.coerce(3, path: []) }.to raise_error(StrongJSON::Type::IllegalTypeError)
10
- end
11
- end
12
-
13
5
  context ":number" do
14
6
  let (:type) { StrongJSON::Type::Base.new(:number) }
15
7
 
@@ -178,4 +170,20 @@ describe StrongJSON::Type::Base do
178
170
  end
179
171
  end
180
172
  end
173
+
174
+ describe "alias" do
175
+ let (:type) { StrongJSON::Type::Base.new(:number) }
176
+
177
+ it "returns nil" do
178
+ expect(type.alias).to be_nil
179
+ end
180
+
181
+ describe "with_alias" do
182
+ let (:aliased_type) { type.with_alias(:count) }
183
+
184
+ it "returns alias name" do
185
+ expect(aliased_type.alias).to eq(:count)
186
+ end
187
+ end
188
+ end
181
189
  end
data/spec/enum_spec.rb CHANGED
@@ -22,14 +22,28 @@ describe StrongJSON::Type::Enum do
22
22
 
23
23
  describe "#coerce" do
24
24
  let (:type) {
25
- StrongJSON::Type::Enum.new([
26
- StrongJSON::Type::Object.new(id: StrongJSON::Type::Literal.new("id1"),
27
- value: StrongJSON::Type::Base.new(:string)),
28
- StrongJSON::Type::Object.new(id: StrongJSON::Type::Base.new(:string),
29
- value: StrongJSON::Type::Base.new(:symbol)),
30
- StrongJSON::Type::Optional.new(StrongJSON::Type::Literal.new(3)),
31
- StrongJSON::Type::Literal.new(false),
32
- ])
25
+ StrongJSON::Type::Enum.new(
26
+ [
27
+ StrongJSON::Type::Object.new(
28
+ {
29
+ id: StrongJSON::Type::Literal.new("id1"),
30
+ value: StrongJSON::Type::Base.new(:string)
31
+ },
32
+ ignored_attributes: nil,
33
+ prohibited_attributes: Set.new
34
+ ),
35
+ StrongJSON::Type::Object.new(
36
+ {
37
+ id: StrongJSON::Type::Base.new(:string),
38
+ value: StrongJSON::Type::Base.new(:symbol)
39
+ },
40
+ ignored_attributes: nil,
41
+ prohibited_attributes: Set.new
42
+ ),
43
+ StrongJSON::Type::Optional.new(StrongJSON::Type::Literal.new(3)),
44
+ StrongJSON::Type::Literal.new(false),
45
+ ]
46
+ )
33
47
  }
34
48
 
35
49
  it "returns object with string value" do
@@ -49,7 +63,71 @@ describe StrongJSON::Type::Enum do
49
63
  end
50
64
 
51
65
  it "raises error" do
52
- expect { type.coerce(3.14) }.to raise_error(StrongJSON::Type::Error)
66
+ expect { type.coerce(3.14) }.to raise_error(StrongJSON::Type::TypeError)
67
+ end
68
+
69
+ context "with detector" do
70
+ let(:regexp_pattern) {
71
+ StrongJSON::Type::Object.new(
72
+ {
73
+ regexp: StrongJSON::Type::Base.new(:string),
74
+ option: StrongJSON::Type::Base.new(:string),
75
+ },
76
+ ignored_attributes: nil,
77
+ prohibited_attributes: Set.new
78
+ )
79
+ }
80
+
81
+ let(:literal_pattern) {
82
+ StrongJSON::Type::Object.new(
83
+ {
84
+ literal: StrongJSON::Type::Base.new(:string)
85
+ },
86
+ ignored_attributes: nil,
87
+ prohibited_attributes: Set.new
88
+ )
89
+ }
90
+
91
+ let(:type) {
92
+ StrongJSON::Type::Enum.new(
93
+ [
94
+ regexp_pattern,
95
+ literal_pattern,
96
+ StrongJSON::Type::Base.new(:string),
97
+ ],
98
+ -> (value) {
99
+ case value
100
+ when Hash
101
+ case
102
+ when value.key?(:regexp)
103
+ regexp_pattern
104
+ when value.key?(:literal)
105
+ literal_pattern
106
+ end
107
+ end
108
+ }
109
+ )
110
+ }
111
+
112
+ it "accepts regexp pattern" do
113
+ expect(type.coerce({ regexp: "foo", option: "x" })).to eq({regexp: "foo", option: "x"})
114
+ end
115
+
116
+ it "raises error with base type" do
117
+ expect {
118
+ type.coerce({ regexp: "foo", option: 123 })
119
+ }.to raise_error(StrongJSON::Type::TypeError) {|x|
120
+ expect(x.type).to be_a(StrongJSON::Type::Base)
121
+ }
122
+ end
123
+
124
+ it "raises error with enum type" do
125
+ expect {
126
+ type.coerce({ option: 3 })
127
+ }.to raise_error(StrongJSON::Type::TypeError) {|x|
128
+ expect(x.type).to eq(type)
129
+ }
130
+ end
53
131
  end
54
132
  end
55
133
  end
data/spec/error_spec.rb CHANGED
@@ -1,8 +1,66 @@
1
- describe StrongJSON::Type::Error do
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::ErrorPath do
4
+ ErrorPath = StrongJSON::Type::ErrorPath
2
5
  include StrongJSON::Types
3
6
 
4
- it "hgoehoge" do
5
- exn = StrongJSON::Type::Error.new(value: [], type: array(numeric), path: ["a",1,"b"])
6
- expect(exn.to_s).to be_a(String)
7
+ describe "root path" do
8
+ let(:path) { ErrorPath.root(string) }
9
+
10
+ it "does not have parent" do
11
+ expect(path.parent).to be_nil
12
+ end
13
+
14
+ it "has type" do
15
+ expect(path.type).to be_a(StrongJSON::Type::Base)
16
+ end
17
+
18
+ it "prints" do
19
+ expect(path.to_s).to eq("$")
20
+ end
21
+ end
22
+
23
+ describe "appended path" do
24
+ let(:path) {
25
+ ErrorPath.root(object(foo: array(number)))
26
+ .dig(key: :foo, type: array(number))
27
+ .dig(key: 0, type: number)
28
+ }
29
+
30
+ it "does have parent" do
31
+ expect(path.parent).to be_a(Array)
32
+ expect(path.parent[0]).to eq(0)
33
+ expect(path.parent[1]).to be_a(ErrorPath)
34
+ end
35
+
36
+ it "has type" do
37
+ expect(path.type).to be_a(StrongJSON::Type::Base)
38
+ end
39
+
40
+ it "prints" do
41
+ expect(path.to_s).to eq("$.foo[0]")
42
+ end
43
+ end
44
+
45
+ describe "expanded path" do
46
+ let(:path) {
47
+ ErrorPath.root(array(enum(number, string)))
48
+ .dig(key: 0, type: enum(number, string))
49
+ .expand(type: string)
50
+ }
51
+
52
+ it "does have parent" do
53
+ expect(path.parent).to be_a(Array)
54
+ expect(path.parent[0]).to be_nil
55
+ expect(path.parent[1]).to be_a(ErrorPath)
56
+ end
57
+
58
+ it "has type" do
59
+ expect(path.type).to be_a(StrongJSON::Type::Base)
60
+ end
61
+
62
+ it "prints" do
63
+ expect(path.to_s).to eq("$[0]")
64
+ end
7
65
  end
8
66
  end
data/spec/json_spec.rb CHANGED
@@ -3,11 +3,90 @@ require "strong_json"
3
3
  describe "StrongJSON.new" do
4
4
  it "tests the structure of a JSON object" do
5
5
  s = StrongJSON.new do
6
- let :item, object(name: string, count: numeric, price: numeric, comment: ignored)
7
- let :checkout, object(items: array(item), change: optional(number), type: enum(literal(1), symbol))
6
+ let :item, object(name: string, count: numeric, price: numeric).ignore(Set.new([:comment]))
7
+ let :items, array(item)
8
+ let :checkout,
9
+ object(items: items,
10
+ change: optional(number),
11
+ type: enum(literal(1), symbol),
12
+ customer: object?(
13
+ name: string,
14
+ id: string,
15
+ birthday: string,
16
+ gender: enum(literal("man"), literal("woman"), literal("other")),
17
+ phone: string
18
+ )
19
+ )
8
20
  end
9
21
 
10
- expect(s.checkout.coerce(items: [ { name: "test", count: 1, price: "2.33", comment: "dummy" }], type: 1)).to eq(items: [ { name: "test", count: 1, price: "2.33" }], type: 1)
22
+ expect(
23
+ s.checkout.coerce(items: [{ name: "test", count: 1, price: "2.33", comment: "dummy" }], type: 1)
24
+ ).to eq(items: [ { name: "test", count: 1, price: "2.33" }], type: 1, change: nil, customer: nil)
25
+
26
+ expect {
27
+ s.checkout.coerce(items: [{ name: "test", count: 1, price: [], comment: "dummy" }], type: 1)
28
+ }.to raise_error(StrongJSON::Type::TypeError) {|e|
29
+ expect(e.path.to_s).to eq("$.items[0].price")
30
+ expect(e.type).to be_a(StrongJSON::Type::Base)
31
+
32
+ expect(e.message).to eq("TypeError at $.items[0].price: expected=numeric, value=[]")
33
+ reporter = StrongJSON::ErrorReporter.new(path: e.path)
34
+ expect(reporter.to_s).to eq(<<MSG.chop)
35
+ "price" expected to be numeric
36
+ 0 expected to be item
37
+ "items" expected to be items
38
+ $ expected to be checkout
39
+
40
+ Where:
41
+ item = { "name": string, "count": numeric, "price": numeric }
42
+ items = array(item)
43
+ checkout = {
44
+ "items": items,
45
+ "change": optional(number),
46
+ "type": enum(1, symbol),
47
+ "customer": optional(
48
+ {
49
+ "name": string,
50
+ "id": string,
51
+ "birthday": string,
52
+ "gender": enum("man", "woman", "other"),
53
+ "phone": string
54
+ }
55
+ )
56
+ }
57
+ MSG
58
+ }
59
+
60
+ expect {
61
+ s.checkout.coerce(items: [], change: "", type: 1)
62
+ }.to raise_error(StrongJSON::Type::TypeError) {|e|
63
+ expect(e.path.to_s).to eq("$.change")
64
+ expect(e.type).to be_a(StrongJSON::Type::Base)
65
+
66
+ expect(e.message).to eq('TypeError at $.change: expected=number, value=""')
67
+ reporter = StrongJSON::ErrorReporter.new(path: e.path)
68
+ expect(reporter.to_s).to eq(<<MSG.chop)
69
+ Expected to be number
70
+ "change" expected to be optional(number)
71
+ $ expected to be checkout
72
+
73
+ Where:
74
+ checkout = {
75
+ "items": items,
76
+ "change": optional(number),
77
+ "type": enum(1, symbol),
78
+ "customer": optional(
79
+ {
80
+ "name": string,
81
+ "id": string,
82
+ "birthday": string,
83
+ "gender": enum("man", "woman", "other"),
84
+ "phone": string
85
+ }
86
+ )
87
+ }
88
+ MSG
89
+ }
11
90
  end
12
91
 
13
92
  it "tests enums" do
@@ -15,11 +94,58 @@ describe "StrongJSON.new" do
15
94
  let :enum, object(e1: enum(boolean, number), e2: enum?(literal(1), literal(2)))
16
95
  end
17
96
 
18
- expect(s.enum.coerce(e1: false)).to eq(e1: false)
19
- expect(s.enum.coerce(e1: 0)).to eq(e1: 0)
97
+ expect(s.enum.coerce(e1: false)).to eq(e1: false, e2: nil)
98
+ expect(s.enum.coerce(e1: 0)).to eq(e1: 0, e2: nil)
20
99
  expect(s.enum.coerce(e1: 0, e2: 1)).to eq(e1: 0, e2: 1)
21
100
  expect(s.enum.coerce(e1: 0, e2: 2)).to eq(e1: 0, e2: 2)
22
- expect{ s.enum.coerce(e1: "", e2: 3) }.to raise_error(StrongJSON::Type::Error)
23
- expect{ s.enum.coerce(e1: false, e2: "") }.to raise_error(StrongJSON::Type::Error)
101
+ expect{ s.enum.coerce(e1: "", e2: 3) }.to raise_error(StrongJSON::Type::TypeError) {|e|
102
+ expect(e.path.to_s).to eq("$.e1")
103
+ }
104
+ expect{ s.enum.coerce(e1: false, e2: "") }.to raise_error(StrongJSON::Type::TypeError) {|e|
105
+ expect(e.path.to_s).to eq("$.e2")
106
+ }
107
+ end
108
+
109
+ describe "#let" do
110
+ it "defines aliased type" do
111
+ s = StrongJSON.new do
112
+ let :count, number
113
+ let :age, number
114
+ end
115
+
116
+ expect(s.count.alias).to eq(:count)
117
+ expect(s.age.alias).to eq(:age)
118
+ end
119
+ end
120
+
121
+ it "pretty print" do
122
+ s = StrongJSON.new do
123
+ let :regexp_pattern, object(pattern: string, multiline: boolean?, case_sensitive: boolean?)
124
+ let :token_pattern, object(token: string, case_sensitive: boolean?)
125
+ let :literal_pattern, object(literal: string)
126
+ let :string_pattern, string
127
+ let :pattern, enum(regexp_pattern, token_pattern, literal_pattern, string_pattern, string?)
128
+
129
+ let :rule, object(pattern: pattern, glob: enum?(string, array(string)))
130
+ end
131
+
132
+ expect { s.rule.coerce({ pattern: { pattern: 3 } }) }.to raise_error(StrongJSON::Type::TypeError) {|e|
133
+ expect(e.message).to eq("TypeError at $.pattern: expected=pattern, value={:pattern=>3}")
134
+ reporter = StrongJSON::ErrorReporter.new(path: e.path)
135
+ expect(reporter.to_s).to eq(<<MSG.chop)
136
+ "pattern" expected to be pattern
137
+ $ expected to be rule
138
+
139
+ Where:
140
+ pattern = enum(
141
+ regexp_pattern,
142
+ token_pattern,
143
+ literal_pattern,
144
+ string_pattern,
145
+ optional(string)
146
+ )
147
+ rule = { "pattern": pattern, "glob": optional(enum(string, array(string))) }
148
+ MSG
149
+ }
24
150
  end
25
151
  end