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.

Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jamf.rb +169 -0
  3. data/lib/jamf/api/abstract_classes/collection_resource.rb +422 -0
  4. data/lib/jamf/api/abstract_classes/generic_reference.rb +145 -0
  5. data/lib/jamf/api/abstract_classes/json_object.rb +1074 -0
  6. data/lib/jamf/api/abstract_classes/prestage.rb +219 -0
  7. data/lib/jamf/api/abstract_classes/prestage_skip_setup_items.rb +126 -0
  8. data/lib/jamf/api/abstract_classes/resource.rb +250 -0
  9. data/lib/jamf/api/abstract_classes/singleton_resource.rb +87 -0
  10. data/lib/jamf/api/attribute_classes/ip_address.rb +66 -0
  11. data/lib/jamf/api/attribute_classes/timestamp.rb +144 -0
  12. data/lib/jamf/api/connection.rb +734 -0
  13. data/lib/jamf/api/connection/api_error.rb +111 -0
  14. data/lib/jamf/api/connection/api_error_styleguide.rb +96 -0
  15. data/lib/jamf/api/connection/token.rb +220 -0
  16. data/lib/jamf/api/json_objects/account_prefs.rb +79 -0
  17. data/lib/jamf/api/json_objects/android_details.rb +139 -0
  18. data/lib/jamf/api/json_objects/appletv_details.rb +110 -0
  19. data/lib/jamf/api/json_objects/attachment.rb +68 -0
  20. data/lib/jamf/api/json_objects/cellular_network.rb +151 -0
  21. data/lib/jamf/api/json_objects/change_log_entry.rb +77 -0
  22. data/lib/jamf/api/json_objects/computer_prestage_skip_setup_items.rb +67 -0
  23. data/lib/jamf/api/json_objects/country.rb +51 -0
  24. data/lib/jamf/api/json_objects/extension_attribute_value.rb +128 -0
  25. data/lib/jamf/api/json_objects/installed_application.rb +59 -0
  26. data/lib/jamf/api/json_objects/installed_certificate.rb +53 -0
  27. data/lib/jamf/api/json_objects/installed_configuration_profile.rb +67 -0
  28. data/lib/jamf/api/json_objects/installed_ebook.rb +58 -0
  29. data/lib/jamf/api/json_objects/installed_provisioning_profile.rb +59 -0
  30. data/lib/jamf/api/json_objects/inventory_preload_extension_attribute.rb +52 -0
  31. data/lib/jamf/api/json_objects/ios_details.rb +244 -0
  32. data/lib/jamf/api/json_objects/location.rb +95 -0
  33. data/lib/jamf/api/json_objects/md_prestage_name.rb +57 -0
  34. data/lib/jamf/api/json_objects/md_prestage_names.rb +82 -0
  35. data/lib/jamf/api/json_objects/md_prestage_skip_setup_items.rb +165 -0
  36. data/lib/jamf/api/json_objects/mobile_device_details.rb +219 -0
  37. data/lib/jamf/api/json_objects/mobile_device_security.rb +101 -0
  38. data/lib/jamf/api/json_objects/prestage_assignment.rb +61 -0
  39. data/lib/jamf/api/json_objects/prestage_location.rb +104 -0
  40. data/lib/jamf/api/json_objects/prestage_purchasing_data.rb +132 -0
  41. data/lib/jamf/api/json_objects/prestage_scope.rb +54 -0
  42. data/lib/jamf/api/json_objects/prestage_sync_status.rb +63 -0
  43. data/lib/jamf/api/json_objects/purchasing_data.rb +125 -0
  44. data/lib/jamf/api/mixins/abstract.rb +58 -0
  45. data/lib/jamf/api/mixins/bulk_deletable.rb +39 -0
  46. data/lib/jamf/api/mixins/change_log.rb +136 -0
  47. data/lib/jamf/api/mixins/extendable.rb +75 -0
  48. data/lib/jamf/api/mixins/immutable.rb +39 -0
  49. data/lib/jamf/api/mixins/locatable.rb +124 -0
  50. data/lib/jamf/api/mixins/lockable.rb +48 -0
  51. data/lib/jamf/api/mixins/referable.rb +92 -0
  52. data/lib/jamf/api/mixins/searchable.rb +202 -0
  53. data/lib/jamf/api/mixins/uncreatable.rb +40 -0
  54. data/lib/jamf/api/mixins/undeletable.rb +40 -0
  55. data/lib/jamf/api/resources/collection_resources/account.rb +163 -0
  56. data/lib/jamf/api/resources/collection_resources/building.rb +114 -0
  57. data/lib/jamf/api/resources/collection_resources/category.rb +82 -0
  58. data/lib/jamf/api/resources/collection_resources/computer.rb +49 -0
  59. data/lib/jamf/api/resources/collection_resources/computer_prestage.rb +80 -0
  60. data/lib/jamf/api/resources/collection_resources/department.rb +79 -0
  61. data/lib/jamf/api/resources/collection_resources/extension_attribute.rb +45 -0
  62. data/lib/jamf/api/resources/collection_resources/inventory_preload_record.rb +274 -0
  63. data/lib/jamf/api/resources/collection_resources/md_prestage.rb +139 -0
  64. data/lib/jamf/api/resources/collection_resources/mobile_device.rb +315 -0
  65. data/lib/jamf/api/resources/collection_resources/script.rb +190 -0
  66. data/lib/jamf/api/resources/collection_resources/site.rb +77 -0
  67. data/lib/jamf/api/resources/singleton_resources/app_store_country_codes.rb +131 -0
  68. data/lib/jamf/api/resources/singleton_resources/authorization.rb +88 -0
  69. data/lib/jamf/api/resources/singleton_resources/client_checkin_settings.rb +139 -0
  70. data/lib/jamf/api/resources/singleton_resources/reenrollment_settings.rb +95 -0
  71. data/lib/jamf/client.rb +301 -0
  72. data/lib/jamf/client/jamf_binary.rb +132 -0
  73. data/lib/jamf/client/jamf_helper.rb +298 -0
  74. data/lib/jamf/client/management_action.rb +114 -0
  75. data/lib/jamf/compatibility.rb +88 -0
  76. data/lib/jamf/composer.rb +190 -0
  77. data/lib/jamf/configuration.rb +281 -0
  78. data/lib/jamf/exceptions.rb +107 -0
  79. data/lib/jamf/ruby_extensions.rb +36 -0
  80. data/lib/jamf/ruby_extensions/array.rb +35 -0
  81. data/lib/jamf/ruby_extensions/array/predicates.rb +46 -0
  82. data/lib/jamf/ruby_extensions/array/utils.rb +47 -0
  83. data/lib/jamf/ruby_extensions/filetest.rb +32 -0
  84. data/lib/jamf/ruby_extensions/filetest/predicates.rb +46 -0
  85. data/lib/jamf/ruby_extensions/hash.rb +33 -0
  86. data/lib/jamf/ruby_extensions/hash/backports.rb +92 -0
  87. data/lib/jamf/ruby_extensions/ipaddr.rb +37 -0
  88. data/lib/jamf/ruby_extensions/ipaddr/utils.rb +95 -0
  89. data/lib/jamf/ruby_extensions/object.rb +30 -0
  90. data/lib/jamf/ruby_extensions/object/predicates.rb +51 -0
  91. data/lib/jamf/ruby_extensions/pathname.rb +39 -0
  92. data/lib/jamf/ruby_extensions/pathname/predicates.rb +50 -0
  93. data/lib/jamf/ruby_extensions/pathname/utils.rb +75 -0
  94. data/lib/jamf/ruby_extensions/string.rb +35 -0
  95. data/lib/jamf/ruby_extensions/string/backports.rb +66 -0
  96. data/lib/jamf/ruby_extensions/string/conversions.rb +65 -0
  97. data/lib/jamf/ruby_extensions/string/predicates.rb +47 -0
  98. data/lib/jamf/utility.rb +423 -0
  99. data/lib/jamf/validate.rb +224 -0
  100. data/lib/jamf/version.rb +32 -0
  101. data/lib/jpapi.rb +26 -0
  102. data/lib/jss/version.rb +1 -1
  103. 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