strong_json 0.9.0 → 1.0.0

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