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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +68 -16
- data/lib/strong_json/error_reporter.rb +130 -0
- data/lib/strong_json/type.rb +232 -86
- data/lib/strong_json/types.rb +11 -14
- data/lib/strong_json/version.rb +1 -1
- data/lib/strong_json.rb +3 -1
- data/sig/strong_json.rbi +25 -6
- data/sig/type.rbi +55 -28
- data/spec/array_spec.rb +3 -3
- data/spec/basetype_spec.rb +16 -8
- data/spec/enum_spec.rb +87 -9
- data/spec/error_spec.rb +62 -4
- data/spec/json_spec.rb +133 -7
- data/spec/object_spec.rb +122 -57
- data/spec/optional_spec.rb +4 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdd0f07ab15fb780a80f1f1aa00ca73cf253d3fd1dc606548c17c3044d53aba8
|
4
|
+
data.tar.gz: 9b6928be45c28fd0f25cefdeccbcdcadd60d9f5cf4dd08641ec1d913928f61a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e0d57e0a35ade9741f5f2b4b53d86dc56f8175a06da3c12c2d09751c23c0f2696e97f1e9439949ecdf924208eb1d779bc6fc0f8d83b71e19e4799d3fd8b8124
|
7
|
+
data.tar.gz: eecb31e269885a382c90be9ec8988b5b2cfef7c39f5669e1f0d1be90507742259359a4f74cbcadebc4db5817ef2901a375efe2792e58fac00a9d332638690832
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -57,25 +57,29 @@ If an attribute has a value which does not match with given type, the `coerce` m
|
|
57
57
|
* Fields, `f1`, `f2`, and ..., must be present and its values must be of `type1`, `type2`, ..., respectively
|
58
58
|
* Objects with other fields will be rejected
|
59
59
|
|
60
|
-
####
|
60
|
+
#### Ignoring unknown attributes
|
61
61
|
|
62
|
-
Object
|
62
|
+
`Object` type ignores unknown attributes by default.
|
63
|
+
You can reject the unknown attributes.
|
63
64
|
|
64
|
-
```
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
)
|
65
|
+
```
|
66
|
+
object(attrs).ignore(Set.new) # Ignores nothing (== raise an error)
|
67
|
+
object(attrs).ignore!(Set.new) # Destructive version
|
68
|
+
```
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
)
|
70
|
+
You can selectively ignore attributes:
|
71
|
+
|
72
|
+
```
|
73
|
+
object(attrs).ignore(Set.new([:a, :b, :c])) # Ignores only :a, :b, and :c
|
74
|
+
object(attrs).ignore(:any) # Ignores everything (default)
|
74
75
|
```
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
82
|
+
```
|
79
83
|
|
80
84
|
### array(type)
|
81
85
|
|
@@ -92,6 +96,25 @@ Testing `type` is done first, and it soon determines if the object is `"person"`
|
|
92
96
|
* The value can be one of the given types
|
93
97
|
* First successfully coerced value will return
|
94
98
|
|
99
|
+
#### `detector` keyword
|
100
|
+
|
101
|
+
`enum` has optional keyword argument `detector`, which helps identify the type of value.
|
102
|
+
|
103
|
+
```rb
|
104
|
+
enum(person,
|
105
|
+
contact,
|
106
|
+
detector: -> (value) {
|
107
|
+
if value.is_a?(Hash)
|
108
|
+
case
|
109
|
+
when value[:type] == "person"
|
110
|
+
person
|
111
|
+
when value[:type] == "contact"
|
112
|
+
contact
|
113
|
+
end
|
114
|
+
end
|
115
|
+
})
|
116
|
+
```
|
117
|
+
|
95
118
|
### Base types
|
96
119
|
|
97
120
|
* `number` The value must be an instance of `Numeric`
|
@@ -99,7 +122,6 @@ Testing `type` is done first, and it soon determines if the object is `"person"`
|
|
99
122
|
* `boolean` The value must be `true` or `false`
|
100
123
|
* `numeric` The value must be an instance of `Numeric` or a string which represents a number
|
101
124
|
* `any` Any value except `nil` is accepted
|
102
|
-
* `ignored` Any value will be ignored
|
103
125
|
* `symbol` The value must be an instance of `String` or `Symbol`; returns the result ot `#to_sym`
|
104
126
|
|
105
127
|
### Literals
|
@@ -116,7 +138,6 @@ There are some alias for `optional(base)`, where base is base types, as the foll
|
|
116
138
|
* `numeric?`
|
117
139
|
* `symbol?`
|
118
140
|
* `literal?(lit)`
|
119
|
-
* `any?`
|
120
141
|
|
121
142
|
Shortcuts for complex data are also defined as the following:
|
122
143
|
|
@@ -124,6 +145,37 @@ Shortcuts for complex data are also defined as the following:
|
|
124
145
|
* `optional(object(fields))` → `object?(fields)`
|
125
146
|
* `optional(enum(types))` → `enum?(types)`
|
126
147
|
|
148
|
+
## ErrorReporter
|
149
|
+
|
150
|
+
You can pretty print type error using `ErrorReporter`.
|
151
|
+
|
152
|
+
```rb
|
153
|
+
begin
|
154
|
+
type_check()
|
155
|
+
rescue StrongJSON::TypeError, StrongJSON::UnexpectedAttributeError => exn
|
156
|
+
puts exn.message
|
157
|
+
puts StrongJSON::ErrorReporter.new(path: exn.path).to_s
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
It will print a _user-friendly_ error message like:
|
162
|
+
|
163
|
+
```
|
164
|
+
TypeError at $.pattern: expected=pattern, value={:pattern=>3}
|
165
|
+
"pattern" expected to be pattern
|
166
|
+
$ expected to be rule
|
167
|
+
|
168
|
+
Where:
|
169
|
+
pattern = enum(
|
170
|
+
regexp_pattern,
|
171
|
+
token_pattern,
|
172
|
+
literal_pattern,
|
173
|
+
string_pattern,
|
174
|
+
optional(string)
|
175
|
+
)
|
176
|
+
rule = { "pattern": pattern, "glob": optional(enum(string, array(string))) }
|
177
|
+
```
|
178
|
+
|
127
179
|
## Type checking
|
128
180
|
|
129
181
|
StrongJSON ships with type definitions for [Steep](https://github.com/soutaro/steep).
|
@@ -0,0 +1,130 @@
|
|
1
|
+
class StrongJSON
|
2
|
+
class ErrorReporter
|
3
|
+
# @dynamic path
|
4
|
+
attr_reader :path
|
5
|
+
|
6
|
+
def initialize(path:)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
format() unless @string
|
12
|
+
@string
|
13
|
+
end
|
14
|
+
|
15
|
+
def format
|
16
|
+
@string = ""
|
17
|
+
|
18
|
+
format_trace(path: path)
|
19
|
+
where = format_aliases(path: path, where: [])
|
20
|
+
unless where.empty?
|
21
|
+
@string << "\nWhere:\n"
|
22
|
+
@string << where.map {|x| x.gsub(/^/, " ") }.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_trace(path:, index: 1)
|
27
|
+
@string << (" " * index)
|
28
|
+
type_string = pretty_str(path.type)
|
29
|
+
if parent = path.parent
|
30
|
+
case parent[0]
|
31
|
+
when Symbol
|
32
|
+
@string << "\"#{parent[0]}\" expected to be #{type_string}\n"
|
33
|
+
when Integer
|
34
|
+
@string << "#{parent[0]} expected to be #{type_string}\n"
|
35
|
+
else
|
36
|
+
@string << "Expected to be #{type_string}\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
format_trace(path: parent[1], index: index + 1)
|
40
|
+
else
|
41
|
+
@string << "$ expected to be #{type_string}\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_aliases(path:, where:)
|
46
|
+
ty = path.type
|
47
|
+
|
48
|
+
if ty.alias
|
49
|
+
# @type const PrettyPrint: any
|
50
|
+
where << PrettyPrint.format do |pp|
|
51
|
+
pp.text(ty.alias.to_s)
|
52
|
+
pp.text(" = ")
|
53
|
+
pp.group do
|
54
|
+
pretty(ty, pp, expand_alias: true)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if parent = path.parent
|
60
|
+
format_aliases(path: parent[1], where: where)
|
61
|
+
end
|
62
|
+
|
63
|
+
where
|
64
|
+
end
|
65
|
+
|
66
|
+
def pretty_str(type, expand_alias: false)
|
67
|
+
# @type const PrettyPrint: any
|
68
|
+
PrettyPrint.singleline_format do |pp|
|
69
|
+
pretty(type, pp, expand_alias: expand_alias)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def pretty(type, pp, expand_alias: false)
|
74
|
+
if !expand_alias && type.alias
|
75
|
+
pp.text type.alias.to_s
|
76
|
+
else
|
77
|
+
case type
|
78
|
+
when Type::Object
|
79
|
+
pp.group 0, "{", "}" do
|
80
|
+
pp.nest(2) do
|
81
|
+
pp.breakable(" ")
|
82
|
+
type.fields.each.with_index do |pair, index|
|
83
|
+
key, ty = pair
|
84
|
+
pp.text "#{key.to_s.inspect}: "
|
85
|
+
pretty(ty, pp)
|
86
|
+
if index < type.fields.size-1
|
87
|
+
pp.text ","
|
88
|
+
pp.breakable(" ")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
pp.breakable(" ")
|
93
|
+
end
|
94
|
+
when Type::Enum
|
95
|
+
pp.group 0, "enum(", ")" do
|
96
|
+
pp.nest(2) do
|
97
|
+
pp.breakable("")
|
98
|
+
type.types.each.with_index do |ty, index|
|
99
|
+
pretty(ty, pp)
|
100
|
+
if index < type.types.size - 1
|
101
|
+
pp.text ","
|
102
|
+
pp.breakable " "
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
pp.breakable("")
|
107
|
+
end
|
108
|
+
when Type::Optional
|
109
|
+
pp.group 0, "optional(", ")" do
|
110
|
+
pp.nest(2) do
|
111
|
+
pp.breakable ""
|
112
|
+
pretty(type.type, pp)
|
113
|
+
end
|
114
|
+
pp.breakable ""
|
115
|
+
end
|
116
|
+
when Type::Array
|
117
|
+
pp.group 0, "array(", ")" do
|
118
|
+
pp.nest(2) do
|
119
|
+
pp.breakable ""
|
120
|
+
pretty(type.type, pp)
|
121
|
+
end
|
122
|
+
pp.breakable ""
|
123
|
+
end
|
124
|
+
else
|
125
|
+
pp.text type.to_s
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|