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