File: document.rb

package info (click to toggle)
ruby-bson 5.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,828 kB
  • sloc: ruby: 11,712; ansic: 1,427; java: 514; makefile: 8
file content (355 lines) | stat: -rw-r--r-- 11,887 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
343
344
345
346
347
348
349
350
351
352
353
354
355
# frozen_string_literal: true
# rubocop:todo all
# Copyright (C) 2009-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "yaml"

# Since we have a custom bsondoc type for yaml serialization, we need
# to ensure that it's properly deserialized when parsed.
#
# @since 2.0.0
YAML.add_builtin_type("bsondoc") do |type, value|
  BSON::Document[value.map{ |val| val.to_a.first }]
end

module BSON

  # This module provides behaviour for serializing and deserializing entire
  # BSON documents, according to the BSON specification.
  #
  # @note The specification is: document ::= int32 e_list "\x00"
  #
  # @see http://bsonspec.org/#/specification
  #
  # @since 2.0.0
  class Document < ::Hash

    # Get a value from the document for the provided key. Can use string or
    # symbol access, with string access being the faster of the two.
    #
    # @overload fetch(key)
    #   Returns a value from the hash for the given key. If the key does
    #   not exist, raises KeyError exception.
    #
    # @overload fetch(key, default)
    #   Returns a value from the hash for the given key. If the key does not
    #   exist, returns *default*.
    #
    # @overload fetch(key, &block)
    #   Returns a value from the hash for the given key. If the key does not
    #   exist, returns the value of the block called with the key.
    #
    # @example Get an element for the key.
    #   document.fetch("field")
    #
    # @example Get an element for the key by symbol with a default.
    #   document.fetch(:field, 'foo')
    #
    # @example Get an element for the key by symbol with a block default.
    #   document.fetch(:field) { |key| key.upcase }
    #
    # @param [ String, Symbol ] key The key to look up.
    # @param [ Object ] default Returned value if key does not exist.
    # @yield [key] Block returning default value for the given key.
    #
    # @return [ Object ] The found value. Raises KeyError if none found.
    #
    # @since 4.4.0
    def fetch(key, *args, &block)
      key = convert_key(key)
      super(key, *args, &block)
    end

    # Get a value from the document for the provided key. Can use string or
    # symbol access, with string access being the faster of the two.
    #
    # @example Get an element for the key.
    #   document["field"]
    #
    # @example Get an element for the key by symbol.
    #   document[:field]
    #
    # @param [ String, Symbol ] key The key to look up.
    #
    # @return [ Object ] The found value, or nil if none found.
    #
    # @since 2.0.0
    def [](key)
      super(convert_key(key))
    end

    # Stores a key-value pair in the current document.
    #
    # Since BSON documents provide deep indifferent access (both strings and
    # symbols are accepted as keys, recursively), the value may be converted
    # to facilitate indifferent access. This conversion is performed for
    # built-in Array and Hash classes, and other classes can override
    # +to_bson_normalized_value+ method to provide custom conversion logic.
    # For example:
    #
    #     doc = BSON::Document.new
    #     doc[:a] = {b: {c: 'd'}}
    #     doc['a']['b']['c']
    #     # => "d"
    #
    # Note that due to this conversion, the object that is stored in the
    # receiver Document may be different from the object supplied as the
    # right hand side of the assignment. In Ruby, the result of assignment
    # is the right hand side, not the return value of []= method.
    # Because of this, modifying the result of assignment generally does not
    # work as intended:
    #
    #     doc = BSON::Document.new
    #     foo = (doc[:a] = {b: {c: 'd'}})
    #     # foo is original Hash with symbol keys
    #     foo['test'] = 'test'
    #     # doc is not modified
    #     doc
    #     # => {"a"=>{"b"=>{"c"=>"d"}}}
    #
    # This behavior can be encountered when defaulting document contents with
    # []= in a method, such as:
    #
    #     def foo
    #       # @doc is a BSON::Document
    #       @doc[:foo] ||= calculation
    #     end
    #
    # The above method should be written as follows to allow chaining:
    #
    #     def foo
    #       # @doc is a BSON::Document
    #       @doc[:foo] ||= calculation and @doc[:foo]
    #     end
    #
    # @example Set a value on the document.
    #   document[:test] = "value"
    #
    # @param [ String, Symbol ] key The key to update.
    # @param [ Object ] value The value to update.
    #
    # @return [ Object ] The updated value.
    #
    # @since 3.0.0
    def []=(key, value)
      super(convert_key(key), convert_value(value))
    end

    # Returns true if the given key is present in the document.  Will normalize
    # symbol keys into strings.
    #
    # @example Test if a key exists using a symbol
    #   document.has_key?(:test)
    #
    # @param [ Object ] key The key to check for.
    #
    # @return [ true, false]
    #
    # @since 4.0.0
    def has_key?(key)
      super(convert_key(key))
    end

    alias :include? :has_key?
    alias :key?     :has_key?
    alias :member?  :has_key?

    # Returns true if the given value is present in the document.  Will normalize
    # symbols into strings.
    #
    # @example Test if a key exists using a symbol
    #   document.has_value?(:test)
    #
    # @param [ Object ] value THe value to check for.
    #
    # @return [ true, false]
    #
    # @since 4.0.0
    def has_value?(value)
      super(convert_value(value))
    end

    alias :value :has_value?

    # Deletes the key-value pair and returns the value from the document
    # whose key is equal to key.
    # If the key is not found, returns the default value. If the optional code
    # block is given and the key is not found, pass in the key and return the
    # result of block.
    #
    # @example Delete a key-value pair
    #   document.delete(:test)
    #
    # @param [ Object ] key The key of the key-value pair to delete.
    #
    # @return [ Object ]
    #
    # @since 4.0.0
    def delete(key, &block)
      super(convert_key(key), &block)
    end

    # Instantiate a new Document. Valid parameters for instantiation is a hash
    # only or nothing.
    #
    # @example Create the new Document.
    #   BSON::Document.new(name: "Joe", age: 33)
    #
    # @param [ Hash ] elements The elements of the document.
    #
    # @since 3.0.0
    def initialize(elements = nil)
      super()
      (elements || {}).each_pair{ |key, value| self[key] = value }
    end

    # Merge this document with another document, returning a new document in
    # the process.
    #
    # @example Merge with another document.
    #   document.merge(name: "Bob")
    #
    # @param [ BSON::Document, Hash ] other The document/hash to merge with.
    #
    # @return [ BSON::Document ] The result of the merge.
    #
    # @since 3.0.0
    def merge(other, &block)
      dup.merge!(other, &block)
    end

    # Merge this document with another document, returning the same document in
    # the process.
    #
    # @example Merge with another document.
    #   document.merge(name: "Bob")
    #
    # @param [ BSON::Document, Hash ] other The document/hash to merge with.
    #
    # @return [ BSON::Document ] The result of the merge.
    #
    # @since 3.0.0
    def merge!(other)
      other.each_pair do |key, value|
        value = yield(convert_key(key), self[key], convert_value(value)) if block_given? && self[key]
        self[key] = value
      end
      self
    end

    alias :update :merge!

    if instance_methods.include?(:dig)
      # Retrieves the value object corresponding to the each key objects repeatedly.
      # Will normalize symbol keys into strings.
      #
      # @example Get value from nested sub-documents, handling missing levels.
      #   document # => { :key1 => { "key2" => "value"}}
      #   document.dig(:key1, :key2) # => "value"
      #   document.dig("key1", "key2") # => "value"
      #   document.dig("foo", "key2") # => nil
      #
      # @param [ Array<String, Symbol> ] *keys Keys, which constitute a "path" to the nested value.
      #
      # @return [ Object, NilClass ] The requested value or nil.
      #
      # @since 3.0.0
      def dig(*keys)
        super(*keys.map{|key| convert_key(key)})
      end
    end

    # Slices a document to include only the given keys.
    # Will normalize symbol keys into strings.
    # (this method is backported from ActiveSupport::Hash)
    #
    # @example Get a document/hash with only the `name` and `age` fields present
    #   document # => { _id: <ObjectId>, :name => "John", :age => 30, :location => "Earth" }
    #   document.slice(:name, 'age') # => { "name": "John", "age" => 30 }
    #   document.slice('name') # => { "name" => "John" }
    #   document.slice(:foo) # => {}
    #
    # @param [ Array<String, Symbol> ] *keys Keys, that will be kept in the resulting document
    #
    # @return [ BSON::Document ] The document with only the selected keys
    #
    # @since 4.3.1
    def slice(*keys)
      keys.each_with_object(self.class.new) do |key, hash|
        if key?(key)
          hash[key] = self[key]
        end
      end
    end

    # Returns a new document consisting of the current document minus the
    # specified keys.
    #
    # The keys to be removed can be specified as either strings or symbols.
    #
    # @example Get a document/hash with only the `name` and `age` fields removed
    #   document # => { _id: <ObjectId>, :name => 'John', :age => 30, :location => 'Earth' }
    #   document.except(:name, 'age') # => { _id: <ObjectId>, location: 'Earth' }
    #
    # @param [ Array<String, Symbol> ] *keys Keys, that will be removed in the resulting document
    #
    # @return [ BSON::Document ] The document with the specified keys removed.
    #
    # @note This method is always defined, even if Hash already contains a
    #   definition of #except, because ActiveSupport unconditionally defines
    #   its version of #except which doesn't work for BSON::Document which
    #   causes problems if ActiveSupport is loaded after bson-ruby is.
    def except(*keys)
      copy = dup
      keys.each {|key| copy.delete(key)}
      copy
    end

    def symbolize_keys!
      raise ArgumentError, 'symbolize_keys! is not supported on BSON::Document instances. Please convert the document to hash first (using #to_h), then call #symbolize_keys! on the Hash instance'
    end

    def deep_symbolize_keys!
      warn <<~WARN
        [DEPRECATION] `deep_symbolize_keys!` is not supported on BSON::Document instances.
        Please convert the document to a Hash first (using `#to_h`), then call `#deep_symbolize_keys!` on the Hash.
        This will raise an error starting with the v6.0.0 release.
      WARN
    end

    # Override the Hash implementation of to_bson_normalized_value.
    #
    # BSON::Document is already of the correct type and already provides
    # indifferent access to keys, hence no further conversions are necessary.
    #
    # Attempting to perform Hash's conversion on Document instances converts
    # DBRefs to Documents which is wrong.
    #
    # @return [ BSON::Document ] The normalized hash.
    def to_bson_normalized_value
      self
    end

    private

    def convert_key(key)
      key.to_bson_normalized_key
    end

    def convert_value(value)
      value.to_bson_normalized_value
    end
  end
end