json-schema 0.2.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  h1. Ruby JSON Schema Validator
2
2
 
3
- This library is intended to provide Ruby with an interface for validating JSON objects against a JSON schema conforming to "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03.
3
+ This library is intended to provide Ruby with an interface for validating JSON objects against a JSON schema conforming to "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03. Legacy support for "JSON Schema Draft 2":http://tools.ietf.org/html/draft-zyp-json-schema-02 and "JSON Schema Draft 1":http://tools.ietf.org/html/draft-zyp-json-schema-01 is also included.
4
4
 
5
5
  h2. Dependencies
6
6
 
@@ -18,7 +18,7 @@ From the git repo:
18
18
 
19
19
  <pre>
20
20
  $ gem build json-schema.gemspec
21
- $ gem install json-schema-0.2.0.gem
21
+ $ gem install json-schema-0.9.0.gem
22
22
  </pre>
23
23
 
24
24
 
@@ -26,9 +26,9 @@ h2. Usage
26
26
 
27
27
  Two base validation methods exist: <code>validate</code> and <code>validate!</code>. The first returns a boolean on whether a validation attempt passes and the latter will throw a <code>JSON::Schema::ValidationError</code> with an appropriate message/trace on where the validation failed.
28
28
 
29
- Both methods take two arguments, which can be either a JSON string, a file containing JSON, or a Ruby object representing JSON data. The first argument to these methods is always the schema, the second is always the data to validate.
29
+ Both methods take two arguments, which can be either a JSON string, a file containing JSON, or a Ruby object representing JSON data. The first argument to these methods is always the schema, the second is always the data to validate. An optional third options argument is also accepted; available options are used in the examples below.
30
30
 
31
- By default, the validator uses (and only supports) the "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03 specification for validation; however, the user is free to specify additional specifications or extend existing ones.
31
+ By default, the validator uses the "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03 specification for validation; however, the user is free to specify additional specifications or extend existing ones. Legacy support for Draft 1 and Draft 2 is included by either passing an optional <code>:version</code> parameter to the <code>validate</code> method (set either as <code>:draft1</code> or <code>draft2</code>), or by declaring the <code>$schema</code> attribute in the schema and referencing the appropriate specification URI. Note that the <code>$schema</code> attribute takes precedence over the <code>:version</code> option during parsing and validation.
32
32
 
33
33
  h3. Validate Ruby objects against a Ruby schema
34
34
 
@@ -93,6 +93,27 @@ rescue JSON::Schema::ValidationError
93
93
  end
94
94
  </pre>
95
95
 
96
+ h3. Validate an object against a JSON Schema Draft 2 schema
97
+
98
+ <pre>
99
+ require 'rubygems'
100
+ require 'json-schema'
101
+
102
+ schema = {
103
+ "type" => "object",
104
+ "properties" => {
105
+ "a" => {"type" => "integer", "optional" => true}
106
+ }
107
+ }
108
+
109
+ data = {
110
+ "a" => 5
111
+ }
112
+
113
+ JSON::Validator.validate(schema, data, :version => :draft2)
114
+ </pre>
115
+
116
+
96
117
  h3. Extend an existing schema and validating against it
97
118
 
98
119
  For this example, we are going to extend the "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03 specification by adding a 'bitwise-and' property for validation.
@@ -5,4 +5,5 @@ require 'schema'
5
5
  require 'validator'
6
6
  Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/*")].each {|file| require file }
7
7
  Dir[File.join(File.dirname(__FILE__), "json-schema/validators/*")].each {|file| require file }
8
- require 'uri/file'
8
+ require 'uri/file'
9
+ require 'uri/uuid'
@@ -0,0 +1,15 @@
1
+ module JSON
2
+ class Schema
3
+ class MaxDecimalAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Numeric)
6
+ s = data.to_s.split(".")[1]
7
+ if s && s.length > current_schema.schema['maxDecimal']
8
+ message = "The property '#{build_fragment(fragments)}' had more decimal places than the allowed #{current_schema.schema['maxDecimal']}"
9
+ raise ValidationError.new(message, fragments, current_schema)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module JSON
2
+ class Schema
3
+ class MaximumInclusiveAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Numeric)
6
+ current_schema.schema['maximumCanEqual'] = true if current_schema.schema['maximumCanEqual'].nil?
7
+ if (current_schema.schema['maximumCanEqual'] ? data > current_schema.schema['maximum'] : data >= current_schema.schema['maximum'])
8
+ message = "The property '#{build_fragment(fragments)}' did not have a maximum value of #{current_schema.schema['maximum']}, "
9
+ message += current_schema.schema['exclusiveMaximum'] ? 'exclusively' : 'inclusively'
10
+ raise ValidationError.new(message, fragments, current_schema)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module JSON
2
+ class Schema
3
+ class MinimumInclusiveAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Numeric)
6
+ current_schema.schema['minimumCanEqual'] = true if current_schema.schema['minimumCanEqual'].nil?
7
+ if (current_schema.schema['minimumCanEqual'] ? data < current_schema.schema['minimum'] : data <= current_schema.schema['minimum'])
8
+ message = "The property '#{build_fragment(fragments)}' did not have a minimum value of #{current_schema.schema['minimum']}, "
9
+ message += current_schema.schema['exclusiveMinimum'] ? 'exclusively' : 'inclusively'
10
+ raise ValidationError.new(message, fragments, current_schema)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ module JSON
2
+ class Schema
3
+ class PropertiesOptionalAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Hash)
6
+ current_schema.schema['properties'].each do |property,property_schema|
7
+ if ((property_schema['optional'].nil? || property_schema['optional'] == false) && !data.has_key?(property))
8
+ message = "The property '#{build_fragment(fragments)}' did not contain a required property of '#{property}'"
9
+ raise ValidationError.new(message, fragments, current_schema)
10
+ end
11
+
12
+ if data.has_key?(property)
13
+ schema = JSON::Schema.new(property_schema,current_schema.uri,validator)
14
+ fragments << property
15
+ schema.validate(data[property],fragments)
16
+ fragments.pop
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env ruby
2
+ ### http://mput.dip.jp/mput/uuid.txt
3
+
4
+ # Copyright(c) 2005 URABE, Shyouhei.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this code, to deal in the code without restriction, including without
8
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
9
+ # sublicense, and/or sell copies of the code, and to permit persons to whom the
10
+ # code is furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the code.
14
+ #
15
+ # THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE CODE OR THE USE OR OTHER DEALINGS IN THE
21
+ # CODE.
22
+ #
23
+ # 2009-02-20: Modified by Pablo Lorenzoni <pablo@propus.com.br> to correctly
24
+ # include the version in the raw_bytes.
25
+
26
+
27
+ require 'digest/md5'
28
+ require 'digest/sha1'
29
+ require 'tmpdir'
30
+
31
+ # Pure ruby UUID generator, which is compatible with RFC4122
32
+ UUID = Struct.new :raw_bytes
33
+
34
+ class UUID
35
+ private_class_method :new
36
+
37
+ class << self
38
+ def mask19 v, str # :nodoc
39
+ nstr = str.bytes.to_a
40
+ version = [0, 16, 32, 48, 64, 80][v]
41
+ nstr[6] &= 0b00001111
42
+ nstr[6] |= version
43
+ # nstr[7] &= 0b00001111
44
+ # nstr[7] |= 0b01010000
45
+ nstr[8] &= 0b00111111
46
+ nstr[8] |= 0b10000000
47
+ str = ''
48
+ nstr.each { |s| str << s.chr }
49
+ str
50
+ end
51
+
52
+ def mask18 v, str # :nodoc
53
+ version = [0, 16, 32, 48, 64, 80][v]
54
+ str[6] &= 0b00001111
55
+ str[6] |= version
56
+ # str[7] &= 0b00001111
57
+ # str[7] |= 0b01010000
58
+ str[8] &= 0b00111111
59
+ str[8] |= 0b10000000
60
+ str
61
+ end
62
+
63
+ def mask v, str
64
+ if RUBY_VERSION >= "1.9.0"
65
+ return mask19 v, str
66
+ else
67
+ return mask18 v, str
68
+ end
69
+ end
70
+ private :mask, :mask18, :mask19
71
+
72
+ # UUID generation using SHA1. Recommended over create_md5.
73
+ # Namespace object is another UUID, some of them are pre-defined below.
74
+ def create_sha1 str, namespace
75
+ sha1 = Digest::SHA1.new
76
+ sha1.update namespace.raw_bytes
77
+ sha1.update str
78
+ sum = sha1.digest
79
+ raw = mask 5, sum[0..15]
80
+ ret = new raw
81
+ ret.freeze
82
+ ret
83
+ end
84
+ alias :create_v5 :create_sha1
85
+
86
+ # UUID generation using MD5 (for backward compat.)
87
+ def create_md5 str, namespace
88
+ md5 = Digest::MD5.new
89
+ md5.update namespace.raw_bytes
90
+ md5.update str
91
+ sum = md5.digest
92
+ raw = mask 3, sum[0..16]
93
+ ret = new raw
94
+ ret.freeze
95
+ ret
96
+ end
97
+ alias :create_v3 :create_md5
98
+
99
+ # UUID generation using random-number generator. From it's random
100
+ # nature, there's no warranty that the created ID is really universaly
101
+ # unique.
102
+ def create_random
103
+ rnd = [
104
+ rand(0x100000000),
105
+ rand(0x100000000),
106
+ rand(0x100000000),
107
+ rand(0x100000000),
108
+ ].pack "N4"
109
+ raw = mask 4, rnd
110
+ ret = new raw
111
+ ret.freeze
112
+ ret
113
+ end
114
+ alias :create_v4 :create_random
115
+
116
+ def read_state fp # :nodoc:
117
+ fp.rewind
118
+ Marshal.load fp.read
119
+ end
120
+
121
+ def write_state fp, c, m # :nodoc:
122
+ fp.rewind
123
+ str = Marshal.dump [c, m]
124
+ fp.write str
125
+ end
126
+
127
+ private :read_state, :write_state
128
+ STATE_FILE = 'ruby-uuid'
129
+
130
+ # create the "version 1" UUID with current system clock, current UTC
131
+ # timestamp, and the IEEE 802 address (so-called MAC address).
132
+ #
133
+ # Speed notice: it's slow. It writes some data into hard drive on every
134
+ # invokation. If you want to speed this up, try remounting tmpdir with a
135
+ # memory based filesystem (such as tmpfs). STILL slow? then no way but
136
+ # rewrite it with c :)
137
+ def create clock=nil, time=nil, mac_addr=nil
138
+ c = t = m = nil
139
+ Dir.chdir Dir.tmpdir do
140
+ unless FileTest.exist? STATE_FILE then
141
+ # Generate a pseudo MAC address because we have no pure-ruby way
142
+ # to know the MAC address of the NIC this system uses. Note
143
+ # that cheating with pseudo arresses here is completely legal:
144
+ # see Section 4.5 of RFC4122 for details.
145
+ sha1 = Digest::SHA1.new
146
+ 256.times do
147
+ r = [rand(0x100000000)].pack "N"
148
+ sha1.update r
149
+ end
150
+ str = sha1.digest
151
+ r = rand 14 # 20-6
152
+ node = str[r, 6] || str
153
+ if RUBY_VERSION >= "1.9.0"
154
+ nnode = node.bytes.to_a
155
+ nnode[0] |= 0x01
156
+ node = ''
157
+ nnode.each { |s| node << s.chr }
158
+ else
159
+ node[0] |= 0x01 # multicast bit
160
+ end
161
+ k = rand 0x40000
162
+ open STATE_FILE, 'w' do |fp|
163
+ fp.flock IO::LOCK_EX
164
+ write_state fp, k, node
165
+ fp.chmod 0o777 # must be world writable
166
+ end
167
+ end
168
+ open STATE_FILE, 'r+' do |fp|
169
+ fp.flock IO::LOCK_EX
170
+ c, m = read_state fp
171
+ c = clock % 0x4000 if clock
172
+ m = mac_addr if mac_addr
173
+ t = time
174
+ if t.nil? then
175
+ # UUID epoch is 1582/Oct/15
176
+ tt = Time.now
177
+ t = tt.to_i*10000000 + tt.tv_usec*10 + 0x01B21DD213814000
178
+ end
179
+ c = c.succ # important; increment here
180
+ write_state fp, c, m
181
+ end
182
+ end
183
+
184
+ tl = t & 0xFFFF_FFFF
185
+ tm = t >> 32
186
+ tm = tm & 0xFFFF
187
+ th = t >> 48
188
+ th = th & 0x0FFF
189
+ th = th | 0x1000
190
+ cl = c & 0xFF
191
+ ch = c & 0x3F00
192
+ ch = ch >> 8
193
+ ch = ch | 0x80
194
+ pack tl, tm, th, cl, ch, m
195
+ end
196
+ alias :create_v1 :create
197
+
198
+ # A simple GUID parser: just ignores unknown characters and convert
199
+ # hexadecimal dump into 16-octet object.
200
+ def parse obj
201
+ str = obj.to_s.sub %r/\Aurn:uuid:/, ''
202
+ str.gsub! %r/[^0-9A-Fa-f]/, ''
203
+ raw = str[0..31].lines.to_a.pack 'H*'
204
+ ret = new raw
205
+ ret.freeze
206
+ ret
207
+ end
208
+
209
+ # The 'primitive constructor' of this class
210
+ # Note UUID.pack(uuid.unpack) == uuid
211
+ def pack tl, tm, th, ch, cl, n
212
+ raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
213
+ ret = new raw
214
+ ret.freeze
215
+ ret
216
+ end
217
+ end
218
+
219
+ # The 'primitive deconstructor', or the dual to pack.
220
+ # Note UUID.pack(uuid.unpack) == uuid
221
+ def unpack
222
+ raw_bytes.unpack "NnnCCa6"
223
+ end
224
+
225
+ # Generate the string representation (a.k.a GUID) of this UUID
226
+ def to_s
227
+ a = unpack
228
+ tmp = a[-1].unpack 'C*'
229
+ a[-1] = sprintf '%02x%02x%02x%02x%02x%02x', *tmp
230
+ "%08x-%04x-%04x-%02x%02x-%s" % a
231
+ end
232
+ alias guid to_s
233
+
234
+ # Convert into a RFC4122-comforming URN representation
235
+ def to_uri
236
+ "urn:uuid:" + self.to_s
237
+ end
238
+ alias urn to_uri
239
+
240
+ # Convert into 128-bit unsigned integer
241
+ # Typically a Bignum instance, but can be a Fixnum.
242
+ def to_int
243
+ tmp = self.raw_bytes.unpack "C*"
244
+ tmp.inject do |r, i|
245
+ r * 256 | i
246
+ end
247
+ end
248
+ alias to_i to_int
249
+
250
+ # Gets the version of this UUID
251
+ # returns nil if bad version
252
+ def version
253
+ a = unpack
254
+ v = (a[2] & 0xF000).to_s(16)[0].chr.to_i
255
+ return v if (1..5).include? v
256
+ return nil
257
+ end
258
+
259
+ # Two UUIDs are said to be equal if and only if their (byte-order
260
+ # canonicalized) integer representations are equivallent. Refer RFC4122 for
261
+ # details.
262
+ def == other
263
+ to_i == other.to_i
264
+ end
265
+
266
+ include Comparable
267
+ # UUIDs are comparable (don't know what benefits are there, though).
268
+ def <=> other
269
+ to_s <=> other.to_s
270
+ end
271
+
272
+ # Pre-defined UUID Namespaces described in RFC4122 Appendix C.
273
+ NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
274
+ NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
275
+ NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
276
+ NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
277
+
278
+ # The Nil UUID in RFC4122 Section 4.1.7
279
+ Nil = parse "00000000-0000-0000-0000-000000000000"
280
+ end
281
+
282
+ __END__
283
+ if __FILE__ == $0 then
284
+ require 'test/unit'
285
+
286
+ class TC_UUID < Test::Unit::TestCase
287
+ def test_v1
288
+ u1 = UUID.create
289
+ u2 = UUID.create
290
+ assert_not_equal u1, u2
291
+ end
292
+
293
+ def test_v1_repeatability
294
+ u1 = UUID.create 1, 2, "345678"
295
+ u2 = UUID.create 1, 2, "345678"
296
+ assert_equal u1, u2
297
+ end
298
+
299
+ def test_v3
300
+ u1 = UUID.create_md5 "foo", UUID::NameSpace_DNS
301
+ u2 = UUID.create_md5 "foo", UUID::NameSpace_DNS
302
+ u3 = UUID.create_md5 "foo", UUID::NameSpace_URL
303
+ assert_equal u1, u2
304
+ assert_not_equal u1, u3
305
+ end
306
+
307
+ def test_v5
308
+ u1 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
309
+ u2 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
310
+ u3 = UUID.create_sha1 "foo", UUID::NameSpace_URL
311
+ assert_equal u1, u2
312
+ assert_not_equal u1, u3
313
+ end
314
+
315
+ def test_v4
316
+ # This test is not perfect, because the random nature of version 4
317
+ # UUID it is not always true that the three objects below really
318
+ # differ. But in real life it's enough to say we're OK when this
319
+ # passes.
320
+ u1 = UUID.create_random
321
+ u2 = UUID.create_random
322
+ u3 = UUID.create_random
323
+ assert_not_equal u1.raw_bytes, u2.raw_bytes
324
+ assert_not_equal u1.raw_bytes, u3.raw_bytes
325
+ assert_not_equal u2.raw_bytes, u3.raw_bytes
326
+ end
327
+
328
+ def test_pack
329
+ u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
330
+ "\000\300O\3240\310"
331
+ assert_equal UUID::NameSpace_DNS, u1
332
+ end
333
+
334
+ def test_unpack
335
+ tl, tm, th, cl, ch, m = UUID::NameSpace_DNS.unpack
336
+ assert_equal 0x6ba7b810, tl
337
+ assert_equal 0x9dad, tm
338
+ assert_equal 0x11d1, th
339
+ assert_equal 0x80, cl
340
+ assert_equal 0xb4, ch
341
+ assert_equal "\000\300O\3240\310", m
342
+ end
343
+
344
+ def test_parse
345
+ u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
346
+ "\000\300O\3240\310"
347
+ u2 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
348
+ u3 = UUID.parse "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
349
+ assert_equal u1, u2
350
+ assert_equal u1, u3
351
+ end
352
+
353
+ def test_to_s
354
+ u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
355
+ assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", u1.to_s
356
+ end
357
+
358
+ def test_to_i
359
+ u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
360
+ assert_equal 0x6ba7b8109dad11d180b400c04fd430c8, u1.to_i
361
+ end
362
+
363
+ def test_version
364
+ u1 = UUID.create_v1
365
+ assert_equal 1, u1.version
366
+ u3 = UUID.create_v3 "foo", UUID::NameSpace_DNS
367
+ assert_equal 3, u3.version
368
+ u4 = UUID.create_v4
369
+ assert_equal 4, u4.version
370
+ u5 = UUID.create_v5 "foo", UUID::NameSpace_DNS
371
+ assert_equal 5, u5.version
372
+ end
373
+ end
374
+ end
375
+
376
+
377
+
378
+ # Local Variables:
379
+ # mode: ruby
380
+ # code: utf-8
381
+ # indent-tabs-mode: t
382
+ # tab-width: 3
383
+ # ruby-indent-level: 3
384
+ # fill-column: 79
385
+ # default-justification: full
386
+ # End:
387
+ # vi: ts=3 sw=3