Class: Jamf::JSONObject Abstract
- Extended by:
- BaseClass
- Defined in:
- lib/jamf/api/base_classes/json_object.rb
Overview
# 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:
-
so ruby-jss knows when changes are made and need to be saved
-
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:
-
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.
-
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.
Direct Known Subclasses
APIError, APIErrorDetail, ChangeLogEntry, Country, DeviceEnrollmentDevice, DeviceEnrollmentDeviceSyncState, DeviceEnrollmentSyncStatus, InventoryPreloadExtensionAttribute, Locale, MobileDevicePrestageName, MobileDevicePrestageNames, PrestageAssignment, PrestageLocation, PrestagePurchasingData, PrestageScope, PrestageSyncStatus, Resource, Searchable::OrderBy, Searchable::SeachParams, TimeZone
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
-
.allocate(*args, &block) ⇒ Object
extended
from 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.
- .base_class? ⇒ Boolean extended from BaseClass
-
.define_predicates(attr_name) ⇒ Object
create the default aliases for booleans.
-
.mutable? ⇒ Boolean
By default, JSONObjects (as a whole) are mutable, although some attributes may not be (see OBJECT_MODEL in the JSONObject docs).
-
.new(*args, &block) ⇒ Object
extended
from BaseClass
Can't instantiate if base_class.
-
.required_attributes ⇒ Object
An array of attribute names that are required when making new instances See the OBJECT_MODEL documentation in JSONObject.
-
.stop_if_base_class(action = DEFAULT_ACTION) ⇒ Object
extended
from BaseClass
raise an exception if this class is a base class.
Instance Method Summary collapse
- #clear_unsaved_changes ⇒ Object
-
#initialize(data, cnx: Jamf.cnx) ⇒ JSONObject
constructor
Make an instance.
-
#pretty_jamf_json ⇒ Object
Print the JSON version of the to_jamf outout mostly for debugging/troubleshooting.
-
#pretty_print_instance_variables ⇒ Array
Remove large cached items from the instance_variables used to create pretty-print (pp) output.
-
#to_jamf ⇒ Hash
The data to be sent to the API, as a Hash to be converted to JSON by the Jamf::Connection.
-
#to_jamf_changes_only ⇒ Hash
Only works for PATCH endpoints.
-
#unsaved_changes ⇒ Object
a hash of all unsaved changes, including embedded JSONObjects.
-
#unsaved_changes? ⇒ Boolean
return true if we or any of our attributes have unsaved changes.
Constructor Details
#initialize(data, cnx: Jamf.cnx) ⇒ JSONObject
Make an instance. Data comes from the API
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.
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
.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
529 530 531 |
# File 'lib/jamf/api/base_classes/json_object.rb', line 529 def self.mutable? true end |
.required_attributes ⇒ Object
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 |
Instance Method Details
#clear_unsaved_changes ⇒ Object
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_json ⇒ Object
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_variables ⇒ Array
Remove large cached items from the instance_variables used to create pretty-print (pp) output.
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_jamf ⇒ Hash
Returns 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_only ⇒ Hash
Only works for PATCH endpoints.
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_changes ⇒ Object
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
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 |