Class: Jamf::JSONObject Abstract

Inherits:
Object show all
Extended by:
BaseClass
Defined in:
lib/jamf/api/base_classes/json_object.rb

Overview

This class is abstract.

# Jamf::JSONObject

In JSON & Javascript, an 'object' is a data structure equivalent to a Hash in Ruby. Much of the JSON data exchaged with the API is formatted as these JSON objects.

Jamf::JSONObject is a meta class that provides a way to convert those JSON 'objects' into not just Hashes (that's done by the Jamf::Connection) but into full-fledged ruby Classes. Once implemented in ruby-jss, all JSON objects (Hashes) used anywhere in the Jamf Pro API have a matching Class in ruby-jss which is a subclass of Jamf::JSONObject

The Jamf::JSONObject class is a base class, and cannot be instantiated or used directly. It merely provides the common functionality needed for dealing with all JSON objects in the API.

## Subclassing

When implementing a JSON object in the API as a class in ruby-jss, you will make a subclass of either Jamf::JSONObject, Jamf::SingletonResource or Jamf::CollectionResource.

Here's the relationship between these base classes:

                 Jamf::JSONObject
                    (abstract)
                        |
                        |
              -----------------------
             |                       |
       Jamf::Resource                |
         (abstract)                  |
             |                       |
             |                       |
             |            Jamf::Computer::Reference
             |                  Jamf::Location
             |              Jamf::ChangeLog::Entry
             |        (more non-resource JSON object classes)
             |
             |----------------------------------------
             |                                        |
             |                                        |
    Jamf::SingletonResource                Jamf::CollectionResource
         (abstract)                               (abstract)
             |                                        |
             |                                        |
  Jamf::Settings::ReEnrollment                  Jamf::Computer
  Jamf::Settings::SelfService                   Jamf::Building
       Jamf::SystemInfo                       Jamf::PatchPolicy
(more singleton resource classes)     (more collection resource classes)

Direct descendents of Jamf::JSONObject are arbitrary JSON objects that appear inside other objects, e.g. the Location data for a computer, or a reference to a building.

Resource classes represent direct resources of the API, i.e. items accessible with a URL. The ability to interact with those URLs is defined in the metaclass Jamf::Resource, and all resources must define a RSRC_VERSION and a RSRC_PATH. See Resource for more info.

There are two kinds of resources in the API:

SingletonResource classes represent objects in the API that have only one instance, such as various settings, or server-wide state. These objects cannot be created or deleted, only fetched and updated.

CollectionResource classes represent collections of objects in the API. These resources can list all of their members, and individual members can be retrieved, updated, created and deleted.

Subclasses need to meet the requirements for all of their ancestors, so once you decide which one you're subclassing, be sure to read the docs for each one. E.g. to implement Jamf::Package, it will be a CollectionResource, which is a Resource, which is a JSONObject, and the requirements for all must be met.

The remainder of this page documents the requirements and details of Jamf::JSONObject.

NOTES:

  • subclasses may define more methods, include mix-ins, and if needed can override methods defined in metaclasses. Please read the docs before overriding.

  • Throughout the documentation 'parsed JSON object' means the result of running a raw JSON string thru `JSON.parse raw_json, symbolize_names: true`. This is performed in the Connection methods which interact with the API: Connection#get, Connection#post, Connection#put Connection#patch and Connection#delete.

  • Related to the above, the Connection methods Connection#post and Connection#put call `#to_json` on the data passed to them, before sending it to the API. Subclasses and application code should never call #to_json anywhere. The data passed to put and post should be the output of `#to_jamf` on a Jamf::JSONObject, which is handled by the the #update and #create methods as needed.

###

### Required Constant: OBJECT_MODEL & call to parse_object_model

Each descendent of JSONObject must define the constant OBJECT_MODEL, which is a Hash of Hashes that collectively define the top-level keys of the JSON object as attributes of the matching ruby class.

Immediately after the definition of OBJECT_MODEL, the subclass MUST call `self.parse_object_model` to convert the model into actual ruby attributes with getters and setters.

The OBJECT_MODEL Hash directly implements the matching JSON object model defined at developer.jamf.com/apis/jamf-pro-api/index and is used to automatically create attributes & accessor methods mirroring those in the API.

The keys of the main hash are the symbolized names of the attributes as they come from the JSON fetched from the API.

_ATTRIBUTE NAMES:_

The attribute names in the Jamf Pro API JSON data are in 'lowerCamelCase' (en.wikipedia.org/wiki/Camel_case), and are used that way throughout the Jamf module in order to maintain consistency with the API

itself. This differs from the ruby standard of using 'snake_case'

(en.wikipedia.org/wiki/Snake_case) for attributes, methods, & local variables. I believe that maintaining consistency with the API we are mirroring is more important (and simpler) than conforming with ruby's community standards. I also believe that doing so is in-line with the ruby community's larger philosophy.

“There's more than one way to do it” - because context matters. If that weren't true, I'd be writing Python.

Each attribute key has a Hash of details defining how the attribute is used in the class. Getters and setters are created from these details, and they are used to parse incoming, and generate outgoing JSON data

The possible keys of the details Hash for each attribute are:

  • class:

  • identfier:

  • required:

  • readonly:

  • multi:

  • enum:

  • validator:

  • aliases:

  • filter_key:

For an example of an OBJECT_MODEL hash, see MobileDeviceDetails::OBJECT_MODEL

The details for each key's value are as follows. Note that omitting a boolean key is the same as setting it to false.

class: [Symbol or Class]


This is the only required key for all attributes.


Symbol is one of :string, :integer, :float, :boolean, or :j_id

The first four are the JSON data types that don't need parsing into ruby beyond that done by `JSON.parse`. When processing an attribute with one of these symbols as the `class:`, the JSON value is used as-is.

The ':j_id' symbol means this value is an id used to reference an object in a collection resource of the API - all such objects have an 'id' attribute which is a String containing an Integer.

These ids are used not only as the id attribute of the object itself, but if an object contains references to one or more other objects, those references are also ':j_id' values. In setters and .create, :j_id values can take either an integer or an integer-in-a-string, and are stored as integer-in-a-string/

When 'class:' is not a Symbol, it must be an actual class, such as Jamf::Timestamp or Jamf::PurchasingData.

Actual classes used this way must:

  • Have an #initialize method that takes two parameters and performs validation on them:

    A first positional parameter, the value used to create the instance, which accepts, at the very least, the Parsed JSON data for the attribute. This can be a single value (e.g. a string for Jamf::Timestamp), or a Hash (e.g. for Jamf::Location), or whatever. Other values are allowed if your initialize method handles them properly.

    A keyword parameter `cnx:`. This can be ignored if not needed, but #initialize must accept it. If used, it will contain a Jamf::Connection object, either the one from which the first param came, or the one to which we'll be validating or creating a new object

  • Define a #to_jamf method that returns a value that can be used in the data sent back to the API. Subclasses of JSONObject already have this requirement, and the value is a Hash.

Classes used in the class: value of an attribute definition are often also subclasses of JSONObject (e.g. Jamf::Location) but do not have to be as long as they conform to the standards above, e.g. Jamf::Timestamp.

See also: [Data Validation](#data_validation) below.

identifier: [Boolean or Symbol :primary]


Only applicable to descendents of Jamf::CollectionResource

If true, this value must be unique among all members of the class in the JAMF, and can be used to look up objects.

If the symbol :primary, this is the primary identifier, used in API resource paths for this particular object. Usually its the :id attribute, but for some objects may be some other attribute, e.g. for config- profiles, it would be a uuid.

required: [Boolean]


If true, this attribute must be provided when creating a new local instance and cannot be set to nil or empty

readonly: [Boolean]


If true, no setter method(s) will be created, and the value is not sent to the API with #create or #update

multi: [Boolean]


When true, this value comes as a JSON array and its items are defined by the 'class:' setting described above. The JSON array is used to contstruct an attribute array of the correct kind of item.

Example: > When `class:` is Jamf::Computer::Reference the incoming JSON array > of Hashes (computer references) will become an array of > Jamf::Computer::Reference instances.

The stored array is not directly accessible, the getter will return a frozen duplicate of it.

If not readonly, several setters are created:

  • a direct setter which takes an Array of 'class:', replacing the original

  • a <attrname>_append method, appends a new value to the array, aliased as `<<`

  • a <attrname>_prepend method, prepends a new value to the array

  • a <attrname>_insert method, inserts a new value to the array at the given index

  • a <attrname>_delete_at method, deletes a value at the given index

This protection of the underlying array is needed for two reasons:

  1. so ruby-jss knows when changes are made and need to be saved

  2. so that validation can be performed on values added to the array.

enum: [Constant -> Array ]


This is a constant defined somewhere in the Jamf module. The constant must contain an Array of values, usually Strings. You may or may not choose to define the array members as constants themselves.

Example: > Attribute `:type` has enum: Jamf::ExtentionAttribute::DATA_TYPES > > The constant Jamf::ExtentionAttribute::DATA_TYPES is defined thus: > > DATA_TYPE_STRING = 'STRING'.freeze > DATA_TYPE_INTEGER = 'INTEGER'.freeze > DATA_TYPE_DATE = 'DATE'.freeze > > DATA_TYPES = [ > DATA_TYPE_STRING, > DATA_TYPE_INTEGER, > DATA_TYPE_DATE, > ] > > When setting the type attribute via `#type = newval`, > `Jamf::ExtentionAttribute::DATA_TYPES.include? newval` must be true >

Setters for attributes with an enum require that the new value is a member of the array as seen above. When using such setters, If you defined the array members as constants themselves, it is wise to use those rather than a different but identical string, however either will work. In other words, this:

my_ea.dataType = Jamf::ExtentionAttribute::DATA_TYPE_INTEGER

is preferred over:

my_ea.dataType = 'INTEGER'

since the second version creates a new string in memory, but the first uses the one already stored in a constant.

See also: [Data Validation](#data_validation) below.

validator: [Symbol]


(ignored if readonly: is true, or if enum: is set)

The symbol is the name of a Jamf::Validators class method used in the setter to validate new values for this attribute. It only is used when class: is :string, :integer, :boolean, and :float

If omitted, the setter will take any value passed to it, which is generally unwise.

When the class: is an actual class, the setter will instantiate a new one with the value to be set, and validation is handled by the class itself.

Example: > If the `class:` for an attrib named ':releaseDate' is class: Jamf::Timestamp > then the setter method will look like this: > > def releaseDate=(newval) > newval = Jamf::Timestamp.new newval unless newval.is_a? Jamf::Timestamp > # ^^^ This will validate newval > return if newval == @releaseDate > @releaseDate = newval > @need_to_update = true > end

see also: [Data Validation](#data_validation) below.

aliases: [Array of Symbols]


Other names for this attribute. If provided, getters, and setters will be made for all aliases. Should be used very sparingly.

Attributes of class :boolean automatically have a getter alias ending with a '?'.

filter_key: [Boolean]


For subclasses of CollectionResource, GETting the main endpoint will return the entire collection. Some of these endpoints support RSQL filters to return only those objects that match the filter. If this attribute can be used as a field for filtering, set filter_key: to true, and filters will be used where possible to optimize GET requests.

Documenting your code


For documenting attributes with YARD, put this above each attribute name key:

“`

# @!attribute <attrname>
#   @param [Class] <Describe setter value if needed>
#   @return [Class] <Describe value if needed>

“`

If the value is readonly, remove the @param line, and add [r], like this:

“`

# @!attribute [r] <attrname

“`

for more info see www.rubydoc.info/gems/yard/file/docs/Tags.md#attribute

#### Sub-subclassing

If you need to subclass a subclass of JSONObject, and the new subclass needs to expand on the OBJECT_MODEL in its parent, then you must use Hash#merge to combine them in the subclass. Here's an example of ComputerPrestage which inherits from Prestage:

class ComputerPrestage < Jamf::Prestage

   OBJECT_MODEL = superclass::OBJECT_MODEL.merge(

         newAttr: {
           [attr details]
         }

     ).freeze

#### Data Validation #data_validation

Attributes that are not readonly are subject to data validation when values are assigned. How that validation happens depends on the definition of the attribute as described above. Validation failure will raise an exception, usually Jamf::InvalidDataError.

Only one value-validation is applied, depending on the attribute definition:

  • If the attribute is defined with a specific validator, the value is passed to that validator, and other validators are ignored

  • If the attribute is defined with an enum, the value must be a value of the enum.

  • If the attribute is defined as a :string, :integer, :float or :bool without an enum or validator, it is confirmed to be the correct type

  • If the attribute is defined to hold a :j_id, the Validate.j_id method is used, it must be an integer or integer-in-string

  • If the attribute is defined to hold a JAMF class, (e.g. Jamf::Timestamp) the class itself performs validation on the value when instantiated with the value.

  • Otherwise, the value is used unchanged with no validation

Additionally:

  • If an attribute is an identifier, it must be unique in its class and API connection.

  • If an attribute is required, it may not be nil or empty

  • If an attribute is :multi, the value must be an array and each member value is validated individually

### Constructor / Instantiation #constructor

The .new method should rarely (never?) be called directly for any JSONObject class.

The Resource classes are instantiated via the .fetch and .create methods.

Other JSONObject classes are embedded inside the Resource classes and are instantiated while parsing data from the API or by the setters for the attributes holding them.

When subclassing JSONObject, you can often just use the #initialize defined here. You may want to override #initialize to accept different kinds of data and if you do, you must:

  • Have an #initialize method that takes two parameters and performs validation using them:

    1. A positional first parameter: the value used to create the instance Your method may accept any kind of value, as long as it can use it to create a valid object. At the very least it must accept a Hash that comes from the API for this object. If you call `super` then that Hash must be passed.

      For example, Jamf::GenericReference, which defines references to other resources, such as Buildings, can take a Hash containing the name: and id: of the building (as provided by the API), or can take just a name or id, or can take a Jamf::Building object.

      The initialize method must perform validation as necessary and raise an exception if the data provided is not acceptable.

    2. A keyword parameter `cnx:` containing a Jamf::Connection instance. This is the API connection through which this JSON object interacts with the appropriate Jamf Pro server. Usually this is used to validate the data recieved in the first positional parameter.

### Required Instance Methods

Subclasses of JSONObject must have a #to_jamf method. For most simple objects, the one defined in JSONObject will work as is.

If you need to override it, it must

  • Return a Hash that can be used in the data sent back to the API.

  • Not call #.to_json. All conversion to and from JSON happens in the Jamf::Connection class.

Constant Summary collapse

JSON_TYPE_CLASSES =

These classes are used from JSON in the raw

%i[string integer float boolean].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data, cnx: Jamf.cnx) ⇒ JSONObject

Make an instance. Data comes from the API

Parameters:

  • data (Hash)

    the data for constructing a new object.

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

    the API connection for the object

Raises:



850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# File 'lib/jamf/api/base_classes/json_object.rb', line 850

def initialize(data, cnx: Jamf.cnx)
  raise Jamf::InvalidDataError, 'Invalid JSONObject data - must be a Hash' unless data.is_a? Hash

  @cnx = cnx
  @unsaved_changes = {} if self.class.mutable?

  creating = data.delete :creating_from_create

  if creating
    self.class::OBJECT_MODEL.keys.each do |attr_name|
      next unless data.key? attr_name
      # use our setters for each value so that they are in the unsaved changes
      send "#{attr_name}=", data[attr_name]
    end
    return
  end

  parse_init_data data
end

Class Method Details

.allocate(*args, &block) ⇒ Object Originally defined in module BaseClass

Can't allocate if base class

.attr_key_for_alias(als) ⇒ Symbol?

Given a Symbol that might be an alias of a key fron OBJECT_MODEL return the real key

e.g. if OBJECT_MODEL has an entry like this:

displayName: { aliases: [:name, :display_name] }

Then

attr_key_for_alias(:name) and attr_key_for_alias(:display_name)

will return :displayName

Returns nil if no such alias exists.

Parameters:

  • als (Symbol)

    the alias to look up

Returns:

  • (Symbol, nil)

    The real object model key for the alias



555
556
557
558
559
# File 'lib/jamf/api/base_classes/json_object.rb', line 555

def self.attr_key_for_alias(als)
  stop_if_base_class
  self::OBJECT_MODEL.each { |k, deets| return k if k == als || deets[:aliases].to_a.include?(als) }
  nil
end

.base_class?Boolean Originally defined in module BaseClass

Returns:

  • (Boolean)

.define_predicates(attr_name) ⇒ Object

create the default aliases for booleans



624
625
626
# File 'lib/jamf/api/base_classes/json_object.rb', line 624

def self.define_predicates(attr_name)
  alias_method("#{attr_name}?", attr_name)
end

.mutable?Boolean

By default, JSONObjects (as a whole) are mutable, although some attributes may not be (see OBJECT_MODEL in the JSONObject docs)

When an entire sublcass of JSONObject is read-only/immutable, `extend Jamf::Immutable`, which will override this to return false. Doing so will prevent any setters from being created for the subclass and will cause Jamf::Resource.save to raise an error

Returns:

  • (Boolean)


529
530
531
# File 'lib/jamf/api/base_classes/json_object.rb', line 529

def self.mutable?
  true
end

.new(*args, &block) ⇒ Object Originally defined in module BaseClass

Can't instantiate if base_class

.required_attributesObject

An array of attribute names that are required when making new instances See the OBJECT_MODEL documentation in Jamf::JSONObject



536
537
538
# File 'lib/jamf/api/base_classes/json_object.rb', line 536

def self.required_attributes
  self::OBJECT_MODEL.select { |_attr, deets| deets[:required] }.keys
end

.stop_if_base_class(action = DEFAULT_ACTION) ⇒ Object Originally defined in module BaseClass

raise an exception if this class is a base class

Instance Method Details

#clear_unsaved_changesObject



908
909
910
911
912
913
914
915
916
917
918
919
920
921
# File 'lib/jamf/api/base_classes/json_object.rb', line 908

def clear_unsaved_changes
  return unless self.class.mutable?

  unsaved_changes.keys.each do |attr_name|
    attrib_val = instance_variable_get "@#{attr_name}"
    if self.class::OBJECT_MODEL[attr_name][:multi]
      attrib_val.each { |item| item.send :clear_unsaved_changes if item.respond_to? :clear_unsaved_changes }
    elsif attrib_val.respond_to? :clear_unsaved_changes
      attrib_val.send :clear_unsaved_changes
    end
  end
  ext_attrs_clear_unsaved_changes if self.class.include? Jamf::Extendable
  @unsaved_changes = {}
end

#pretty_jamf_jsonObject

Print the JSON version of the to_jamf outout mostly for debugging/troubleshooting



981
982
983
# File 'lib/jamf/api/base_classes/json_object.rb', line 981

def pretty_jamf_json
  puts JSON.pretty_generate(to_jamf)
end

#pretty_print_instance_variablesArray

Remove large cached items from the instance_variables used to create pretty-print (pp) output.

Returns:

  • (Array)

    the desired instance_variables



991
992
993
994
995
# File 'lib/jamf/api/base_classes/json_object.rb', line 991

def pretty_print_instance_variables
  vars = super.sort
  vars.delete :@cnx
  vars
end

#to_jamfHash

Returns The data to be sent to the API, as a Hash to be converted to JSON by the Jamf::Connection.

Returns:

  • (Hash)

    The data to be sent to the API, as a Hash to be converted to JSON by the Jamf::Connection



926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
# File 'lib/jamf/api/base_classes/json_object.rb', line 926

def to_jamf
  data = {}
  self.class::OBJECT_MODEL.each do |attr_name, attr_def|

    raw_value = instance_variable_get "@#{attr_name}"

    # If its a multi-value attribute, process it and  go on
    if attr_def[:multi]
      data[attr_name] = multi_to_jamf(raw_value, attr_def)
      next
    end

    # if its a single-value object, process it and go on.
    cooked_value = single_to_jamf(raw_value, attr_def)
    # next if cooked_value.nil? # ignore nil
    data[attr_name] = cooked_value
  end # unsaved_changes.each
  data
end

#to_jamf_changes_onlyHash

Only works for PATCH endpoints.

Returns:

  • (Hash)

    The changes that need to be sent to the API, as a Hash to be converted to JSON by the Jamf::Connection



951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
# File 'lib/jamf/api/base_classes/json_object.rb', line 951

def to_jamf_changes_only
  return unless self.class.mutable?

  data = {}
  unsaved_changes.each do |attr_name, changes|
    attr_def = self.class::OBJECT_MODEL[attr_name]

    # readonly attributes can't be changed
    next if attr_def[:readonly]

    # here's the new value for this attribute
    raw_value = changes[:new]

    # If its a multi-value attribute, process it and  go on
    if attr_def[:multi]
      data[attr_name] = multi_to_jamf(raw_value, attr_def)
      next
    end

    # if its a single-value object, process it and go on.
    cooked_value = single_to_jamf(raw_value, attr_def)
    next if cooked_value.nil? # ignore nil

    data[attr_name] = cooked_value
  end # unsaved_changes.each
  data
end

#unsaved_changesObject

a hash of all unsaved changes, including embedded JSONObjects



875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
# File 'lib/jamf/api/base_classes/json_object.rb', line 875

def unsaved_changes
  return {} unless self.class.mutable?

  changes = @unsaved_changes.dup

  self.class::OBJECT_MODEL.each do |attr_name, attr_def|
    # skip non-Class attrs
    next unless attr_def[:class].is_a? Class

    # the current value of the thing, e.g. a Location
    # which may have unsaved changes
    value = instance_variable_get "@#{attr_name}"

    # skip those that don't have any changes
    next unless value.respond_to? :unsaved_changes?
    attr_changes = value.unsaved_changes
    next if attr_changes.empty?

    # add the sub-changes to ours
    changes[attr_name] = attr_changes
  end
  changes[:ext_attrs] = ext_attrs_unsaved_changes if self.class.include? Jamf::Extendable
  changes
end

#unsaved_changes?Boolean

return true if we or any of our attributes have unsaved changes

Returns:

  • (Boolean)


902
903
904
905
906
# File 'lib/jamf/api/base_classes/json_object.rb', line 902

def unsaved_changes?
  return false unless self.class.mutable?

  !unsaved_changes.empty?
end