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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34cb280b80e097153297adf6032193ce6479c3facb6aabf8f8bf0794a83030e0
4
- data.tar.gz: 12e64c35a8376a275d6cf0e4819c2820d5f5db4fa94b1b294ecba2ccd133fd42
3
+ metadata.gz: fdd0f07ab15fb780a80f1f1aa00ca73cf253d3fd1dc606548c17c3044d53aba8
4
+ data.tar.gz: 9b6928be45c28fd0f25cefdeccbcdcadd60d9f5cf4dd08641ec1d913928f61a8
5
5
  SHA512:
6
- metadata.gz: 3258f86f9fe623a016658fc19a8f333a60fb99a9af4a9d55c23087e4bcf3edb137723083921d4f55ed5764dd659e964a0b4afbbf36cf4e9294247bf5a377fdbb
7
- data.tar.gz: f52cfe275312e777495d5d5d7d2822aeec61fb5cedcc01573c630ce34c204861072e996bd4ee9c621121cb97a83aed27642ec2bc035df4e4c87d551d35acc449
6
+ metadata.gz: 0e0d57e0a35ade9741f5f2b4b53d86dc56f8175a06da3c12c2d09751c23c0f2696e97f1e9439949ecdf924208eb1d779bc6fc0f8d83b71e19e4799d3fd8b8124
7
+ data.tar.gz: eecb31e269885a382c90be9ec8988b5b2cfef7c39f5669e1f0d1be90507742259359a4f74cbcadebc4db5817ef2901a375efe2792e58fac00a9d332638690832
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.0.0 (2019-05-26)
6
+
7
+ * [#14](https://github.com/soutaro/strong_json/pull/14): Revise error reporting
8
+
5
9
  ## 0.9.0 (2019-3-20)
6
10
 
7
11
  * [#12](https://github.com/soutaro/strong_json/pull/12): Fix type signature
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
- #### Performance hint
60
+ #### Ignoring unknown attributes
61
61
 
62
- Object attributes test is done in order of the keys.
62
+ `Object` type ignores unknown attributes by default.
63
+ You can reject the unknown attributes.
63
64
 
64
- ```ruby
65
- slower_object = enum(
66
- object(id: numeric, created_at: string, updated_at: string, type: literal("person"), name: string),
67
- object(id: numeric, created_at: string, updated_at: string, type: literal("food"), object: any)
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
- faster_object = enum(
71
- object(type: literal("person"), id: numeric, created_at: string, updated_at: string, name: string),
72
- object(type: literal("food"), id: numeric, created_at: string, updated_at: string, object: any)
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
- The two enums represents same object, but testing runs faster with `faster_object`.
77
- Objects in `faster_object` have `type` attribute as their first keys.
78
- Testing `type` is done first, and it soon determines if the object is `"person"` or `"food"`.
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