Module: BSON::ExtJSON
- Defined in:
- lib/bson/ext_json.rb
Overview
This module contains methods for parsing Extended JSON 2.0. github.com/mongodb/specifications/blob/master/source/extended-json.rst
Class Method Summary collapse
- .create_binary(encoded_value, encoded_subtype) ⇒ Object
- .create_regexp(pattern, options) ⇒ Object
- .dbref?(hash) ⇒ Boolean
- .map_hash(hash, **options) ⇒ Object
-
.parse(str, **options) ⇒ Object
Parses JSON in a string into a Ruby object tree.
- .parse_hash(hash, **options) ⇒ Object
-
.parse_obj(value, **options) ⇒ Object
Transforms a Ruby object tree containing extended JSON type hashes into a Ruby object tree with said hashes replaced by BSON or Ruby native types.
- .verify_no_reserved_keys(hash, **options) ⇒ Object
Class Method Details
.create_binary(encoded_value, encoded_subtype) ⇒ Object
366 367 368 369 370 371 372 373 374 |
# File 'lib/bson/ext_json.rb', line 366 module_function def create_binary(encoded_value, encoded_subtype) subtype = encoded_subtype.hex type = Binary::TYPES[subtype.chr] unless type # Requires https://jira.mongodb.org/browse/RUBY-2056 raise NotImplementedError, "Binary subtype #{encoded_subtype} is not currently supported" end Binary.new(Base64.decode64(encoded_value), type) end |
.create_regexp(pattern, options) ⇒ Object
376 377 378 |
# File 'lib/bson/ext_json.rb', line 376 module_function def create_regexp(pattern, ) Regexp::Raw.new(pattern, ) end |
.dbref?(hash) ⇒ Boolean
380 381 382 383 384 385 386 387 |
# File 'lib/bson/ext_json.rb', line 380 module_function def dbref?(hash) if db = hash.key?('$db') unless db.is_a?(String) return false end end return hash['$ref']&.is_a?(String) && hash.key?('$id') end |
.map_hash(hash, **options) ⇒ Object
357 358 359 360 361 362 363 364 |
# File 'lib/bson/ext_json.rb', line 357 module_function def map_hash(hash, **) ::Hash[hash.map do |key, value| if (key.is_a?(String) || key.is_a?(Symbol)) && key.to_s.include?(NULL_BYTE) raise Error::ExtJSONParseError, "Hash key cannot contain a null byte: #{key}" end [key, parse_obj(value, **)] end] end |
.parse(str, **options) ⇒ Object
This method uses Ruby standard library’s JSON.parse method to
Parses JSON in a string into a Ruby object tree.
There are two strategies that this method can follow. If the canonical strategy is used which is the default, this method returns BSON types as much as possible. This allows the resulting object tree to be serialized back to extended JSON or to BSON while preserving the types. The relaxed strategy, enabled by passing true option, returns native Ruby types as much as possible which makes the resulting object tree easier to work with but may lose type information.
Please note the following aspects of this method when emitting relaxed object trees:
-
$numberInt and $numberLong inputs produce Integer instances.
-
$regularExpression inputs produce BSON Regexp instances. This may change in a future version of bson-ruby to produce Ruby Regexp instances, potentially depending on regular expression options.
-
$numberDecimal inputs produce BSON Decimal128 instances. This may change in a future version of bson-ruby to produce Ruby BigDecimal instances instead.
This method accepts canonical extended JSON, relaxed extended JSON and JSON without type information as well as a mix of the above.
perform JSON parsing. As the JSON.parse method accepts inputs other than hashes, so does this method and therefore this method can return objects of any type.
59 60 61 |
# File 'lib/bson/ext_json.rb', line 59 module_function def parse(str, **) parse_obj(::JSON.parse(str), **) end |
.parse_hash(hash, **options) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/bson/ext_json.rb', line 138 module_function def parse_hash(hash, **) if hash.empty? return {} end if dbref?(hash) # Legacy dbref handling. # Note that according to extended json spec, only hash values (but # not the top-level BSON document itself) may be of type "dbref". # This code applies to both hash values and the hash overall; however, # since we do not have DBRef as a distinct type, applying the below # logic to top level hashes doesn't cause harm. hash = hash.dup ref = hash.delete('$ref') # $id, if present, can be anything id = hash.delete('$id') if id.is_a?(Hash) id = parse_hash(id) end # Preserve $id value as it was, do not convert either to ObjectId # or to a string. But if the value was in {'$oid' => ...} format, # the value is converted to an ObjectId instance so that # serialization to BSON later on works correctly. out = {'$ref' => ref, '$id' => id} if hash.key?('$db') # $db must always be a string, if provided out['$db'] = hash.delete('$db') end return out.update(parse_hash(hash)) end if hash.length == 1 key, value = hash.first return case key when '$oid' ObjectId.from_string(value) when '$symbol' Symbol::Raw.new(value) when '$numberInt' unless value.is_a?(String) raise Error::ExtJSONParseError, "$numberInt value is of an incorrect type: #{value}" end value.to_i when '$numberLong' unless value.is_a?(String) raise Error::ExtJSONParseError, "$numberLong value is of an incorrect type: #{value}" end value = value.to_i if [:mode] != :bson value else Int64.new(value) end when '$numberDouble' # This handles string to double conversion as well as inf/-inf/nan unless value.is_a?(String) raise Error::ExtJSONParseError, "Invalid $numberDouble value: #{value}" end BigDecimal(value).to_f when '$numberDecimal' # TODO consider returning BigDecimal here instead of Decimal128 Decimal128.new(value) when '$binary' unless value.is_a?(Hash) raise Error::ExtJSONParseError, "Invalid $binary value: #{value}" end unless value.keys.sort == %w(base64 subType) raise Error::ExtJSONParseError, "Invalid $binary value: #{value}" end encoded_value = value['base64'] unless encoded_value.is_a?(String) raise Error::ExtJSONParseError, "Invalid base64 value in $binary: #{value}" end subtype = value['subType'] unless subtype.is_a?(String) raise Error::ExtJSONParseError, "Invalid subType value in $binary: #{value}" end create_binary(encoded_value, subtype) when '$uuid' unless /\A[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\z/.match(value) raise Error::ExtJSONParseError, "Invalid $uuid value: #{value}" end return Binary.from_uuid(value) when '$code' unless value.is_a?(String) raise Error::ExtJSONParseError, "Invalid $code value: #{value}" end Code.new(value) when '$timestamp' unless value.keys.sort == %w(i t) raise Error::ExtJSONParseError, "Invalid $timestamp value: #{value}" end t = value['t'] unless t.is_a?(Integer) raise Error::ExtJSONParseError, "Invalid t value: #{value}" end i = value['i'] unless i.is_a?(Integer) raise Error::ExtJSONParseError, "Invalid i value: #{value}" end Timestamp.new(t, i) when '$regularExpression' unless value.keys.sort == %w(options pattern) raise Error::ExtJSONParseError, "Invalid $regularExpression value: #{value}" end # TODO consider returning Ruby regular expression object here create_regexp(value['pattern'], value['options']) when '$dbPointer' unless value.keys.sort == %w($id $ref) raise Error::ExtJSONParseError, "Invalid $dbPointer value: #{value}" end DbPointer.new(value['$ref'], parse_hash(value['$id'])) when '$date' case value when String ::Time.parse(value).utc when Hash unless value.keys.sort == %w($numberLong) raise Error::ExtJSONParseError, "Invalid value for $date: #{value}" end sec, msec = value.values.first.to_i.divmod(1000) ::Time.at(sec, msec*1000).utc else raise Error::ExtJSONParseError, "Invalid value for $date: #{value}" end when '$minKey' unless value == 1 raise Error::ExtJSONParseError, "Invalid $minKey value: #{value}" end MinKey.new when '$maxKey' unless value == 1 raise Error::ExtJSONParseError, "Invalid $maxKey value: #{value}" end MaxKey.new when '$undefined' unless value == true raise Error::ExtJSONParseError, "Invalid $undefined value: #{value}" end Undefined.new else map_hash(hash, **) end end if hash.length == 2 sorted_keys = hash.keys.sort first_key = sorted_keys.first last_key = sorted_keys.last if first_key == '$code' unless sorted_keys == %w($code $scope) raise Error::ExtJSONParseError, "Invalid $code value: #{hash}" end unless hash['$code'].is_a?(String) raise Error::ExtJSONParseError, "Invalid $code value: #{value}" end return CodeWithScope.new(hash['$code'], map_hash(hash['$scope'])) end if first_key == '$binary' unless sorted_keys == %w($binary $type) raise Error::ExtJSONParseError, "Invalid $binary value: #{hash}" end unless hash['$binary'].is_a?(String) raise Error::ExtJSONParseError, "Invalid $binary value: #{value}" end unless hash['$type'].is_a?(String) raise Error::ExtJSONParseError, "Invalid $binary subtype: #{hash['$type']}" end return create_binary(hash['$binary'], hash['$type']) end if last_key == '$regex' unless sorted_keys == %w($options $regex) raise Error::ExtJSONParseError, "Invalid $regex value: #{hash}" end if hash['$regex'].is_a?(Hash) return { '$regex' => parse_hash(hash['$regex']), '$options' => hash['$options'] } end unless hash['$regex'].is_a?(String) raise Error::ExtJSONParseError, "Invalid $regex pattern: #{hash['$regex']}" end unless hash['$options'].is_a?(String) raise Error::ExtJSONParseError, "Invalid $regex options: #{hash['$options']}" end return create_regexp(hash['$regex'], hash['$options']) end verify_no_reserved_keys(hash, **) end verify_no_reserved_keys(hash, **) end |
.parse_obj(value, **options) ⇒ Object
This method accepts any types as input, not just Hash instances.
Transforms a Ruby object tree containing extended JSON type hashes into a Ruby object tree with said hashes replaced by BSON or Ruby native types.
There are two strategies that this method can follow. If the canonical strategy is used which is the default, this method returns BSON types as much as possible. This allows the resulting object tree to be serialized back to extended JSON or to BSON while preserving the types. The relaxed strategy, enabled by passing true option, returns native Ruby types as much as possible which makes the resulting object tree easier to work with but may lose type information.
Please note the following aspects of this method when emitting relaxed object trees:
-
$numberInt and $numberLong inputs produce Integer instances.
-
$regularExpression inputs produce BSON Regexp instances. This may change in a future version of bson-ruby to produce Ruby Regexp instances, potentially depending on regular expression options.
-
$numberDecimal inputs produce BSON Decimal128 instances. This may change in a future version of bson-ruby to produce Ruby BigDecimal instances instead.
This method accepts object trees resulting from parsing canonical extended JSON, relaxed extended JSON and JSON without type information as well as a mix of the above.
Consequently, it can return values of any type.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/bson/ext_json.rb', line 106 module_function def parse_obj(value, **) # TODO implement :ruby and :ruby! modes unless [nil, :bson].include?([:mode]) raise ArgumentError, "Invalid value for :mode option: #{[:mode].inspect}" end case value when String, TrueClass, FalseClass, NilClass, Numeric value when Hash parse_hash(value, **) when Array value.map do |item| parse_obj(item, **) end else raise Error::ExtJSONParseError, "Unknown value type: #{value}" end end |
.verify_no_reserved_keys(hash, **options) ⇒ Object
344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/bson/ext_json.rb', line 344 module_function def verify_no_reserved_keys(hash, **) if hash.length > RESERVED_KEYS.length if RESERVED_KEYS.any? { |key| hash.key?(key) } raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}" end else if hash.keys.any? { |key| RESERVED_KEYS_HASH.key?(key) } raise Error::ExtJSONParseError, "Hash uses reserved keys but does not match a known type: #{hash}" end end map_hash(hash, **) end |