bson 4.7.0 → 4.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -2
  3. data/ext/bson/bson-endian.h +1 -1
  4. data/ext/bson/bson-native.h +16 -5
  5. data/ext/bson/bytebuf.c +1 -1
  6. data/ext/bson/endian.c +2 -1
  7. data/ext/bson/init.c +26 -3
  8. data/ext/bson/read.c +127 -33
  9. data/ext/bson/util.c +41 -1
  10. data/ext/bson/write.c +18 -14
  11. data/lib/bson.rb +4 -1
  12. data/lib/bson/active_support.rb +1 -1
  13. data/lib/bson/array.rb +34 -5
  14. data/lib/bson/binary.rb +42 -4
  15. data/lib/bson/boolean.rb +12 -3
  16. data/lib/bson/code.rb +16 -2
  17. data/lib/bson/code_with_scope.rb +32 -5
  18. data/lib/bson/config.rb +1 -1
  19. data/lib/bson/date.rb +1 -1
  20. data/lib/bson/date_time.rb +1 -1
  21. data/lib/bson/db_pointer.rb +110 -0
  22. data/lib/bson/decimal128.rb +16 -2
  23. data/lib/bson/decimal128/builder.rb +1 -1
  24. data/lib/bson/document.rb +1 -1
  25. data/lib/bson/environment.rb +2 -1
  26. data/lib/bson/error.rb +21 -0
  27. data/lib/bson/ext_json.rb +375 -0
  28. data/lib/bson/false_class.rb +1 -1
  29. data/lib/bson/float.rb +48 -2
  30. data/lib/bson/hash.rb +36 -5
  31. data/lib/bson/int32.rb +22 -2
  32. data/lib/bson/int64.rb +29 -4
  33. data/lib/bson/integer.rb +35 -1
  34. data/lib/bson/json.rb +1 -1
  35. data/lib/bson/max_key.rb +13 -1
  36. data/lib/bson/min_key.rb +13 -1
  37. data/lib/bson/nil_class.rb +4 -2
  38. data/lib/bson/object.rb +28 -1
  39. data/lib/bson/object_id.rb +16 -2
  40. data/lib/bson/open_struct.rb +1 -1
  41. data/lib/bson/regexp.rb +20 -3
  42. data/lib/bson/registry.rb +1 -1
  43. data/lib/bson/specialized.rb +4 -2
  44. data/lib/bson/string.rb +4 -2
  45. data/lib/bson/symbol.rb +93 -4
  46. data/lib/bson/time.rb +63 -4
  47. data/lib/bson/time_with_zone.rb +1 -1
  48. data/lib/bson/timestamp.rb +16 -2
  49. data/lib/bson/true_class.rb +1 -1
  50. data/lib/bson/undefined.rb +12 -1
  51. data/lib/bson/version.rb +2 -2
  52. data/spec/bson/array_spec.rb +1 -1
  53. data/spec/bson/binary_spec.rb +34 -4
  54. data/spec/bson/binary_uuid_spec.rb +1 -1
  55. data/spec/bson/boolean_spec.rb +1 -1
  56. data/spec/bson/code_spec.rb +1 -1
  57. data/spec/bson/code_with_scope_spec.rb +1 -1
  58. data/spec/bson/date_spec.rb +1 -1
  59. data/spec/bson/date_time_spec.rb +1 -1
  60. data/spec/bson/decimal128_spec.rb +1 -1
  61. data/spec/bson/document_spec.rb +1 -1
  62. data/spec/bson/ext_json_parse_spec.rb +308 -0
  63. data/spec/bson/false_class_spec.rb +1 -1
  64. data/spec/bson/float_spec.rb +37 -1
  65. data/spec/bson/hash_spec.rb +71 -1
  66. data/spec/bson/int32_spec.rb +21 -1
  67. data/spec/bson/int64_spec.rb +39 -1
  68. data/spec/bson/integer_spec.rb +27 -1
  69. data/spec/bson/json_spec.rb +1 -1
  70. data/spec/bson/max_key_spec.rb +1 -1
  71. data/spec/bson/min_key_spec.rb +1 -1
  72. data/spec/bson/nil_class_spec.rb +1 -1
  73. data/spec/bson/object_id_spec.rb +1 -1
  74. data/spec/bson/object_spec.rb +1 -1
  75. data/spec/bson/open_struct_spec.rb +1 -1
  76. data/spec/bson/raw_spec.rb +22 -1
  77. data/spec/bson/regexp_spec.rb +1 -1
  78. data/spec/bson/registry_spec.rb +1 -1
  79. data/spec/bson/string_spec.rb +1 -1
  80. data/spec/bson/symbol_raw_spec.rb +45 -0
  81. data/spec/bson/symbol_spec.rb +61 -1
  82. data/spec/bson/time_spec.rb +205 -2
  83. data/spec/bson/time_with_zone_spec.rb +1 -1
  84. data/spec/bson/timestamp_spec.rb +1 -1
  85. data/spec/bson/true_class_spec.rb +1 -1
  86. data/spec/bson/undefined_spec.rb +1 -1
  87. data/spec/bson_spec.rb +1 -1
  88. data/spec/{support → runners}/common_driver.rb +1 -1
  89. data/spec/runners/corpus.rb +185 -0
  90. data/spec/{support/corpus.rb → runners/corpus_legacy.rb} +41 -59
  91. data/spec/spec_helper.rb +10 -3
  92. data/spec/{bson/driver_bson_spec.rb → spec_tests/common_driver_spec.rb} +1 -0
  93. data/spec/{bson/corpus_spec.rb → spec_tests/corpus_legacy_spec.rb} +4 -4
  94. data/spec/spec_tests/corpus_spec.rb +124 -0
  95. data/spec/spec_tests/data/corpus/README.md +15 -0
  96. data/spec/spec_tests/data/corpus/array.json +49 -0
  97. data/spec/spec_tests/data/corpus/binary.json +85 -0
  98. data/spec/spec_tests/data/corpus/boolean.json +27 -0
  99. data/spec/spec_tests/data/corpus/code.json +67 -0
  100. data/spec/spec_tests/data/corpus/code_w_scope.json +78 -0
  101. data/spec/spec_tests/data/corpus/datetime.json +42 -0
  102. data/spec/spec_tests/data/corpus/dbpointer.json +56 -0
  103. data/spec/spec_tests/data/corpus/dbref.json +31 -0
  104. data/spec/spec_tests/data/corpus/decimal128-1.json +317 -0
  105. data/spec/spec_tests/data/corpus/decimal128-2.json +793 -0
  106. data/spec/spec_tests/data/corpus/decimal128-3.json +1771 -0
  107. data/spec/spec_tests/data/corpus/decimal128-4.json +117 -0
  108. data/spec/spec_tests/data/corpus/decimal128-5.json +402 -0
  109. data/spec/spec_tests/data/corpus/decimal128-6.json +119 -0
  110. data/spec/spec_tests/data/corpus/decimal128-7.json +323 -0
  111. data/spec/spec_tests/data/corpus/document.json +36 -0
  112. data/spec/spec_tests/data/corpus/double.json +87 -0
  113. data/spec/spec_tests/data/corpus/int32.json +43 -0
  114. data/spec/spec_tests/data/corpus/int64.json +43 -0
  115. data/spec/spec_tests/data/corpus/maxkey.json +12 -0
  116. data/spec/spec_tests/data/corpus/minkey.json +12 -0
  117. data/spec/spec_tests/data/corpus/multi-type-deprecated.json +15 -0
  118. data/spec/spec_tests/data/corpus/multi-type.json +11 -0
  119. data/spec/spec_tests/data/corpus/null.json +12 -0
  120. data/spec/spec_tests/data/corpus/oid.json +28 -0
  121. data/spec/spec_tests/data/corpus/regex.json +65 -0
  122. data/spec/spec_tests/data/corpus/string.json +72 -0
  123. data/spec/spec_tests/data/corpus/symbol.json +80 -0
  124. data/spec/spec_tests/data/corpus/timestamp.json +24 -0
  125. data/spec/spec_tests/data/corpus/top.json +236 -0
  126. data/spec/spec_tests/data/corpus/undefined.json +15 -0
  127. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/array.json +0 -0
  128. data/spec/{support/corpus-tests/failures → spec_tests/data/corpus_legacy}/binary.json +0 -0
  129. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/boolean.json +0 -0
  130. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/code.json +1 -1
  131. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/code_w_scope.json +1 -1
  132. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/document.json +1 -1
  133. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/double.json +1 -1
  134. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/datetime.json +0 -0
  135. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/dbpointer.json +0 -0
  136. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/int64.json +0 -0
  137. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/failures/symbol.json +0 -0
  138. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/int32.json +1 -1
  139. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/maxkey.json +1 -1
  140. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/minkey.json +1 -1
  141. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/null.json +1 -1
  142. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/oid.json +0 -0
  143. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/regex.json +1 -1
  144. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/string.json +0 -0
  145. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/timestamp.json +1 -1
  146. data/spec/{support/corpus-tests → spec_tests/data/corpus_legacy}/top.json +0 -0
  147. data/spec/{support/corpus-tests/failures → spec_tests/data/corpus_legacy}/undefined.json +0 -0
  148. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-1.json +0 -0
  149. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-2.json +0 -0
  150. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-3.json +0 -0
  151. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-4.json +0 -0
  152. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-5.json +0 -0
  153. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-6.json +0 -0
  154. data/spec/{support/driver-spec-tests → spec_tests/data}/decimal128/decimal128-7.json +0 -0
  155. data/spec/support/shared_examples.rb +1 -1
  156. metadata +172 -120
  157. checksums.yaml.gz.sig +0 -0
  158. data.tar.gz.sig +0 -1
  159. metadata.gz.sig +0 -1
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2016-2019 MongoDB Inc.
1
+ # Copyright (C) 2016-2020 MongoDB Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -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
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2016-2019 MongoDB Inc.
1
+ # Copyright (C) 2016-2020 MongoDB Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2009-2019 MongoDB Inc.
1
+ # Copyright (C) 2009-2020 MongoDB Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2009-2019 MongoDB Inc.
1
+ # Copyright (C) 2009-2020 MongoDB Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ module BSON
41
41
  # @return [ true, false ] If the Ruby version is 1.9.
42
42
  #
43
43
  # @since 4.2.0
44
+ # @deprecated
44
45
  def ruby_1_9?
45
46
  @ruby_1_9 ||= RUBY_VERSION < '2.0.0'
46
47
  end
@@ -0,0 +1,21 @@
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
+
17
+ # Exception raised when BSON decoding fails.
18
+ class BSONDecodeError < Error
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,375 @@
1
+ # Copyright (C) 2019-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
+ 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).utc
255
+ when Hash
256
+ unless value.keys.sort == %w($numberLong)
257
+ raise Error::ExtJSONParseError, "Invalid value for $date: #{value}"
258
+ end
259
+ sec, msec = value.values.first.to_i.divmod(1000)
260
+ ::Time.at(sec, msec*1000).utc
261
+ else
262
+ raise Error::ExtJSONParseError, "Invalid value for $date: #{value}"
263
+ end
264
+ when '$minKey'
265
+ unless value == 1
266
+ raise Error::ExtJSONParseError, "Invalid $minKey value: #{value}"
267
+ end
268
+ MinKey.new
269
+ when '$maxKey'
270
+ unless value == 1
271
+ raise Error::ExtJSONParseError, "Invalid $maxKey value: #{value}"
272
+ end
273
+ MaxKey.new
274
+ when '$undefined'
275
+ unless value == true
276
+ raise Error::ExtJSONParseError, "Invalid $undefined value: #{value}"
277
+ end
278
+ Undefined.new
279
+ else
280
+ map_hash(hash, **options)
281
+ end
282
+ end
283
+
284
+ if hash.length == 2
285
+ sorted_keys = hash.keys.sort
286
+ first_key = sorted_keys.first
287
+ last_key = sorted_keys.last
288
+
289
+ if first_key == '$code'
290
+ unless sorted_keys == %w($code $scope)
291
+ raise Error::ExtJSONParseError, "Invalid $code value: #{hash}"
292
+ end
293
+ unless hash['$code'].is_a?(String)
294
+ raise Error::ExtJSONParseError, "Invalid $code value: #{value}"
295
+ end
296
+
297
+ return CodeWithScope.new(hash['$code'], map_hash(hash['$scope']))
298
+ end
299
+
300
+ if first_key == '$binary'
301
+ unless sorted_keys == %w($binary $type)
302
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{hash}"
303
+ end
304
+ unless hash['$binary'].is_a?(String)
305
+ raise Error::ExtJSONParseError, "Invalid $binary value: #{value}"
306
+ end
307
+ unless hash['$type'].is_a?(String)
308
+ raise Error::ExtJSONParseError, "Invalid $binary subtype: #{hash['$type']}"
309
+ end
310
+
311
+ return create_binary(hash['$binary'], hash['$type'])
312
+ end
313
+
314
+ if last_key == '$regex'
315
+ unless sorted_keys == %w($options $regex)
316
+ raise Error::ExtJSONParseError, "Invalid $regex value: #{hash}"
317
+ end
318
+
319
+ if hash['$regex'].is_a?(Hash)
320
+ return {
321
+ '$regex' => parse_hash(hash['$regex']),
322
+ '$options' => hash['$options']
323
+ }
324
+ end
325
+
326
+ unless hash['$regex'].is_a?(String)
327
+ raise Error::ExtJSONParseError, "Invalid $regex pattern: #{hash['$regex']}"
328
+ end
329
+ unless hash['$options'].is_a?(String)
330
+ raise Error::ExtJSONParseError, "Invalid $regex options: #{hash['$options']}"
331
+ end
332
+
333
+ return create_regexp(hash['$regex'], hash['$options'])
334
+ end
335
+
336
+ verify_no_reserved_keys(hash, **options)
337
+ end
338
+
339
+ verify_no_reserved_keys(hash, **options)
340
+ end
341
+
342
+ module_function def verify_no_reserved_keys(hash, **options)
343
+ if hash.length > RESERVED_KEYS.length
344
+ if RESERVED_KEYS.any? { |key| hash.key?(key) }
345
+ raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}"
346
+ end
347
+ else
348
+ if hash.keys.any? { |key| RESERVED_KEYS_HASH.key?(key) }
349
+ raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}"
350
+ end
351
+ end
352
+ map_hash(hash, **options)
353
+ end
354
+
355
+ module_function def map_hash(hash, **options)
356
+ ::Hash[hash.map do |key, value|
357
+ [key, parse_obj(value, **options)]
358
+ end]
359
+ end
360
+
361
+ module_function def create_binary(encoded_value, encoded_subtype)
362
+ subtype = encoded_subtype.hex
363
+ type = Binary::TYPES[subtype.chr]
364
+ unless type
365
+ # Requires https://jira.mongodb.org/browse/RUBY-2056
366
+ raise NotImplementedError, "Binary subtype #{encoded_subtype} is not currently supported"
367
+ end
368
+ Binary.new(Base64.decode64(encoded_value), type)
369
+ end
370
+
371
+ module_function def create_regexp(pattern, options)
372
+ Regexp::Raw.new(pattern, options)
373
+ end
374
+ end
375
+ end