# Copyright 2014 Google Inc. All rights reserved. # # 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 "gcloud/datastore/key" require "gcloud/datastore/properties" module Gcloud module Datastore ## # # Entity # # Entity represents a Datastore record. # Every Entity has a {Key}, and a list of properties. # # Entities in Datastore form a hierarchically structured space similar to # the directory structure of a file system. When you create an entity, you # can optionally designate another entity as its parent; the new entity is a # child of the parent entity. # # @see https://cloud.google.com/datastore/docs/concepts/entities Entities, # Properties, and Keys # # @example Create a new entity using a block: # task = datastore.entity "Task", "sampleTask" do |t| # t["type"] = "Personal" # t["created"] = Time.now # t["done"] = false # t["priority"] = 4 # t["percent_complete"] = 10.0 # t["description"] = "Learn Cloud Datastore" # end # # @example Create a new entity belonging to an existing parent entity: # task_key = datastore.key "Task", "sampleTask" # task_key.parent = datastore.key "TaskList", "default" # # task = Gcloud::Datastore::Entity.new # task.key = task_key # # task["type"] = "Personal" # task["done"] = false # task["priority"] = 4 # task["description"] = "Learn Cloud Datastore" # class Entity ## # The Key that identifies the entity. attr_reader :key ## # Create a new Entity object. def initialize @properties = Properties.new @key = Key.new @_exclude_indexes = {} end ## # Retrieve a property value by providing the name. # # Property values are converted from the Datastore value type # automatically. Blob properties are returned as StringIO objects. # Location properties are returned as a Hash with `:longitude` and # `:latitude` keys. # # @param [String, Symbol] prop_name The name of the property. # # @return [Object, nil] Returns `nil` if the property doesn't exist # # @example Properties can be retrieved with a string name: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = datastore.find "Task", "sampleTask" # task["description"] #=> "Learn Cloud Datastore" # # @example Or with a symbol name: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = datastore.find "Task", "sampleTask" # task[:description] #=> "Learn Cloud Datastore" # # @example Getting a blob value returns a StringIO object: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["avatar"] #=> StringIO("\x89PNG\r\n\x1A...") # # @example Getting a geo point value returns a Hash: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["location"] #=> { longitude: -122.0862462, # # latitude: 37.4220041 } # # @example Getting a blob value returns a StringIO object: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["avatar"] #=> StringIO("\x89PNG\r\n\x1A...") # def [] prop_name properties[prop_name] end ## # Set a property value by name. # # Property values are converted to use the proper Datastore value type # automatically. Use an IO-compatible object (File, StringIO, Tempfile) to # indicate the property value should be stored as a Datastore `blob`. # IO-compatible objects are converted to StringIO objects when they are # set. Use a Hash with `:longitude` and `:latitude` keys to indicate the # property value should be stored as a Geo Point/LatLng. # # @param [String, Symbol] prop_name The name of the property. # @param [Object] prop_value The value of the property. # # @example Properties can be set with a string name: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = datastore.find "Task", "sampleTask" # task["description"] = "Learn Cloud Datastore" # task["tags"] = ["fun", "programming"] # # @example Or with a symbol name: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = datastore.find "Task", "sampleTask" # task[:description] = "Learn Cloud Datastore" # task[:tags] = ["fun", "programming"] # # @example Setting a blob value using an IO: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["avatar"] = File.open "/avatars/alice.png" # user["avatar"] #=> StringIO("\x89PNG\r\n\x1A...") # # @example Setting a geo point value using a Hash: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["location"] = { longitude: -122.0862462, latitude: 37.4220041 } # # @example Setting a blob value using an IO: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # user = datastore.find "User", "alice" # user["avatar"] = File.open "/avatars/alice.png" # user["avatar"] #=> StringIO("\x89PNG\r\n\x1A...") # def []= prop_name, prop_value properties[prop_name] = prop_value end ## # Retrieve properties in a hash-like structure. # Properties can be accessed or set by string or symbol. # # @return [Gcloud::Datastore::Properties] # # @example # task.properties[:description] = "Learn Cloud Datastore" # task.properties["description"] #=> "Learn Cloud Datastore" # # task.properties.each do |name, value| # puts "property #{name} has a value of #{value}" # end # # @example A property's existence can be determined by calling `exist?`: # task.properties.exist? :description #=> true # task.properties.exist? "description" #=> true # task.properties.exist? :expiration #=> false # # @example A property can be removed from the entity: # task.properties.delete :description # task.save # # @example The properties can be converted to a hash: # prop_hash = task.properties.to_h # attr_reader :properties ## # Sets the {Gcloud::Datastore::Key} that identifies the entity. # # Once the entity is saved, the key is frozen and immutable. Trying to set # a key when immutable will raise a `RuntimeError`. # # @example The key can be set before the entity is saved: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = Gcloud::Datastore::Entity.new # task.key = datastore.key "Task" # datastore.save task # # @example Once the entity is saved, the key is frozen and immutable: # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # task = datastore.find "Task", "sampleTask" # task.persisted? #=> true # task.key = datastore.key "Task" #=> RuntimeError # task.key.frozen? #=> true # task.key.id = 9876543221 #=> RuntimeError # def key= new_key fail "This entity's key is immutable." if persisted? @key = new_key end ## # Indicates if the record is persisted. Default is false. # # @example # require "gcloud" # # gcloud = Gcloud.new # datastore = gcloud.datastore # # task = Gcloud::Datastore::Entity.new # task.persisted? #=> false # # task = datastore.find "Task", "sampleTask" # task.persisted? #=> true # def persisted? @key && @key.frozen? end ## # Indicates if a property is flagged to be excluded from the # Datastore indexes. The default value is `false`. This is another way of # saying that values are indexed by default. # # If the property is multi-valued, each value in the list can be managed # separately for exclusion from indexing. Calling this method for a # multi-valued property will return an array that contains the `excluded` # boolean value for each corresponding value in the property. For example, # if a multi-valued property contains `["a", "b"]`, and only the value # `"b"` is indexed (meaning that `"a"`' is excluded), the return value for # this method will be `[true, false]`. # # @see https://cloud.google.com/datastore/docs/concepts/indexes#Datastore_Unindexed_properties # Unindexed properties # # @example Single property values will return a single flag setting: # task["priority"] = 4 # task.exclude_from_indexes? "priority" #=> false # # @example A multi-valued property will return an array of flag settings: # task["tags"] = ["fun", "programming"] # task.exclude_from_indexes! "tags", [true, false] # # task.exclude_from_indexes? "tags" #=> [true, false] # def exclude_from_indexes? name value = self[name] flag = @_exclude_indexes[name.to_s] map_exclude_flag_to_value flag, value end ## # Sets whether a property should be excluded from the Datastore indexes. # Setting `true` will exclude the property from the indexes. # Setting `false` will include the property on any applicable indexes. # The default value is `false`. This is another way of saying that values # are indexed by default. # # If the property is multi-valued, each value in the list can be managed # separately for exclusion from indexing. When you call this method for a # multi-valued property, you can pass either a single boolean argument to # be applied to all of the values, or an array that contains the boolean # argument for each corresponding value in the property. For example, # if a multi-valued property contains `["a", "b"]`, and only the value # `"b"` should be indexed (meaning that `"a"`' should be excluded), you # should pass the array: `[true, false]`. # # @param [String] name the property name # @param [Boolean, Array, nil] flag whether the value or values # should be excluded from indexing # @yield [value] a block yielding each value of the property # @yieldparam [Object] value a value of the property # @yieldreturn [Boolean] `true` if the value should be excluded from # indexing # # @see https://cloud.google.com/datastore/docs/concepts/indexes#Datastore_Unindexed_properties # Unindexed properties # # @example # entity["priority"] = 4 # entity.exclude_from_indexes! "priority", true # # @example Multi-valued properties can be given multiple exclude flags: # entity["tags"] = ["fun", "programming"] # entity.exclude_from_indexes! "tags", [true, false] # # @example Or, a single flag can be applied to all values in a property: # entity["tags"] = ["fun", "programming"] # entity.exclude_from_indexes! "tags", true # # @example Flags can also be set with a block: # entity["priority"] = 4 # entity.exclude_from_indexes! "priority" do |priority| # priority > 4 # end # def exclude_from_indexes! name, flag = nil, &block name = name.to_s flag = block if block_given? if flag.nil? @_exclude_indexes.delete name else @_exclude_indexes[name] = flag end end ## # The number of bytes the Entity will take to serialize during API calls. def serialized_size to_grpc.to_proto.length end ## # @private Convert the Entity to a Google::Datastore::V1beta3::Entity # object. def to_grpc grpc = Google::Datastore::V1beta3::Entity.new( key: @key.to_grpc, properties: @properties.to_grpc ) update_properties_indexed! grpc.properties grpc end ## # @private Create a new Entity from a Google::Datastore::V1beta3::Key # object. def self.from_grpc grpc entity = Entity.new entity.key = Key.from_grpc grpc.key entity.send :properties=, Properties.from_grpc(grpc.properties) entity.send :update_exclude_indexes!, grpc.properties entity end protected ## # @private Allow friendly objects to set Properties object. attr_writer :properties # rubocop:disable all # Disabled rubocop because this is intentionally complex. ## # @private Map the exclude flag object to value. # The flag object can be a boolean, Proc, or Array. # Procs will be called and passed in the value. # This will return an array of flags for an array value. def map_exclude_flag_to_value flag, value if value.is_a? Array if flag.is_a? Proc value.map { |v| !!flag.call(v) } elsif flag.is_a? Array (flag + Array.new(value.size)).slice(0, value.size).map { |v| !!v } else value.map { |_| !!flag } end else if flag.is_a? Proc !!flag.call(value) elsif flag.is_a? Array !!flag.first else !!flag end end end ## # @private Update the exclude data after a new object is created. def update_exclude_indexes! grpc_map @_exclude_indexes = {} grpc_map.each do |name, value| next if value.nil? @_exclude_indexes[name] = value.exclude_from_indexes unless value.array_value.nil? exclude = value.array_value.values.map(&:exclude_from_indexes) @_exclude_indexes[name] = exclude end end end ## # @private Update the indexed values before the object is saved. def update_properties_indexed! grpc_map grpc_map.each do |name, value| next if value.nil? excluded = exclude_from_indexes? name if excluded.is_a? Array value.array_value.values.each_with_index do |v, i| v.exclude_from_indexes = excluded[i] end else value.exclude_from_indexes = excluded end end end # rubocop:enable all end end end