bson 4.7.1 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data.tar.gz.sig +0 -0
  4. data/README.md +5 -1
  5. data/ext/bson/bson-native.h +14 -4
  6. data/ext/bson/init.c +25 -2
  7. data/ext/bson/read.c +61 -15
  8. data/ext/bson/util.c +40 -0
  9. data/ext/bson/write.c +17 -13
  10. data/lib/bson.rb +3 -0
  11. data/lib/bson/array.rb +21 -3
  12. data/lib/bson/binary.rb +41 -3
  13. data/lib/bson/boolean.rb +3 -1
  14. data/lib/bson/code.rb +15 -1
  15. data/lib/bson/code_with_scope.rb +17 -3
  16. data/lib/bson/db_pointer.rb +104 -0
  17. data/lib/bson/decimal128.rb +15 -1
  18. data/lib/bson/error.rb +17 -0
  19. data/lib/bson/ext_json.rb +374 -0
  20. data/lib/bson/float.rb +47 -1
  21. data/lib/bson/hash.rb +23 -3
  22. data/lib/bson/int32.rb +21 -1
  23. data/lib/bson/int64.rb +28 -3
  24. data/lib/bson/integer.rb +34 -0
  25. data/lib/bson/max_key.rb +12 -0
  26. data/lib/bson/min_key.rb +12 -0
  27. data/lib/bson/nil_class.rb +3 -1
  28. data/lib/bson/object.rb +27 -0
  29. data/lib/bson/object_id.rb +15 -1
  30. data/lib/bson/regexp.rb +19 -2
  31. data/lib/bson/specialized.rb +3 -1
  32. data/lib/bson/string.rb +3 -1
  33. data/lib/bson/symbol.rb +92 -3
  34. data/lib/bson/time.rb +28 -3
  35. data/lib/bson/timestamp.rb +15 -1
  36. data/lib/bson/undefined.rb +11 -0
  37. data/lib/bson/version.rb +1 -1
  38. data/spec/bson/binary_spec.rb +33 -3
  39. data/spec/bson/ext_json_parse_spec.rb +276 -0
  40. data/spec/bson/float_spec.rb +36 -0
  41. data/spec/bson/hash_spec.rb +70 -0
  42. data/spec/bson/int32_spec.rb +20 -0
  43. data/spec/bson/int64_spec.rb +38 -0
  44. data/spec/bson/integer_spec.rb +26 -0
  45. data/spec/bson/raw_spec.rb +22 -1
  46. data/spec/bson/symbol_raw_spec.rb +45 -0
  47. data/spec/bson/symbol_spec.rb +60 -0
  48. data/spec/{support → runners}/common_driver.rb +0 -0
  49. data/spec/runners/corpus.rb +182 -0
  50. data/spec/{support/corpus.rb → runners/corpus_legacy.rb} +40 -58
  51. data/spec/spec_helper.rb +9 -2
  52. data/spec/{bson/driver_bson_spec.rb → spec_tests/common_driver_spec.rb} +1 -0
  53. data/spec/{bson/corpus_spec.rb → spec_tests/corpus_legacy_spec.rb} +4 -4
  54. data/spec/spec_tests/corpus_spec.rb +124 -0
  55. data/spec/spec_tests/data/corpus/README.md +15 -0
  56. data/spec/spec_tests/data/corpus/array.json +49 -0
  57. data/spec/spec_tests/data/corpus/binary.json +85 -0
  58. data/spec/spec_tests/data/corpus/boolean.json +27 -0
  59. data/spec/spec_tests/data/corpus/code.json +67 -0
  60. data/spec/spec_tests/data/corpus/code_w_scope.json +78 -0
  61. data/spec/spec_tests/data/corpus/datetime.json +42 -0
  62. data/spec/spec_tests/data/corpus/dbpointer.json +56 -0
  63. data/spec/spec_tests/data/corpus/dbref.json +31 -0
  64. data/spec/spec_tests/data/corpus/decimal128-1.json +317 -0
  65. data/spec/spec_tests/data/corpus/decimal128-2.json +793 -0
  66. data/spec/spec_tests/data/corpus/decimal128-3.json +1771 -0
  67. data/spec/spec_tests/data/corpus/decimal128-4.json +117 -0
  68. data/spec/spec_tests/data/corpus/decimal128-5.json +402 -0
  69. data/spec/spec_tests/data/corpus/decimal128-6.json +119 -0
  70. data/spec/spec_tests/data/corpus/decimal128-7.json +323 -0
  71. data/spec/spec_tests/data/corpus/document.json +36 -0
  72. data/spec/spec_tests/data/corpus/double.json +87 -0
  73. data/spec/spec_tests/data/corpus/int32.json +43 -0
  74. data/spec/spec_tests/data/corpus/int64.json +43 -0
  75. data/spec/spec_tests/data/corpus/maxkey.json +12 -0
  76. data/spec/spec_tests/data/corpus/minkey.json +12 -0
  77. data/spec/spec_tests/data/corpus/multi-type-deprecated.json +15 -0
  78. data/spec/spec_tests/data/corpus/multi-type.json +11 -0
  79. data/spec/spec_tests/data/corpus/null.json +12 -0
  80. data/spec/spec_tests/data/corpus/oid.json +28 -0
  81. data/spec/spec_tests/data/corpus/regex.json +65 -0
  82. data/spec/spec_tests/data/corpus/string.json +72 -0
  83. data/spec/spec_tests/data/corpus/symbol.json +80 -0
  84. data/spec/spec_tests/data/corpus/timestamp.json +24 -0
  85. data/spec/spec_tests/data/corpus/top.json +240 -0
  86. data/spec/spec_tests/data/corpus/undefined.json +15 -0
  87. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/array.json +0 -0
  88. data/spec/{support/corpus-tests/failures → spec_tests/data/corpus_legacy}/binary.json +0 -0
  89. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/boolean.json +0 -0
  90. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/code.json +1 -1
  91. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/code_w_scope.json +1 -1
  92. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/document.json +1 -1
  93. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/double.json +1 -1
  94. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/datetime.json +0 -0
  95. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/dbpointer.json +0 -0
  96. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/int64.json +0 -0
  97. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/symbol.json +0 -0
  98. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/int32.json +1 -1
  99. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/maxkey.json +1 -1
  100. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/minkey.json +1 -1
  101. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/null.json +1 -1
  102. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/oid.json +0 -0
  103. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/regex.json +1 -1
  104. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/string.json +0 -0
  105. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/timestamp.json +1 -1
  106. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/top.json +0 -0
  107. data/spec/{support/corpus-tests/failures → spec_tests/data/corpus_legacy}/undefined.json +0 -0
  108. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-1.json +0 -0
  109. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-2.json +0 -0
  110. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-3.json +0 -0
  111. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-4.json +0 -0
  112. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-5.json +0 -0
  113. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-6.json +0 -0
  114. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-7.json +0 -0
  115. metadata +170 -95
  116. metadata.gz.sig +2 -4
@@ -31,12 +31,14 @@ module BSON
31
31
  #
32
32
  # @param [ ByteBuffer ] buffer The byte buffer.
33
33
  #
34
+ # @option options [ nil | :bson ] :mode Decoding mode to use.
35
+ #
34
36
  # @return [ TrueClass, FalseClass ] The decoded boolean.
35
37
  #
36
38
  # @see http://bsonspec.org/#/specification
37
39
  #
38
40
  # @since 2.0.0
39
- def self.from_bson(buffer)
41
+ def self.from_bson(buffer, **options)
40
42
  buffer.get_byte == TrueClass::TRUE_BYTE
41
43
  end
42
44
 
@@ -55,7 +55,19 @@ module BSON
55
55
  # @return [ Hash ] The code as a JSON hash.
56
56
  #
57
57
  # @since 2.0.0
58
+ # @deprecated Use as_extended_json instead.
58
59
  def as_json(*args)
60
+ as_extended_json
61
+ end
62
+
63
+ # Converts this object to a representation directly serializable to
64
+ # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst).
65
+ #
66
+ # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode
67
+ # (default is canonical extended JSON)
68
+ #
69
+ # @return [ Hash ] The extended json representation.
70
+ def as_extended_json(**options)
59
71
  { "$code" => javascript }
60
72
  end
61
73
 
@@ -89,12 +101,14 @@ module BSON
89
101
  #
90
102
  # @param [ ByteBuffer ] buffer The byte buffer.
91
103
  #
104
+ # @option options [ nil | :bson ] :mode Decoding mode to use.
105
+ #
92
106
  # @return [ TrueClass, FalseClass ] The decoded code.
93
107
  #
94
108
  # @see http://bsonspec.org/#/specification
95
109
  #
96
110
  # @since 2.0.0
97
- def self.from_bson(buffer)
111
+ def self.from_bson(buffer, **options)
98
112
  new(buffer.get_string)
99
113
  end
100
114
 
@@ -59,8 +59,20 @@ module BSON
59
59
  # @return [ Hash ] The code with scope as a JSON hash.
60
60
  #
61
61
  # @since 2.0.0
62
+ # @deprecated Use as_extended_json instead.
62
63
  def as_json(*args)
63
- { "$code" => javascript, "$scope" => scope }
64
+ as_extended_json
65
+ end
66
+
67
+ # Converts this object to a representation directly serializable to
68
+ # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst).
69
+ #
70
+ # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode
71
+ # (default is canonical extended JSON)
72
+ #
73
+ # @return [ Hash ] The extended json representation.
74
+ def as_extended_json(**options)
75
+ { "$code" => javascript, "$scope" => scope.as_extended_json(**options) }
64
76
  end
65
77
 
66
78
  # Instantiate the new code with scope.
@@ -99,14 +111,16 @@ module BSON
99
111
  #
100
112
  # @param [ ByteBuffer ] buffer The byte buffer.
101
113
  #
114
+ # @option options [ nil | :bson ] :mode Decoding mode to use.
115
+ #
102
116
  # @return [ TrueClass, FalseClass ] The decoded code with scope.
103
117
  #
104
118
  # @see http://bsonspec.org/#/specification
105
119
  #
106
120
  # @since 2.0.0
107
- def self.from_bson(buffer)
121
+ def self.from_bson(buffer, **options)
108
122
  buffer.get_int32 # Throw away the total length.
109
- new(buffer.get_string, ::Hash.from_bson(buffer))
123
+ new(buffer.get_string, ::Hash.from_bson(buffer, **options))
110
124
  end
111
125
 
112
126
  # Register this type when the module is loaded.
@@ -0,0 +1,104 @@
1
+ # Copyright (C) 2020 MongoDB Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module BSON
16
+
17
+ # Injects behaviour for encoding and decoding DBPointer values to and from
18
+ # raw bytes as specified by the BSON spec.
19
+ #
20
+ # @see http://bsonspec.org/#/specification
21
+ class DbPointer
22
+ include JSON
23
+
24
+ # A DBPointer is type 0x0C in the BSON spec.
25
+ BSON_TYPE = 0x0C.chr.force_encoding(BINARY).freeze
26
+
27
+ # Create a new DBPointer object.
28
+ #
29
+ # @param [ String ] ref The database collection name.
30
+ # @param [ BSON::ObjectId ] id The DBPointer id.
31
+ def initialize(ref, id)
32
+ @ref = ref
33
+ @id = id
34
+ end
35
+
36
+ # Return the collection name.
37
+ #
38
+ # @return [ String ] The database collection name.
39
+ attr_reader :ref
40
+
41
+ # Return the DbPointer's id.
42
+ #
43
+ # @return [ BSON::ObjectId ] The id of the DbPointer instance
44
+ attr_reader :id
45
+
46
+ # Determine if this DBPointer object is equal to another object.
47
+ #
48
+ # @param [ Object ] other The object to compare against.
49
+ #
50
+ # @return [ true | false ] If the objects are equal
51
+ def ==(other)
52
+ return false unless other.is_a?(DbPointer)
53
+ ref == other.ref && id == other.id
54
+ end
55
+
56
+ # Get the DBPointer as JSON hash data
57
+ #
58
+ # @return [ Hash ] The DBPointer as a JSON hash.
59
+ #
60
+ # @deprecated Use as_extended_json instead.
61
+ def as_json(*args)
62
+ as_extended_json
63
+ end
64
+
65
+ # Converts this object to a representation directly serializable to
66
+ # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst).
67
+ #
68
+ # @option options [ true | false ] :relaxed Whether to produce relaxed
69
+ # extended JSON representation.
70
+ #
71
+ # @return [ Hash ] The extended json representation.
72
+ def as_extended_json(**options)
73
+ {'$dbPointer' => { "$ref" => ref, '$id' => id.as_extended_json }}
74
+ end
75
+
76
+ # Encode the DBPointer.
77
+ #
78
+ # @return [ BSON::ByteBuffer ] The buffer with the encoded object.
79
+ #
80
+ # @see http://bsonspec.org/#/specification
81
+ def to_bson(buffer = ByteBuffer.new, validating_keys = Config.validating_keys?)
82
+ buffer.put_string(ref)
83
+ id.to_bson(buffer, validating_keys)
84
+ buffer
85
+ end
86
+
87
+ # Deserialize a DBPointer from BSON.
88
+ #
89
+ # @param [ ByteBuffer ] buffer The byte buffer.
90
+ # @param [ Hash ] options
91
+ #
92
+ # @option options [ nil | :bson ] :mode Decoding mode to use.
93
+ #
94
+ # @return [ BSON::DbPointer ] The decoded DBPointer.
95
+ #
96
+ # @see http://bsonspec.org/#/specification
97
+ def self.from_bson(buffer, **options)
98
+ new(buffer.get_string, ObjectId.from_bson(buffer, **options))
99
+ end
100
+
101
+ # Register this type when the module is loaded.
102
+ Registry.register(BSON_TYPE, self)
103
+ end
104
+ end
@@ -63,7 +63,19 @@ module BSON
63
63
  # @return [ Hash ] The number as a JSON hash.
64
64
  #
65
65
  # @since 4.2.0
66
+ # @deprecated Use as_extended_json instead.
66
67
  def as_json(*args)
68
+ as_extended_json
69
+ end
70
+
71
+ # Converts this object to a representation directly serializable to
72
+ # Extended JSON (https://github.com/mongodb/specifications/blob/master/source/extended-json.rst).
73
+ #
74
+ # @option opts [ nil | :relaxed | :legacy ] :mode Serialization mode
75
+ # (default is canonical extended JSON)
76
+ #
77
+ # @return [ Hash ] The extended json representation.
78
+ def as_extended_json(**options)
67
79
  { EXTENDED_JSON_KEY => to_s }
68
80
  end
69
81
 
@@ -197,10 +209,12 @@ module BSON
197
209
  #
198
210
  # @param [ ByteBuffer ] buffer The byte buffer.
199
211
  #
212
+ # @option options [ nil | :bson ] :mode Decoding mode to use.
213
+ #
200
214
  # @return [ BSON::Decimal128 ] The decimal object.
201
215
  #
202
216
  # @since 4.2.0
203
- def from_bson(buffer)
217
+ def from_bson(buffer, **options)
204
218
  from_bits(*buffer.get_decimal128_bytes.unpack('Q<*'))
205
219
  end
206
220
 
@@ -0,0 +1,17 @@
1
+ module BSON
2
+ # Base exception class for all BSON-related errors.
3
+ #
4
+ # @note Many existing exceptions raised by bson-ruby do not derive from
5
+ # this base class. This will change in the next major version (5.0).
6
+ class Error < StandardError
7
+
8
+ # Exception raised when Extended JSON parsing fails.
9
+ class ExtJSONParseError < Error
10
+ end
11
+
12
+ # Exception raised when decoding BSON and the data contains an
13
+ # unsupported binary subtype.
14
+ class UnsupportedBinarySubtype < Error
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,374 @@
1
+ # Copyright (C) 2019 MongoDB Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+
17
+ module BSON
18
+
19
+ # This module contains methods for parsing Extended JSON 2.0.
20
+ # https://github.com/mongodb/specifications/blob/master/source/extended-json.rst
21
+ module ExtJSON
22
+
23
+ # Parses JSON in a string into a Ruby object tree.
24
+ #
25
+ # There are two strategies that this method can follow. If the canonical
26
+ # strategy is used which is the default, this method returns BSON types
27
+ # as much as possible. This allows the resulting object tree to be
28
+ # serialized back to extended JSON or to BSON while preserving the types.
29
+ # The relaxed strategy, enabled by passing {emit_relaxed: true} option,
30
+ # returns native Ruby types as much as possible which makes the resulting
31
+ # object tree easier to work with but may lose type information.
32
+ #
33
+ # Please note the following aspects of this method when emitting relaxed
34
+ # object trees:
35
+ #
36
+ # 1. $numberInt and $numberLong inputs produce Integer instances.
37
+ # 2. $regularExpression inputs produce BSON Regexp instances. This may
38
+ # change in a future version of bson-ruby to produce Ruby Regexp
39
+ # instances, potentially depending on regular expression options.
40
+ # 3. $numberDecimal inputs produce BSON Decimal128 instances. This may
41
+ # change in a future version of bson-ruby to produce Ruby BigDecimal
42
+ # instances instead.
43
+ #
44
+ # This method accepts canonical extended JSON, relaxed extended JSON and
45
+ # JSON without type information as well as a mix of the above.
46
+ #
47
+ # @note This method uses Ruby standard library's JSON.parse method to
48
+ # perform JSON parsing. As the JSON.parse method accepts inputs other
49
+ # than hashes, so does this method and therefore this method can return
50
+ # objects of any type.
51
+ #
52
+ # @param [ String ] str The string to parse.
53
+ #
54
+ # @option options [ nil | :bson ] :mode Which types to emit
55
+ #
56
+ # @return [ Object ] Parsed object tree.
57
+ module_function def parse(str, **options)
58
+ parse_obj(::JSON.parse(str), **options)
59
+ end
60
+
61
+ # Transforms a Ruby object tree containing extended JSON type hashes
62
+ # into a Ruby object tree with said hashes replaced by BSON or Ruby native
63
+ # types.
64
+ #
65
+ # @example Convert extended JSON type hashes:
66
+ # BSON::ExtJSON.parse_obj('foo' => {'$numberLong' => '42'})
67
+ # => {"foo"=>#<BSON::Int64:0x000055e55f4d40f0 @value=42>}
68
+ #
69
+ # @example Convert a non-hash value:
70
+ # BSON::ExtJSON.parse_obj('$numberLong' => '42')
71
+ # => #<BSON::Int64:0x000055e55f4e6ed0 @value=42>
72
+ #
73
+ # There are two strategies that this method can follow. If the canonical
74
+ # strategy is used which is the default, this method returns BSON types
75
+ # as much as possible. This allows the resulting object tree to be
76
+ # serialized back to extended JSON or to BSON while preserving the types.
77
+ # The relaxed strategy, enabled by passing {emit_relaxed: true} option,
78
+ # returns native Ruby types as much as possible which makes the resulting
79
+ # object tree easier to work with but may lose type information.
80
+ #
81
+ # Please note the following aspects of this method when emitting relaxed
82
+ # object trees:
83
+ #
84
+ # 1. $numberInt and $numberLong inputs produce Integer instances.
85
+ # 2. $regularExpression inputs produce BSON Regexp instances. This may
86
+ # change in a future version of bson-ruby to produce Ruby Regexp
87
+ # instances, potentially depending on regular expression options.
88
+ # 3. $numberDecimal inputs produce BSON Decimal128 instances. This may
89
+ # change in a future version of bson-ruby to produce Ruby BigDecimal
90
+ # instances instead.
91
+ #
92
+ # This method accepts object trees resulting from parsing canonical
93
+ # extended JSON, relaxed extended JSON and JSON without type information
94
+ # as well as a mix of the above.
95
+ #
96
+ # @note This method accepts any types as input, not just Hash instances.
97
+ # Consequently, it can return values of any type.
98
+ #
99
+ # @param [ Object ] value The object tree to convert.
100
+ #
101
+ # @option options [ nil | :bson ] :mode Which types to emit
102
+ #
103
+ # @return [ Object ] Converted object tree.
104
+ module_function def parse_obj(value, **options)
105
+ # TODO implement :ruby and :ruby! modes
106
+ unless [nil, :bson].include?(options[:mode])
107
+ raise ArgumentError, "Invalid value for :mode option: #{options[:mode].inspect}"
108
+ end
109
+
110
+ case value
111
+ when String, TrueClass, FalseClass, NilClass, Numeric
112
+ value
113
+ when Hash
114
+ parse_hash(value, **options)
115
+ when Array
116
+ value.map do |item|
117
+ parse_obj(item, **options)
118
+ end
119
+ else
120
+ raise Error::ExtJSONParseError, "Unknown value type: #{value}"
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ RESERVED_KEYS = %w(
127
+ $oid $symbol $numberInt $numberLong $numberDouble $numberDecimal
128
+ $binary $code $scope $timestamp $regularExpression $dbPointer
129
+ $date $minKey $maxKey $undefined
130
+ ).freeze
131
+
132
+ RESERVED_KEYS_HASH = Hash[RESERVED_KEYS.map do |key|
133
+ [key, true]
134
+ end].freeze
135
+
136
+ module_function def parse_hash(hash, **options)
137
+ if hash.empty?
138
+ return {}
139
+ end
140
+
141
+ if hash.key?('$ref')
142
+ # Legacy dbref handling.
143
+ # Note that according to extended json spec, only hash values (but
144
+ # not the top-level BSON document itself) may be of type "dbref".
145
+ # This code applies to both hash values and the hash overall; however,
146
+ # since we do not have DBRef as a distinct type, applying the below
147
+ # logic to top level hashes doesn't cause harm.
148
+ hash = hash.dup
149
+ ref = hash.delete('$ref')
150
+ # $ref can be a string value or an ObjectId
151
+ unless ref.is_a?(String)
152
+ raise Error::ExtJSONParseError, "Invalid $ref value: #{ref}"
153
+ end
154
+ # $id, if present, can be anything
155
+ id = hash.delete('$id')
156
+ if id.is_a?(Hash)
157
+ id = parse_hash(id)
158
+ end
159
+ # Preserve $id value as it was, do not convert either to ObjectId
160
+ # or to a string. But if the value was in {'$oid' => ...} format,
161
+ # the value is converted to an ObjectId instance so that
162
+ # serialization to BSON later on works correctly.
163
+ out = {'$ref' => ref, '$id' => id}
164
+ if hash.key?('$db')
165
+ # $db must always be a string, if provided
166
+ db = hash.delete('$db')
167
+ unless db.is_a?(String)
168
+ raise Error::ExtJSONParseError, "Invalid $db value: #{db}"
169
+ end
170
+ out['$db'] = db
171
+ end
172
+ return out.update(hash)
173
+ end
174
+
175
+ if hash.length == 1
176
+ key, value = hash.first
177
+ return case key
178
+ when '$oid'
179
+ ObjectId.from_string(value)
180
+ when '$symbol'
181
+ Symbol::Raw.new(value)
182
+ when '$numberInt'
183
+ unless value.is_a?(String)
184
+ raise Error::ExtJSONParseError, "$numberInt value is of an incorrect type: #{value}"
185
+ end
186
+ value.to_i
187
+ when '$numberLong'
188
+ unless value.is_a?(String)
189
+ raise Error::ExtJSONParseError, "$numberLong value is of an incorrect type: #{value}"
190
+ end
191
+ value = value.to_i
192
+ if options[:mode] != :bson
193
+ value
194
+ else
195
+ Int64.new(value)
196
+ end
197
+ when '$numberDouble'
198
+ # This handles string to double conversion as well as inf/-inf/nan
199
+ unless value.is_a?(String)
200
+ raise Error::ExtJSONParseError, "Invalid $numberDouble value: #{value}"
201
+ end
202
+ BigDecimal(value).to_f
203
+ when '$numberDecimal'
204
+ # TODO consider returning BigDecimal here instead of Decimal128
205
+ Decimal128.new(value)
206
+ when '$binary'
207
+ unless value.is_a?(Hash)
208
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{value}"
209
+ end
210
+ unless value.keys.sort == %w(base64 subType)
211
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{value}"
212
+ end
213
+ encoded_value = value['base64']
214
+ unless encoded_value.is_a?(String)
215
+ raise Error::ExtJSONParseError, "Invalid base64 value in $binary: #{value}"
216
+ end
217
+ subtype = value['subType']
218
+ unless subtype.is_a?(String)
219
+ raise Error::ExtJSONParseError, "Invalid subType value in $binary: #{value}"
220
+ end
221
+ create_binary(encoded_value, subtype)
222
+ when '$code'
223
+ unless value.is_a?(String)
224
+ raise Error::ExtJSONParseError, "Invalid $code value: #{value}"
225
+ end
226
+ Code.new(value)
227
+ when '$timestamp'
228
+ unless value.keys.sort == %w(i t)
229
+ raise Error::ExtJSONParseError, "Invalid $timestamp value: #{value}"
230
+ end
231
+ t = value['t']
232
+ unless t.is_a?(Integer)
233
+ raise Error::ExtJSONParseError, "Invalid t value: #{value}"
234
+ end
235
+ i = value['i']
236
+ unless i.is_a?(Integer)
237
+ raise Error::ExtJSONParseError, "Invalid i value: #{value}"
238
+ end
239
+ Timestamp.new(t, i)
240
+ when '$regularExpression'
241
+ unless value.keys.sort == %w(options pattern)
242
+ raise Error::ExtJSONParseError, "Invalid $regularExpression value: #{value}"
243
+ end
244
+ # TODO consider returning Ruby regular expression object here
245
+ create_regexp(value['pattern'], value['options'])
246
+ when '$dbPointer'
247
+ unless value.keys.sort == %w($id $ref)
248
+ raise Error::ExtJSONParseError, "Invalid $dbPointer value: #{value}"
249
+ end
250
+ DbPointer.new(value['$ref'], parse_hash(value['$id']))
251
+ when '$date'
252
+ case value
253
+ when String
254
+ ::Time.parse(value)
255
+ when Hash
256
+ unless value.keys.sort == %w($numberLong)
257
+ raise Error::ExtJSONParseError, "Invalid value for $date: #{value}"
258
+ end
259
+ ::Time.at(value.values.first.to_i.to_f / 1000)
260
+ else
261
+ raise Error::ExtJSONParseError, "Invalid value for $date: #{value}"
262
+ end
263
+ when '$minKey'
264
+ unless value == 1
265
+ raise Error::ExtJSONParseError, "Invalid $minKey value: #{value}"
266
+ end
267
+ MinKey.new
268
+ when '$maxKey'
269
+ unless value == 1
270
+ raise Error::ExtJSONParseError, "Invalid $maxKey value: #{value}"
271
+ end
272
+ MaxKey.new
273
+ when '$undefined'
274
+ unless value == true
275
+ raise Error::ExtJSONParseError, "Invalid $undefined value: #{value}"
276
+ end
277
+ Undefined.new
278
+ else
279
+ map_hash(hash, **options)
280
+ end
281
+ end
282
+
283
+ if hash.length == 2
284
+ sorted_keys = hash.keys.sort
285
+ first_key = sorted_keys.first
286
+ last_key = sorted_keys.last
287
+
288
+ if first_key == '$code'
289
+ unless sorted_keys == %w($code $scope)
290
+ raise Error::ExtJSONParseError, "Invalid $code value: #{hash}"
291
+ end
292
+ unless hash['$code'].is_a?(String)
293
+ raise Error::ExtJSONParseError, "Invalid $code value: #{value}"
294
+ end
295
+
296
+ return CodeWithScope.new(hash['$code'], map_hash(hash['$scope']))
297
+ end
298
+
299
+ if first_key == '$binary'
300
+ unless sorted_keys == %w($binary $type)
301
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{hash}"
302
+ end
303
+ unless hash['$binary'].is_a?(String)
304
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{value}"
305
+ end
306
+ unless hash['$type'].is_a?(String)
307
+ raise Error::ExtJSONParseError, "Invalid $binary subtype: #{hash['$type']}"
308
+ end
309
+
310
+ return create_binary(hash['$binary'], hash['$type'])
311
+ end
312
+
313
+ if last_key == '$regex'
314
+ unless sorted_keys == %w($options $regex)
315
+ raise Error::ExtJSONParseError, "Invalid $regex value: #{hash}"
316
+ end
317
+
318
+ if hash['$regex'].is_a?(Hash)
319
+ return {
320
+ '$regex' => parse_hash(hash['$regex']),
321
+ '$options' => hash['$options']
322
+ }
323
+ end
324
+
325
+ unless hash['$regex'].is_a?(String)
326
+ raise Error::ExtJSONParseError, "Invalid $regex pattern: #{hash['$regex']}"
327
+ end
328
+ unless hash['$options'].is_a?(String)
329
+ raise Error::ExtJSONParseError, "Invalid $regex options: #{hash['$options']}"
330
+ end
331
+
332
+ return create_regexp(hash['$regex'], hash['$options'])
333
+ end
334
+
335
+ verify_no_reserved_keys(hash, **options)
336
+ end
337
+
338
+ verify_no_reserved_keys(hash, **options)
339
+ end
340
+
341
+ module_function def verify_no_reserved_keys(hash, **options)
342
+ if hash.length > RESERVED_KEYS.length
343
+ if RESERVED_KEYS.any? { |key| hash.key?(key) }
344
+ raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}"
345
+ end
346
+ else
347
+ if hash.keys.any? { |key| RESERVED_KEYS_HASH.key?(key) }
348
+ raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}"
349
+ end
350
+ end
351
+ map_hash(hash, **options)
352
+ end
353
+
354
+ module_function def map_hash(hash, **options)
355
+ ::Hash[hash.map do |key, value|
356
+ [key, parse_obj(value, **options)]
357
+ end]
358
+ end
359
+
360
+ module_function def create_binary(encoded_value, encoded_subtype)
361
+ subtype = encoded_subtype.hex
362
+ type = Binary::TYPES[subtype.chr]
363
+ unless type
364
+ # Requires https://jira.mongodb.org/browse/RUBY-2056
365
+ raise NotImplementedError, "Binary subtype #{encoded_subtype} is not currently supported"
366
+ end
367
+ Binary.new(Base64.decode64(encoded_value), type)
368
+ end
369
+
370
+ module_function def create_regexp(pattern, options)
371
+ Regexp::Raw.new(pattern, options)
372
+ end
373
+ end
374
+ end