json_patterns 0.1.1 → 0.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.
Files changed (2) hide show
  1. data/README.md +357 -0
  2. metadata +41 -23
@@ -0,0 +1,357 @@
1
+ # JSON Patterns
2
+
3
+ The JSON Patterns Ruby Gem is a domain-specific language (DSL) library for validating
4
+ the structure of JSON documents, using patterns that resemble JSON. Just as regexps
5
+ look a lot like a string, with special symbols to match patterns, a JSON pattern looks
6
+ like a JSON value, with special keywords to allow for abstract pattern matching.
7
+
8
+ It supports the use of alternate object patterns, which can be distinguished by a tag.
9
+ This allows for a JSON structure to mimic subtyping.
10
+
11
+ ## Contents
12
+
13
+ This gem contains:
14
+
15
+ * A family of classes descending from Pattern that represents JSON patterns.
16
+ * A series of helper functions that provide special keywords to make pattern
17
+ creation easy.
18
+ * A family of classes descending from `Validation` or `ObjectMemberValidation`,
19
+ that do the work of validating your JSON structures.
20
+
21
+ ## Installation
22
+
23
+ gem install json_patterns
24
+
25
+ You will also need a JSON parser, such as [yajl-ruby][]. This gem works only in
26
+ Ruby 1.9 and higher.
27
+
28
+ [yajl-ruby]: https://github.com/brianmario/yajl-ruby
29
+
30
+ ## Usage
31
+
32
+ 1. `require 'json_patterns'`
33
+ 2. Create a pattern.
34
+ 3. Call `Validation.new_from_pattern` on the pattern to generate a `Validation`.
35
+ 4. Obtain a JSON string, and use a JSON parser such as [yajl-ruby][] to parse it
36
+ into a nested Ruby data structure.
37
+ 5. Call the `validate_from_root` method on the `Validation` object, passing it
38
+ the parsed JSON. You'll get an array of `ValidationFailure` objects. If it's
39
+ empty, the validation was successful.
40
+
41
+ Example:
42
+
43
+ require 'yajl'
44
+ require 'json_patterns'
45
+
46
+ pattern = { users: array_of({ id: Integer, name: String }) }
47
+ validation = Validation.new_from_pattern(pattern)
48
+
49
+ json = File.new('users.json', 'r')
50
+ parser = Yajl::Parser.new
51
+ hash = parser.parse(json)
52
+
53
+ errors = validation.validate(hash)
54
+ print errors
55
+
56
+ ## Pattern Syntax
57
+
58
+ The simplest patterns are JSON literals:
59
+
60
+ * A boolean value, e.g. `true`
61
+ * A number, e.g. `5` or `-3.26`
62
+ * A string, e.g. `"foo"`
63
+ * The Ruby value `nil` represents a JSON `null`
64
+
65
+ You can match values of a particular type, by using either a Ruby class or the JSON
66
+ name as a symbol:
67
+
68
+ * `Boolean` or `:boolean` matches a `true` or `false`
69
+ * `TrueClass` matches `true`, `FalseClass` matches `false`
70
+ * `String` or `:string` matches a string
71
+ * `Integer` or `:integer` matches an integer
72
+ * `Float`, `Numeric` or `:float` matches a floating-point number
73
+ * `NilClass` matches a JSON `null`
74
+ * `Array` or `:array` matches any array
75
+ * `Hash` or `:object` matches a JSON object
76
+
77
+ You can match some special kinds of strings, too:
78
+
79
+ * `Email` matches an email address
80
+ * `URL` matches an `http`, `https` or `ftp` URL
81
+ * A `Regexp` such as `/a.*b/` matches strings that match the `Regexp`
82
+
83
+ There's a special pattern, `__`, that matches any value.
84
+
85
+ You can match arrays, but only when every element is expected to have a different value.
86
+ These are called *uniform arrays*.
87
+
88
+ array_of(String)
89
+
90
+ will match a JSON array where all elements are expected to be a string.
91
+
92
+ You can match JSON objects using Ruby hashes. Please note the terminology differences:
93
+ **hashes** in Ruby are called **objects** in JSON. **Key-value pairs** in Ruby are called
94
+ **object members** in JSON, and Ruby's **keys** are called **names** in JSON. All class
95
+ names and error messages use the JSON terminology.
96
+
97
+ So, a hash:
98
+
99
+ { id: Integer, name: String }
100
+
101
+ will match a JSON object with names `id` and `name`, with corresponding integer and string
102
+ values, respectively. For example,
103
+
104
+ { "id": 5, "name": "Bob" }
105
+
106
+ Analogous to the ability to match uniform arrays, you can match *uniform objects.* These
107
+ are objects that may have an arbitrary number of names, but each value must match the
108
+ same pattern. For example,
109
+
110
+ { many => String }
111
+
112
+ will match any object where all the values are strings. The `many` keyword is a special
113
+ helper function provided by the gem.
114
+
115
+ Optional names can be matched in an object using the `optional` keyword:
116
+
117
+ { id: Integer, name: String, optional => { address: String } }
118
+
119
+ will match objects with an `id` and a `name`, and optionally an `address` with a string
120
+ value. If multiple members are listed in an `optional` clause, they must occur together.
121
+ So,
122
+
123
+ { id: Integer, optional => { name: String, address: String } }
124
+
125
+ would match objects with an `id` only, or objects with names `id`, `name` and `address`,
126
+ but `name` cannot appear with out `address`, or vice versa.
127
+
128
+ ### Alternate patterns (disjunctions)
129
+
130
+ It is possible to match one of a set of alternate patterns. There are two syntaxes for
131
+ this, depending on the context. If you are trying to match one of a set of values,
132
+ pass a list of patterns to the `one_of` function. E.g.,
133
+
134
+ one_of('circle', 'square', 'triangle')
135
+
136
+ will match any of the three strings. Note that the values passed to `one_of` need not
137
+ all be of the same type.
138
+
139
+ In an object member context, use the keyword `one_of`, with an array value listing the
140
+ { id: Integer, one_of => [ { name: String }, { age: Integer } ] }
141
+
142
+ This will match an object with an name `id` of type integer, and *either* the name
143
+ `name` with a string value, or the name `age` with an integer value. Once again,
144
+ the internal hashes may contain multiple keys. You can think of the curly braces
145
+ in this case as mere syntax: they delineate a pattern of object members, not a
146
+ nested object. You can think of the `one_of` keyword as removing the top level of
147
+ curly braces inside the array.
148
+
149
+ A common use case for alternate object patterns is to indicate different "types"
150
+ that can be considered subtypes of some base type. In Ruby, you might use subclasses
151
+ to represent alternate forms. For example, if you were representing HTML form
152
+ element tags, you might have a base `Tag` class, with subclasses such as
153
+ `InputTag`, `TextAreaTag`, or `SelectTag`. Each of these would in turn have
154
+ different attributes, though they would share some attributes in common.
155
+
156
+ In JSON, you might represent an `<input>` tag with a pattern like this (deliberately
157
+ oversimplified):
158
+
159
+ { tag: 'input', type: one_of('checkbox', 'text'), value: String }
160
+
161
+ and a `<textarea>` tag with a pattern like this:
162
+
163
+ { tag: 'textarea', rows: Integer, cols: Integer }
164
+
165
+ To handle either type of tag, you could use a `one_of` pattern. To simplify the
166
+ code, we can use Ruby variables to hold on to each pattern:
167
+
168
+ inputTag = { tag: 'input', type: one_of('checkbox', 'text'), value: String }
169
+ textAreaTag = { tag: 'textarea', rows: Integer, cols: Integer }
170
+ tag = one_of(inputTag, textAreaTag)
171
+
172
+ So one can think of `tag` as a base class, and `inputTag` and `textAreaTag` as
173
+ subclasses. They are distinguished by the value of the `tag` object member.
174
+
175
+ The Validation objects are smart enough to look at the first object members
176
+ (key-value pairs) of alternate patterns, and use them to distinguish the
177
+ appropriate case to follow. Please note that they must be distinguished by
178
+ the first name (key) or value encountered in the pattern. There is a deliberate
179
+ ordering to the object members in the patterns, but no ordering in the matched
180
+ JSON objects. If the cases are not distinguished by the first object member
181
+ of the pattern, a ValidationAmbiguity error will occur.
182
+
183
+ ### Cyclic patterns
184
+
185
+ Sometimes it is desirable to match nested patterns that repeat. This can
186
+ be done using the `cyclic` function:
187
+
188
+ cyclic { |person| { name: String, email: Email, friends: array_of(person) } }
189
+
190
+ This matches an object with three names: `name`, `email` and `friends`, where
191
+ `friends` points to an array of objects with `name`, `email`, and `friends`,
192
+ and so on. If you create a cyclic structure, you are responsible for ensuring that
193
+ it is *well-founded,* that is, it should not require an infinite-sized JSON
194
+ structure, as all JSON structures are finite. This pattern is well-founded,
195
+ because at any level in the hierarchy, the `friends` list may be empty.
196
+
197
+ Note that we are making use of Ruby's block syntax to create a local variable
198
+ `person` which can be used to refer to the top level of the cyclic structure.
199
+ Here is another well-founded example:
200
+
201
+ tree = cyclic { |tree| one_of(Integer, { left: tree, right: tree }) }
202
+
203
+ This time, we show storing the result in a variable. Please note that due to
204
+ Ruby scoping rules, the `tree` variable on the outside is distinct from
205
+ the other `tree` variables inside the call to `cyclic`, which all refer
206
+ to the same thing. This pattern would match JSON such as:
207
+
208
+ { "left": 4, "right": { "left": 2, "right": 5 } }
209
+
210
+ It's also possible to create cyclic patterns by manipulating structures
211
+ using assignment:
212
+
213
+ branch = {left: nil, right: nil}
214
+ tree = one_of(nil, branch)
215
+ branch[:left] = tree
216
+ branch[:right] = tree
217
+
218
+ This pattern will work as the one declared using `cyclic` above. All
219
+ structural sharing and cycles are preserved in the the transformation
220
+ from a `Pattern` to a `Validation`.
221
+
222
+ ### The members keyword
223
+
224
+ You may have noticed that, whenever we use a special keyword within a hash,
225
+ the value, that is represented as a hash, is treated as a list of object
226
+ members, rather than as an object. There is a special keyword called
227
+ `members`, which does nothing but apply this transformation. It is useful
228
+ if you have a pattern that represents an object, and want to flatten
229
+ it into a list of object members.
230
+
231
+ For example, if you have an object pattern:
232
+
233
+ address = { street: String, city: String }
234
+
235
+ You could use it as a nested object:
236
+
237
+ user = { name: String, address: address }
238
+
239
+ which would match JSON like this:
240
+
241
+ { "name": "Bob", "address": { "street": "10 Forbes Ave", "city": "New York" } }
242
+
243
+ Using the `members` keyword, you can flatten the pattern:
244
+
245
+ user = { name: String, members => address }
246
+
247
+ This will match JSON such as:
248
+
249
+ { "name": "Bob", "street": "10 Forbes Ave", "city": "New York" }
250
+
251
+ Note that the same pattern can be used in both ways, in different places. In
252
+ addition, the same flattening occurs if a pattern is used along with the
253
+ `one_of` or `optional` keywords.
254
+
255
+ ## Errors
256
+
257
+ Errors are descended from the class `ValidationError`, which consists of subclasses
258
+ `ValidationUnexpected` and `ValidationAmbiguity`. Each of them has a `path`
259
+ attribute, which is an array containing the path through the JSON where the error
260
+ was found. Each element in the array is either a string (object name) or integer
261
+ (array index).
262
+
263
+ A `ValidationUnexpected` has `found` and `expected` attributes, describing what was
264
+ found and what was expected, respectively. The description is minimal for indicating
265
+ what went wrong. For example, if a string was expected but an array was found, then
266
+ `found` would indicate "array", rather than representing the entire contents of the
267
+ array. If alternate values were expected, they will be listed in an array, e.g.:
268
+
269
+ expected: ["square", "circle", "triangle"]
270
+ found: "dodecahedron"
271
+
272
+ If a certain name was expected, but not found, all the names found in the object will
273
+ be listed. For example,
274
+
275
+ expected: 'name: "street"'
276
+ found: 'names: "name", "city"'
277
+
278
+ A `ValidationAmbiguity` has `found` and `overlapping_patterns` attributes. The
279
+ `found` attribute describes what was found, and `overlapping_patterns` is an array
280
+ of minimal textual descriptions of the alternate patterns that matched what was
281
+ found.
282
+
283
+ All errors have a `to_s` method for convenient textual representation.
284
+
285
+ == Questions and Answers
286
+
287
+ **Q:** Why only Ruby 1.9? Can you support 1.8?
288
+
289
+ **A:** There are several reasons for using Ruby 1.9, but the most important
290
+ one is that there is a guaranteed ordering of key-value pairs in hashes. This
291
+ is required for the patterns to be ordered. The JSON-like key syntax of following
292
+ the key with a colon instead of using a rocket is nice, too.
293
+
294
+ **Q:** I'd like to match arrays of a fixed length, with different patterns at each
295
+ index. Is that possible?
296
+
297
+ **A:** Not at the moment. I may add this in a future version. The syntax would
298
+ simply be the Ruby array syntax. I just haven't had a use for this yet.
299
+
300
+ **Q:** How about checking the length of a string?
301
+
302
+ **A:** You can use a Regexp. `/.{8,8}` will match 8-character strings.
303
+
304
+ **Q:** How about checking a numeric range?
305
+
306
+ **A:** Not possible in this version.
307
+
308
+ **Q:** I want to use arbitrary functions to validate values.
309
+
310
+ **A:** Not possible at the moment, but I might consider adding this. One thing
311
+ I don't like about it is that it's not serializable.
312
+
313
+ **Q:** Your `one_of` keyword is like an `or`. How about a sort of `and` operation
314
+ on patterns?
315
+
316
+ **A:** Yes, I'm considering creating an `all_of` for that. It's too bad you can't
317
+ overload the `&&` and `||` operators in Ruby.
318
+
319
+ **Q:** How about using regexes for keys?
320
+
321
+ **A:** This creates too much ambiguity. The number of cases that must be tried
322
+ is larger, and the error messages become more confusing. Better to be clear
323
+ with your structure definition.
324
+
325
+ Instead I have special cased the `/.*/` regex using the `many` keyword. With
326
+ this there is no ambiguity about which names should be matched. All are matched.
327
+
328
+ **Q:** What is this "name" thing? Do you mean keys?
329
+
330
+ **A:** See the [JSON specification](http://www.json.org/). They really do call them
331
+ object members, and name/value pairs. Just trying to stick with the spec. Sorry
332
+ for the confusion.
333
+
334
+ **Q:** But you can't do mutually recursive patterns with `cyclic`!
335
+
336
+ **A:** OK, smartypants. I didn't have a use for mutually recursive patterns.
337
+ But it is possible to write them, by manipulating the patterns after you
338
+ create them, to point at each other. The magic `cyclic` keyword isn't really
339
+ required. The Pattern -> Validation transformation preserves all sharing
340
+ and cyclic structures. Exercise left for the daring reader.
341
+
342
+ **Q:** If there's an ambiguity error, it can be hard to find where in the pattern
343
+ the problem is occuring.
344
+
345
+ **A:** At some point I may implement pattern paths. They won't be aware of the
346
+ Ruby variables you've used, but it could help.
347
+
348
+ ## Acknowledgements
349
+
350
+ Thanks to [Janrain](http://janrain.com), my employer, for permitting me to open source
351
+ this code under my own name.
352
+
353
+ ## Support
354
+
355
+ Contact lyle@kopnicky.com for help with this gem.
356
+
357
+ Copyright (c) 2012 Lyle Kopnicky under the BSD2 license.
metadata CHANGED
@@ -1,48 +1,66 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: json_patterns
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.1
5
- prerelease:
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 2
9
+ version: 0.1.2
6
10
  platform: ruby
7
- authors:
11
+ authors:
8
12
  - Lyle Kopnicky
9
13
  autorequire:
10
14
  bindir: bin
11
15
  cert_chain: []
12
- date: 2012-10-01 00:00:00.000000000 Z
16
+
17
+ date: 2012-10-11 00:00:00 -07:00
18
+ default_executable:
13
19
  dependencies: []
14
- description: Validate patterns in JSON using a domain-specific language that looks
15
- as much as possible like the JSON you are trying to validate.
20
+
21
+ description: Validate patterns in JSON using a domain-specific language that looks as much as possible like the JSON you are trying to validate.
16
22
  email: lyle@kopnicky.com
17
23
  executables: []
24
+
18
25
  extensions: []
26
+
19
27
  extra_rdoc_files: []
20
- files:
28
+
29
+ files:
30
+ - README.md
21
31
  - lib/json_patterns.rb
22
32
  - test/test_json_patterns.rb
33
+ has_rdoc: true
23
34
  homepage: http://github.com/lylek/json-patterns-ruby
24
35
  licenses: []
36
+
25
37
  post_install_message:
26
38
  rdoc_options: []
27
- require_paths:
39
+
40
+ require_paths:
28
41
  - lib
29
- required_ruby_version: !ruby/object:Gem::Requirement
30
- none: false
31
- requirements:
32
- - - ! '>='
33
- - !ruby/object:Gem::Version
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ segments:
47
+ - 1
48
+ - 9
49
+ - 0
34
50
  version: 1.9.0
35
- required_rubygems_version: !ruby/object:Gem::Requirement
36
- none: false
37
- requirements:
38
- - - ! '>='
39
- - !ruby/object:Gem::Version
40
- version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
41
58
  requirements: []
59
+
42
60
  rubyforge_project:
43
- rubygems_version: 1.8.24
61
+ rubygems_version: 1.3.6
44
62
  signing_key:
45
63
  specification_version: 3
46
64
  summary: A DSL of patterns to validate JSON structure
47
- test_files:
65
+ test_files:
48
66
  - test/test_json_patterns.rb