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.
@@ -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