Module: Jamf::CollectionResource::ClassMethods

Includes:
JPAPIResource::ClassMethods
Defined in:
lib/jamf/api/jamf_pro/mixins/collection_resource.rb

Overview

Class Methods

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

Dynamically create_identifier_list_methods when one is called.



600
601
602
603
604
605
606
607
608
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 600

def method_missing(method, *args, &block)
  if available_list_methods.key? method.to_s
    attr_name = available_list_methods[method.to_s]
    create_identifier_list_method attr_name.to_sym, method
    send method, *args
  else
    super
  end
end

Class Method Details

.extended(extender) ⇒ Object

when this module is included, also extend our 'parent' class methods



87
88
89
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 87

def self.extended(extender)
  Jamf.load_msg "--> #{extender} is extending Jamf::CollectionResource::ClassMethods"
end

Instance Method Details

#all(sort: nil, filter: nil, instantiate: false, cnx: Jamf.cnx, refresh: nil) ⇒ Array<Hash, Jamf::CollectionResource>

Get all instances of a CollectionResource, possibly sorted or limited by a filter.

By default, this method will return a single Array data about all items in the CollectionResouce, in the server's default sort order, or a sort order you specify. As long as you don't request a filtered result, this full list is cached for future use (see Caching, below)

If you specify a filter, the Array returned by the server will contain only matching objects, and it will not be cached.

#### Server-side Sorting

Sorting criteria can be provided using the 'sort:' parameter, which is a String of the format 'property:direction', where direction is 'asc' or 'desc' E.g.

"username:asc"

Multiple properties are supported, either as separate strings in an Array, or a single string, comma separated. E.g.

"username:asc,timestamp:desc"

is the same as

["username:asc", "timestamp:desc"]

which will sort by username alphabetically, and within each username, sort by timestamp newest first.

Please see the JamfPro API documentation for the resource for details about available sorting properties and default sorting criteria

When the sort: param is provided, the server is always queried, and the results will be cached, as long as the results were not filtered. See Caching, below.

#### Filtering

Some CollectionResouces support RSQL filters to limit which objects are returned by the server. These filters can be applied using the filter: parameter, in which case this `all` method will return “all that match the filter”.

The filter parameter is a string, as it would be provided in the API URL manually, e.g. 'categoryName==“Category”' (be careful about inner quoting)

If the resource doesn't support filters, the filter parameter is ignored.

Please see the JamfPro API documentation for the resource to see if filters are supported, and a list of available fields. See also developer.jamf.com/jamf-pro/docs/filtering-with-rsql

#### Instantiation

All data from the API comes from the server in JSON format, mostly as JSON 'objects', which are the equivalent of ruby Hashes. When fetching an individual instance of an object from the API, ruby-jss uses the JSON Hash to create the ruby object, i.e. to 'instantiate' it as an instance of its ruby class. Doing this for many objects can slow things down.

Because of this, the 'all' method defaults to returning an Array of the minimally-processed JSON Hashes it gets from the API. If you can get your desired data from these Hashes, it may be more efficient to do so.

However sometimes you really need the fully instantiated ruby objects for all items returned - especially if you're using filters and not actually

processing all items of the class.  In such cases you can pass a truthy

value to the instantiate: parameter, and the Array will contain fully instantiated ruby objects, not Hashes of API data.

#### Caching - none for Jamf Pro API objects.

Unlike the Classic APIObjects, Objects from the Jamf Pro API are not cached and any call to 'all' or methods that use it, will always query the API. If you need to use the resulting array for multiple tasks, save it into a variable and use that.

Parameters:

  • sort (String, Array<String>) (defaults to: nil)

    Server-side sorting criteria in the format: property:direction, where direction is 'asc' or 'desc'. Multiple properties are supported, either as separate strings in an Array, or a single string, comma separated.

  • filter (String) (defaults to: nil)

    An RSQL filter string. Not all collection resources currently support filters, and if they don't, this will be ignored.

  • instantiate (Boolean) (defaults to: false)

    Defaults to false. Should the items in the returned Array be ruby instances of the CollectionObject subclass, or plain Hashes of data as returned by the API?

  • cnx (Jamf::Connection) (defaults to: Jamf.cnx)

    The API connection to use, default: Jamf.cnx

Returns:



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 258

def all(sort: nil, filter: nil, instantiate: false, cnx: Jamf.cnx, refresh: nil)

  # if we are here, we need to query for all items, possibly filtered and
  # sorted
  sort = Jamf::Sortable.parse_url_sort_param(sort)
  filter = filterable? ? Jamf::Filterable.parse_url_filter_param(filter) : nil
  instantiate &&= self

  # always use a pager to get all pages, because even if you don't ask for
  # paged data, it comes in pages or 2000
  Jamf::Pager.all_pages(
    list_path: self::LIST_PATH,
    sort: sort,
    filter: filter,
    instantiate: instantiate,
    cnx: cnx
  )
end

#available_list_methodsHash{String: Symbol}

Returns Method name to matching attribute name for all identifiers.

Returns:

  • (Hash{String: Symbol})

    Method name to matching attribute name for all identifiers



617
618
619
620
621
622
623
624
625
626
627
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 617

def available_list_methods
  return @available_list_methods if @available_list_methods

  @available_list_methods = {}

  identifiers.each do |i|
    meth_name = i.to_s.end_with?('s') ? "all_#{i}es" : "all_#{i}s"
    @available_list_methods[meth_name] = i
  end
  @available_list_methods
end

#bulk_deletable?Boolean

Returns:

  • (Boolean)


566
567
568
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 566

def bulk_deletable?
  singleton_class.ancestors.include? Jamf::BulkDeletable
end

#creatable?Boolean

By default, Collection Resources are creatable, i.e. new instances can be created with .create, and added to the JSS with .save If a subclass is NOT creatble for any reason, just add

extend Jamf::Uncreatable

and this method will return false

Returns:

  • (Boolean)


494
495
496
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 494

def creatable?
  true
end

#create(**params) ⇒ Object

Make a new thing to be added to the API



500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 500

def create(**params)


  # no such animal when .creating
  params.delete :id

  # Which connection to use
  params[:cnx] ||= Jamf.cnx

  # So the super constructor knows we are instantiating an object that
  # isn't from the API, and will do validation on all params.
  params[:creating_from_create] = true

  new(**params)
end

#deletable?Boolean

By default, CollectionResource instances are deletable. If not, just extend the subclass with Jamf::Undeletable, and this will return false, and .delete & #delete will raise errors

Returns:

  • (Boolean)


558
559
560
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 558

def deletable?
  true
end

#delete(*ids, cnx: Jamf.cnx) ⇒ Array<Jamf::Connection::APIError::ErrorInfo] Info about any ids that failed to be deleted.

Delete one or more objects by id TODO: fix this return value, no more ErrorInfo

Parameters:

  • ids (Array<String,Integer>)

    The ids to delete

  • cnx (Jamf::Connection) (defaults to: Jamf.cnx)

    The connection to use, default: Jamf.cnx

Returns:

  • (Array<Jamf::Connection::APIError::ErrorInfo] Info about any ids that failed to be deleted.)

    Array<Jamf::Connection::APIError::ErrorInfo] Info about any ids that failed to be deleted.

Raises:



582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 582

def delete(*ids, cnx: Jamf.cnx)
  raise Jamf::UnsupportedError, "Deleting #{self} objects is not currently supported" unless deletable?

  return bulk_delete(ids, cnx: Jamf.cnx) if self.bulk_deletable?

  errs = []
  ids.each do |id_to_delete|
    cnx.jp_delete "#{delete_path}/#{id_to_delete}"
  rescue Jamf::Connection::JamfProAPIError => e
    raise e unless e.http_response.status == 404

    errs += e.errors
  end # ids.each
  errs
end

#delete_pathObject

The path for DELETEing a single object from the collection.

Classes including CollectionResource really need to define DELETE_PATH if it is not the same as the LIST_PATH.



146
147
148
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 146

def delete_path
  @delete_path ||= defined?(self::DELETE_PATH) ? self::DELETE_PATH : self::LIST_PATH
end

#fetch(searchterm = nil, random: false, cnx: Jamf.cnx, **ident_and_val) ⇒ CollectionResource

Retrieve a member of a CollectionResource from the API

To create new members to be added to the JSS, use Jamf::CollectionResource.create

You must know the specific identifier attribute you're looking up, e.g. :id or :name or :udid, (or an aliase thereof) then you can specify it like `.fetch name: 'somename'`, or `.fetch udid: 'someudid'`

Parameters:

  • cnx (Jamf::Connection) (defaults to: Jamf.cnx)

    the connection to use to fetch the object

  • ident_and_val (Hash)

    an identifier attribute key and a search value

Returns:

Raises:



532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 532

def fetch(searchterm = nil, random: false, cnx: Jamf.cnx, **ident_and_val)
  if searchterm == :random
    random = true
    searchterm = nil
  end

  data =
    if searchterm
      raw_data searchterm, cnx: cnx
    elsif random
      all.sample
    else
      ident, value = ident_and_val.first
      ident && value ? raw_data(ident: ident, value: value, cnx: cnx) : nil
    end

  raise Jamf::NoSuchItemError, "No matching #{self}" unless data

  data[:cnx] = cnx
  new(**data)
end

#filterable?Boolean

Returns:

  • (Boolean)


562
563
564
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 562

def filterable?
  singleton_class.ancestors.include? Jamf::Filterable
end

#get_pathObject

The path for GETting a single object. The desired object id will be appended to the end, e.g. if this value is 'v1/buildings' and you want to GET the record for building id 23, then we will GET from 'v1/buildings/23'

Classes including CollectionResource really need to define GET_PATH if it is not the same as the LIST_PATH. rubocop:disable Naming/AccessorMethodName



103
104
105
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 103

def get_path
  @get_path ||= defined?(self::GET_PATH) ? self::GET_PATH : self::LIST_PATH
end

#identifiersArray<Symbol>

Returns the attribute names that are marked as identifiers.

Returns:

  • (Array<Symbol>)

    the attribute names that are marked as identifiers



153
154
155
156
157
158
159
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 153

def identifiers
  idents = self::OAPI_PROPERTIES.select { |_attr, deets| deets[:identifier] }.keys
  idents += self::ALT_IDENTIFIERS if defined? self::ALT_IDENTIFIERS
  idents += self::NON_UNIQUE_IDENTIFIERS if defined? self::NON_UNIQUE_IDENTIFIERS
  idents.delete_if { |i| !self::OAPI_PROPERTIES.key?(i) }
  idents
end

#map_all(ident, to:, cnx: Jamf.cnx, cached_list: nil, refresh: nil) ⇒ Hash {Symbol: Object}

A Hash of all members of this collection where the keys are some identifier and values are any other attribute.

Parameters:

  • ident (Symbol)

    An identifier of this Class, used as the key for the mapping Hash. Aliases are acceptable, e.g. :sn for :serialNumber

  • to (Symbol)

    The attribute to which the ident will be mapped. Aliases are acceptable, e.g. :name for :displayName

  • cached_list (Array<Hash>) (defaults to: nil)

    The result of a previous call to .all can be passed in here, to prevent calling .all again to generate a fresh list.

  • cnx (Jamf::Connection) (defaults to: Jamf.cnx)

    The API connection to use, default: Jamf.cnx

Returns:

  • (Hash {Symbol: Object})

    A Hash of identifier mapped to attribute

Raises:



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 322

def map_all(ident, to:, cnx: Jamf.cnx, cached_list: nil, refresh: nil)
  raise Jamf::InvalidDataError, "No identifier :#{ident} for class #{self}" unless
  identifiers.include? ident

  raise Jamf::NoSuchItemError, "No attribute :#{to} for class #{self}" unless self::OAPI_PROPERTIES.key? to


  list = cached_list || all(cnx: cnx)
  to_class = self::OAPI_PROPERTIES[to][:class]
  to_multi = self::OAPI_PROPERTIES[to][:multi]
  mapped = list.map do |i|
    mapped_val =
      if to_class.is_a?(Symbol)
        i[to]
      elsif to_multi
        i[to].map { |sub_i| to_class.new(sub_i) }
      else
        to_class.new(i[to])
      end

    [i[ident], mapped_val]
  end # do i
  mapped.to_h
end

#new(**data) ⇒ Object Originally defined in module JPAPIResource::ClassMethods

Disallow direct use of ruby's .new class method for creating instances. Require use of .fetch or .create, or 'all'

#pager(page_size: Jamf::Pager::DEFAULT_PAGE_SIZE, sort: nil, filter: nil, instantiate: false, cnx: Jamf.cnx) ⇒ Jamf::Pager

Return a Jamf::Pager object for retrieving all collection items in smaller groups.

For other parameters, see CollectionResource.all

Parameters:

  • page_size (Integer) (defaults to: Jamf::Pager::DEFAULT_PAGE_SIZE)

    The pager object returns results in groups of this many items. Minimum is 1, maximum is 2000, default is 100 Note: the final page of data may contain fewer items than the page_size

Returns:

  • (Jamf::Pager)

    An object from which you can retrieve sequential or arbitrary pages from the collection.



289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 289

def pager(page_size: Jamf::Pager::DEFAULT_PAGE_SIZE, sort: nil, filter: nil, instantiate: false, cnx: Jamf.cnx)

  sort = Jamf::Sortable.parse_url_sort_param(sort)
  filter = filterable? ? Jamf::Filterable.parse_url_filter_param(filter) : nil

  Jamf::Pager.new(
    page_size: page_size,
    list_path: self::LIST_PATH,
    sort: sort,
    filter: filter,
    instantiate: instantiate,
    cnx: cnx
  )
end

#patch_pathObject

The path for PATCHing (updating in-place) a single object. The desired object id will be appended to the end, e.g. if this value is 'v1/buildings' and you want to PATCH the record for building id 23, then we will PATCH to 'v1/buildings/23'

Classes including CollectionResource really need to define PATCH_PATH if it is not the same as the LIST_PATH.



127
128
129
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 127

def patch_path
  @patch_path ||= defined?(self::PATCH_PATH) ? self::PATCH_PATH : self::LIST_PATH
end

#post_pathObject

The path for POSTing to create a single object in the collection.

Classes including CollectionResource really need to define POST_PATH if it is not the same as the LIST_PATH.



137
138
139
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 137

def post_path
  @post_path ||= defined?(self::POST_PATH) ? self::POST_PATH : self::LIST_PATH
end

#put_pathObject

The path for PUTting (replacing) a single object. The desired object id will be appended to the end, e.g. if this value is 'v1/buildings' and you want to PUT the record for building id 23, then we will PUT to 'v1/buildings/23'

Classes including CollectionResource really need to define PUT_PATH if it is not the same as the LIST_PATH.



115
116
117
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 115

def put_path
  @put_path ||= defined?(self::PUT_PATH) ? self::PUT_PATH : self::LIST_PATH
end

#raw_data(searchterm = nil, ident: nil, value: nil, cnx: Jamf.cnx) ⇒ Hash?

Given a key (identifier) and value for this collection, return the raw data Hash (the JSON object) for the matching API object or nil if there's no match for the given value.

In general you should use this if the form:

raw_data identifier: value

where identifier is one of the available identifiers for this class like id:, name:, serialNumber: etc.

In the unlikely event that you dont know which identifier a value is for or want to be able to take any of them without specifying, then you can use

raw_data some_value

If some_value is an integer or a string containing an integer, it is assumed to be an :id otherwise all the available identifers are searched, in the order you see them when you call <class>.identifiers

If no matching object is found, nil is returned.

Everything except :id is treated as a case-insensitive String

Parameters:

  • value (String, Integer) (defaults to: nil)

    The identifier value to search fors

  • key: (Symbol)

    The identifier being used for the search. E.g. if :serialNumber, then the value must be a known serial number, it is not checked against other identifiers. Defaults to :id

Returns:

  • (Hash, nil)

    the basic dataset of the matching object, or nil if it doesn't exist

Raises:

  • (ArgumentError)


384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 384

def raw_data(searchterm = nil, ident: nil, value: nil, cnx: Jamf.cnx)


  # given a value with no ident key
  return raw_data_by_searchterm_only(searchterm, cnx: cnx) if searchterm

  # if we're here, we should know our ident key and value
  raise ArgumentError, 'Required parameter "identifier: value", where identifier is id:, name: etc.' unless ident && value

  return raw_data_by_id(value, cnx: cnx) if ident == :id
  return unless identifiers.include? ident

  raw_data_by_other_identifier(ident, value, cnx: cnx)
end

#raw_data_by_id(id, cnx: Jamf.cnx) ⇒ Object

get the basic dataset by id, with optional request params to get more than basic data



417
418
419
420
421
422
423
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 417

def raw_data_by_id(id, cnx: Jamf.cnx)
  cnx.jp_get "#{get_path}/#{id}"
rescue Jamf::Connection::JamfProAPIError => e
  return nil if e.errors.any? { |err| err.code == 'INVALID_ID' }

  raise e
end

#raw_data_by_other_identifier(identifier, value, cnx: Jamf.cnx) ⇒ Object

Given an indentier attr. key, and a value, return the raw data where that ident has that value, or nil



429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 429

def raw_data_by_other_identifier(identifier, value, cnx: Jamf.cnx)
  # if the API supports filtering by this identifier, just use that
  return pager(filter: "#{identifier}==\"#{value}\"", page_size: 1, cnx: cnx).page(:first).first if filterable? && filter_keys.include?(identifier)

  # otherwise we have to loop thru all the objects looking for the value
  cmp_val = value.to_s
  all(cnx: cnx).each do |data|
    return data if data[identifier].to_s.casecmp? cmp_val
  end

  nil
end

#raw_data_by_searchterm_only(searchterm, cnx: Jamf.cnx) ⇒ Object

Match the given value in all possibly identifiers



400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 400

def raw_data_by_searchterm_only(searchterm, cnx: Jamf.cnx)
  # if this is an integer or j_integer, assume its an ID
  return raw_data_by_id(searchterm, cnx: cnx) if searchterm.to_s.j_integer?

  identifiers.each do |ident|
    next if ident == :id

    data = raw_data_by_other_identifier(ident, searchterm, cnx: cnx)
    return data if data
  end # identifiers.each

  nil
end

#respond_to_missing?(method) ⇒ Boolean

this is needed to prevent problems with method_missing!

Returns:

  • (Boolean)


611
612
613
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 611

def respond_to_missing?(method, *)
  available_list_methods.key?(method.to_s) || super
end

#valid_id(searchterm = nil, cnx: Jamf.cnx, **ident_and_val) ⇒ String?

Look up the valid ID for any arbitrary identifier. In general you should use this if the form:

valid_id identifier: value

where identifier is one of the available identifiers for this class like id:, name:, serialNumber: etc.

In the unlikely event that you dont know which identifier a value is for or want to be able to take any of them without specifying, then you can use

valid_id some_value

If some_value is an integer or a string containing an integer, it is assumed to be an id: otherwise all the available identifers are searched, in the order you see them when you call <class>.identifiers

If no matching object is found, nil is returned.

WARNING: Do not use this to look up ids for getting the raw API data for an object. Since this calls .raw_data itself, it is redundant to use .valid_id to get an id to then pass on to .raw_data Use raw_data directly like this:

data = raw_data(ident: val)

Parameters:

  • value (String, Integer)

    A value for an arbitrary identifier

  • cnx (Jamf::Connection) (defaults to: Jamf.cnx)

    The connection to use. default: Jamf.cnx

  • ident_and_val (Hash{Symbol: String})

    The identifier key and the value to look for in that key, e.g. name: 'foo' or serialNumber: 'ASDFGH'

Returns:

  • (String, nil)

    The id (integer-in-string) of the object, or nil if no match found



481
482
483
# File 'lib/jamf/api/jamf_pro/mixins/collection_resource.rb', line 481

def valid_id(searchterm = nil, cnx: Jamf.cnx, **ident_and_val)
  raw_data(searchterm, cnx: cnx, **ident_and_val)&.dig(:id)
end

#which_apiObject Originally defined in module JPAPIResource::ClassMethods

Indicate that this class comes from the Jamf Pro API. The same method exists in APIObject to indicate coming from Classic