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.
@@ -1,20 +1,17 @@
1
1
  class StrongJSON
2
2
  module Types
3
- # @type method object: (?Hash<Symbol, ty>) -> Type::Object<any>
4
3
  def object(fields = {})
5
4
  if fields.empty?
6
- Type::Object.new(fields, ignored_attributes: nil, prohibited_attributes: Set.new)
5
+ Type::Object.new(fields, on_unknown: :ignore, exceptions: Set.new)
7
6
  else
8
- Type::Object.new(fields, ignored_attributes: :any, prohibited_attributes: Set.new)
7
+ Type::Object.new(fields, on_unknown: :reject, exceptions: Set.new)
9
8
  end
10
9
  end
11
10
 
12
- # @type method array: (?ty) -> Type::Array<any>
13
11
  def array(type = any)
14
12
  Type::Array.new(type)
15
13
  end
16
14
 
17
- # @type method optional: (?ty) -> Type::Optional<any>
18
15
  def optional(type = any)
19
16
  Type::Optional.new(type)
20
17
  end
@@ -31,6 +28,10 @@ class StrongJSON
31
28
  StrongJSON::Type::Base.new(:number)
32
29
  end
33
30
 
31
+ def integer
32
+ StrongJSON::Type::Base.new(:integer)
33
+ end
34
+
34
35
  def boolean
35
36
  StrongJSON::Type::Base.new(:boolean)
36
37
  end
@@ -63,6 +64,10 @@ class StrongJSON
63
64
  optional(numeric)
64
65
  end
65
66
 
67
+ def integer?
68
+ optional(integer)
69
+ end
70
+
66
71
  def number?
67
72
  optional(number)
68
73
  end
@@ -79,7 +84,6 @@ class StrongJSON
79
84
  optional(array(ty))
80
85
  end
81
86
 
82
- # @type method object?: (?Hash<Symbol, ty>) -> Type::Optional<any>
83
87
  def object?(fields={})
84
88
  optional(object(fields))
85
89
  end
@@ -91,5 +95,13 @@ class StrongJSON
91
95
  def enum?(*types, detector: nil)
92
96
  optional(enum(*types, detector: detector))
93
97
  end
98
+
99
+ def hash(type)
100
+ StrongJSON::Type::Hash.new(type)
101
+ end
102
+
103
+ def hash?(type)
104
+ optional(hash(type))
105
+ end
94
106
  end
95
107
  end
@@ -1,5 +1,5 @@
1
1
  class StrongJSON
2
2
  # @dynamic initialize, let
3
-
4
- VERSION = "1.0.1"
3
+
4
+ VERSION = "2.1.2"
5
5
  end
data/pp.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "prettyprint"
2
+
3
+ pp = PrettyPrint.new
4
+
5
+ pp.group 0 do
6
+ pp.text "hello = "
7
+
8
+ pp.group 0, "enum_____(", ")" do
9
+ pp.nest(2) do
10
+ pp.breakable ""
11
+ count = 7
12
+ count.times do |i|
13
+ pp.text "hello #{i}"
14
+
15
+ if i < count - 1
16
+ pp.text ","
17
+ pp.breakable " "
18
+ end
19
+ end
20
+ end
21
+
22
+ pp.breakable ""
23
+ end
24
+ end
25
+
26
+ pp.flush
27
+ puts pp.output
@@ -0,0 +1,13 @@
1
+ class PrettyPrint
2
+ def text: (String) -> void
3
+
4
+ def group: (?Integer, ?String, ?String) { () -> void } -> void
5
+
6
+ def nest: (Integer) { () -> void } -> void
7
+
8
+ def breakable: (String) -> void
9
+
10
+ def self.format: { (instance) -> void } -> String
11
+
12
+ def self.singleline_format: { (instance) -> void } -> String
13
+ end
@@ -0,0 +1,67 @@
1
+ class StrongJSON
2
+ def initialize: { (StrongJSON) -> void } -> untyped
3
+ def let: (Symbol, ty) -> void
4
+ include Types
5
+
6
+ VERSION: String
7
+
8
+ interface _Schema[T]
9
+ def coerce: (untyped, ?path: Type::ErrorPath) -> T
10
+ def =~: (untyped) -> bool
11
+ def to_s: -> String
12
+ def is_a?: (untyped) -> bool
13
+ def `alias`: -> Symbol?
14
+ def with_alias: (Symbol) -> self
15
+ def ==: (untyped) -> bool
16
+ def yield_self: [X] () { (self) -> X } -> X
17
+ end
18
+
19
+ type ty = _Schema[untyped]
20
+
21
+ module Types
22
+ def object: [X] (Hash[Symbol, ty]) -> Type::Object[X]
23
+ | () -> Type::Object[bot]
24
+ def object?: [X] (Hash[Symbol, ty]) -> Type::Optional[X]
25
+ | () -> Type::Optional[bot]
26
+ def any: () -> Type::Base[untyped]
27
+ def any?: () -> Type::Optional[untyped]
28
+ def optional: [X] (_Schema[X]) -> Type::Optional[X]
29
+ | () -> Type::Optional[untyped]
30
+ def string: () -> Type::Base[String]
31
+ def string?: () -> Type::Optional[String]
32
+ def number: () -> Type::Base[Numeric]
33
+ def number?: () -> Type::Optional[Numeric]
34
+ def numeric: () -> Type::Base[Numeric]
35
+ def numeric?: () -> Type::Optional[Numeric]
36
+ def integer: () -> Type::Base[Integer]
37
+ def integer?: () -> Type::Optional[Integer]
38
+ def boolean: () -> Type::Base[bool]
39
+ def boolean?: () -> Type::Optional[bool]
40
+ def symbol: () -> Type::Base[Symbol]
41
+ def symbol?: () -> Type::Optional[Symbol]
42
+ def array: [X] (_Schema[X]) -> Type::Array[X]
43
+ | () -> Type::Array[untyped]
44
+ def array?: [X] (_Schema[X]) -> Type::Optional[::Array[X]]
45
+ def literal: [X] (X) -> Type::Literal[X]
46
+ def literal?: [X] (X) -> Type::Optional[X]
47
+ def enum: [X] (*_Schema[untyped], ?detector: Type::detector?) -> Type::Enum[X]
48
+ def enum?: [X] (*_Schema[untyped], ?detector: Type::detector?) -> Type::Optional[X]
49
+ def hash: [X] (_Schema[X]) -> Type::Hash[X]
50
+ def hash?: [X] (_Schema[X]) -> Type::Optional[Hash[Symbol, X]]
51
+ end
52
+
53
+ class ErrorReporter
54
+ attr_reader path: Type::ErrorPath
55
+ @string: String
56
+
57
+ def initialize: (path: Type::ErrorPath) -> untyped
58
+ def format: -> void
59
+ def pretty_str: (ty, ?expand_alias: bool) -> ::String
60
+
61
+ private
62
+ def format_trace: (path: Type::ErrorPath, ?index: Integer) -> void
63
+ def format_aliases: (path: Type::ErrorPath, where: ::Array[String]) -> ::Array[String]
64
+ def format_single_alias: (Symbol, ty) -> String
65
+ def pretty: (ty, PrettyPrint, ?expand_alias: bool) -> void
66
+ end
67
+ end
@@ -0,0 +1,132 @@
1
+ class StrongJSON
2
+ module Type
3
+ module Match : _Schema[untyped]
4
+ def =~: (untyped) -> bool
5
+ def ===: (untyped) -> bool
6
+ end
7
+
8
+ module WithAlias : ::Object
9
+ @alias: Symbol?
10
+ def `alias`: -> Symbol?
11
+ def with_alias: (Symbol) -> self
12
+ end
13
+
14
+ type base_type_name = :any | :number | :string | :boolean | :numeric | :symbol | :integer
15
+
16
+ class Base[A]
17
+ include Match
18
+ include WithAlias
19
+
20
+ attr_reader type: base_type_name
21
+
22
+ def initialize: (base_type_name) -> untyped
23
+ def test: (untyped) -> bool
24
+ def coerce: (untyped, ?path: ErrorPath) -> A
25
+ end
26
+
27
+ class Optional[T]
28
+ include Match
29
+ include WithAlias
30
+
31
+ attr_reader type: _Schema[T]
32
+
33
+ def initialize: (_Schema[T]) -> untyped
34
+ def coerce: (untyped, ?path: ErrorPath) -> (T | nil)
35
+ end
36
+
37
+ class Literal[T]
38
+ include Match
39
+ include WithAlias
40
+
41
+ attr_reader value: T
42
+
43
+ def initialize: (T) -> untyped
44
+ def coerce: (untyped, ?path: ErrorPath) -> T
45
+ end
46
+
47
+ class Array[T]
48
+ include Match
49
+ include WithAlias
50
+
51
+ attr_reader type: _Schema[T]
52
+
53
+ def initialize: (_Schema[T]) -> untyped
54
+ def coerce: (untyped, ?path: ErrorPath) -> ::Array[T]
55
+ end
56
+
57
+ class Object[T]
58
+ include Match
59
+ include WithAlias
60
+
61
+ attr_reader fields: ::Hash[Symbol, _Schema[untyped]]
62
+ attr_reader on_unknown: :ignore | :reject
63
+ attr_reader exceptions: Set[Symbol]
64
+
65
+ def initialize: (::Hash[Symbol, _Schema[T]], on_unknown: :ignore | :reject, exceptions: Set[Symbol]) -> untyped
66
+ def coerce: (untyped, ?path: ErrorPath) -> T
67
+
68
+ # If no argument is given, it ignores all unknown attributes.
69
+ # If `Symbol`s are given, it ignores the listed attributes, but rejects if other unknown attributes are detected.
70
+ # If `except:` is specified, it rejects attributes listed in `except` are detected, but ignores other unknown attributes.
71
+ def ignore: (*Symbol ignores, ?except: Set[Symbol]?) -> Object[T]
72
+
73
+ # If no argument is given, it rejects on untyped unknown attribute.
74
+ # If `Symbol`s are given, it rejects the listed attributes are detected, but ignores other unknown attributes.
75
+ # If `except:` is specified, it ignores given attributes, but rejects if other unknown attributes are detected.
76
+ def reject: (*Symbol rejecteds, ?except: Set[Symbol]?) -> Object[T]
77
+
78
+ def update_fields: [X] { (::Hash[Symbol, _Schema[untyped]]) -> void } -> Object[X]
79
+ end
80
+
81
+ type detector = ^(untyped) -> _Schema[untyped]?
82
+
83
+ class Enum[T]
84
+ include Match
85
+ include WithAlias
86
+
87
+ attr_reader types: ::Array[_Schema[untyped]]
88
+ attr_reader detector: detector?
89
+
90
+ def initialize: (::Array[_Schema[untyped]], ?detector?) -> untyped
91
+ def coerce: (untyped, ?path: ErrorPath) -> T
92
+ end
93
+
94
+ class ErrorPath
95
+ attr_reader type: _Schema[untyped]
96
+ attr_reader parent: [Symbol | Integer | nil, ErrorPath]?
97
+
98
+ def initialize: (type: _Schema[untyped], parent: [Symbol | Integer | nil, ErrorPath]?) -> untyped
99
+ def dig: (key: Symbol | Integer, type: _Schema[untyped]) -> ErrorPath
100
+ def expand: (type: _Schema[untyped]) -> ErrorPath
101
+
102
+ def self.root: (_Schema[untyped]) -> ErrorPath
103
+ def root?: -> bool
104
+ end
105
+
106
+ class TypeError < StandardError
107
+ attr_reader path: ErrorPath
108
+ attr_reader value: untyped
109
+
110
+ def initialize: (path: ErrorPath, value: untyped) -> untyped
111
+ def type: -> _Schema[untyped]
112
+ end
113
+
114
+ class UnexpectedAttributeError < StandardError
115
+ attr_reader path: ErrorPath
116
+ attr_reader attribute: Symbol
117
+
118
+ def initialize: (path: ErrorPath, attribute: Symbol) -> untyped
119
+ def type: -> _Schema[untyped]
120
+ end
121
+
122
+ class Hash[T]
123
+ include Match
124
+ include WithAlias
125
+
126
+ attr_reader type: _Schema[T]
127
+
128
+ def initialize: (_Schema[T]) -> untyped
129
+ def coerce: (untyped, ?path: ErrorPath) -> ::Hash[Symbol, T]
130
+ end
131
+ end
132
+ end
@@ -94,6 +94,18 @@ describe StrongJSON::Type::Base do
94
94
  end
95
95
  end
96
96
 
97
+ context ":integer" do
98
+ let (:type) { StrongJSON::Type::Base.new(:integer) }
99
+
100
+ it "accepts integer" do
101
+ expect(type.test(123)).to be_truthy
102
+ end
103
+
104
+ it "rejects float" do
105
+ expect(type.test(1.23)).to be_falsey
106
+ end
107
+ end
108
+
97
109
  context ":numeric" do
98
110
  let (:type) { StrongJSON::Type::Base.new(:numeric) }
99
111
 
@@ -29,16 +29,16 @@ describe StrongJSON::Type::Enum do
29
29
  id: StrongJSON::Type::Literal.new("id1"),
30
30
  value: StrongJSON::Type::Base.new(:string)
31
31
  },
32
- ignored_attributes: nil,
33
- prohibited_attributes: Set.new
32
+ on_unknown: :reject,
33
+ exceptions: Set[]
34
34
  ),
35
35
  StrongJSON::Type::Object.new(
36
36
  {
37
37
  id: StrongJSON::Type::Base.new(:string),
38
38
  value: StrongJSON::Type::Base.new(:symbol)
39
39
  },
40
- ignored_attributes: nil,
41
- prohibited_attributes: Set.new
40
+ on_unknown: :reject,
41
+ exceptions: Set[]
42
42
  ),
43
43
  StrongJSON::Type::Optional.new(StrongJSON::Type::Literal.new(3)),
44
44
  StrongJSON::Type::Literal.new(false),
@@ -73,8 +73,8 @@ describe StrongJSON::Type::Enum do
73
73
  regexp: StrongJSON::Type::Base.new(:string),
74
74
  option: StrongJSON::Type::Base.new(:string),
75
75
  },
76
- ignored_attributes: nil,
77
- prohibited_attributes: Set.new
76
+ on_unknown: :reject,
77
+ exceptions: Set[]
78
78
  )
79
79
  }
80
80
 
@@ -83,8 +83,8 @@ describe StrongJSON::Type::Enum do
83
83
  {
84
84
  literal: StrongJSON::Type::Base.new(:string)
85
85
  },
86
- ignored_attributes: nil,
87
- prohibited_attributes: Set.new
86
+ on_unknown: :reject,
87
+ exceptions: Set[]
88
88
  )
89
89
  }
90
90
 
@@ -0,0 +1,32 @@
1
+ require "strong_json"
2
+
3
+ describe StrongJSON::Type::Hash do
4
+ let(:number) { StrongJSON::Type::Base.new(:number) }
5
+
6
+ describe "#coerce" do
7
+ it "returns a hash" do
8
+ type = StrongJSON::Type::Hash.new(number)
9
+ expect(type.coerce({ foo: 123, bar: 234 })).to eq({ foo: 123, bar: 234 })
10
+ end
11
+
12
+ it "raises an error if number is given" do
13
+ type = StrongJSON::Type::Hash.new(number)
14
+
15
+ expect {
16
+ type.coerce(1)
17
+ }.to raise_error(StrongJSON::Type::TypeError) {|error|
18
+ expect(error.path.to_s).to eq("$")
19
+ }
20
+ end
21
+
22
+ it "raises an error if hash value is unexpected" do
23
+ type = StrongJSON::Type::Hash.new(number)
24
+
25
+ expect {
26
+ type.coerce({ foo: "hello" })
27
+ }.to raise_error(StrongJSON::Type::TypeError) {|error|
28
+ expect(error.path.to_s).to eq("$.foo")
29
+ }
30
+ end
31
+ end
32
+ end
@@ -3,7 +3,7 @@ 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).ignore(Set.new([:comment]))
6
+ let :item, object(name: string, count: numeric, price: numeric).ignore(:comment)
7
7
  let :items, array(item)
8
8
  let :checkout,
9
9
  object(items: items,
@@ -20,11 +20,11 @@ describe "StrongJSON.new" do
20
20
  end
21
21
 
22
22
  expect(
23
- s.checkout.coerce(items: [{ name: "test", count: 1, price: "2.33", comment: "dummy" }], type: 1)
23
+ s.checkout.coerce({ items: [{ name: "test", count: 1, price: "2.33", comment: "dummy" }], type: 1 })
24
24
  ).to eq(items: [ { name: "test", count: 1, price: "2.33" }], type: 1, change: nil, customer: nil)
25
25
 
26
26
  expect {
27
- s.checkout.coerce(items: [{ name: "test", count: 1, price: [], comment: "dummy" }], type: 1)
27
+ s.checkout.coerce({ items: [{ name: "test", count: 1, price: [], comment: "dummy" }], type: 1 })
28
28
  }.to raise_error(StrongJSON::Type::TypeError) {|e|
29
29
  expect(e.path.to_s).to eq("$.items[0].price")
30
30
  expect(e.type).to be_a(StrongJSON::Type::Base)
@@ -58,7 +58,7 @@ MSG
58
58
  }
59
59
 
60
60
  expect {
61
- s.checkout.coerce(items: [], change: "", type: 1)
61
+ s.checkout.coerce({ items: [], change: "", type: 1 })
62
62
  }.to raise_error(StrongJSON::Type::TypeError) {|e|
63
63
  expect(e.path.to_s).to eq("$.change")
64
64
  expect(e.type).to be_a(StrongJSON::Type::Base)
@@ -94,14 +94,14 @@ MSG
94
94
  let :enum, object(e1: enum(boolean, number), e2: enum?(literal(1), literal(2)))
95
95
  end
96
96
 
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)
99
- expect(s.enum.coerce(e1: 0, e2: 1)).to eq(e1: 0, e2: 1)
100
- expect(s.enum.coerce(e1: 0, e2: 2)).to eq(e1: 0, e2: 2)
101
- expect{ s.enum.coerce(e1: "", e2: 3) }.to raise_error(StrongJSON::Type::TypeError) {|e|
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)
99
+ expect(s.enum.coerce({ e1: 0, e2: 1 })).to eq(e1: 0, e2: 1)
100
+ expect(s.enum.coerce({ e1: 0, e2: 2 })).to eq(e1: 0, e2: 2)
101
+ expect{ s.enum.coerce({ e1: "", e2: 3 }) }.to raise_error(StrongJSON::Type::TypeError) {|e|
102
102
  expect(e.path.to_s).to eq("$.e1")
103
103
  }
104
- expect{ s.enum.coerce(e1: false, e2: "") }.to raise_error(StrongJSON::Type::TypeError) {|e|
104
+ expect{ s.enum.coerce({ e1: false, e2: ""} ) }.to raise_error(StrongJSON::Type::TypeError) {|e|
105
105
  expect(e.path.to_s).to eq("$.e2")
106
106
  }
107
107
  end