bson 4.7.1 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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