json-schema-pvdgm 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +19 -0
  3. data/README.textile +354 -0
  4. data/lib/json-schema.rb +25 -0
  5. data/lib/json-schema/attributes/additionalitems.rb +23 -0
  6. data/lib/json-schema/attributes/additionalproperties.rb +67 -0
  7. data/lib/json-schema/attributes/allof.rb +37 -0
  8. data/lib/json-schema/attributes/anyof.rb +41 -0
  9. data/lib/json-schema/attributes/dependencies.rb +30 -0
  10. data/lib/json-schema/attributes/dependencies_v4.rb +20 -0
  11. data/lib/json-schema/attributes/disallow.rb +11 -0
  12. data/lib/json-schema/attributes/divisibleby.rb +16 -0
  13. data/lib/json-schema/attributes/enum.rb +24 -0
  14. data/lib/json-schema/attributes/extends.rb +49 -0
  15. data/lib/json-schema/attributes/format.rb +123 -0
  16. data/lib/json-schema/attributes/items.rb +25 -0
  17. data/lib/json-schema/attributes/maxdecimal.rb +15 -0
  18. data/lib/json-schema/attributes/maximum.rb +15 -0
  19. data/lib/json-schema/attributes/maximum_inclusive.rb +15 -0
  20. data/lib/json-schema/attributes/maxitems.rb +12 -0
  21. data/lib/json-schema/attributes/maxlength.rb +14 -0
  22. data/lib/json-schema/attributes/maxproperties.rb +12 -0
  23. data/lib/json-schema/attributes/minimum.rb +15 -0
  24. data/lib/json-schema/attributes/minimum_inclusive.rb +15 -0
  25. data/lib/json-schema/attributes/minitems.rb +12 -0
  26. data/lib/json-schema/attributes/minlength.rb +14 -0
  27. data/lib/json-schema/attributes/minproperties.rb +12 -0
  28. data/lib/json-schema/attributes/multipleof.rb +16 -0
  29. data/lib/json-schema/attributes/not.rb +28 -0
  30. data/lib/json-schema/attributes/oneof.rb +32 -0
  31. data/lib/json-schema/attributes/pattern.rb +15 -0
  32. data/lib/json-schema/attributes/patternproperties.rb +23 -0
  33. data/lib/json-schema/attributes/properties.rb +58 -0
  34. data/lib/json-schema/attributes/properties_optional.rb +23 -0
  35. data/lib/json-schema/attributes/properties_v4.rb +57 -0
  36. data/lib/json-schema/attributes/ref.rb +70 -0
  37. data/lib/json-schema/attributes/required.rb +23 -0
  38. data/lib/json-schema/attributes/type.rb +102 -0
  39. data/lib/json-schema/attributes/type_v4.rb +54 -0
  40. data/lib/json-schema/attributes/uniqueitems.rb +16 -0
  41. data/lib/json-schema/model_validator.rb +85 -0
  42. data/lib/json-schema/schema.rb +73 -0
  43. data/lib/json-schema/uri/file.rb +36 -0
  44. data/lib/json-schema/uri/uuid.rb +285 -0
  45. data/lib/json-schema/util/array_set.rb +14 -0
  46. data/lib/json-schema/util/hash.rb +8 -0
  47. data/lib/json-schema/validator.rb +672 -0
  48. data/lib/json-schema/validators/draft1.rb +32 -0
  49. data/lib/json-schema/validators/draft2.rb +33 -0
  50. data/lib/json-schema/validators/draft3.rb +38 -0
  51. data/lib/json-schema/validators/draft4.rb +45 -0
  52. data/resources/draft-01.json +155 -0
  53. data/resources/draft-02.json +166 -0
  54. data/resources/draft-03.json +174 -0
  55. data/resources/draft-04.json +150 -0
  56. data/test/data/all_of_ref_data.json +3 -0
  57. data/test/data/any_of_ref_data.json +7 -0
  58. data/test/data/bad_data_1.json +3 -0
  59. data/test/data/good_data_1.json +3 -0
  60. data/test/data/one_of_ref_links_data.json +5 -0
  61. data/test/schemas/all_of_ref_base_schema.json +6 -0
  62. data/test/schemas/all_of_ref_schema.json +7 -0
  63. data/test/schemas/any_of_ref_jane_schema.json +4 -0
  64. data/test/schemas/any_of_ref_jimmy_schema.json +4 -0
  65. data/test/schemas/any_of_ref_john_schema.json +4 -0
  66. data/test/schemas/any_of_ref_schema.json +15 -0
  67. data/test/schemas/extends_and_additionalProperties-1-filename.schema.json +34 -0
  68. data/test/schemas/extends_and_additionalProperties-1-ref.schema.json +34 -0
  69. data/test/schemas/extends_and_additionalProperties-2-filename.schema.json +33 -0
  70. data/test/schemas/extends_and_additionalProperties-2-ref.schema.json +33 -0
  71. data/test/schemas/good_schema_1.json +10 -0
  72. data/test/schemas/good_schema_2.json +10 -0
  73. data/test/schemas/good_schema_extends1.json +10 -0
  74. data/test/schemas/good_schema_extends2.json +13 -0
  75. data/test/schemas/inner.schema.json +21 -0
  76. data/test/schemas/one_of_ref_links_schema.json +16 -0
  77. data/test/schemas/self_link_schema.json +17 -0
  78. data/test/schemas/up_link_schema.json +17 -0
  79. data/test/test_all_of_ref_schema.rb +11 -0
  80. data/test/test_any_of_ref_schema.rb +11 -0
  81. data/test/test_bad_schema_ref.rb +33 -0
  82. data/test/test_extended_schema.rb +68 -0
  83. data/test/test_extends_and_additionalProperties.rb +50 -0
  84. data/test/test_files_v3.rb +52 -0
  85. data/test/test_fragment_resolution.rb +31 -0
  86. data/test/test_full_validation.rb +209 -0
  87. data/test/test_jsonschema_draft1.rb +701 -0
  88. data/test/test_jsonschema_draft2.rb +773 -0
  89. data/test/test_jsonschema_draft3.rb +1236 -0
  90. data/test/test_jsonschema_draft4.rb +1356 -0
  91. data/test/test_model_validator.rb +52 -0
  92. data/test/test_one_of.rb +42 -0
  93. data/test/test_ruby_schema.rb +38 -0
  94. data/test/test_schema_type_attribute.rb +21 -0
  95. data/test/test_schema_validation.rb +85 -0
  96. metadata +180 -0
@@ -0,0 +1,102 @@
1
+ module JSON
2
+ class Schema
3
+ class TypeAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
5
+ union = true
6
+ if options[:disallow]
7
+ types = current_schema.schema['disallow']
8
+ else
9
+ types = current_schema.schema['type']
10
+ end
11
+
12
+ if !types.is_a?(Array)
13
+ types = [types]
14
+ union = false
15
+ end
16
+ valid = false
17
+
18
+ # Create an array to hold errors that are generated during union validation
19
+ union_errors = []
20
+
21
+ types.each do |type|
22
+ if type.is_a?(String)
23
+ valid = data_valid_for_type?(data, type)
24
+ elsif type.is_a?(Hash) && union
25
+ # Validate as a schema
26
+ schema = JSON::Schema.new(type,current_schema.uri,validator)
27
+
28
+ # We're going to add a little cruft here to try and maintain any validation errors that occur in this union type
29
+ # We'll handle this by keeping an error count before and after validation, extracting those errors and pushing them onto a union error
30
+ pre_validation_error_count = validation_errors(processor).count
31
+
32
+ begin
33
+ schema.validate(data,fragments,processor,options)
34
+ valid = true
35
+ rescue ValidationError
36
+ # We don't care that these schemas don't validate - we only care that one validated
37
+ end
38
+
39
+ diff = validation_errors(processor).count - pre_validation_error_count
40
+ valid = false if diff > 0
41
+ while diff > 0
42
+ diff = diff - 1
43
+ union_errors.push(validation_errors(processor).pop)
44
+ end
45
+ end
46
+
47
+ break if valid
48
+ end
49
+
50
+ if (options[:disallow])
51
+ if valid
52
+ message = "The property '#{build_fragment(fragments)}' matched one or more of the following types:"
53
+ types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
54
+ message.chop!
55
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :type })
56
+ end
57
+ elsif !valid
58
+ if union
59
+ message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match one or more of the following types:"
60
+ types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
61
+ message.chop!
62
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :type })
63
+ validation_errors(processor).last.sub_errors = union_errors
64
+ else
65
+ message = "The property '#{build_fragment(fragments)}' of type #{type_of_data(data)} did not match the following type:"
66
+ types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
67
+ message.chop!
68
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :type })
69
+ end
70
+ end
71
+ end
72
+
73
+ TYPE_CLASS_MAPPINGS = {
74
+ "string" => String,
75
+ "number" => Numeric,
76
+ "integer" => Integer,
77
+ "boolean" => [TrueClass, FalseClass],
78
+ "object" => Hash,
79
+ "array" => Array,
80
+ "null" => NilClass,
81
+ "any" => Object
82
+ }
83
+
84
+ def self.data_valid_for_type?(data, type)
85
+ valid_classes = TYPE_CLASS_MAPPINGS.fetch(type) { return true }
86
+ Array(valid_classes).any? { |c| data.is_a?(c) }
87
+ end
88
+
89
+ # Lookup Schema type of given class instance
90
+ def self.type_of_data(data)
91
+ type, klass = TYPE_CLASS_MAPPINGS.map { |k,v| [k,v] }.sort_by { |i|
92
+ k,v = i
93
+ -Array(v).map { |klass| klass.ancestors.size }.max
94
+ }.find { |i|
95
+ k,v = i
96
+ Array(v).any? { |klass| data.kind_of?(klass) }
97
+ }
98
+ type
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
1
+ module JSON
2
+ class Schema
3
+ class TypeV4Attribute < Attribute
4
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
5
+ union = true
6
+ types = current_schema.schema['type']
7
+ if !types.is_a?(Array)
8
+ types = [types]
9
+ union = false
10
+ end
11
+ valid = false
12
+
13
+ # Create an array to hold errors that are generated during union validation
14
+ union_errors = []
15
+
16
+ types.each do |type|
17
+ valid = data_valid_for_type?(data, type)
18
+ break if valid
19
+ end
20
+
21
+ if !valid
22
+ if union
23
+ message = "The property '#{build_fragment(fragments)}' of type #{data.class} did not match one or more of the following types:"
24
+ types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
25
+ message.chop!
26
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :type })
27
+ validation_errors(processor).last.sub_errors = union_errors
28
+ else
29
+ message = "The property '#{build_fragment(fragments)}' of type #{data.class} did not match the following type:"
30
+ types.each {|type| message += type.is_a?(String) ? " #{type}," : " (schema)," }
31
+ message.chop!
32
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :type })
33
+ end
34
+ end
35
+ end
36
+
37
+ TYPE_CLASS_MAPPINGS = {
38
+ "string" => String,
39
+ "number" => Numeric,
40
+ "integer" => Integer,
41
+ "boolean" => [TrueClass, FalseClass],
42
+ "object" => Hash,
43
+ "array" => Array,
44
+ "null" => NilClass,
45
+ "any" => Object
46
+ }
47
+
48
+ def self.data_valid_for_type?(data, type)
49
+ valid_classes = TYPE_CLASS_MAPPINGS.fetch(type) { return true }
50
+ Array(valid_classes).any? { |c| data.is_a?(c) }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ module JSON
2
+ class Schema
3
+ class UniqueItemsAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, processor, validator, options = {})
5
+ if data.is_a?(Array)
6
+ d = data.clone
7
+ dupes = d.uniq!
8
+ if dupes
9
+ message = "The property '#{build_fragment(fragments)}' contained duplicated array values"
10
+ validation_error(processor, message, fragments, current_schema, self, options[:record_errors], { property: last_fragment_as_symbol(fragments), failure: :unique })
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,85 @@
1
+ module JSON
2
+
3
+ class ModelValidator
4
+
5
+ attr_reader :schema
6
+
7
+ DYNAMIC_VALIDATORS = [
8
+ [ [ 'requiredIf', :requiredIf ], :process_required_if ],
9
+ [ [ 'noneOfTheAbove', :noneOfTheAbove ], :process_none_of_the_above ]
10
+ ]
11
+
12
+ def initialize(schema)
13
+ if schema.is_a?(String)
14
+ @schema = JSON.parse(File.new(schema).read)
15
+ elsif schema.is_a?(Hash)
16
+ @schema = schema
17
+ else
18
+ raise "Schema must be a path or a hash!"
19
+ end
20
+ end
21
+
22
+ #
23
+ # Validate the specified model having the hash data contained
24
+ # in the hash_attribute attribute. By default the hash data
25
+ # is assumed to be located in the model's 'responses'
26
+ # attribute.
27
+ #
28
+ def validate(model, hash_attribute=:responses)
29
+ data = model[hash_attribute] || {}
30
+
31
+ # Put the hash data into an OpenStruct to allow access to
32
+ # the hash data as an object. We're using the short variable
33
+ # name of 'r' because that's what we're using in the schema
34
+ # definition; it keeps the schema stuff short and to the point.
35
+ r = OpenStruct.new(data)
36
+
37
+ m = model
38
+
39
+ # Do the static validation first
40
+ errors = JSON::Validator.fully_validate(schema, data, errors_as_objects: true).map { | e | e[:error_details] }.uniq
41
+
42
+ # Now perform dynamic validation
43
+ schema['properties'].each_pair do | property, sch |
44
+ errors.concat process_dynamic_validation(property, sch, r, m)
45
+ end
46
+
47
+ # Add the errors to the model
48
+ errors.each do | error |
49
+ model.errors.add(error[:property], stringize_error(error))
50
+ end
51
+
52
+ errors
53
+ end
54
+
55
+ def stringize_error(error)
56
+ s = "#{error[:property]}_#{error[:failure]}"
57
+ defined?(I18n) ? I18n.t(s) : s
58
+ end
59
+
60
+ def process_dynamic_validation(property, sch, r, m)
61
+ errors = []
62
+ DYNAMIC_VALIDATORS.each do | dv |
63
+ dv_value = sch[dv.first.first] || sch[dv.first.last]
64
+ error = send(dv.last, m, r, property, dv_value) if dv_value.present?
65
+ errors << error if error.present?
66
+ end
67
+ errors
68
+ end
69
+
70
+ def process_required_if(m, r, property, required_if_expression)
71
+ if eval(required_if_expression)
72
+ { property: property.to_sym, failure: :required } if r[property.to_sym].blank?
73
+ end
74
+ end
75
+
76
+ def process_none_of_the_above(m, r, property, none_of_the_above_value)
77
+ value = r[property.to_sym]
78
+ if value.is_a?(Array)
79
+ { property: property.to_sym, failure: :noneOfTheAbove } if value.include?(none_of_the_above_value) && value.size > 1
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,73 @@
1
+ require 'pathname'
2
+
3
+ module JSON
4
+ class Schema
5
+
6
+ attr_accessor :schema, :uri, :validator
7
+
8
+ def initialize(schema,uri,parent_validator=nil)
9
+ @schema = schema
10
+ @uri = uri
11
+
12
+ self.class.add_indifferent_access(@schema)
13
+
14
+ # If there is an ID on this schema, use it to generate the URI
15
+ if @schema['id'] && @schema['id'].kind_of?(String)
16
+ temp_uri = URI.parse(@schema['id'])
17
+ if temp_uri.relative?
18
+ uri = uri.merge(@schema['id'])
19
+ temp_uri = uri
20
+ end
21
+ @uri = temp_uri
22
+ end
23
+ @uri.fragment = ''
24
+
25
+ # If there is a $schema on this schema, use it to determine which validator to use
26
+ if @schema['$schema']
27
+ u = URI.parse(@schema['$schema'])
28
+ @validator = JSON::Validator.validators["#{u.scheme}://#{u.host}#{u.path}"]
29
+ if @validator.nil?
30
+ raise SchemaError.new("This library does not have support for schemas defined by #{u.scheme}://#{u.host}#{u.path}")
31
+ end
32
+ elsif parent_validator
33
+ @validator = parent_validator
34
+ else
35
+ @validator = JSON::Validator.default_validator
36
+ end
37
+ end
38
+
39
+ def validate(data, fragments, processor, options = {})
40
+ @validator.validate(self, data, fragments, processor, options)
41
+ end
42
+
43
+ def self.add_indifferent_access(schema)
44
+ if schema.is_a?(Hash)
45
+ schema.default_proc = proc do |hash,key|
46
+ if hash.has_key?(key)
47
+ hash[key]
48
+ else
49
+ key = case key
50
+ when Symbol then key.to_s
51
+ when String then key.to_sym
52
+ end
53
+ hash.has_key?(key) ? hash[key] : nil
54
+ end
55
+ end
56
+ schema.keys.each do |key|
57
+ add_indifferent_access(schema[key])
58
+ end
59
+ end
60
+ end
61
+
62
+ def base_uri
63
+ parts = @uri.to_s.split('/')
64
+ parts.pop
65
+ parts.join('/') + '/'
66
+ end
67
+
68
+ def to_s
69
+ @schema.to_json
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,36 @@
1
+ require 'rbconfig'
2
+ require 'uri'
3
+
4
+ module URI
5
+
6
+ # Ruby does not have built-in support for filesystem URIs, and definitely does not have built-in support for
7
+ # using open-uri with filesystem URIs
8
+ class File < Generic
9
+
10
+ COMPONENT = [
11
+ :scheme,
12
+ :path,
13
+ :fragment,
14
+ :host
15
+ ].freeze
16
+
17
+ def initialize(*arg)
18
+ # arg[2] is the 'host'; this logic to set it to "" causes file schemes with UNC to break
19
+ # so don't do it on windows platforms
20
+ is_windows = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
21
+ arg[2] = "" unless is_windows
22
+ super(*arg)
23
+ end
24
+
25
+ def self.build(args)
26
+ tmp = Util::make_components_hash(self, args)
27
+ return super(tmp)
28
+ end
29
+
30
+ def open(*rest, &block)
31
+ ::File.open(self.path, *rest, &block)
32
+ end
33
+
34
+ @@schemes['FILE'] = File
35
+ end
36
+ end
@@ -0,0 +1,285 @@
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
+ module JSON
32
+ module Util
33
+
34
+ # Pure ruby UUID generator, which is compatible with RFC4122
35
+ UUID = Struct.new :raw_bytes
36
+
37
+ class UUID
38
+ private_class_method :new
39
+
40
+ class << self
41
+ def mask19 v, str # :nodoc
42
+ nstr = str.bytes.to_a
43
+ version = [0, 16, 32, 48, 64, 80][v]
44
+ nstr[6] &= 0b00001111
45
+ nstr[6] |= version
46
+ # nstr[7] &= 0b00001111
47
+ # nstr[7] |= 0b01010000
48
+ nstr[8] &= 0b00111111
49
+ nstr[8] |= 0b10000000
50
+ str = ''
51
+ nstr.each { |s| str << s.chr }
52
+ str
53
+ end
54
+
55
+ def mask18 v, str # :nodoc
56
+ version = [0, 16, 32, 48, 64, 80][v]
57
+ str[6] &= 0b00001111
58
+ str[6] |= version
59
+ # str[7] &= 0b00001111
60
+ # str[7] |= 0b01010000
61
+ str[8] &= 0b00111111
62
+ str[8] |= 0b10000000
63
+ str
64
+ end
65
+
66
+ def mask v, str
67
+ if RUBY_VERSION >= "1.9.0"
68
+ return mask19 v, str
69
+ else
70
+ return mask18 v, str
71
+ end
72
+ end
73
+ private :mask, :mask18, :mask19
74
+
75
+ # UUID generation using SHA1. Recommended over create_md5.
76
+ # Namespace object is another UUID, some of them are pre-defined below.
77
+ def create_sha1 str, namespace
78
+ sha1 = Digest::SHA1.new
79
+ sha1.update namespace.raw_bytes
80
+ sha1.update str
81
+ sum = sha1.digest
82
+ raw = mask 5, sum[0..15]
83
+ ret = new raw
84
+ ret.freeze
85
+ ret
86
+ end
87
+ alias :create_v5 :create_sha1
88
+
89
+ # UUID generation using MD5 (for backward compat.)
90
+ def create_md5 str, namespace
91
+ md5 = Digest::MD5.new
92
+ md5.update namespace.raw_bytes
93
+ md5.update str
94
+ sum = md5.digest
95
+ raw = mask 3, sum[0..16]
96
+ ret = new raw
97
+ ret.freeze
98
+ ret
99
+ end
100
+ alias :create_v3 :create_md5
101
+
102
+ # UUID generation using random-number generator. From it's random
103
+ # nature, there's no warranty that the created ID is really universaly
104
+ # unique.
105
+ def create_random
106
+ rnd = [
107
+ rand(0x100000000),
108
+ rand(0x100000000),
109
+ rand(0x100000000),
110
+ rand(0x100000000),
111
+ ].pack "N4"
112
+ raw = mask 4, rnd
113
+ ret = new raw
114
+ ret.freeze
115
+ ret
116
+ end
117
+ alias :create_v4 :create_random
118
+
119
+ def read_state fp # :nodoc:
120
+ fp.rewind
121
+ Marshal.load fp.read
122
+ end
123
+
124
+ def write_state fp, c, m # :nodoc:
125
+ fp.rewind
126
+ str = Marshal.dump [c, m]
127
+ fp.write str
128
+ end
129
+
130
+ private :read_state, :write_state
131
+ STATE_FILE = 'ruby-uuid'
132
+
133
+ # create the "version 1" UUID with current system clock, current UTC
134
+ # timestamp, and the IEEE 802 address (so-called MAC address).
135
+ #
136
+ # Speed notice: it's slow. It writes some data into hard drive on every
137
+ # invokation. If you want to speed this up, try remounting tmpdir with a
138
+ # memory based filesystem (such as tmpfs). STILL slow? then no way but
139
+ # rewrite it with c :)
140
+ def create clock=nil, time=nil, mac_addr=nil
141
+ c = t = m = nil
142
+ Dir.chdir Dir.tmpdir do
143
+ unless FileTest.exist? STATE_FILE then
144
+ # Generate a pseudo MAC address because we have no pure-ruby way
145
+ # to know the MAC address of the NIC this system uses. Note
146
+ # that cheating with pseudo arresses here is completely legal:
147
+ # see Section 4.5 of RFC4122 for details.
148
+ sha1 = Digest::SHA1.new
149
+ 256.times do
150
+ r = [rand(0x100000000)].pack "N"
151
+ sha1.update r
152
+ end
153
+ str = sha1.digest
154
+ r = rand 14 # 20-6
155
+ node = str[r, 6] || str
156
+ if RUBY_VERSION >= "1.9.0"
157
+ nnode = node.bytes.to_a
158
+ nnode[0] |= 0x01
159
+ node = ''
160
+ nnode.each { |s| node << s.chr }
161
+ else
162
+ node[0] |= 0x01 # multicast bit
163
+ end
164
+ k = rand 0x40000
165
+ open STATE_FILE, 'w' do |fp|
166
+ fp.flock IO::LOCK_EX
167
+ write_state fp, k, node
168
+ fp.chmod 0o777 # must be world writable
169
+ end
170
+ end
171
+ open STATE_FILE, 'r+' do |fp|
172
+ fp.flock IO::LOCK_EX
173
+ c, m = read_state fp
174
+ c = clock % 0x4000 if clock
175
+ m = mac_addr if mac_addr
176
+ t = time
177
+ if t.nil? then
178
+ # UUID epoch is 1582/Oct/15
179
+ tt = Time.now
180
+ t = tt.to_i*10000000 + tt.tv_usec*10 + 0x01B21DD213814000
181
+ end
182
+ c = c.succ # important; increment here
183
+ write_state fp, c, m
184
+ end
185
+ end
186
+
187
+ tl = t & 0xFFFF_FFFF
188
+ tm = t >> 32
189
+ tm = tm & 0xFFFF
190
+ th = t >> 48
191
+ th = th & 0x0FFF
192
+ th = th | 0x1000
193
+ cl = c & 0xFF
194
+ ch = c & 0x3F00
195
+ ch = ch >> 8
196
+ ch = ch | 0x80
197
+ pack tl, tm, th, cl, ch, m
198
+ end
199
+ alias :create_v1 :create
200
+
201
+ # A simple GUID parser: just ignores unknown characters and convert
202
+ # hexadecimal dump into 16-octet object.
203
+ def parse obj
204
+ str = obj.to_s.sub %r/\Aurn:uuid:/, ''
205
+ str.gsub! %r/[^0-9A-Fa-f]/, ''
206
+ raw = str[0..31].lines.to_a.pack 'H*'
207
+ ret = new raw
208
+ ret.freeze
209
+ ret
210
+ end
211
+
212
+ # The 'primitive constructor' of this class
213
+ # Note UUID.pack(uuid.unpack) == uuid
214
+ def pack tl, tm, th, ch, cl, n
215
+ raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
216
+ ret = new raw
217
+ ret.freeze
218
+ ret
219
+ end
220
+ end
221
+
222
+ # The 'primitive deconstructor', or the dual to pack.
223
+ # Note UUID.pack(uuid.unpack) == uuid
224
+ def unpack
225
+ raw_bytes.unpack "NnnCCa6"
226
+ end
227
+
228
+ # Generate the string representation (a.k.a GUID) of this UUID
229
+ def to_s
230
+ a = unpack
231
+ tmp = a[-1].unpack 'C*'
232
+ a[-1] = sprintf '%02x%02x%02x%02x%02x%02x', *tmp
233
+ "%08x-%04x-%04x-%02x%02x-%s" % a
234
+ end
235
+ alias guid to_s
236
+
237
+ # Convert into a RFC4122-comforming URN representation
238
+ def to_uri
239
+ "urn:uuid:" + self.to_s
240
+ end
241
+ alias urn to_uri
242
+
243
+ # Convert into 128-bit unsigned integer
244
+ # Typically a Bignum instance, but can be a Fixnum.
245
+ def to_int
246
+ tmp = self.raw_bytes.unpack "C*"
247
+ tmp.inject do |r, i|
248
+ r * 256 | i
249
+ end
250
+ end
251
+ alias to_i to_int
252
+
253
+ # Gets the version of this UUID
254
+ # returns nil if bad version
255
+ def version
256
+ a = unpack
257
+ v = (a[2] & 0xF000).to_s(16)[0].chr.to_i
258
+ return v if (1..5).include? v
259
+ return nil
260
+ end
261
+
262
+ # Two UUIDs are said to be equal if and only if their (byte-order
263
+ # canonicalized) integer representations are equivallent. Refer RFC4122 for
264
+ # details.
265
+ def == other
266
+ to_i == other.to_i
267
+ end
268
+
269
+ include Comparable
270
+ # UUIDs are comparable (don't know what benefits are there, though).
271
+ def <=> other
272
+ to_s <=> other.to_s
273
+ end
274
+
275
+ # Pre-defined UUID Namespaces described in RFC4122 Appendix C.
276
+ NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
277
+ NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
278
+ NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
279
+ NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
280
+
281
+ # The Nil UUID in RFC4122 Section 4.1.7
282
+ Nil = parse "00000000-0000-0000-0000-000000000000"
283
+ end
284
+ end
285
+ end