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