ruby-jss 1.2.3 → 1.2.4a1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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