lib/jss/api_object.rb in ruby-jss-0.8.2 vs lib/jss/api_object.rb in ruby-jss-0.9.0.b1

- old
+ new

@@ -106,28 +106,22 @@ # so VALID_DATA_KEYS will be [:computers, :is_smart] # # *NOTE* Some API objects have data broken into subsections, in which case the # VALID_DATA_KEYS are expected in the section :general. # + # + # === Optional Constants + # + # ==== OTHER_LOOKUP_KEYS = [Hash{Symbol=>Hash}] Every object can be looked up by + # :id and :name, but some have other uniq identifiers that can also be used, + # e.g. :serial_number, :mac_address, and so on. This Hash, if defined, + # speficies those other keys for the subclass + # For more details about this hash, see {APIObject::DEFAULT_LOOKUP_KEYS}, + # {APIObject.fetch}, and {APIObject#lookup_object_data} + # class APIObject - # Mix-Ins - ##################################### - - # Class Variables - ##################################### - - # This Hash holds the most recent API query for a list of all items in any subclass, - # keyed by the subclass's RSRC_LIST_KEY. See the self.all class method. - # - # When the .all method is called without an argument, and this hash has - # a matching value, the value is returned, rather than requerying the - # API. The first time a class calls .all, or whnever refresh is - # not false, the API is queried and the value in this hash is updated. - # - @@all_items = {} - # Class Methods ##################################### # Return an Array of Hashes for all objects of this subclass in the JSS. # @@ -146,11 +140,11 @@ # # Subclasses implementing those API classes should provide .all_xxx # class methods for accessing those other values as mapped Arrays, # e.g. JSS::Computer.all_udids # - # The results of the first query for each subclass is stored in @@all_items + # The results of the first query for each subclass is stored in JSS.api.object_list_cache # and returned at every future call, so as to not requery the server every time. # # To force requerying to get updated data, provided a non-false argument. # I usually use :refresh, so that it's obvious what I'm doing, but true, 1, # or anything besides false or nil will work. @@ -159,24 +153,24 @@ # # @return [Array<Hash{:name=>String, :id=> Integer}>] # def self.all(refresh = false) raise JSS::UnsupportedError, '.all can only be called on subclasses of JSS::APIObject' if self == JSS::APIObject - @@all_items[self::RSRC_LIST_KEY] = nil if refresh - return @@all_items[self::RSRC_LIST_KEY] if @@all_items[self::RSRC_LIST_KEY] - @@all_items[self::RSRC_LIST_KEY] = JSS::API.get_rsrc(self::RSRC_BASE)[self::RSRC_LIST_KEY] + JSS.api.object_list_cache[self::RSRC_LIST_KEY] = nil if refresh + return JSS.api.object_list_cache[self::RSRC_LIST_KEY] if JSS.api.object_list_cache[self::RSRC_LIST_KEY] + JSS.api.object_list_cache[self::RSRC_LIST_KEY] = JSS.api.get_rsrc(self::RSRC_BASE)[self::RSRC_LIST_KEY] end # Returns an Array of the JSS id numbers of all the members # of the subclass. # # e.g. When called from subclass JSS::Computer, # returns the id's of all computers in the JSS # # @param refresh[Boolean] should the data be re-queried from the API? # - # @return [Array<Integer>] the ids of all items of this subclass in the JSS + # @return [Array<Integer>] the ids of all it1ems of this subclass in the JSS # def self.all_ids(refresh = false) all(refresh).map { |i| i[:id] } end @@ -236,13 +230,13 @@ # @param refresh[Boolean] should the data re-queried from the API? # # @return [Hash{Integer => Object}] the objects requested def self.all_objects(refresh = false) objects_key = "#{self::RSRC_LIST_KEY}_objects".to_sym - @@all_items[objects_key] = nil if refresh - return @@all_items[objects_key] if @@all_items[objects_key] - @@all_items[objects_key] = all(refresh).map { |o| new id: o[:id] } + JSS.api.object_list_cache[objects_key] = nil if refresh + return JSS.api.object_list_cache[objects_key] if JSS.api.object_list_cache[objects_key] + JSS.api.object_list_cache[objects_key] = all(refresh).map { |o| new id: o[:id] } end # Return true or false if an object of this subclass # with the given name or id exists on the server # @@ -366,10 +360,48 @@ when nil nil end end + # What are all the lookup keys available for this class? + # + # @return [Array<Symbol>] the DEFAULT_LOOKUP_KEYS plus any OTHER_LOOKUP_KEYS + # defined for this class + # + def self.lookup_keys + return DEFAULT_LOOKUP_KEYS.keys unless defined? self::OTHER_LOOKUP_KEYS + DEFAULT_LOOKUP_KEYS.keys + self::OTHER_LOOKUP_KEYS.keys + end + + # @return [Hash] the available lookup keys mapped to the appropriate + # resource key for building a REST url to retrieve an object. + # + def self.rsrc_keys + hash = {} + all_keys = if defined?(self::OTHER_LOOKUP_KEYS) + DEFAULT_LOOKUP_KEYS.merge self::OTHER_LOOKUP_KEYS + else + DEFAULT_LOOKUP_KEYS + end + all_keys.each { |key, deets| hash[key] = deets[:rsrc_key]} + hash + end + + # @return [Hash] the available lookup keys mapped to the appropriate + # list class method (e.g. id: :all_ids ) + # + def self.lookup_key_list_methods + hash = {} + all_keys = if defined?(self::OTHER_LOOKUP_KEYS) + DEFAULT_LOOKUP_KEYS.merge self::OTHER_LOOKUP_KEYS + else + DEFAULT_LOOKUP_KEYS + end + all_keys.each { |key, deets| hash[key] = deets[:list]} + hash + end + # Retrieve an object from the API. # # This is the preferred way to retrieve existing objects from the JSS. # It's a wrapper for using APIObject.new # and avoids the confusion of using ruby's .new class method when you're not @@ -380,16 +412,31 @@ # @param args[Hash] The data for fetching an object, such as id: or name: # See {APIObject#initialize} # # @return [APIObject] The ruby-instance of a JSS object # - def self.fetch(**args) + def self.fetch(arg) raise JSS::UnsupportedError, 'JSS::APIObject cannot be instantiated' if self.class == JSS::APIObject - raise ArgumentError, 'Use .create to create new JSS objects' if args[:id] == :new - new args - end + # if given a hash (or a colletion of named params) + # pass to .new + if arg.is_a? Hash + raise ArgumentError, 'Use .create to create new JSS objects' if arg[:id] == :new + return new arg + end + + # loop thru the lookup_key list methods for this class + # and if it's result includes the desired value, + # the pass they key and arg to .new + lookup_key_list_methods.each do |key, method_name| + return new({key => arg}) if self.send(method_name).include? arg + end # each key + + # if we're here, we couldn't find a matching object + raise NoSuchItemError, "No #{self::RSRC_OBJECT_KEY} found matching '#{arg}'" + end # fetch + # Make a ruby instance of a not-yet-existing APIObject. # # This is the preferred way to create new objects in the JSS. # It's a wrapper for using APIObject.new with the 'id: :new' parameter. # and helps avoid the confusion of using ruby's .new class method for making @@ -402,33 +449,56 @@ # @param args[Hash] The data for creating an object, such as name: # See {APIObject#initialize} # # @return [APIObject] The un-created ruby-instance of a JSS object # - def self.make(**args) + def self.make(args = {}) raise JSS::UnsupportedError, 'JSS::APIObject cannot be instantiated' if self.class == JSS::APIObject raise ArgumentError, "Use '#{self.class}.fetch id: xx' to retrieve existing JSS objects" if args[:id] args[:id] = :new new args end - ### Class Constants ##################################### # These Symbols are added to VALID_DATA_KEYS for performing the # :data validity test described above. # REQUIRED_DATA_KEYS = [:id, :name].freeze - # By default, these keys are available for object lookups - # Others can be added by subclasses using an array of them - # as the second argument to super(initialize) - # The keys must be Symbols that match the keyname in the resource url. - # e.g. :serialnumber for JSSResource/computers/serialnumber/xxxxx + # All API objects have an id and a name. As such By these keys are available + # for object lookups. # - DEFAULT_LOOKUP_KEYS = [:id, :name].freeze + # Others can be defined by subclasses in their OTHER_LOOKUP_KEYS constant + # which has the same format, described here: + # + # The merged Hashes DEFAULT_LOOKUP_KEYS and OTHER_LOOKUP_KEYS + # define what unique identifiers can be passed as parameters to the + # fetch method for retrieving an object from the API. + # They also define the class methods that return a list (Array) of all such + # identifiers for the class (e.g. the :all_ids class method returns an array + # of all id's for an APIObject subclass) + # + # Since there's often a discrepency between the name of the identifier as + # an attribute (e.g. serial_number) and the REST resource key for + # retrieving that object (e.g. ../computers/serialnumber/xxxxx) this hash + # also explicitly provides the REST resource key for a given lookup key, so + # e.g. both serialnumber and serial_number can be used, and both will have + # the resource key 'serialnumber' and the list method ':all_serial_numbers' + # + # Here's how the Hash is structured, using serialnumber as an example: + # + # LOOKUP_KEYS = { + # serialnumber: {rsrc_key: :serialnumber, list: :all_serial_numbers}, + # serial_number: {rsrc_key: :serialnumber, list: :all_serial_numbers} + # } + # + DEFAULT_LOOKUP_KEYS = { + id: {rsrc_key: :id, list: :all_ids}, + name: {rsrc_key: :name, list: :all_names} + }.freeze # Attributes ##################################### # @return [Integer] the JSS id number @@ -459,24 +529,17 @@ # @option args :id[Integer] the jss id to look up # # @option args :name[String] the name to look up # # @option args :data[Hash] the JSON output of a separate {JSS::APIConnection} query + # NOTE: This arg is deprecated and will be removed in a future release. # - # @param other_lookup_keys[Array<Symbol>] Hash keys other than :id and :name, by which an API - # lookup may be performed. # - def initialize(args = {}, other_lookup_keys = []) - args[:other_lookup_keys] ||= other_lookup_keys + def initialize(args = {}) raise JSS::UnsupportedError, 'JSS::APIObject cannot be instantiated' if self.class == JSS::APIObject - ### what lookup key are we using, if any? - lookup_keys = DEFAULT_LOOKUP_KEYS - lookup_keys += self.class::OTHER_LOOKUP_KEYS if defined? self.class::OTHER_LOOKUP_KEYS - lookup_key = (lookup_keys & args.keys)[0] - ####### Previously looked-up JSON data # DEPRECATED: pre-lookedup data is never used # and support for it will be going away. if args[:data] @@ -484,20 +547,21 @@ validate_external_init_data ###### Make a new one in the JSS, but only if we've included the Creatable module elsif args[:id] == :new - validate_init_for_creation(args) setup_object_for_creation(args) + return ###### Look up the data via the API else @init_data = look_up_object_data(args) end ## end arg parsing + parse_init_data @need_to_update = false end # init # Public Instance Methods @@ -594,11 +658,11 @@ # # @return [void] # def delete return nil unless @in_jss - JSS::API.delete_rsrc @rest_rsrc + JSS.api.delete_rsrc @rest_rsrc @rest_rsrc = "#{self.class::RSRC_BASE}/name/#{CGI.escape @name}" @id = nil @in_jss = false @need_to_update = false :deleted @@ -666,27 +730,32 @@ # # @return [Hash] The parsed JSON data for the object from the API # def look_up_object_data(args) # what lookup key are we using? - combined_lookup_keys = self.class::DEFAULT_LOOKUP_KEYS + args[:other_lookup_keys] - lookup_key = (combined_lookup_keys & args.keys)[0] + lookup_keys = self.class.lookup_keys + lookup_key = (self.class.lookup_keys & args.keys)[0] + raise JSS::MissingDataError, "Args must include a lookup key, one of: :#{lookup_keys.join(', :')}" unless lookup_key + rsrc_key = self.class.rsrc_keys[lookup_key] - raise JSS::MissingDataError, "Args must include a lookup key, one of: :#{combined_lookup_keys.join(', :')}" unless lookup_key + rsrc = "#{self.class::RSRC_BASE}/#{rsrc_key}/#{args[lookup_key]}" - rsrc = "#{self.class::RSRC_BASE}/#{lookup_key}/#{args[lookup_key]}" + # if needed, a non-standard object key can be passed by a subclass. + # e.g. User when loookup is by email. + rsrc_object_key = args[:rsrc_object_key] ? args[:rsrc_object_key] : self.class::RSRC_OBJECT_KEY - return JSS::API.get_rsrc(rsrc)[self.class::RSRC_OBJECT_KEY] + return JSS.api.get_rsrc(rsrc)[rsrc_object_key] rescue RestClient::ResourceNotFound - raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} found matching: #{lookup_key}/#{args[lookup_key]}" + raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} found matching: #{rsrc_key}/#{args[lookup_key]}" end # Start examining the @init_data recieved from the API # # @return [void] # def parse_init_data + @init_data ||= {} # set empty strings to nil @init_data.jss_nillify! '', :recurse # Find the "main" subset which contains :id and :name @main_subset = find_main_subset @@ -800,10 +869,10 @@ # @return [Type] description_of_returned_object # def setup_object_for_creation(args) # NOTE: subclasses may want to pre-populate more keys in @init_data when :id == :new # then parse them into attributes later. - @init_data = { name: args[:name] } + @init_data = args @name = args[:name] @in_jss = false @rest_rsrc = "#{self.class::RSRC_BASE}/name/#{CGI.escape @name}" @need_to_update = true end