ruby-jss 1.2.3 → 1.2.4a1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-jss might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/jamf.rb +169 -0
- data/lib/jamf/api/abstract_classes/collection_resource.rb +422 -0
- data/lib/jamf/api/abstract_classes/generic_reference.rb +145 -0
- data/lib/jamf/api/abstract_classes/json_object.rb +1074 -0
- data/lib/jamf/api/abstract_classes/prestage.rb +219 -0
- data/lib/jamf/api/abstract_classes/prestage_skip_setup_items.rb +126 -0
- data/lib/jamf/api/abstract_classes/resource.rb +250 -0
- data/lib/jamf/api/abstract_classes/singleton_resource.rb +87 -0
- data/lib/jamf/api/attribute_classes/ip_address.rb +66 -0
- data/lib/jamf/api/attribute_classes/timestamp.rb +144 -0
- data/lib/jamf/api/connection.rb +734 -0
- data/lib/jamf/api/connection/api_error.rb +111 -0
- data/lib/jamf/api/connection/api_error_styleguide.rb +96 -0
- data/lib/jamf/api/connection/token.rb +220 -0
- data/lib/jamf/api/json_objects/account_prefs.rb +79 -0
- data/lib/jamf/api/json_objects/android_details.rb +139 -0
- data/lib/jamf/api/json_objects/appletv_details.rb +110 -0
- data/lib/jamf/api/json_objects/attachment.rb +68 -0
- data/lib/jamf/api/json_objects/cellular_network.rb +151 -0
- data/lib/jamf/api/json_objects/change_log_entry.rb +77 -0
- data/lib/jamf/api/json_objects/computer_prestage_skip_setup_items.rb +67 -0
- data/lib/jamf/api/json_objects/country.rb +51 -0
- data/lib/jamf/api/json_objects/extension_attribute_value.rb +128 -0
- data/lib/jamf/api/json_objects/installed_application.rb +59 -0
- data/lib/jamf/api/json_objects/installed_certificate.rb +53 -0
- data/lib/jamf/api/json_objects/installed_configuration_profile.rb +67 -0
- data/lib/jamf/api/json_objects/installed_ebook.rb +58 -0
- data/lib/jamf/api/json_objects/installed_provisioning_profile.rb +59 -0
- data/lib/jamf/api/json_objects/inventory_preload_extension_attribute.rb +52 -0
- data/lib/jamf/api/json_objects/ios_details.rb +244 -0
- data/lib/jamf/api/json_objects/location.rb +95 -0
- data/lib/jamf/api/json_objects/md_prestage_name.rb +57 -0
- data/lib/jamf/api/json_objects/md_prestage_names.rb +82 -0
- data/lib/jamf/api/json_objects/md_prestage_skip_setup_items.rb +165 -0
- data/lib/jamf/api/json_objects/mobile_device_details.rb +219 -0
- data/lib/jamf/api/json_objects/mobile_device_security.rb +101 -0
- data/lib/jamf/api/json_objects/prestage_assignment.rb +61 -0
- data/lib/jamf/api/json_objects/prestage_location.rb +104 -0
- data/lib/jamf/api/json_objects/prestage_purchasing_data.rb +132 -0
- data/lib/jamf/api/json_objects/prestage_scope.rb +54 -0
- data/lib/jamf/api/json_objects/prestage_sync_status.rb +63 -0
- data/lib/jamf/api/json_objects/purchasing_data.rb +125 -0
- data/lib/jamf/api/mixins/abstract.rb +58 -0
- data/lib/jamf/api/mixins/bulk_deletable.rb +39 -0
- data/lib/jamf/api/mixins/change_log.rb +136 -0
- data/lib/jamf/api/mixins/extendable.rb +75 -0
- data/lib/jamf/api/mixins/immutable.rb +39 -0
- data/lib/jamf/api/mixins/locatable.rb +124 -0
- data/lib/jamf/api/mixins/lockable.rb +48 -0
- data/lib/jamf/api/mixins/referable.rb +92 -0
- data/lib/jamf/api/mixins/searchable.rb +202 -0
- data/lib/jamf/api/mixins/uncreatable.rb +40 -0
- data/lib/jamf/api/mixins/undeletable.rb +40 -0
- data/lib/jamf/api/resources/collection_resources/account.rb +163 -0
- data/lib/jamf/api/resources/collection_resources/building.rb +114 -0
- data/lib/jamf/api/resources/collection_resources/category.rb +82 -0
- data/lib/jamf/api/resources/collection_resources/computer.rb +49 -0
- data/lib/jamf/api/resources/collection_resources/computer_prestage.rb +80 -0
- data/lib/jamf/api/resources/collection_resources/department.rb +79 -0
- data/lib/jamf/api/resources/collection_resources/extension_attribute.rb +45 -0
- data/lib/jamf/api/resources/collection_resources/inventory_preload_record.rb +274 -0
- data/lib/jamf/api/resources/collection_resources/md_prestage.rb +139 -0
- data/lib/jamf/api/resources/collection_resources/mobile_device.rb +315 -0
- data/lib/jamf/api/resources/collection_resources/script.rb +190 -0
- data/lib/jamf/api/resources/collection_resources/site.rb +77 -0
- data/lib/jamf/api/resources/singleton_resources/app_store_country_codes.rb +131 -0
- data/lib/jamf/api/resources/singleton_resources/authorization.rb +88 -0
- data/lib/jamf/api/resources/singleton_resources/client_checkin_settings.rb +139 -0
- data/lib/jamf/api/resources/singleton_resources/reenrollment_settings.rb +95 -0
- data/lib/jamf/client.rb +301 -0
- data/lib/jamf/client/jamf_binary.rb +132 -0
- data/lib/jamf/client/jamf_helper.rb +298 -0
- data/lib/jamf/client/management_action.rb +114 -0
- data/lib/jamf/compatibility.rb +88 -0
- data/lib/jamf/composer.rb +190 -0
- data/lib/jamf/configuration.rb +281 -0
- data/lib/jamf/exceptions.rb +107 -0
- data/lib/jamf/ruby_extensions.rb +36 -0
- data/lib/jamf/ruby_extensions/array.rb +35 -0
- data/lib/jamf/ruby_extensions/array/predicates.rb +46 -0
- data/lib/jamf/ruby_extensions/array/utils.rb +47 -0
- data/lib/jamf/ruby_extensions/filetest.rb +32 -0
- data/lib/jamf/ruby_extensions/filetest/predicates.rb +46 -0
- data/lib/jamf/ruby_extensions/hash.rb +33 -0
- data/lib/jamf/ruby_extensions/hash/backports.rb +92 -0
- data/lib/jamf/ruby_extensions/ipaddr.rb +37 -0
- data/lib/jamf/ruby_extensions/ipaddr/utils.rb +95 -0
- data/lib/jamf/ruby_extensions/object.rb +30 -0
- data/lib/jamf/ruby_extensions/object/predicates.rb +51 -0
- data/lib/jamf/ruby_extensions/pathname.rb +39 -0
- data/lib/jamf/ruby_extensions/pathname/predicates.rb +50 -0
- data/lib/jamf/ruby_extensions/pathname/utils.rb +75 -0
- data/lib/jamf/ruby_extensions/string.rb +35 -0
- data/lib/jamf/ruby_extensions/string/backports.rb +66 -0
- data/lib/jamf/ruby_extensions/string/conversions.rb +65 -0
- data/lib/jamf/ruby_extensions/string/predicates.rb +47 -0
- data/lib/jamf/utility.rb +423 -0
- data/lib/jamf/validate.rb +224 -0
- data/lib/jamf/version.rb +32 -0
- data/lib/jpapi.rb +26 -0
- data/lib/jss/version.rb +1 -1
- metadata +104 -4
@@ -0,0 +1,145 @@
|
|
1
|
+
# Copyright 2019 Pixar
|
2
|
+
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "Apache License")
|
5
|
+
# with the following modification; you may not use this file except in
|
6
|
+
# compliance with the Apache License and the following modification to it:
|
7
|
+
# Section 6. Trademarks. is deleted and replaced with:
|
8
|
+
#
|
9
|
+
# 6. Trademarks. This License does not grant permission to use the trade
|
10
|
+
# names, trademarks, service marks, or product names of the Licensor
|
11
|
+
# and its affiliates, except as required to comply with Section 4(c) of
|
12
|
+
# the License and to reproduce the content of the NOTICE file.
|
13
|
+
#
|
14
|
+
# You may obtain a copy of the Apache License at
|
15
|
+
#
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
17
|
+
#
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
19
|
+
# distributed under the Apache License with the above modification is
|
20
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
21
|
+
# KIND, either express or implied. See the Apache License for the specific
|
22
|
+
# language governing permissions and limitations under the Apache License.
|
23
|
+
#
|
24
|
+
#
|
25
|
+
|
26
|
+
module Jamf
|
27
|
+
|
28
|
+
# This class is a reference to an individual API object from some other
|
29
|
+
# API object.
|
30
|
+
#
|
31
|
+
# This class is subclassed automatically when the {Jamf::Referable} module
|
32
|
+
# is included into a class.
|
33
|
+
#
|
34
|
+
# See {Jamf::Referable} for how to use the subclasses of GenericReference.
|
35
|
+
#
|
36
|
+
# Subclasses must define:
|
37
|
+
#
|
38
|
+
# REFERENT_CLASS - the full class to which this is a reference
|
39
|
+
# e.g. for BuildingReference it would be Jamf::Building
|
40
|
+
#
|
41
|
+
# Defining REFERENT_CLASS is handled automatically by including the
|
42
|
+
# Referable module
|
43
|
+
#
|
44
|
+
# @abstract
|
45
|
+
#
|
46
|
+
class GenericReference < Jamf::JSONObject
|
47
|
+
|
48
|
+
extend Jamf::Abstract
|
49
|
+
|
50
|
+
# Constants
|
51
|
+
#####################################
|
52
|
+
|
53
|
+
OBJECT_MODEL = {
|
54
|
+
|
55
|
+
id: {
|
56
|
+
class: :integer,
|
57
|
+
identifier: :primary,
|
58
|
+
readonly: true
|
59
|
+
},
|
60
|
+
|
61
|
+
name: {
|
62
|
+
class: :string,
|
63
|
+
readonly: true
|
64
|
+
}
|
65
|
+
|
66
|
+
}.freeze
|
67
|
+
parse_object_model
|
68
|
+
|
69
|
+
# Constructor
|
70
|
+
#####################################
|
71
|
+
|
72
|
+
# Make a new reference to an API CollectionResource Object
|
73
|
+
#
|
74
|
+
# The data parameter can be one of:
|
75
|
+
#
|
76
|
+
# 1) A Hash with an :id and :name
|
77
|
+
# This is mostly used automatically when parsing fetched API data.
|
78
|
+
# When some attribute of an OBJECT_MODEL has `class: Someclass::Reference`
|
79
|
+
# the JSON hash from the API will be passed as the data param.
|
80
|
+
#
|
81
|
+
# e.g.
|
82
|
+
# - Policy::OBJECT_MODEL[:category][:class] is Jamf::Category::Reference
|
83
|
+
# - the policy JSON from the api might contain `category: { id: 234, name: 'foobar' }`
|
84
|
+
# - that hash will be passed into Jamf::Category::Reference.new, and the
|
85
|
+
# resulting instance used as the value of the policy's :category attribute.
|
86
|
+
#
|
87
|
+
# 2) An instance of the REFERENT_CLASS.
|
88
|
+
# This can be used to make a reference to some specific instance of
|
89
|
+
# the referent class.
|
90
|
+
#
|
91
|
+
# e.g. if you have an instance of Category in the variable `my_cat`
|
92
|
+
# then `ref_to_my_cat = Category::Reference.new my_cat` will work as
|
93
|
+
# expected.
|
94
|
+
#
|
95
|
+
# 3) A valid identifier for an existing REFERENT_CLASS in the JSS.
|
96
|
+
# The given value will be used with the REFERENT_CLASS's .valid_id method
|
97
|
+
# to see if there's a matching instance, which the reference refers to.
|
98
|
+
#
|
99
|
+
# e.g. `ref_to_my_cat = Category::Reference.new 12` creates a reference
|
100
|
+
# to Category id 12, and `ref_to_my_cat = Category::Reference.new 'foo'`
|
101
|
+
# creates a reference to the category named 'foo' - assuming they exist.
|
102
|
+
#
|
103
|
+
# The last two of these are commonly used with setters for attributes that
|
104
|
+
# have class: <some reference class>
|
105
|
+
#
|
106
|
+
# e.g. setting the category of a policy when
|
107
|
+
# Policy::OBJECT_MODEL[:category] is Category::Reference
|
108
|
+
#
|
109
|
+
# `mypolicy.category = a_cat` # a_cat is a Category instance
|
110
|
+
# `mypolicy.category = 12` # use categoty id 12
|
111
|
+
# `mypolicy.category = 'foo'` # use categoty named 'foo'
|
112
|
+
#
|
113
|
+
#
|
114
|
+
#
|
115
|
+
# @param data[Hash,CollectionResource,String,Integer]
|
116
|
+
#
|
117
|
+
def initialize(data, cnx: Jamf.cnx)
|
118
|
+
ref_class = self.class::REFERENT_CLASS
|
119
|
+
case data
|
120
|
+
when Hash
|
121
|
+
super
|
122
|
+
when ref_class
|
123
|
+
raise Jamf::InvalidDataError, "Provided #{ref_class} hasn't been created" unless data.exist?
|
124
|
+
@id = data.id
|
125
|
+
@name = data.name if data.respond_to? :name
|
126
|
+
@cnx = data.cnx
|
127
|
+
when nil
|
128
|
+
@id = nil
|
129
|
+
@name = nil
|
130
|
+
@cnx = cnx
|
131
|
+
else
|
132
|
+
@id = ref_class.valid_id data, cnx: cnx
|
133
|
+
raise "No matching #{ref_class}" unless @id
|
134
|
+
@name = ref_class.map_all(:id, to: :name, cnx: cnx)[@id]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_jamf
|
139
|
+
return nil if @id.nil?
|
140
|
+
{ id: @id }
|
141
|
+
end
|
142
|
+
|
143
|
+
end # class
|
144
|
+
|
145
|
+
end # module
|
@@ -0,0 +1,1074 @@
|
|
1
|
+
# Copyright 2019 Pixar
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "Apache License")
|
4
|
+
# with the following modification; you may not use this file except in
|
5
|
+
# compliance with the Apache License and the following modification to it:
|
6
|
+
# Section 6. Trademarks. is deleted and replaced with:
|
7
|
+
#
|
8
|
+
# 6. Trademarks. This License does not grant permission to use the trade
|
9
|
+
# names, trademarks, service marks, or product names of the Licensor
|
10
|
+
# and its affiliates, except as required to comply with Section 4(c) of
|
11
|
+
# the License and to reproduce the content of the NOTICE file.
|
12
|
+
#
|
13
|
+
# You may obtain a copy of the Apache License at
|
14
|
+
#
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
16
|
+
#
|
17
|
+
# Unless required by applicable law or agreed to in writing, software
|
18
|
+
# distributed under the Apache License with the above modification is
|
19
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
20
|
+
# KIND, either express or implied. See the Apache License for the specific
|
21
|
+
# language governing permissions and limitations under the Apache License.
|
22
|
+
#
|
23
|
+
#
|
24
|
+
|
25
|
+
# The module
|
26
|
+
module Jamf
|
27
|
+
|
28
|
+
# Classes
|
29
|
+
#####################################
|
30
|
+
|
31
|
+
# # Jamf::JSONObject
|
32
|
+
#
|
33
|
+
# In JSON & Javascript, an 'object' is a data structure equivalent to a
|
34
|
+
# Hash in Ruby. Much of the JSON data exchaged with the API is formatted as
|
35
|
+
# these JSON objects.
|
36
|
+
#
|
37
|
+
# Jamf::JSONObject is a meta class that provides a way to convert those JSON
|
38
|
+
# 'objects' into not just Hashes (that's done by the Jamf::Connection) but
|
39
|
+
# into full-fledged ruby Classes. Once implemented in ruby-jss, all JSON
|
40
|
+
# objects (Hashes) used anywhere in the Jamf Pro API have a matching Class in
|
41
|
+
# ruby-jss which is a subclass of Jamf::JSONObject
|
42
|
+
#
|
43
|
+
# The Jamf::JSONObject class is abstract, and cannot be instantiated or used
|
44
|
+
# directly. It merely provides the common functionality needed for dealing
|
45
|
+
# with all JSON objects in the API.
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# ## Subclassing
|
49
|
+
#
|
50
|
+
# When implementing a JSON object in the API as a class in ruby-jss,
|
51
|
+
# you will make a subclass of either Jamf::JSONObject, Jamf::SingletonResource
|
52
|
+
# or Jamf::CollectionResource.
|
53
|
+
#
|
54
|
+
# Here's the relationship between these MetaClasses:
|
55
|
+
#
|
56
|
+
# Jamf::JSONObject
|
57
|
+
# (abstract)
|
58
|
+
# |
|
59
|
+
# |
|
60
|
+
# -----------------------
|
61
|
+
# | |
|
62
|
+
# Jamf::Resource |
|
63
|
+
# (abstract) |
|
64
|
+
# | |
|
65
|
+
# | |
|
66
|
+
# | Jamf::Computer::Reference
|
67
|
+
# | Jamf::Location
|
68
|
+
# | Jamf::ChangeLog::Entry
|
69
|
+
# | (more non-resource JSON object classes)
|
70
|
+
# |
|
71
|
+
# |----------------------------------------
|
72
|
+
# | |
|
73
|
+
# | |
|
74
|
+
# Jamf::SingletonResource Jamf::CollectionResource
|
75
|
+
# (abstract) (abstract)
|
76
|
+
# | |
|
77
|
+
# | |
|
78
|
+
# Jamf::Settings::ReEnrollment Jamf::Computer
|
79
|
+
# Jamf::Settings::SelfService Jamf::Building
|
80
|
+
# Jamf::SystemInfo Jamf::PatchPolicy
|
81
|
+
# (more singleton resource classes) (more collection resource classes)
|
82
|
+
#
|
83
|
+
#
|
84
|
+
# Direct descendents of Jamf::JSONObject are arbitrary JSON objects that
|
85
|
+
# appear inside other objects, e.g. the Location data for a computer,
|
86
|
+
# or a reference to a building.
|
87
|
+
#
|
88
|
+
# {Jamf::Resource} classes represent direct resources of the API, i.e. items
|
89
|
+
# accessible with a URL. The ability to interact with those URLs is defined in
|
90
|
+
# the metaclass Jamf::Resource, and all resources must define a RSRC_VERSION
|
91
|
+
# and a RSRC_PATH. See {Jamf::Resource} for more info.
|
92
|
+
#
|
93
|
+
# There are two kinds of resources in the API:
|
94
|
+
#
|
95
|
+
# {Jamf::SingletonResource} classes represent objects in the API that have
|
96
|
+
# only one instance, such as various settings, or server-wide state. These
|
97
|
+
# objects cannot be created or deleted, only fetched and updated.
|
98
|
+
#
|
99
|
+
# {Jamf::CollectionResource} classes represent collections of objects in the
|
100
|
+
# API. These resources can list all of their members, and individual members
|
101
|
+
# can be retrieved, updated, created and deleted.
|
102
|
+
#
|
103
|
+
# Subclasses need to meet the requirements for all of their ancestors,
|
104
|
+
# so once you decide which one you're subclassing, be sure to read the docs
|
105
|
+
# for each one. E.g. to implement Jamf::Package, it will be a
|
106
|
+
# {Jamf::CollectionResource}, which is a {Jamf::Resource}, which is a
|
107
|
+
# {Jamf::JSONObject}, and the requirements for all must be met.
|
108
|
+
#
|
109
|
+
# The remainder of this page documents the requirements and details of
|
110
|
+
# Jamf::JSONObject.
|
111
|
+
#
|
112
|
+
#
|
113
|
+
# NOTES:
|
114
|
+
#
|
115
|
+
# - subclasses may define more methods, include mix-ins, and if
|
116
|
+
# needed can override methods defined in metaclasses. Please read the
|
117
|
+
# docs before overriding.
|
118
|
+
#
|
119
|
+
# - Throughout the documentation 'parsed JSON object' means the result of running
|
120
|
+
# a raw JSON string thru `JSON.parse raw_json, symbolize_names: true`. This
|
121
|
+
# is performed in the {Jamf::Connection} methods which interact with the API:
|
122
|
+
# {Jamf::Connection#get}, {Jamf::Connection#post}, {Jamf::Connection#put}
|
123
|
+
# {Jamf::Connection#patch} and {Jamf::Connection#delete}.
|
124
|
+
#
|
125
|
+
# - Related to the above, the {Jamf::Connection} methods
|
126
|
+
# {Jamf::Connection#post} and {Jamf::Connection#put} call `#to_json` on the
|
127
|
+
# data passed to them, before sending it to the API. Subclasses and
|
128
|
+
# application code should never call #to_json anywhere. The data passed
|
129
|
+
# to put and post should be the output of `#to_jamf` on a Jamf::JSONObject,
|
130
|
+
# which is handled by the the #update and #create methods as needed.
|
131
|
+
#
|
132
|
+
#
|
133
|
+
# ###
|
134
|
+
#
|
135
|
+
# ### Required Constant: OBJECT_MODEL & call to parse_object_model
|
136
|
+
#
|
137
|
+
# Each descendent of JSONObject must define the constant OBJECT_MODEL, which
|
138
|
+
# is a Hash of Hashes that collectively define the top-level keys of the JSON
|
139
|
+
# object as attributes of the matching ruby class.
|
140
|
+
#
|
141
|
+
# Immediately after the definition of OBJECT_MODEL, the subclass *MUST* call
|
142
|
+
# `self.parse_object_model` to convert the model into actual ruby attributes
|
143
|
+
# with getters and setters.
|
144
|
+
#
|
145
|
+
# The OBJECT_MODEL Hash directly implements the matching JSON object model
|
146
|
+
# defined at https://developer.jamf.com/apis/jamf-pro-api/index and is used
|
147
|
+
# to automatically create attributes & accessor methods mirroring those
|
148
|
+
# in the API.
|
149
|
+
#
|
150
|
+
# The keys of the main hash are the symbolized names of the attributes as they
|
151
|
+
# come from the JSON fetched from the API.
|
152
|
+
#
|
153
|
+
# _ATTRIBUTE NAMES:_
|
154
|
+
#
|
155
|
+
# The attribute names in the Jamf Pro API JSON data are in 'lowerCamelCase'
|
156
|
+
# (https://en.wikipedia.org/wiki/Camel_case), and are used that way
|
157
|
+
# throughout the Jamf module in order to maintain consistency with the API
|
158
|
+
# itself. This differs from the ruby standard of using 'snake_case'
|
159
|
+
# (https://en.wikipedia.org/wiki/Snake_case) for attributes,
|
160
|
+
# methods, & local variables. I believe that maintaining consistency with the
|
161
|
+
# API we are mirroring is more important (and simpler) than conforming with
|
162
|
+
# ruby's community standards. I also believe that doing so is in-line with the
|
163
|
+
# ruby community's larger philosophy.
|
164
|
+
#
|
165
|
+
# "There's more than one way to do it" - because context matters.
|
166
|
+
# If that weren't true, I'd be writing Python.
|
167
|
+
#
|
168
|
+
# Each attribute key has a Hash of details defining how the attribute is
|
169
|
+
# used in the class. Getters and setters are created from these details, and
|
170
|
+
# they are used to parse incoming, and generate outgoing JSON data
|
171
|
+
#
|
172
|
+
# The possible keys of the details Hash for each attribute are:
|
173
|
+
#
|
174
|
+
# - class:
|
175
|
+
# - identfier:
|
176
|
+
# - required:
|
177
|
+
# - readonly:
|
178
|
+
# - multi:
|
179
|
+
# - enum:
|
180
|
+
# - validator:
|
181
|
+
# - aliases:
|
182
|
+
#
|
183
|
+
# For an example of an OBJECT_MODEL hash, see {Jamf::MobileDeviceDetails::OBJECT_MODEL}
|
184
|
+
#
|
185
|
+
# The details for each key's value are as follows. Note that omitting a
|
186
|
+
# boolean key is the same as setting it to false.
|
187
|
+
#
|
188
|
+
# class: \[Symbol or Class]
|
189
|
+
# -----------------
|
190
|
+
# This is the only required key for all attributes.
|
191
|
+
#
|
192
|
+
# Symbol is one of :string, :integer, :float, or :boolean
|
193
|
+
# These are the JSON data types that don't need parsing into ruby
|
194
|
+
# beyond that done by `JSON.parse`. When processing an attribute with one of
|
195
|
+
# these symbols as the `class:`, the JSON value is used as-is.
|
196
|
+
#
|
197
|
+
# When this is not a Symbol, it must be an actual class, such as
|
198
|
+
# Jamf::Timestamp or Jamf::PurchasingData.
|
199
|
+
#
|
200
|
+
# Classes used this way _must_:
|
201
|
+
#
|
202
|
+
# - Have an #initialize method that takes two parameters and performs
|
203
|
+
# validation on them:
|
204
|
+
#
|
205
|
+
# A first positional parameter, the value used to create the instance,
|
206
|
+
# which accepts, at the very least, the Parsed JSON data for the attribute.
|
207
|
+
# This can be a single value (e.g. a string for Jamf::Timestamp), or a Hash
|
208
|
+
# (e.g. for Jamf::Location), or whatever. Other values are
|
209
|
+
# allowed if your initialize method handles them properly.
|
210
|
+
#
|
211
|
+
# A keyword parameter `cnx:`. This can be ignored if not needed, but
|
212
|
+
# #initialize must accept it. If used, it will contain a Jamf::Connection
|
213
|
+
# object, either the one from which the first param came, or the one
|
214
|
+
# to which we'll be validating or creating a new object
|
215
|
+
#
|
216
|
+
# - Define a #to_jamf method that returns a value that can be used
|
217
|
+
# in the data sent back to the API. Subclasses of JSONObject already
|
218
|
+
# have this requirement, and the value is a Hash.
|
219
|
+
#
|
220
|
+
#
|
221
|
+
# Classes used in the class: value of an attribute definition are often
|
222
|
+
# also subclasses of JSONObject (e.g. Jamf::Location) but do not have to be
|
223
|
+
# as long as they conform to the standards above, e.g. Jamf::Timestamp.
|
224
|
+
#
|
225
|
+
# See also: [Data Validation](#data_validation) below.
|
226
|
+
#
|
227
|
+
#
|
228
|
+
# identifier: \[Boolean or Symbol :primary]
|
229
|
+
# -----------------
|
230
|
+
# Only applicable to descendents of Jamf::CollectionResource
|
231
|
+
#
|
232
|
+
# If true, this value must be unique among all members of the class in
|
233
|
+
# the JAMF, and can be used to look up objects.
|
234
|
+
#
|
235
|
+
# If the symbol :primary, this is the primary identifier, used in API
|
236
|
+
# resource paths for this particular object. Usually its the :id attribute,
|
237
|
+
# but for some objects may be some other attribute, e.g. for config-
|
238
|
+
# profiles, it would be a uuid.
|
239
|
+
#
|
240
|
+
#
|
241
|
+
# required: \[Boolean]
|
242
|
+
# -----------------
|
243
|
+
# If true, this attribute must be provided when creating a new local instance
|
244
|
+
# with .make, and cannot be nil or empty when sending a new instance to the
|
245
|
+
# API with \#create
|
246
|
+
#
|
247
|
+
#
|
248
|
+
# readonly: \[Boolean]
|
249
|
+
# -----------------
|
250
|
+
# If true, no setter method(s) will be created, and the value is not
|
251
|
+
# sent to the API with #create or #update
|
252
|
+
#
|
253
|
+
#
|
254
|
+
# multi: \[Boolean]
|
255
|
+
# -----------------
|
256
|
+
# When true, this value comes as a JSON array and its items are defined by
|
257
|
+
# the 'class:' setting described above. The JSON array is used
|
258
|
+
# to contstruct an attribute array of the correct kind of item.
|
259
|
+
#
|
260
|
+
# Example:
|
261
|
+
# > When `class:` is Jamf::Computer::Reference the incoming JSON array
|
262
|
+
# > of Hashes (computer references) will become an array of
|
263
|
+
# > Jamf::Computer::Reference instances.
|
264
|
+
#
|
265
|
+
# The stored array is not directly accessible, the getter will return a
|
266
|
+
# frozen duplicate of it.
|
267
|
+
#
|
268
|
+
# If not readonly, several setters are created:
|
269
|
+
#
|
270
|
+
# - a direct setter which takes an Array of 'class:', replacing the original
|
271
|
+
# - a <attrname>\_append method, appends a new value to the array,
|
272
|
+
# aliased as `<<`
|
273
|
+
# - a <attrname>\_prepend method, prepends a new value to the array
|
274
|
+
# - a <attrname>\_insert method, inserts a new value to the array
|
275
|
+
# at the given index
|
276
|
+
# - a <attrname>\_delete\_at method, deletes a value at the given index
|
277
|
+
#
|
278
|
+
# This protection of the underlying array is needed for two reasons:
|
279
|
+
#
|
280
|
+
# 1. so ruby-jss knows when changes are made and need to be saved
|
281
|
+
# 2. so that validation can be performed on values added to the array.
|
282
|
+
#
|
283
|
+
#
|
284
|
+
# enum: \[Constant -> Array<Constants> ]
|
285
|
+
# -----------------
|
286
|
+
# This is a constant defined somewhere in the Jamf module. The constant
|
287
|
+
# must contain an Array of other Constant values, usually Strings.
|
288
|
+
#
|
289
|
+
# Example:
|
290
|
+
# > Attribute `:type` has enum: Jamf::ExtentionAttribute::DATA_TYPES
|
291
|
+
# >
|
292
|
+
# > The constant Jamf::ExtentionAttribute::DATA_TYPES is defined thus:
|
293
|
+
# >
|
294
|
+
# > DATA_TYPE_STRING = 'STRING'.freeze
|
295
|
+
# > DATA_TYPE_INTEGER = 'INTEGER'.freeze
|
296
|
+
# > DATA_TYPE_DATE = 'DATE'.freeze
|
297
|
+
# >
|
298
|
+
# > DATA_TYPES = [
|
299
|
+
# > DATA_TYPE_STRING,
|
300
|
+
# > DATA_TYPE_INTEGER,
|
301
|
+
# > DATA_TYPE_DATE,
|
302
|
+
# > ]
|
303
|
+
# >
|
304
|
+
# > When setting the type attribute via `#type = newval`,
|
305
|
+
# > `Jamf::ExtentionAttribute::DATA_TYPES.include? newval` must be true
|
306
|
+
# >
|
307
|
+
#
|
308
|
+
# Setters for attributes with an enum require that the new value is
|
309
|
+
# a member of the array as seen above. When using such setters, its wise to
|
310
|
+
# use the array members themselves rather than a different but identical string,
|
311
|
+
# however either will work. In other words, this:
|
312
|
+
#
|
313
|
+
# my_ea.dataType = Jamf::ExtentionAttribute::DATA_TYPE_INTEGER
|
314
|
+
#
|
315
|
+
# is preferred over:
|
316
|
+
#
|
317
|
+
# my_ea.dataType = 'INTEGER'
|
318
|
+
#
|
319
|
+
# since the second version creates a new string in memory, but the first uses
|
320
|
+
# the one already stored in a constant.
|
321
|
+
#
|
322
|
+
# See also: [Data Validation](#data_validation) below.
|
323
|
+
#
|
324
|
+
# validator: \[Symbol]
|
325
|
+
# -----------------
|
326
|
+
# (ignored if readonly: is true, or if enum: is set)
|
327
|
+
#
|
328
|
+
# The symbol is the name of a Jamf::Validators class method used in the
|
329
|
+
# setter to validate new values for this attribute. It only is used when
|
330
|
+
# class: is :string, :integer, :boolean, and :float
|
331
|
+
#
|
332
|
+
# If omitted, the setter will take any value passed to it, which is
|
333
|
+
# generally unwise.
|
334
|
+
#
|
335
|
+
# When the class: is an actual class, the setter will instantiate a new one
|
336
|
+
# with the value to be set, and validation is handled by the class itself.
|
337
|
+
#
|
338
|
+
# Example:
|
339
|
+
# > If the `class:` for an attrib named ':releaseDate' is class: Jamf::Timestamp
|
340
|
+
# > then the setter method will look like this:
|
341
|
+
# >
|
342
|
+
# > def releaseDate=(newval)
|
343
|
+
# > newval = Jamf::Timestamp.new newval unless newval.is_a? Jamf::Timestamp
|
344
|
+
# > # ^^^ This will validate newval
|
345
|
+
# > return if newval == @releaseDate
|
346
|
+
# > @releaseDate = newval
|
347
|
+
# > @need_to_update = true
|
348
|
+
# > end
|
349
|
+
#
|
350
|
+
# see also: [Data Validation](#data_validation) below.
|
351
|
+
#
|
352
|
+
#
|
353
|
+
# aliases: \[Array of Symbols]
|
354
|
+
# -----------------
|
355
|
+
# Other names for this attribute. If provided, getters, and setters will
|
356
|
+
# be made for all aliases. Should be used very sparingly.
|
357
|
+
#
|
358
|
+
# Attributes of class :boolean automatically have a getter alias ending with a '?'.
|
359
|
+
#
|
360
|
+
# Documenting your code
|
361
|
+
# ---------------------
|
362
|
+
# For documenting attributes with YARD, put this above each
|
363
|
+
# attribute name key:
|
364
|
+
#
|
365
|
+
# ```
|
366
|
+
# # @!attribute <attrname>
|
367
|
+
# # @param [Class] <Describe setter value if needed>
|
368
|
+
# # @return [Class] <Describe value if needed>
|
369
|
+
# ```
|
370
|
+
#
|
371
|
+
# If the value is readonly, remove the @param line, and add \[r], like this:
|
372
|
+
#
|
373
|
+
# ```
|
374
|
+
# # @!attribute [r] <attrname
|
375
|
+
# ```
|
376
|
+
#
|
377
|
+
# for more info see https://www.rubydoc.info/gems/yard/file/docs/Tags.md#attribute
|
378
|
+
#
|
379
|
+
#
|
380
|
+
# #### Sub-subclassing
|
381
|
+
#
|
382
|
+
# If you need to subclass a subclass of JSONObject, and the new subclass needs
|
383
|
+
# to expand on the OBJECT_MODEL in its parent, then you must use Hash#merge
|
384
|
+
# to combine them in the subclass. Here's an example of ComputerPrestage
|
385
|
+
# which inherits from Prestage:
|
386
|
+
#
|
387
|
+
# class ComputerPrestage < Jamf::Prestage
|
388
|
+
#
|
389
|
+
# OBJECT_MODEL = superclass::OBJECT_MODEL.merge(
|
390
|
+
#
|
391
|
+
# newAttr: {
|
392
|
+
# [attr details]
|
393
|
+
# }
|
394
|
+
#
|
395
|
+
# ).freeze
|
396
|
+
#
|
397
|
+
#
|
398
|
+
# #### Data Validation \{#data_validation}
|
399
|
+
#
|
400
|
+
# Attributes that are not readonly are subject to data validation when values are
|
401
|
+
# assigned. How that validation happens depends on the definition of the
|
402
|
+
# attribute as described above. Validation failure will raise an exception,
|
403
|
+
# usually Jamf::InvalidDataError.
|
404
|
+
#
|
405
|
+
# If the attribute is defined with an enum, the value must be
|
406
|
+
# a key or value of the enum.
|
407
|
+
#
|
408
|
+
# If the attribute's class: is defined as a Class, (e.g. Jamf::Timestamp)
|
409
|
+
# its .new method is called with the value and the current API connection.
|
410
|
+
# The class itself performs valuation when the value is used to
|
411
|
+
# instantiate it.
|
412
|
+
#
|
413
|
+
# If the attribute is defined with a validator, the value is passed
|
414
|
+
# to that validator.
|
415
|
+
#
|
416
|
+
# If the attribute is defined as a :string, :integer, :float or :bool
|
417
|
+
# without an enum or validator, it is checked to be the correct type
|
418
|
+
#
|
419
|
+
# If an attribute is an identifier, it must be unique in its class and
|
420
|
+
# API connection.
|
421
|
+
#
|
422
|
+
# ### Constructor / Instantiation {#constructor}
|
423
|
+
#
|
424
|
+
# The .new method should rarely (never?) be called directly for any JSONObject
|
425
|
+
# class.
|
426
|
+
#
|
427
|
+
# The Resource classes are instantiated via the .fetch and .create methods.
|
428
|
+
#
|
429
|
+
# Other JSONObject classes are embedded inside the Resource classes
|
430
|
+
# and are instantiated while parsing data from the API or by the setters for
|
431
|
+
# the attributes holding them.
|
432
|
+
#
|
433
|
+
# When subclassing JSONObject, you can often just use the #initialize defined
|
434
|
+
# here. You may want to override #initialize to accept different kinds of data
|
435
|
+
# and if you do, you _must_:
|
436
|
+
#
|
437
|
+
# - Have an #initialize method that takes two parameters and performs
|
438
|
+
# validation using them:
|
439
|
+
#
|
440
|
+
# 1. A positional first parameter: the value used to create the instance
|
441
|
+
# Your method may accept any kind of value, as long as it can use it
|
442
|
+
# to create a valid object. At the very least it _must_ accept a Hash
|
443
|
+
# that comes from the API for this object. If you call `super` then
|
444
|
+
# that Hash must be passed.
|
445
|
+
#
|
446
|
+
# For example, Jamf::GenericReference, which defines references to
|
447
|
+
# other resources, such as Buildings, can take a Hash containing the
|
448
|
+
# name: and id: of the building (as provided by the API), or can take
|
449
|
+
# just a name or id, or can take a Jamf::Building object.
|
450
|
+
#
|
451
|
+
# The initialize method must perform validation as necessary and raise
|
452
|
+
# an exception if the data provided is not acceptable.
|
453
|
+
#
|
454
|
+
# 2. A keyword parameter `cnx:` containing a Jamf::Connection instance.
|
455
|
+
# This is the API connection through which this JSON object interacts
|
456
|
+
# with the appropriate Jamf Pro server. Usually this is used to validate
|
457
|
+
# the data recieved in the first positional parameter.
|
458
|
+
#
|
459
|
+
# ### Required Instance Methods
|
460
|
+
#
|
461
|
+
# Subclasses of JSONObject must have a #to_jamf method.
|
462
|
+
# For most simple objects, the one defined in JSONObject will work as is.
|
463
|
+
#
|
464
|
+
# If you need to override it, it _must_
|
465
|
+
#
|
466
|
+
# - Return a Hash that can be used in the data sent back to the API.
|
467
|
+
# - Not call #.to_json. All conversion to and from JSON happens in the
|
468
|
+
# Jamf::Connection class.
|
469
|
+
#
|
470
|
+
# @abstract
|
471
|
+
#
|
472
|
+
class JSONObject
|
473
|
+
|
474
|
+
extend Jamf::Abstract
|
475
|
+
|
476
|
+
# Constants
|
477
|
+
#####################################
|
478
|
+
|
479
|
+
# These classes are used from JSON in the raw
|
480
|
+
JSON_TYPE_CLASSES = %i[string integer float boolean].freeze
|
481
|
+
|
482
|
+
# Predicate (boolean) attibutes that start with this RE will
|
483
|
+
# have an alias without the 'is' so :isManaged will have
|
484
|
+
# getters isManaged? and managed?
|
485
|
+
#
|
486
|
+
PREDICATE_RE = /^is([A-Z]\w*)$/.freeze
|
487
|
+
|
488
|
+
# Public Class Methods
|
489
|
+
#####################################
|
490
|
+
|
491
|
+
# By default, JSONObjects (as a whole) are mutable,
|
492
|
+
# although some attributes may not be (see OBJECT_MODEL in the JSONObject
|
493
|
+
# docs)
|
494
|
+
#
|
495
|
+
# When an entire sublcass of JSONObject is read-only/immutable,
|
496
|
+
# `extend Jamf::Immutable`, which will override this to return false.
|
497
|
+
# Doing so will prevent any setters from being created for the subclass
|
498
|
+
# and will cause Jamf::Resource.save to raise an error
|
499
|
+
#
|
500
|
+
def self.mutable?
|
501
|
+
true
|
502
|
+
end
|
503
|
+
|
504
|
+
# Given a Symbol that might be an alias of a key fron OBJECT_MODEL
|
505
|
+
# return the real key
|
506
|
+
#
|
507
|
+
# e.g. if OBJECT_MODEL has an entry like this:
|
508
|
+
# displayName: { aliases: [:name, :display_name] }
|
509
|
+
# Then
|
510
|
+
# attr_key_for_alias(:name) and attr_key_for_alias(:display_name)
|
511
|
+
# will return :displayName
|
512
|
+
#
|
513
|
+
# Returns nil if no such alias exists.
|
514
|
+
#
|
515
|
+
# @param als [Symbol] the alias to look up
|
516
|
+
#
|
517
|
+
# @return [Symbol, nil] The real object model key for the alias
|
518
|
+
#
|
519
|
+
def self.attr_key_for_alias(als)
|
520
|
+
validate_not_abstract
|
521
|
+
self::OBJECT_MODEL.each { |k, deets| return k if k == als || deets[:aliases]&.include?(als) }
|
522
|
+
nil
|
523
|
+
end
|
524
|
+
|
525
|
+
# Private Class Methods
|
526
|
+
#####################################
|
527
|
+
|
528
|
+
# create getters and setters for subclasses of APIObject
|
529
|
+
# based on their OBJECT_MODEL Hash.
|
530
|
+
#
|
531
|
+
##############################
|
532
|
+
def self.parse_object_model
|
533
|
+
return if @object_model_parsed
|
534
|
+
|
535
|
+
got_primary = false
|
536
|
+
need_list_methods = ancestors.include?(Jamf::CollectionResource)
|
537
|
+
|
538
|
+
self::OBJECT_MODEL.each do |attr_name, attr_def|
|
539
|
+
create_list_methods(attr_name, attr_def) if need_list_methods && attr_def[:identifier]
|
540
|
+
|
541
|
+
# there can be only one (primary ident)
|
542
|
+
if attr_def[:identifier] == :primary
|
543
|
+
raise Jamf::UnsupportedError, 'Two identifiers marked as :primary' if got_primary
|
544
|
+
|
545
|
+
got_primary = true
|
546
|
+
end
|
547
|
+
|
548
|
+
create_getters attr_name, attr_def
|
549
|
+
next if attr_def[:readonly]
|
550
|
+
|
551
|
+
create_setters attr_name, attr_def if mutable?
|
552
|
+
end # do |attr_name, attr_def|
|
553
|
+
|
554
|
+
@object_model_parsed = true
|
555
|
+
end # parse_object_model
|
556
|
+
private_class_method :parse_object_model
|
557
|
+
|
558
|
+
# create a getter for an attribute, and any aliases needed
|
559
|
+
##############################
|
560
|
+
def self.create_getters(attr_name, attr_def)
|
561
|
+
# multi_value - only return a frozen dup, no direct editing of Array
|
562
|
+
if attr_def[:multi]
|
563
|
+
define_method(attr_name) do
|
564
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
565
|
+
instance_variable_get("@#{attr_name}").dup.freeze
|
566
|
+
end
|
567
|
+
|
568
|
+
# single value
|
569
|
+
else
|
570
|
+
define_method(attr_name) { instance_variable_get("@#{attr_name}") }
|
571
|
+
|
572
|
+
# all booleans get a predicate alias
|
573
|
+
alias_method("#{attr_name}?", attr_name) if attr_def[:class] == :boolean
|
574
|
+
end
|
575
|
+
|
576
|
+
return unless attr_def[:aliases]
|
577
|
+
|
578
|
+
# aliases
|
579
|
+
attr_def[:aliases].each { |a| alias_method a, attr_name }
|
580
|
+
end # create getters
|
581
|
+
private_class_method :create_getters
|
582
|
+
|
583
|
+
# create setter(s) for an attribute, and any aliases needed
|
584
|
+
##############################
|
585
|
+
def self.create_setters(attr_name, attr_def)
|
586
|
+
# multi_value
|
587
|
+
if attr_def[:multi]
|
588
|
+
create_array_setters(attr_name, attr_def)
|
589
|
+
return
|
590
|
+
end
|
591
|
+
|
592
|
+
# single value
|
593
|
+
define_method("#{attr_name}=") do |new_value|
|
594
|
+
new_value = validate_attr attr_name, new_value
|
595
|
+
old_value = instance_variable_get("@#{attr_name}")
|
596
|
+
return if new_value == old_value
|
597
|
+
instance_variable_set("@#{attr_name}", new_value)
|
598
|
+
note_unsaved_change attr_name, old_value
|
599
|
+
end # define method
|
600
|
+
|
601
|
+
return unless attr_def[:aliases]
|
602
|
+
|
603
|
+
# setter aliases
|
604
|
+
attr_def[:aliases].each { |a| alias_method "#{a}=", "#{attr_name}=" }
|
605
|
+
end # create_setters
|
606
|
+
private_class_method :create_setters
|
607
|
+
|
608
|
+
##############################
|
609
|
+
def self.create_array_setters(attr_name, attr_def)
|
610
|
+
create_full_array_setters(attr_name, attr_def)
|
611
|
+
create_append_setters(attr_name, attr_def)
|
612
|
+
create_prepend_setters(attr_name, attr_def)
|
613
|
+
create_insert_setters(attr_name, attr_def)
|
614
|
+
create_delete_at_setters(attr_name, attr_def)
|
615
|
+
create_delete_if_setters(attr_name, attr_def)
|
616
|
+
end # def create_multi_setters
|
617
|
+
private_class_method :create_array_setters
|
618
|
+
|
619
|
+
# The attr=(newval) setter method for array values
|
620
|
+
##############################
|
621
|
+
def self.create_full_array_setters(attr_name, attr_def)
|
622
|
+
define_method("#{attr_name}=") do |new_value|
|
623
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
624
|
+
raise Jamf::InvalidDataError, 'Value must be an Array' unless new_value.is_a? Array
|
625
|
+
new_value.map! { |item| validate_attr attr_name, item }
|
626
|
+
old_value = instance_variable_get("@#{attr_name}")
|
627
|
+
return if new_value == old_value
|
628
|
+
instance_variable_set("@#{attr_name}", new_value)
|
629
|
+
note_unsaved_change attr_name, old_value
|
630
|
+
end # define method
|
631
|
+
|
632
|
+
return unless attr_def[:aliases]
|
633
|
+
|
634
|
+
attr_def[:aliases].each { |al| alias_method "#{al}=", "#{attr_name}=" }
|
635
|
+
end # create_full_array_setter
|
636
|
+
private_class_method :create_full_array_setters
|
637
|
+
|
638
|
+
# The attr_append(newval) setter method for array values
|
639
|
+
##############################
|
640
|
+
def self.create_append_setters(attr_name, attr_def)
|
641
|
+
define_method("#{attr_name}_append") do |new_value|
|
642
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
643
|
+
new_value = validate_attr attr_name, new_value
|
644
|
+
old_array = instance_variable_get("@#{attr_name}").dup
|
645
|
+
instance_variable_get("@#{attr_name}") << new_value
|
646
|
+
note_unsaved_change attr_name, old_array
|
647
|
+
end # define method
|
648
|
+
|
649
|
+
# always have a << alias
|
650
|
+
alias_method "#{attr_name}<<", "#{attr_name}_append"
|
651
|
+
|
652
|
+
return unless attr_def[:aliases]
|
653
|
+
|
654
|
+
attr_def[:aliases].each do |al|
|
655
|
+
alias_method "#{al}_append", "#{attr_name}_append"
|
656
|
+
alias_method "#{al}<<", "#{attr_name}_append"
|
657
|
+
end
|
658
|
+
end # create_append_setters
|
659
|
+
private_class_method :create_append_setters
|
660
|
+
|
661
|
+
# The attr_prepend(newval) setter method for array values
|
662
|
+
##############################
|
663
|
+
def self.create_prepend_setters(attr_name, attr_def)
|
664
|
+
define_method("#{attr_name}_prepend") do |new_value|
|
665
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
666
|
+
new_value = validate_attr attr_name, new_value
|
667
|
+
old_array = instance_variable_get("@#{attr_name}").dup
|
668
|
+
instance_variable_get("@#{attr_name}").unshift new_value
|
669
|
+
note_unsaved_change attr_name, old_array
|
670
|
+
end # define method
|
671
|
+
|
672
|
+
return unless attr_def[:aliases]
|
673
|
+
|
674
|
+
attr_def[:aliases].each { |al| alias_method "#{al}_prepend", "#{attr_name}_prepend" }
|
675
|
+
end # create_prepend_setters
|
676
|
+
private_class_method :create_prepend_setters
|
677
|
+
|
678
|
+
# The attr_insert(index, newval) setter method for array values
|
679
|
+
def self.create_insert_setters(attr_name, attr_def)
|
680
|
+
define_method("#{attr_name}_insert") do |index, new_value|
|
681
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
682
|
+
new_value = validate_attr attr_name, new_value
|
683
|
+
old_array = instance_variable_get("@#{attr_name}").dup
|
684
|
+
instance_variable_get("@#{attr_name}").insert index, new_value
|
685
|
+
note_unsaved_change attr_name, old_array
|
686
|
+
end # define method
|
687
|
+
|
688
|
+
return unless attr_def[:aliases]
|
689
|
+
|
690
|
+
attr_def[:aliases].each { |al| alias_method "#{al}_insert", "#{attr_name}_insert" }
|
691
|
+
end # create_insert_setters
|
692
|
+
private_class_method :create_insert_setters
|
693
|
+
|
694
|
+
# The attr_delete_at(index) setter method for array values
|
695
|
+
##############################
|
696
|
+
def self.create_delete_at_setters(attr_name, attr_def)
|
697
|
+
define_method("#{attr_name}_delete_at") do |index|
|
698
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
699
|
+
old_array = instance_variable_get("@#{attr_name}").dup
|
700
|
+
deleted = instance_variable_get("@#{attr_name}").delete_at index
|
701
|
+
note_unsaved_change attr_name, old_array if deleted
|
702
|
+
end # define method
|
703
|
+
|
704
|
+
return unless attr_def[:aliases]
|
705
|
+
|
706
|
+
attr_def[:aliases].each { |al| alias_method "#{al}_delete_at", "#{attr_name}_delete_at" }
|
707
|
+
end # create_insert_setters
|
708
|
+
private_class_method :create_delete_at_setters
|
709
|
+
|
710
|
+
# The attr_delete_at(index) setter method for array values
|
711
|
+
##############################
|
712
|
+
def self.create_delete_if_setters(attr_name, attr_def)
|
713
|
+
define_method("#{attr_name}_delete_if") do |index, &block|
|
714
|
+
instance_variable_set("@#{attr_name}", []) unless instance_variable_get("@#{attr_name}").is_a?(Array)
|
715
|
+
old_array = instance_variable_get("@#{attr_name}").dup
|
716
|
+
instance_variable_get("@#{attr_name}").delete_if &block
|
717
|
+
note_unsaved_change attr_name, old_array if old_array != instance_variable_get("@#{attr_name}")
|
718
|
+
end # define method
|
719
|
+
|
720
|
+
return unless attr_def[:aliases]
|
721
|
+
|
722
|
+
attr_def[:aliases].each { |al| alias_method "#{al}_delete_if", "#{attr_name}_delete_if" }
|
723
|
+
end # create_insert_setters
|
724
|
+
private_class_method :create_delete_at_setters
|
725
|
+
|
726
|
+
# Raise an exception if this is an abstract class
|
727
|
+
# Used in class methods that are defined in abstract classes.
|
728
|
+
# Instantiation is already prevented by the Abstract mixin
|
729
|
+
##############################
|
730
|
+
def self.validate_not_abstract
|
731
|
+
raise Jamf::UnsupportedError, "Unsupported: #{self} is an abstract class, ." if Jamf::Abstract.abstract_classes.include? self
|
732
|
+
end
|
733
|
+
private_class_method :validate_not_abstract
|
734
|
+
|
735
|
+
# Used by auto-generated setters and .create to validate new values.
|
736
|
+
#
|
737
|
+
# returns a valid value or raises an exception
|
738
|
+
#
|
739
|
+
# If the attribute is defined to hold a JAMF class, (e.g. Jamf::Timestamp)
|
740
|
+
# the class itself performs validation on the value when instantiated
|
741
|
+
# with the value.
|
742
|
+
#
|
743
|
+
# If the attribute is defined with an enum, the value must be
|
744
|
+
# a key of the enum.
|
745
|
+
#
|
746
|
+
# If the attribute is defined with a validator, the value is passed
|
747
|
+
# to that validator.
|
748
|
+
#
|
749
|
+
# If the attribute is defined as a :string, :integer, :float or :bool
|
750
|
+
# without an enum or validator, it is confirmed to be the correct type
|
751
|
+
#
|
752
|
+
# Otherwise, the value is returned unchanged.
|
753
|
+
#
|
754
|
+
# If the attribute is defined as an identifier, it must be unique among
|
755
|
+
# the other objects of this subclass in the JSS.
|
756
|
+
#
|
757
|
+
# This method only validates single values. When called from multi-value
|
758
|
+
# setters, it is used for each value individually.
|
759
|
+
#
|
760
|
+
#
|
761
|
+
# @param attr_name[Symbol], a top-level key from OBJECT_MODEL for this class
|
762
|
+
#
|
763
|
+
# @param value [Object] the value to validate for that attribute.
|
764
|
+
#
|
765
|
+
# @return [Object] The validated, possibly converted, value.
|
766
|
+
#
|
767
|
+
def self.validate_attr(attr_name, value, cnx: Jamf.cnx)
|
768
|
+
attr_def = self::OBJECT_MODEL[attr_name]
|
769
|
+
|
770
|
+
# validate our value, which will raise an error or
|
771
|
+
# convert the value to the required type.
|
772
|
+
good_value =
|
773
|
+
|
774
|
+
# by enum, must be a value of the enum
|
775
|
+
if attr_def[:enum]
|
776
|
+
return value if attr_def[:enum].include? value
|
777
|
+
raise Jamf::InvalidDataError, "Value must be one of: :#{attr_def[:enum].join ', :'}"
|
778
|
+
|
779
|
+
# by class, the class validates the value passed with .new
|
780
|
+
elsif attr_def[:class].is_a? Class
|
781
|
+
|
782
|
+
klass = attr_def[:class]
|
783
|
+
# validation happens in klass.new
|
784
|
+
value.is_a?(klass) ? value : klass.new(value, cnx: cnx)
|
785
|
+
|
786
|
+
# by Validate method - pass to the method
|
787
|
+
elsif attr_def[:validator]
|
788
|
+
Jamf::Validate.send(attr_def[:validator], value)
|
789
|
+
|
790
|
+
# By json type - pass to the matching validate method
|
791
|
+
elsif JSON_TYPE_CLASSES.include? attr_def[:class]
|
792
|
+
Jamf::Validate.send(attr_def[:class], value)
|
793
|
+
|
794
|
+
# raw, no validation, should be rare
|
795
|
+
else
|
796
|
+
value
|
797
|
+
end # if
|
798
|
+
|
799
|
+
# if this is an identifier, it must be unique
|
800
|
+
Jamf::Validate.send(:unique_identifier, good_value, self.class, attr_name, cnx: @cnx) if attr_def[:identfier]
|
801
|
+
|
802
|
+
good_value
|
803
|
+
end # validate_attr(attr_name, value)
|
804
|
+
private_class_method :validate_attr
|
805
|
+
|
806
|
+
# Attributes
|
807
|
+
#####################################
|
808
|
+
|
809
|
+
# @return [Jamf::Connection] the API connection thru which we deal with
|
810
|
+
# this objcet.
|
811
|
+
attr_reader :cnx
|
812
|
+
|
813
|
+
# Constructor
|
814
|
+
#####################################
|
815
|
+
|
816
|
+
# Make an instance. Data comes from the API
|
817
|
+
#
|
818
|
+
# @param data[Hash] the data for constructing a new object.
|
819
|
+
# @param cnx[Jamf::Connection] the API connection for the object
|
820
|
+
#
|
821
|
+
def initialize(data, cnx: Jamf.cnx)
|
822
|
+
|
823
|
+
raise Jamf::InvalidDataError, 'Invalid JSONObject data - must be a Hash' unless data.is_a? Hash
|
824
|
+
|
825
|
+
@cnx = cnx
|
826
|
+
@unsaved_changes = {} if self.class.mutable?
|
827
|
+
|
828
|
+
creating = data.delete :creating_from_create
|
829
|
+
|
830
|
+
if creating
|
831
|
+
self.class::OBJECT_MODEL.keys.each do |attr_name|
|
832
|
+
next unless data.key? attr_name
|
833
|
+
# use our setters for each value so that they are in the unsaved changes
|
834
|
+
send "#{attr_name}=", data[attr_name]
|
835
|
+
end
|
836
|
+
return
|
837
|
+
end
|
838
|
+
|
839
|
+
parse_init_data data
|
840
|
+
end # init
|
841
|
+
|
842
|
+
# Instance Methods
|
843
|
+
#####################################
|
844
|
+
|
845
|
+
# a hash of all unsaved changes, including embedded JSONObjects
|
846
|
+
#
|
847
|
+
def unsaved_changes
|
848
|
+
return {} unless self.class.mutable?
|
849
|
+
|
850
|
+
changes = @unsaved_changes.dup
|
851
|
+
|
852
|
+
self.class::OBJECT_MODEL.each do |attr_name, attr_def|
|
853
|
+
# skip non-Class attrs
|
854
|
+
next unless attr_def[:class].is_a? Class
|
855
|
+
|
856
|
+
# the current value of the thing, e.g. a Location
|
857
|
+
# which may have unsaved changes
|
858
|
+
value = instance_variable_get "@#{attr_name}"
|
859
|
+
|
860
|
+
# skip those that don't have any changes
|
861
|
+
next unless value.respond_to? :unsaved_changes?
|
862
|
+
attr_changes = value.unsaved_changes
|
863
|
+
next if attr_changes.empty?
|
864
|
+
|
865
|
+
# add the sub-changes to ours
|
866
|
+
changes[attr_name] = attr_changes
|
867
|
+
end
|
868
|
+
changes[:ext_attrs] = ext_attrs_unsaved_changes if self.class.include? Jamf::Extendable
|
869
|
+
changes
|
870
|
+
end
|
871
|
+
|
872
|
+
# return true if we or any of our attributes have unsaved changes
|
873
|
+
#
|
874
|
+
def unsaved_changes?
|
875
|
+
return false unless self.class.mutable?
|
876
|
+
|
877
|
+
!unsaved_changes.empty?
|
878
|
+
end
|
879
|
+
|
880
|
+
def clear_unsaved_changes
|
881
|
+
return unless self.class.mutable?
|
882
|
+
|
883
|
+
unsaved_changes.keys.each do |attr_name|
|
884
|
+
attrib_val = instance_variable_get "@#{attr_name}"
|
885
|
+
if self.class::OBJECT_MODEL[attr_name][:multi]
|
886
|
+
attrib_val.each { |item| item.send :clear_unsaved_changes if item.respond_to? :clear_unsaved_changes }
|
887
|
+
elsif attrib_val.respond_to? :clear_unsaved_changes
|
888
|
+
attrib_val.send :clear_unsaved_changes
|
889
|
+
end
|
890
|
+
end
|
891
|
+
ext_attrs_clear_unsaved_changes if self.class.include? Jamf::Extendable
|
892
|
+
@unsaved_changes = {}
|
893
|
+
end
|
894
|
+
|
895
|
+
# @return [Hash] The data to be sent to the API, as a Hash
|
896
|
+
# to be converted to JSON by the Jamf::Connection
|
897
|
+
#
|
898
|
+
def to_jamf
|
899
|
+
data = {}
|
900
|
+
self.class::OBJECT_MODEL.each do |attr_name, attr_def|
|
901
|
+
|
902
|
+
raw_value = instance_variable_get "@#{attr_name}"
|
903
|
+
|
904
|
+
# If its a multi-value attribute, process it and go on
|
905
|
+
if attr_def[:multi]
|
906
|
+
data[attr_name] = multi_to_jamf(raw_value, attr_def)
|
907
|
+
next
|
908
|
+
end
|
909
|
+
|
910
|
+
# if its a single-value object, process it and go on.
|
911
|
+
cooked_value = single_to_jamf(raw_value, attr_def)
|
912
|
+
# next if cooked_value.nil? # ignore nil
|
913
|
+
data[attr_name] = cooked_value
|
914
|
+
end # unsaved_changes.each
|
915
|
+
data
|
916
|
+
end
|
917
|
+
|
918
|
+
# DOESN"T SEEM TO WORK FOR BUILDINGS - need the whole JSON object
|
919
|
+
# not just the changes.
|
920
|
+
# @return [Hash] The changes that need to be sent to the API, as a Hash
|
921
|
+
# to be converted to JSON by the Jamf::Connection
|
922
|
+
#
|
923
|
+
def to_jamf_changes_only
|
924
|
+
return unless self.class.mutable?
|
925
|
+
|
926
|
+
data = {}
|
927
|
+
unsaved_changes.each do |attr_name, changes|
|
928
|
+
attr_def = self.class::OBJECT_MODEL[attr_name]
|
929
|
+
|
930
|
+
# readonly attributes can't be changed
|
931
|
+
next if attr_def[:readonly]
|
932
|
+
|
933
|
+
# here's the new value for this attribute
|
934
|
+
raw_value = changes[:new]
|
935
|
+
|
936
|
+
# If its a multi-value attribute, process it and go on
|
937
|
+
if attr_def[:multi]
|
938
|
+
data[attr_name] = multi_to_jamf(raw_value, attr_def)
|
939
|
+
next
|
940
|
+
end
|
941
|
+
|
942
|
+
# if its a single-value object, process it and go on.
|
943
|
+
cooked_value = single_to_jamf(raw_value, attr_def)
|
944
|
+
next if cooked_value.nil? # ignore nil
|
945
|
+
|
946
|
+
data[attr_name] = cooked_value
|
947
|
+
end # unsaved_changes.each
|
948
|
+
data
|
949
|
+
end
|
950
|
+
|
951
|
+
# Print the JSON version of the to_jamf outout
|
952
|
+
# mostly for debugging/troubleshooting
|
953
|
+
def pretty_jamf_json
|
954
|
+
puts JSON.pretty_generate(to_jamf)
|
955
|
+
end
|
956
|
+
|
957
|
+
# Remove large cached items from
|
958
|
+
# the instance_variables used to create
|
959
|
+
# pretty-print (pp) output.
|
960
|
+
#
|
961
|
+
# @return [Array] the desired instance_variables
|
962
|
+
#
|
963
|
+
def pretty_print_instance_variables
|
964
|
+
vars = super.sort
|
965
|
+
vars.delete :@cnx
|
966
|
+
vars
|
967
|
+
end
|
968
|
+
|
969
|
+
# Private Instance Methods
|
970
|
+
#####################################
|
971
|
+
private
|
972
|
+
|
973
|
+
def note_unsaved_change(attr_name, old_value)
|
974
|
+
return unless self.class.mutable?
|
975
|
+
|
976
|
+
new_val = instance_variable_get "@#{attr_name}"
|
977
|
+
if @unsaved_changes[attr_name]
|
978
|
+
@unsaved_changes[attr_name][:new] = new_val
|
979
|
+
else
|
980
|
+
@unsaved_changes[attr_name] = { old: old_value, new: new_val }
|
981
|
+
end
|
982
|
+
end
|
983
|
+
|
984
|
+
# take data from the API and populate an our instance attributes
|
985
|
+
#
|
986
|
+
# @param data[Hash] The parsed API JSON data for this instance
|
987
|
+
#
|
988
|
+
# @return [void]
|
989
|
+
#
|
990
|
+
def parse_init_data(data)
|
991
|
+
self.class::OBJECT_MODEL.each do |attr_name, attr_def|
|
992
|
+
value =
|
993
|
+
if attr_def[:multi]
|
994
|
+
raw_array = data[attr_name] || []
|
995
|
+
|
996
|
+
raw_array.map! { |v| parse_single_init_value v, attr_name, attr_def }
|
997
|
+
else
|
998
|
+
parse_single_init_value data[attr_name], attr_name, attr_def
|
999
|
+
end
|
1000
|
+
instance_variable_set "@#{attr_name}", value
|
1001
|
+
end # OBJECT_MODEL.each
|
1002
|
+
end # parse_init_data(data)
|
1003
|
+
|
1004
|
+
# Parse an individual value from the API into an
|
1005
|
+
# attribute or a member of a multi attribute
|
1006
|
+
# Description of #parse_single_init_value
|
1007
|
+
#
|
1008
|
+
# @param api_value [Object] The parsed JSON value from the API
|
1009
|
+
# @param attr_name [Symbol] The attribute we're processing
|
1010
|
+
# @param attr_def [Hash] The attribute definition
|
1011
|
+
#
|
1012
|
+
# @return [Object] The storable value.
|
1013
|
+
#
|
1014
|
+
def parse_single_init_value(api_value, attr_name, attr_def)
|
1015
|
+
# we do get nils from the API, and they should stay nil
|
1016
|
+
return nil if api_value.nil?
|
1017
|
+
|
1018
|
+
# an enum value
|
1019
|
+
if attr_def[:enum]
|
1020
|
+
parse_enum_value(api_value, attr_name, attr_def)
|
1021
|
+
|
1022
|
+
# a Class value
|
1023
|
+
elsif attr_def[:class].class == Class
|
1024
|
+
attr_def[:class].new api_value, cnx: @cnx
|
1025
|
+
|
1026
|
+
# a JSON value
|
1027
|
+
else
|
1028
|
+
api_value
|
1029
|
+
end # if attr_def[:class].class
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
# Parse an api value into an attribute with an enum
|
1033
|
+
#
|
1034
|
+
# @param (see parse_single_init_value)
|
1035
|
+
# @return (see parse_single_init_value)
|
1036
|
+
#
|
1037
|
+
def parse_enum_value(api_value, attr_name, attr_def)
|
1038
|
+
if attr_def[:enum].include? api_value
|
1039
|
+
api_value
|
1040
|
+
else
|
1041
|
+
raise Jamf::InvalidDataError, "#{api_value} is not in the enum for attribute #{attr_name}"
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
# call to_jamf on a single value
|
1046
|
+
#
|
1047
|
+
def single_to_jamf(raw_value, attr_def)
|
1048
|
+
# if the attrib class is a Class,
|
1049
|
+
# call its changes_to_jamf or to_jamf method
|
1050
|
+
if attr_def[:class].is_a? Class
|
1051
|
+
data = raw_value.to_jamf
|
1052
|
+
data.is_a?(Hash) && data.empty? ? nil : data
|
1053
|
+
|
1054
|
+
# otherwise, use the value as-is
|
1055
|
+
else
|
1056
|
+
raw_value
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
# Call to_jamf on an array value
|
1061
|
+
#
|
1062
|
+
def multi_to_jamf(raw_array, attr_def)
|
1063
|
+
raw_array ||= []
|
1064
|
+
raw_array.map { |raw_value| single_to_jamf(raw_value, attr_def) }.compact
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# wrapper for class method
|
1068
|
+
def validate_attr(attr_name, value)
|
1069
|
+
self.class.send :validate_attr, attr_name, value, cnx: @cnx
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
end # class JSONObject
|
1073
|
+
|
1074
|
+
end # module JAMF
|