ruby-jss 1.0.4 → 1.1.0b1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +26 -0
  3. data/lib/jss.rb +47 -24
  4. data/lib/jss/api_connection.rb +39 -7
  5. data/lib/jss/api_object.rb +651 -319
  6. data/lib/jss/api_object/account.rb +19 -5
  7. data/lib/jss/api_object/advanced_search/advanced_computer_search.rb +0 -3
  8. data/lib/jss/api_object/advanced_search/advanced_mobile_device_search.rb +0 -3
  9. data/lib/jss/api_object/advanced_search/advanced_user_search.rb +0 -3
  10. data/lib/jss/api_object/building.rb +0 -3
  11. data/lib/jss/api_object/category.rb +0 -3
  12. data/lib/jss/api_object/computer.rb +83 -28
  13. data/lib/jss/api_object/computer_invitation.rb +1 -11
  14. data/lib/jss/api_object/configuration_profile/osx_configuration_profile.rb +0 -3
  15. data/lib/jss/api_object/department.rb +0 -3
  16. data/lib/jss/api_object/distribution_point.rb +0 -3
  17. data/lib/jss/api_object/extendable.rb +113 -57
  18. data/lib/jss/api_object/extension_attribute.rb +46 -13
  19. data/lib/jss/api_object/extension_attribute/computer_extension_attribute.rb +0 -3
  20. data/lib/jss/api_object/extension_attribute/mobile_device_extension_attribute.rb +56 -19
  21. data/lib/jss/api_object/extension_attribute/user_extension_attribute.rb +0 -3
  22. data/lib/jss/api_object/group/computer_group.rb +0 -3
  23. data/lib/jss/api_object/group/mobile_device_group.rb +0 -3
  24. data/lib/jss/api_object/group/user_group.rb +0 -3
  25. data/lib/jss/api_object/ldap_server.rb +0 -3
  26. data/lib/jss/api_object/mobile_device.rb +25 -29
  27. data/lib/jss/api_object/mobile_device_application.rb +1 -9
  28. data/lib/jss/api_object/netboot_server.rb +0 -3
  29. data/lib/jss/api_object/network_segment.rb +0 -3
  30. data/lib/jss/api_object/package.rb +0 -3
  31. data/lib/jss/api_object/patch_source/patch_external_source.rb +0 -2
  32. data/lib/jss/api_object/patch_source/patch_internal_source.rb +0 -3
  33. data/lib/jss/api_object/patch_title.rb +1 -2
  34. data/lib/jss/api_object/peripheral.rb +0 -3
  35. data/lib/jss/api_object/peripheral_type.rb +0 -2
  36. data/lib/jss/api_object/policy.rb +20 -2
  37. data/lib/jss/api_object/removable_macaddr.rb +0 -3
  38. data/lib/jss/api_object/restricted_software.rb +0 -3
  39. data/lib/jss/api_object/scopable.rb +0 -12
  40. data/lib/jss/api_object/scopable/scope.rb +1 -1
  41. data/lib/jss/api_object/script.rb +0 -3
  42. data/lib/jss/api_object/self_servable.rb +3 -1
  43. data/lib/jss/api_object/site.rb +0 -3
  44. data/lib/jss/api_object/software_update_server.rb +0 -3
  45. data/lib/jss/api_object/user.rb +0 -3
  46. data/lib/jss/api_object/webhook.rb +0 -3
  47. data/lib/jss/compatibility.rb +74 -53
  48. data/lib/jss/exceptions.rb +5 -0
  49. data/lib/jss/ruby_extensions/string.rb +4 -48
  50. data/lib/jss/ruby_extensions/string/conversions.rb +69 -0
  51. data/lib/jss/ruby_extensions/string/predicates.rb +47 -0
  52. data/lib/jss/version.rb +1 -1
  53. data/test/README.md +2 -4
  54. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84108db86594df694b40a7e0c5ee07bcc263372eab4f9b3a692fd586b1d8a4fa
4
- data.tar.gz: f8fb1968963290d637a4f32a7b84cb3d5cb314c4828d3c79f9261e0cc441c6a6
3
+ metadata.gz: 18ed86e89f9b9994f582c691ac460e1ee6262e67cb02fcc42352f84b6d2b1e8a
4
+ data.tar.gz: f5b535f7cf9f778b42b2cc7367f2e5ddce6cbc08e766f75f5ed8413cc42efc78
5
5
  SHA512:
6
- metadata.gz: 2363d13088ece6a3fa09b2edef2f473c865d1817a3439daed7f58ecc5b6d18b3722406c7bcb5fcf0d13ec20f9dc37a956687ddeefc38e55523a2abdb6ff39ead
7
- data.tar.gz: 2f2cb5b45150378f4d48d70f72e6aeae0788052e7a417a4948b8cb64af3def31f7658421e8f022d688090c94fb397919ddd2000d7000e2e2cedd13368fffcd8f
6
+ metadata.gz: 88e9391e8d5d5c8af60176599dd86303e8a48c96ba9a643664c7cf7a8d0ac257340a4d561a426ff5cd4e8c73fbfc102a0f735703fa462222fa95f4cc75a4078d
7
+ data.tar.gz: 87ef3da3ad033c0f17b9511a4a265ddcf5479aee5e71d385c880a0e298627e881bb73f90814ebb4c7feac08455c321036d69802a74ef24eca74e092737d33054
data/CHANGES.md CHANGED
@@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## \[Unreleased]
8
8
  ### Added
9
+ - MobileDeviceExtensionAttribute now has a `.history` class method matching that of ComputerExtensionAttribute. Requires direct MySQL database access. Thanks @aurica!
10
+ - JSS::AmbiguousError exception class
11
+ - More caching of API data to improve general speed
12
+ - The hashes created by `APIObject.map_all_ids_to(blah)`
13
+ - ExtensionAttribute definitions when used by extendable classes
14
+ - Implemented Ruby2.4's `String#casecmp?` in older Rubies
15
+ - APIObject.fetch can take the search term `:random` and you'll get a randomly selected object. Example: `a_random_computer = JSS::Computer.fetch :random`
16
+ - Keys of the hash returned by `Computer#hardware` are now available as instance methods on Computer objects. So instead of `a_computer.hardware[:total_ram]` you can also do `a_computer.total_ram`
17
+
18
+
19
+ ### Fixed
20
+ - Can't Modify Frozen Hash error when instantiating JSS::Scopbable::Scope. Thanks to @shahn for reporting this one.
21
+ - MobileDeviceExtensionAttribute now handles empty `as_of` timestamp. Thanks @aurica!
22
+ - A couple of typos. Thanks to @cybertunnel for finding one.
23
+ - A bug when parsing the `server_path` parameter to `API::Connection.new`
24
+
25
+ ### Changed
26
+ - Monkey Patches are being moved to a better, more traceable technique, see https://www.justinweiss.com/articles/3-ways-to-monkey-patch-without-making-a-mess/
27
+ - MobileDevices and Computers now raise JSS::AmbiguousError when being fetched by explicitly by name, e.g. `JSS::Computer.fetch name: 'foo'` and that name is not unique in the JSS. Previously, you'd get an object back, but no guarantee as to which one it was. You'll still get an undefined object if you use a bare searchterm, e.g. `JSS::Computer.fetch 'foo'`
28
+ - Documentation for subclassing APIObject is updated & expanded. See the comments above the class definition in api_object.rb
29
+ - `APIObject.valid_id` is now case-insensitive
30
+ - Removed deprecated VALID_DATA_KEYS constants from APIObject subclasses
31
+ - Various changes in APIObject and its subclasses to try making `.fetch` and other lookup-methods faster.
32
+
33
+ ## \[1.0.4] - 2019-05-06
34
+ ### Added
9
35
  - JSS::Group (and its subclasses) now have a `change_membership` class and instance method for static groups.
10
36
  - The class method allows adding and removing members without fetching an instance of the group.
11
37
  - The instance method adds and/or removes members immediately, without needing to call #update or #save
data/lib/jss.rb CHANGED
@@ -92,6 +92,9 @@ module JSS
92
92
  ### Module Methods
93
93
  #####################################
94
94
 
95
+ # TODO: Find a better way to do all this - possibly with
96
+ # autoloading.
97
+
95
98
  ### Define classes and submodules here so that they don't
96
99
  ### generate errors when referenced during the loading of
97
100
  ### the library.
@@ -102,31 +105,68 @@ module JSS
102
105
 
103
106
  module Composer; end
104
107
 
108
+ ### Mix-in Sub Modules
109
+
110
+ module Creatable; end
111
+ module FileUpload; end
112
+ module Locatable; end
113
+ module Matchable; end
114
+ module Purchasable; end
115
+ module Updatable; end
116
+ module Extendable; end
117
+ module SelfServable; end
118
+ module Categorizable; end
119
+ module VPPable; end
120
+ module Sitable; end
121
+ module MDM; end
122
+ module ManagementHistory; end
123
+
105
124
  ### Mix-in Sub Modules with Classes
106
125
 
107
126
  module Criteriable
127
+
108
128
  class Criteria; end
109
129
  class Criterion; end
130
+
110
131
  end
132
+
111
133
  module Scopable
112
- class Scope ; end
134
+
135
+ class Scope; end
136
+
113
137
  end
114
138
 
115
139
  ### Classes
116
140
  #####################################
117
141
 
118
- class APIObject; end
119
142
  class APIConnection; end
120
143
  class DBConnection; end
121
144
  class Server; end
122
145
  class Icon; end
123
146
  class Preferences; end
124
- class Client; end # TODO: see if this can be made into a module.
125
-
126
- ### SubClasses
147
+ # TODO: see if this can be made into a module:
148
+ class Client; end
149
+
150
+ # Parent of all fetchable objects.
151
+ #
152
+ class APIObject
153
+
154
+ # Builtin ruby callback, whenver a subclass is created.
155
+ #
156
+ # Just store the subclass name, at the end of all the requires, we'll
157
+ # call define_identifier_list_methods on everything we stored here.
158
+ #
159
+ def self.inherited(subclass)
160
+ @subclasses ||= []
161
+ @subclasses << subclass
162
+ end
163
+
164
+ end # class APIObject
165
+
166
+ ### APIObject SubClasses
127
167
  #####################################
128
168
 
129
- ### APIObject Classes with SubClasses
169
+ ### APIObject SubClasses with SubClasses
130
170
 
131
171
  class AdvancedSearch < JSS::APIObject; end
132
172
  class AdvancedComputerSearch < JSS::AdvancedSearch; end
@@ -147,7 +187,7 @@ module JSS
147
187
  class OSXConfigurationProfile < JSS::ConfigurationProfile; end
148
188
  class MobileDeviceConfigurationProfile < JSS::ConfigurationProfile; end
149
189
 
150
- ### APIObject Classes without SubClasses
190
+ ### APIObject SubClasses without SubClasses
151
191
 
152
192
  class Account < JSS::APIObject; end
153
193
  class Building < JSS::APIObject; end
@@ -175,23 +215,6 @@ module JSS
175
215
  class User < JSS::APIObject; end
176
216
  class WebHook < JSS::APIObject; end
177
217
 
178
- ### Mix-in Sub Modules
179
-
180
- module Creatable; end
181
- module FileUpload; end
182
- module Locatable; end
183
- module Matchable; end
184
- module Purchasable; end
185
- module Updatable; end
186
- module Extendable; end
187
- module SelfServable; end
188
- module Categorizable; end
189
- module VPPable; end
190
- module Sitable; end
191
- module MDM; end
192
- module ManagementHistory; end
193
-
194
-
195
218
  end # module JSS
196
219
 
197
220
  ### Load the rest of the module
@@ -365,16 +365,46 @@ module JSS
365
365
  attr_reader :name
366
366
 
367
367
  # @return [Hash]
368
- # This Hash holds the most recent API query for a list of all items in any
369
- # APIObject subclass, keyed by the subclass's RSRC_LIST_KEY.
368
+ # This Hash caches the result of the the first API query for an APIObject
369
+ # subclass's .all summary list, keyed by the subclass's RSRC_LIST_KEY.
370
370
  # See the APIObject.all class method.
371
371
  #
372
- # When the APIObject.all method is called without an argument,
372
+ # It also holds related data items for speedier processing:
373
+ #
374
+ # - The Hashes created by APIObject.map_all_ids_to(foo), keyed by
375
+ # "#{RSRC_LIST_KEY}_map_#{other_key}".to_sym
376
+ #
377
+ # - This hash also holds a cache of the rarely-used APIObject.all_objects
378
+ # hash, keyed by "#{RSRC_LIST_KEY}_objects".to_sym
379
+ #
380
+ #
381
+ # When APIObject.all, and related methods are called without an argument,
373
382
  # and this hash has a matching value, the value is returned, rather than
374
383
  # requerying the API. The first time a class calls .all, or whnever refresh
375
384
  # is not false, the API is queried and the value in this hash is updated.
376
385
  attr_reader :object_list_cache
377
386
 
387
+ # @return [Hash{Class: Hash{String => JSS::ExtensionAttribute}}]
388
+ # This Hash caches the Extension Attribute
389
+ # definition objects for the three types of ext. attribs:
390
+ # ComputerExtensionAttribute, MobileDeviceExtensionAttribute, and
391
+ # UserExtensionAttribute, whenever they are fetched for parsing or
392
+ # validating extention attribute data.
393
+ #
394
+ # The top-level keys are the EA classes themselves:
395
+ # - ComputerExtensionAttribute
396
+ # - MobileDeviceExtensionAttribute
397
+ # - UserExtensionAttribute
398
+ #
399
+ # These each point to a Hash of their instances, keyed by name, e.g.
400
+ # {
401
+ # "A Computer EA" => <JSS::ComputerExtensionAttribute...>,
402
+ # "A different Computer EA" => <JSS::ComputerExtensionAttribute...>,
403
+ # ...
404
+ # }
405
+ #
406
+ attr_reader :ext_attr_definition_cache
407
+
378
408
  # Constructor
379
409
  #####################################
380
410
 
@@ -393,6 +423,7 @@ module JSS
393
423
  @name ||= :disconnected
394
424
  @connected = false
395
425
  @object_list_cache = {}
426
+ @ext_attr_definition_cache = {}
396
427
  connect args unless args.empty?
397
428
  end # init
398
429
 
@@ -936,6 +967,7 @@ module JSS
936
967
  vars.delete :@network_ranges
937
968
  vars.delete :@my_distribution_point
938
969
  vars.delete :@master_distribution_point
970
+ vars.delete :@ext_attr_definition_cache
939
971
  vars
940
972
  end
941
973
 
@@ -1070,11 +1102,11 @@ module JSS
1070
1102
  @server_host = args[:server]
1071
1103
  @port = args[:port].to_i
1072
1104
 
1105
+ # trim any potential leading slash on server_path, ensure a trailing slash
1073
1106
  if args[:server_path]
1074
- # remove leading & trailing slashes in serverpath if any
1075
- @server_path = args[:server_path].sub %r{^/*(.*?)/*$}, Regexp.last_match(1)
1076
- # re-add single trailing slash
1077
- @server_path << '/'
1107
+ @server_path = args[:server_path]
1108
+ @server_path = @server_path[1..-1] if @server_path.start_with? '/'
1109
+ @server_path << '/' unless @server_path.end_with? '/'
1078
1110
  end
1079
1111
 
1080
1112
  # we're using ssl if:
@@ -26,44 +26,46 @@
26
26
  ###
27
27
  module JSS
28
28
 
29
- # Module Variables
30
- #####################################
31
-
32
- # Module Methods
33
- #####################################
34
-
35
29
  # Classes
36
30
  #####################################
37
31
 
38
- # This class is the parent to all JSS API objects. It provides standard methods and structures
39
- # that apply to all API resouces.
32
+ # This class is the parent to all JSS API objects. It provides standard
33
+ # methods and constants that apply to all API resouces.
40
34
  #
41
- # See the README.md file for general info about using subclasses of JSS::APIObject
35
+ # See the README.md file for general info about using subclasses of
36
+ # JSS::APIObject
42
37
  #
43
38
  # == Subclassing
44
39
  #
45
- # === Constructor
40
+ # === Initilize / Constructor
41
+ #
42
+ # All subclasses must call `super` in their initialize method, which will
43
+ # call the method defined here in APIObject. Not only does this retrieve the
44
+ # data from the API, it parses the raw JSON data into a Hash, & stores it in
45
+ # @init_data.
46
46
  #
47
47
  # In general, subclasses should do any class-specific argument checking before
48
- # calling super, and then afterwards, use the contents of @init_data to populate
49
- # any class-specific attributes. @id, @name, @rest_rsrc, and @in_jss are handled here.
48
+ # calling super, and then afterwards use the contents of @init_data to
49
+ # populate any class-specific attributes. Populating @id, @name, @rest_rsrc,
50
+ # and @in_jss are handled here.
50
51
  #
51
- # If a subclass can be looked up by some key other than :name or :id, the subclass must
52
- # pass the keys as an Array in the second argument when calling super from #initialize.
53
- # See {JSS::Computer#initialize} for an example of how to implement this feature.
52
+ # This class also handles parsing @init_data for any mixed-in modules, e.g.
53
+ # Scopable, Categorizable or Extendable. See those modules for any
54
+ # requirements they have when including them.
54
55
  #
55
56
  # === Object Creation
56
57
  #
57
- # If a subclass should be able to be created in the JSS be sure to include {JSS::Creatable}
58
+ # If a subclass should be able to be created in the JSS be sure to include
59
+ # {JSS::Creatable}
58
60
  #
59
- # The constructor should verify any extra required data (aside from :name) in the args before or after
60
- # calling super.
61
+ # The constructor should verify any extra required data in the args
61
62
  #
62
63
  # See {JSS::Creatable} for more details.
63
64
  #
64
65
  # === Object Modification
65
66
  #
66
- # If a subclass should be modifiable in the JSS, include {JSS::Updatable}, q.v. for details.
67
+ # If a subclass should be modifiable in the JSS, include {JSS::Updatable},
68
+ # q.v. for details.
67
69
  #
68
70
  # === Object Deletion
69
71
  #
@@ -71,90 +73,370 @@ module JSS
71
73
  #
72
74
  # === Required Constants
73
75
  #
74
- # Subclasses *must* provide certain Constants in order to correctly interpret API data and
75
- # communicate with the API.
76
+ # Subclasses *must* provide certain constants in order to correctly interpret
77
+ # API data and communicate with the API:
78
+ #
79
+ # ==== RSRC_BASE [String]
80
+ #
81
+ # The base for REST resources of this class
82
+ #
83
+ # e.g. 'computergroups' in
84
+ # https://casper.mycompany.com:8443/JSSResource/computergroups/id/12
76
85
  #
77
- # ==== RSRC_BASE = [String], The base for REST resources of this class
86
+ # ==== RSRC_LIST_KEY [Symbol]
78
87
  #
79
- # e.g. 'computergroups' in "https://casper.mycompany.com:8443/JSSResource/computergroups/id/12"
88
+ # When GETting the RSRC_BASE for a subclass, an Array of Hashes is returned
89
+ # with one Hash of basic info for each object of that type in the JSS. All
90
+ # objects have their JSS id and name in that Hash, some have other data as
91
+ # well. This Array is used for a variety of purposes when using ruby-jss,
92
+ # since it gives you basic info about all objects, without having to fetch
93
+ # each one individually.
80
94
  #
81
- # ==== RSRC_LIST_KEY = [Symbol] The Hash key for the JSON list output of all objects of this class in the JSS.
95
+ # Here's the top of the output from the 'computergroups' RSRC_BASE:
82
96
  #
83
- # e.g. the JSON output of resource "JSSResource/computergroups" is a hash
84
- # with one item (an Array of computergroups). That item's key is the Symbol :computer_groups
97
+ # {:computer_groups=>
98
+ # [{:id=>1020, :name=>"config-no-turnstile", :is_smart=>true},
99
+ # {:id=>1357, :name=>"10.8 Laptops", :is_smart=>true},
100
+ # {:id=>1094, :name=>"wifi_cert-expired", :is_smart=>true},
101
+ # {:id=>1144, :name=>"mytestgroup", :is_smart=>false},
102
+ # ...
85
103
  #
86
- # ==== RSRC_OBJECT_KEY = [Symbol] The Hash key used for individual JSON object output.
87
- # It's also used in various error messages
104
+ # Notice that the Array we want is embedded in a one-item Hash, and the
105
+ # key in that Hash for the desired Array is the Symbol :computer_groups.
88
106
  #
89
- # e.g. the JSON output of the resource "JSSResource/computergroups/id/436" is
90
- # a hash with one item (another hash with details of one computergroup).
91
- # That item's key is the Symbol :computer_group
107
+ # That symbol is the value needed in the RSRC_LIST_KEY constant.
92
108
  #
93
- # ==== VALID_DATA_KEYS = [Array<Symbol>] The Hash keys used to verify validity of :data
94
- # When instantiating a subclass using :data => somehash, some minimal checks are performed
95
- # to ensure the data is valid for the subclass
109
+ # The '.all_ids', '.all_names' and other '.all_*' class methods use the
110
+ # list-resource Array to extract other Arrays of the desired values - which
111
+ # can be used to check for existance without retrieving an entire object,
112
+ # among other uses.
96
113
  #
97
- # The Symbols in this Array are compared to the keys of the hash provided.
98
- # If any of these don't exist in the hash's keys, then the :data is
99
- # not valid and an exception is raised.
114
+ # ==== RSRC_OBJECT_KEY [Symbol]
100
115
  #
101
- # The keys :id and :name must always exist in the hash.
102
- # If only :id and :name are valid, VALID_DATA_KEYS should be an empty array.
116
+ # The one-item Hash key used for individual JSON object output. It's also
117
+ # used in various error messages
103
118
  #
104
- # e.g. for a department, only :id and :name are valid, so VALID_DATA_KEYS is an empty Array ([])
105
- # but for a computer group, the keys :computers and :is_smart must be present as well.
106
- # so VALID_DATA_KEYS will be [:computers, :is_smart]
119
+ # As with the list-resource output mentioned above, when GETting a specific
120
+ # object resource, there's an extra layer of encapsulation in a one-item Hash.
121
+ # Here's the top of the JSON for a single computer group fetched
122
+ # from '...computergroups/id/1043'
107
123
  #
108
- # *NOTE* Some API objects have data broken into subsections, in which case the
109
- # VALID_DATA_KEYS are expected in the section :general.
124
+ # {:computer_group=>
125
+ # {:id=>1043,
126
+ # :name=>"tmp-no-d3",
127
+ # :is_smart=>false,
128
+ # :site=>{:id=>-1, :name=>"None"},
129
+ # :criteria=>[],
130
+ # :computers=>[
131
+ # ...
110
132
  #
133
+ # The data for the group itself is the inner Hash.
134
+ #
135
+ # The RSRC_OBJECT_KEY in this case is set to :computer_group - the key
136
+ # in the top-level, one-item Hash that we need to get the real Hash about the
137
+ # object.
111
138
  #
112
139
  # === Optional Constants
113
140
  #
114
- # ==== OTHER_LOOKUP_KEYS = [Hash{Symbol=>Hash}] Every object can be looked up by
115
- # :id and :name, but some have other uniq identifiers that can also be used,
116
- # e.g. :serial_number, :mac_address, and so on. This Hash, if defined,
117
- # speficies those other keys for the subclass
118
- # For more details about this hash, see {APIObject::DEFAULT_LOOKUP_KEYS},
119
- # {APIObject.fetch}, and {APIObject#lookup_object_data}
141
+ # === OTHER_LOOKUP_KEYS
142
+ #
143
+ # Fetching individual objects from the API is usuallly done via the object's
144
+ # unique JSS id, via a resrouce URL like so:
145
+ #
146
+ # ...JSSResource/<RSRC_BASE>/id/<idnumber>
147
+ #
148
+ # Most objects can also be looked-up by name, because the API also has
149
+ # and endpoint ..JSSResource/<RSRC_BASE>/name/<name>
150
+ # (See {NON_UNIQUE_NAMES} below)
151
+ #
152
+ # Some objects, like Computers and MobileDevices, have other values that
153
+ # serve as unique identifiers and can also be used as 'lookup keys' for
154
+ # fetching individual objects. When this is the case, those values always
155
+ # appear in the objects list-resource data (See {RSRC_LIST_KEY} above).
156
+ #
157
+ # For example, here's a summary-hash for a single MobileDevice from the
158
+ # list-resource '...JSSResource/mobiledevices', which you might get in the
159
+ # Array returned by JSS::MobileDevice.all:
160
+ #
161
+ # {
162
+ # :id=>3964,
163
+ # :name=>"Bear",
164
+ # :device_name=>"Bear",
165
+ # :udid=>"XXX",
166
+ # :serial_number=>"YYY2244MM60",
167
+ # :phone_number=>"510-555-1212",
168
+ # :wifi_mac_address=>"00:00:00:00:00:00",
169
+ # :managed=>true,
170
+ # :supervised=>false,
171
+ # :model=>"iPad Pro (9.7-inch Cellular)",
172
+ # :model_identifier=>"iPad6,4",
173
+ # :modelDisplay=>"iPad Pro (9.7-inch Cellular)",
174
+ # :model_display=>"iPad Pro (9.7-inch Cellular)",
175
+ # :username=>"fred"
176
+ # }
177
+ #
178
+ # For MobileDevices, serial_number, udid, and wifi_mac_address are also
179
+ # all unique identifiers for an individual device, and can be used to
180
+ # fetch them.
181
+ #
182
+ # To specify other identifiers for an APIObject subclass, create the constant
183
+ # OTHER_LOOKUP_KEYS containing a Hash of Hashes, like so:
184
+ #
185
+ # OTHER_LOOKUP_KEYS = {
186
+ # serial_number: {
187
+ # aliases: [:serialnumber, :sn],
188
+ # fetch_rsrc_key: :serialnumber
189
+ # },
190
+ # udid: {
191
+ # fetch_rsrc_key: :udid
192
+ # },
193
+ # wifi_mac_address: {
194
+ # aliases: [:macaddress, :macaddr],
195
+ # fetch_rsrc_key: :macaddress
196
+ # }
197
+ # }.freeze
198
+ #
199
+ # The keys in OTHER_LOOKUP_KEYS are the keys in a summary-hash data from .all
200
+ # that hold a unique identifier. Each value is a Hash with one or two keys:
201
+ #
202
+ # - aliases: [Array<Symbol>]
203
+ # Aliases for that identifier, i.e. abbreviations or spelling variants.
204
+ # These aliases can be used in fetching, and they also have
205
+ # matching `.all_<aliase>s` methods.
206
+ #
207
+ # If no aliases are needed, don't specify anything, as with the udid:
208
+ # in the example above
209
+ #
210
+ # - fetch_rsrc_key: [Symbol]
211
+ # Often a unique identifier can be used to build a URL for fetching (or
212
+ # updating or deleteing) an object with that value, rather than with id.
213
+ # For example, while the MobileDevice in the example data above would
214
+ # normally be fetched at the resource 'JSSResource/mobiledevices/id/3964'
215
+ # it can also be fetched at
216
+ # 'JSSResource/mobiledevices/serialnumber/YYY2244MM60'.
217
+ # Since the URL is built using 'serialnumber', the symbol :serialnumber
218
+ # is used as the fetch_rsrc_key.
219
+ #
220
+ # Setting a fetch_rsrc_key: for one of the OTHER_LOOKUP_KEYS tells ruby-jss
221
+ # that such a URL is available, and fetching by that lookup key will be
222
+ # faster when using that URL.
223
+ #
224
+ # If a fetch_rsrc_key is not set, fetching will be slower, since the fetch
225
+ # method must first refresh the list of all available objects to find the
226
+ # id to use for building the resource URL.
227
+ # This is also true when fetching without specifying which lookup key to
228
+ # use, e.g. `.fetch 'foo'` vs. `.fetch sn: 'foo'`
229
+ #
230
+ # The OTHER_LOOKUP_KEYS, if defined, are merged with the DEFAULT_LOOKUP_KEYS
231
+ # defined below via the {APIObject.lookup_keys} class method, They are used for:
232
+ #
233
+ # - creating list-methods:
234
+ # For each lookup key, a class method `.all_<key>s` is created
235
+ # automatically, e.g. `.all_serial_numbers`. The aliases are used to
236
+ # make alises of those methods, e.g. `.all_sns`
237
+ #
238
+ # - finding valid ids:
239
+ # The {APIObject.valid_id} class method looks at the known lookup keys to
240
+ # find an object's id.
241
+ #
242
+ # - fetching:
243
+ # When an indentifier is given to `.fetch`, the fetch_rsrc_key is used to
244
+ # build the resource URL for fetching the object. If there is no
245
+ # fetch_rsrc_key, the lookup_keys and aliases are used to find the matching
246
+ # id, which is used to build the URL.
247
+ #
248
+ # When no identifier is specified, .fetch uses .valid_id, described above.
249
+ #
250
+ # ==== NON_UNIQUE_NAMES
251
+ #
252
+ # Some JSS objects, like Computers and MobileDevices, do not treat names
253
+ # as unique in the JSS, but they can still be used for fetching objects.
254
+ # The API itself will return data for a non-unique name lookup, but there's
255
+ # no way to guarantee which object you get back.
256
+ #
257
+ # In those subclasses, set NON_UNIQUE_NAMES to any value, and a
258
+ # JSS::AmbiguousError exception will be raised when trying to fetch by name
259
+ # and the name isn't unique.
260
+ #
261
+ # Because of the extra processing, the check for this state will only happen
262
+ # when NON_UNIQUE_NAMES is set. If not set at all, the check doesn't happen
263
+ # and if multiple objects have the same name, which one is returned is
264
+ # undefined.
265
+ #
266
+ # When that's the case, fetching explicitly by name, or when fetching with a
267
+ # plain search term that matches a non-unique name, will raise a
268
+ # JSS::AmbiguousError exception,when the name isn't unique. If that happens,
269
+ # you'll have to use some other identifier to fetch the desired object.
270
+ #
271
+ # Note: Fetching, finding valid id, and name collisions are case-insensitive.
120
272
  #
121
273
  class APIObject
122
274
 
123
275
  # Constants
124
276
  ####################################
125
277
 
278
+ # '.new' can only be called from these methods:
126
279
  OK_INSTANTIATORS = ['make', 'fetch', 'block in fetch'].freeze
127
280
 
281
+ # See the discussion of 'Lookup Keys' in the comments/docs
282
+ # for {JSS::APIObject}
283
+ #
284
+ DEFAULT_LOOKUP_KEYS = {
285
+ id: { fetch_rsrc_key: :id },
286
+ name: { fetch_rsrc_key: :name }
287
+ }.freeze
288
+
289
+ # This table holds the object history for JSS objects.
290
+ # Object history is not available via the API,
291
+ # only MySQL.
292
+ OBJECT_HISTORY_TABLE = 'object_history'.freeze
293
+
128
294
  # Class Methods
129
295
  #####################################
130
296
 
297
+ # What are all the lookup keys available for this class, with
298
+ # all their aliases (or optionally not) or with their fetch_rsrc_keys
299
+ #
300
+ # This method combines the DEFAULT_LOOOKUP_KEYS defined above, with the
301
+ # optional OTHER_LOOKUP_KEYS from a subclass (See 'Lookup Keys' in the
302
+ # class comments/docs above)
303
+ #
304
+ # The hash returned flattens and inverts the two source hashes, so that
305
+ # all possible lookup keys (the keys and their aliases) are hash keys
306
+ # and the non-aliased lookup key is the value.
307
+ #
308
+ # For example, when
309
+ #
310
+ # OTHER_LOOKUP_KEYS = {
311
+ # serial_number: { aliases: [:serialnumber, :sn], fetch_rsrc_key: :serialnumber },
312
+ # udid: { fetch_rsrc_key: :udid },
313
+ # wifi_mac_address: { aliases: [:macaddress, :macaddr], fetch_rsrc_key: :macaddress }
314
+ # }
315
+ #
316
+ # It is combined with DEFAULT_LOOKUP_KEYS to produce:
317
+ #
318
+ # {
319
+ # id: :id,
320
+ # name: :name,
321
+ # serial_number: :serial_number,
322
+ # serialnumber: :serial_number,
323
+ # sn: :serial_number,
324
+ # udid: :udid,
325
+ # wifi_mac_address: :wifi_mac_address,
326
+ # macaddress: :wifi_mac_address,
327
+ # macaddr: :wifi_mac_address
328
+ # }
329
+ #
330
+ # If the optional parameter no_aliases: is truthy, only the real keynames
331
+ # are returned in an array, so the above would become
332
+ #
333
+ # [:id, :name, :serial_number, :udid, :wifi_mac_address]
334
+ #
335
+ # @param no_aliases [Boolean] Only return the real keys, no aliases.
336
+ #
337
+ # @return [Hash {Symbol: Symbol}] when no_aliases is falsey, the lookup keys
338
+ # and aliases for this subclass.
339
+ #
340
+ # @return [Array<Symbol>] when no_aliases is truthy, the lookup keys for this
341
+ # subclass
342
+ #
343
+ def self.lookup_keys(no_aliases: false, fetch_rsrc_keys: false)
344
+ parse_lookup_keys unless @lookup_keys
345
+ no_aliases ? @lookup_keys.values.uniq : @lookup_keys
346
+ end
347
+
348
+ # Given a lookup or, or an alias of one, return the matching fetch_rsrc_key
349
+ # for building a fetch/GET resource URL, or nil if no fetch_rsrc_key is defined.
350
+ #
351
+ # See {OTHER_LOOKUP_KEYS} in the APIObject class comments/docs above for details.
352
+ #
353
+ # @param lookup_key [Symbol] A lookup key, or an aliases of one, for this
354
+ # subclass.
355
+ #
356
+ # @return [Symbol, nil] the fetch_rsrc_key for that lookup key.
357
+ #
358
+ def self.fetch_rsrc_key(lookup_key)
359
+ parse_lookup_keys unless @fetch_rsrc_keys
360
+ @fetch_rsrc_keys[lookup_key]
361
+ end
362
+
363
+ # Used by .lookup_keys
364
+ #
365
+ def self.parse_lookup_keys
366
+ @lookup_keys = {}
367
+ @fetch_rsrc_keys = {}
368
+
369
+ hsh = DEFAULT_LOOKUP_KEYS.dup
370
+ hsh.merge!(self::OTHER_LOOKUP_KEYS) if defined? self::OTHER_LOOKUP_KEYS
371
+
372
+ hsh.each do |key, info|
373
+ @lookup_keys[key] = key
374
+ @fetch_rsrc_keys[key] = info[:fetch_rsrc_key]
375
+ next unless info[:aliases]
376
+
377
+ info[:aliases].each do |a|
378
+ @lookup_keys[a] = key
379
+ @fetch_rsrc_keys[a] = info[:fetch_rsrc_key]
380
+ end
381
+ end # self::OTHER_LOOKUP_KEYS.each
382
+ end
383
+ private_class_method :parse_lookup_keys
384
+
385
+ # get the real lookup key frm a given alias
386
+ #
387
+ # @param key[Symbol] the key or an aliase of the key
388
+ #
389
+ # @return [Symbol] the real key for the given key
390
+ #
391
+ def self.real_lookup_key(key)
392
+ real_key = lookup_keys[key]
393
+ raise ArgumentError, "Unknown lookup key '#{key}' for #{self}" unless real_key
394
+
395
+ real_key
396
+ end
397
+
131
398
  # Return an Array of Hashes for all objects of this subclass in the JSS.
132
399
  #
133
400
  # This method is only valid in subclasses of JSS::APIObject, and is
134
- # the parsed JSON output of an API query for the resource defined in the subclass's RSRC_BASE,
401
+ # the parsed JSON output of an API query for the resource defined in the
402
+ # subclass's RSRC_BASE
403
+ #
135
404
  # e.g. for JSS::Computer, with the RSRC_BASE of :computers,
136
405
  # This method retuens the output of the 'JSSResource/computers' resource,
137
406
  # which is a list of all computers in the JSS.
138
407
  #
139
408
  # Each item in the Array is a Hash with at least two keys, :id and :name.
140
- # The class methods .all_ids and .all_names provide easier access to those data
141
- # as mapped Arrays.
409
+ # The class methods .all_ids and .all_names provide easier access to those
410
+ # dataas mapped Arrays.
142
411
  #
143
- # Some API classes provide other data in each Hash, e.g. :udid (for computers
144
- # and mobile devices) or :is_smart (for groups).
412
+ # Some API classes provide other keys in each Hash, e.g. :udid (for
413
+ # computers and mobile devices) or :is_smart (for groups).
145
414
  #
146
- # Subclasses implementing those API classes should provide .all_xxx
147
- # class methods for accessing those other values as mapped Arrays,
148
- # e.g. JSS::Computer.all_udids
415
+ # For those keys that are listed in a subclass's lookup_keys method,
416
+ # there are matching methods `.all_(key)s` which return an array
417
+ # just of those values, from the values of this hash. For example,
418
+ # `.all_udids` will use the .all array to return an array of just udids,
419
+ # if the subclass defines :udid in its OTHER_LOOKUP_KEYS (See 'Lookup Keys'
420
+ # in the class comments/docs above)
149
421
  #
150
- # The results of the first query for each subclass is stored in the .object_list_cache
151
- # of the given JSS::APIConnection and returned at every future call, so as
152
- # to not requery the server every time.
422
+ # Subclasses should provide appropriate .all_xxx class methods for accessing
423
+ # any other other values as Arrays, e.g. JSS::Computer.all_managed
153
424
  #
154
- # To force requerying to get updated data, provided a non-false argument.
425
+ # -- Caching
426
+ #
427
+ # The results of the first call to .all for each subclass is cached in the
428
+ # .object_list_cache of the given {JSS::APIConnection} and that cache is
429
+ # used for all future calls, so as to not requery the server every time.
430
+ #
431
+ # To force requerying to get updated data, provided a truthy argument.
155
432
  # I usually use :refresh, so that it's obvious what I'm doing, but true, 1,
156
433
  # or anything besides false or nil will work.
157
434
  #
435
+ # The various methods that use the output of this method also take the
436
+ # refresh parameter which will be passed here as needed.
437
+ #
438
+ # -- Alternate API connections
439
+ #
158
440
  # To query an APIConnection other than the currently active one,
159
441
  # provide one via the api: named parameter.
160
442
  #
@@ -166,69 +448,68 @@ module JSS
166
448
  # @return [Array<Hash{:name=>String, :id=> Integer}>]
167
449
  #
168
450
  def self.all(refresh = false, api: JSS.api)
169
- raise JSS::UnsupportedError, '.all can only be called on subclasses of JSS::APIObject' if self == JSS::APIObject
170
- api.object_list_cache[self::RSRC_LIST_KEY] = nil if refresh
171
- return api.object_list_cache[self::RSRC_LIST_KEY] if api.object_list_cache[self::RSRC_LIST_KEY]
172
- api.object_list_cache[self::RSRC_LIST_KEY] = api.get_rsrc(self::RSRC_BASE)[self::RSRC_LIST_KEY]
173
- end
451
+ validate_not_metaclass(self)
174
452
 
175
- # Returns an Array of the JSS id numbers of all the members
176
- # of the subclass.
177
- #
178
- # e.g. When called from subclass JSS::Computer,
179
- # returns the id's of all computers in the JSS
180
- #
181
- # @param refresh[Boolean] should the data be re-queried from the API?
182
- #
183
- # @param api[JSS::APIConnection] an API connection to use for the query.
184
- # Defaults to the corrently active API. See {JSS::APIConnection}
185
- #
186
- # @return [Array<Integer>] the ids of all it1ems of this subclass in the JSS
187
- #
188
- def self.all_ids(refresh = false, api: JSS.api)
189
- all(refresh, api: api).map { |i| i[:id] }
453
+ cache = api.object_list_cache
454
+ cache_key = self::RSRC_LIST_KEY
455
+ cache[cache_key] = nil if refresh
456
+ return cache[cache_key] if cache[cache_key]
457
+
458
+ cache[cache_key] = api.get_rsrc(self::RSRC_BASE)[cache_key]
190
459
  end
191
460
 
192
- # Returns an Array of the JSS names of all the members
193
- # of the subclass.
194
- #
195
- # e.g. When called from subclass JSS::Computer,
196
- # returns the names of all computers in the JSS
461
+ # @return [Hash {String => Integer}] name => number of occurances
197
462
  #
198
- # @param refresh[Boolean] should the data be re-queried from the API?
199
- #
200
- # @param api[JSS::APIConnection] an API connection to use for the query.
201
- # Defaults to the corrently active API. See {JSS::APIConnection}
202
- #
203
- # @return [Array<String>] the names of all item of this subclass in the JSS
204
- #
205
- def self.all_names(refresh = false, api: JSS.api)
206
- all(refresh, api: api).map { |i| i[:name] }
463
+ def self.duplicate_names(refresh = false, api: JSS.api)
464
+ return {} unless defined? self::NON_UNIQUE_NAMES
465
+
466
+ dups = {}
467
+ all(refresh, api: api).each do |obj|
468
+ if dups[obj[:name]]
469
+ dups[obj[:name]] += 1
470
+ else
471
+ dups[obj[:name]] = 1
472
+ end # if
473
+ end # all(refresh, api: api).each
474
+ dups.delete_if { |k,v| v == 1 }
475
+ dups
207
476
  end
208
477
 
209
478
  # Return a hash of all objects of this subclass
210
479
  # in the JSS where the key is the id, and the value
211
480
  # is some other key in the data items returned by the JSS::APIObject.all.
212
481
  #
213
- # If the other key doesn't exist in the API
214
- # data, (eg :udid for JSS::Department) the values will be nil.
482
+ # If the other key doesn't exist in the API summary data from .all
483
+ # (eg :udid for JSS::Department) the values will be nil.
215
484
  #
216
485
  # Use this method to map ID numbers to other identifiers returned
217
486
  # by the API list resources. Invert its result to map the other
218
487
  # identfier to ids.
219
488
  #
220
489
  # @example
221
- # JSS::Computer.map_all_ids_to(:name)
490
+ # JSS::Computer.map_all_ids_to(:serial_number)
222
491
  #
223
- # # Returns, eg {2 => "kimchi", 5 => "mantis"}
492
+ # # Returns, eg {2 => "C02YD3U8JHD3", 5 => "VMMz7xgg8lYZ"}
224
493
  #
225
- # JSS::Computer.map_all_ids_to(:name).invert
494
+ # JSS::Computer.map_all_ids_to(:serial_number).invert
226
495
  #
227
- # # Returns, eg {"kimchi" => 2, "mantis" => 5}
496
+ # # Returns, eg {"C02YD3U8JHD3" => 2, "VMMz7xgg8lYZ" => 5}
497
+ #
498
+ # These hashes are cached separately from the .all data, and when
499
+ # the refresh parameter is truthy, both will be refreshed.
500
+ #
501
+ # WARNING: Some values in the output of .all are not guaranteed to be unique
502
+ # in Jamf Pro. This is fine in the direct output of this method, each id
503
+ # will be the key for some value and many ids might have the same value.
504
+ # However if you invert that hash, the values become keys, and the ids
505
+ # become the values, and there can be only one id per each new key. Which
506
+ # id becomes associated with a value is undefined, and data about the others
507
+ # is lost. This is especially important if you `.map_all_ids_to :name`,
508
+ # since, for some objects, names are not unique.
228
509
  #
229
510
  # @param other_key[Symbol] the other data key with which to associate each id
230
511
  #
231
- # @param refresh[Boolean] should the data re-queried from the API?
512
+ # @param refresh[Boolean] should the data re-queried from the API?
232
513
  #
233
514
  # @param api[JSS::APIConnection] an API connection to use for the query.
234
515
  # Defaults to the corrently active API. See {JSS::APIConnection}
@@ -236,16 +517,26 @@ module JSS
236
517
  # @return [Hash{Integer => Oject}] the associated ids and data
237
518
  #
238
519
  def self.map_all_ids_to(other_key, refresh = false, api: JSS.api)
239
- h = {}
240
- all(refresh, api: api).each { |i| h[i[:id]] = i[other_key] }
241
- h
520
+ # we will accept any key, it'll just return nil if not in the
521
+ # .all hashes. However if we're given an alias of a lookup key
522
+ # we need to convert it to its real name.
523
+ other_key = lookup_keys[other_key] if lookup_keys[other_key]
524
+
525
+ cache_key = "#{self::RSRC_LIST_KEY}_map_#{other_key}".to_sym
526
+ cache = api.object_list_cache
527
+ cache[cache_key] = nil if refresh
528
+ return cache[cache_key] if cache[cache_key]
529
+
530
+ map = {}
531
+ all(refresh, api: api).each { |i| map[i[:id]] = i[other_key] }
532
+ cache[cache_key] = map
242
533
  end
243
534
 
244
535
  # Return an Array of JSS::APIObject subclass instances
245
- # e.g when called on JSS::Package, return all JSS::Package
246
- # objects in the JSS.
536
+ # e.g when called on JSS::Package, return a hash of JSS::Package instancesa
537
+ # for every package in the JSS.
247
538
  #
248
- # NOTE: This may be slow as it has to look up each object individually!
539
+ # WARNING: This may be slow as it has to look up each object individually!
249
540
  # use it wisely.
250
541
  #
251
542
  # @param refresh[Boolean] should the data re-queried from the API?
@@ -253,16 +544,32 @@ module JSS
253
544
  # @param api[JSS::APIConnection] an API connection to use for the query.
254
545
  # Defaults to the corrently active API. See {JSS::APIConnection}
255
546
  #
256
- # @return [Hash{Integer => Object}] the objects requested
547
+ # @return [Array<APIObject>] the objects requested
257
548
  #
258
549
  def self.all_objects(refresh = false, api: JSS.api)
259
- objects_key = "#{self::RSRC_LIST_KEY}_objects".to_sym
260
- return api.object_list_cache[objects_key] unless refresh || api.object_list_cache[objects_key].nil?
261
- api.object_list_cache[objects_key] = all(refresh, api: api).map { |o| fetch id: o[:id], api: api }
550
+ objects_cache_key ||= "#{self::RSRC_LIST_KEY}_objects".to_sym
551
+ api_cache = api.object_list_cache
552
+ api_cache[objects_cache_key] = nil if refresh
553
+
554
+ return api_cache[objects_cache_key] if api_cache[objects_cache_key]
555
+ all = all(refresh, api: api)
556
+ api_cache[objects_cache_key] = all.map do |o|
557
+ fetch id: o[:id], api: api, refresh: false
558
+ end
262
559
  end
263
560
 
264
- # Return true or false if an object of this subclass
265
- # with the given Identifier exists on the server
561
+
562
+ # Return the id of the object of this subclass with the given identifier.
563
+ #
564
+ # Return nil if no object has an identifier that matches.
565
+ #
566
+ # For all objects the 'name' is an identifier. Some objects have more, e.g.
567
+ # udid, mac_address & serial_number. Matches are case-insensitive.
568
+ #
569
+ # NOTE: while name is an identifier, for Computers and MobileDevices, it
570
+ # need not be unique in Jamf. If name is matched, which one gets returned
571
+ # is undefined. In short - dont' use names here unless you know they are
572
+ # unique.
266
573
  #
267
574
  # @param identfier [String,Integer] An identifier for an object, a value for
268
575
  # one of the available lookup_keys
@@ -272,14 +579,94 @@ module JSS
272
579
  # @param api[JSS::APIConnection] an API connection to use for the query.
273
580
  # Defaults to the corrently active API. See {JSS::APIConnection}
274
581
  #
275
- # @return [Boolean] does an object with the given identifier exist?
582
+ # @return [Integer, nil] the id of the matching object, or nil if it doesn't exist
276
583
  #
277
- def self.exist?(identifier, refresh = false, api: JSS.api)
278
- !valid_id(identifier, refresh, api: api).nil?
584
+ def self.valid_id(identifier, refresh = false, api: JSS.api)
585
+ # refresh if needed
586
+ all(refresh, api: api) if refresh
587
+
588
+ # it its a valid id, return it
589
+ return identifier if all_ids.include? identifier
590
+
591
+ keys_to_check = lookup_keys(no_aliases: true)
592
+ keys_to_check.delete :id # we've already checked :id
593
+
594
+ # downcase for speedy case-insensitivity -
595
+ # include?, and I assume value?, is faster with downcasing. See
596
+ # https://stackoverflow.com/questions/9333952/case-insensitive-arrayinclude/9334066#9334066
597
+ identifier.downcase! if identifier.is_a? String
598
+
599
+ keys_to_check.each do |key|
600
+ mapped_ids = map_all_ids_to key, api: api
601
+ # downcase - see comment above
602
+ mapped_ids.each { |_k, v| v.downcase! if v.is_a? String }
603
+
604
+ # if name is not unique, skip to the next key, there is no
605
+ # valid id for a non-unique name
606
+ if key == :name
607
+ num_name_matches = mapped_ids.values.select { |n| n == identifier }.size
608
+ next unless num_name_matches == 1
609
+ else
610
+ next unless mapped_ids.value? identifier
611
+ end
612
+
613
+ return mapped_ids.invert[identifier]
614
+ end
615
+
616
+ nil
617
+ end
618
+
619
+ # Return the id of the object of this subclass with the given
620
+ # lookup key == a given identifier.
621
+ #
622
+ # Return nil if no object has that value in that key
623
+ #
624
+ # @example
625
+ # # get the id for the computer with serialnum 'xyxyxyxy'
626
+ # JSS::Computer.id_for_identifier :serial_number, 'xyxyxyxy'
627
+ #
628
+ # # => the Integer id, or nil if no such serial number
629
+ #
630
+ # Raises a JSS::Ambiguous error if NON_UNIQUE_NAMES is set and
631
+ # a :name isn't unique
632
+ #
633
+ # @param key [Symbol] they key in which to look for the identifier. Must be
634
+ # a valid lookup key for this subclass.
635
+ #
636
+ # @param identfier [String,Integer] An identifier for an object, a value for
637
+ # one of the available lookup_keys
638
+ #
639
+ # @param refresh [Boolean] Should the cached summary data be re-read from
640
+ # the server first?
641
+ #
642
+ # @param api[JSS::APIConnection] an API connection to use for the query.
643
+ # Defaults to the corrently active API. See {JSS::APIConnection}
644
+ #
645
+ # @return [Integer, nil] the id of the matching object, or nil if it
646
+ # doesn't exist
647
+ #
648
+ def self.id_for_identifier(key, ident, refresh = false, api: JSS.api)
649
+ # refresh if needed
650
+ all(refresh, api: api) if refresh
651
+
652
+ # get the real key if an alias was used
653
+ key = real_lookup_key key
654
+
655
+ return all_ids.include?(ident) ? ident : nil if key == :id
656
+
657
+ validate_unique_name(ident) if key == :name
658
+
659
+ # downcase for speed
660
+ ident.downcase! if ident.is_a? String
661
+ mapped_ids = map_all_ids_to key, api: api
662
+ mapped_ids.each { |_k, v| v.downcase! if v.is_a? String }
663
+ return nil unless mapped_ids.value? ident
664
+
665
+ mapped_ids.invert[ident]
279
666
  end
280
667
 
281
- # Return an id or nil if an object of this subclass
282
- # with the given name or id exists on the server
668
+ # Return true or false if an object of this subclass
669
+ # with the given Identifier exists on the server
283
670
  #
284
671
  # @param identfier [String,Integer] An identifier for an object, a value for
285
672
  # one of the available lookup_keys
@@ -289,16 +676,10 @@ module JSS
289
676
  # @param api[JSS::APIConnection] an API connection to use for the query.
290
677
  # Defaults to the corrently active API. See {JSS::APIConnection}
291
678
  #
292
- # @return [Integer, nil] the id of the matching object, or nil if it doesn't exist
679
+ # @return [Boolean] does an object with the given identifier exist?
293
680
  #
294
- def self.valid_id(identifier, refresh = false, api: JSS.api)
295
- return identifier if all_ids(refresh, api: api).include? identifier
296
- all_lookup_keys.keys.each do |key|
297
- next if key == :id
298
- id = map_all_ids_to(key, api: api).invert[identifier]
299
- return id if id
300
- end # do key
301
- nil
681
+ def self.exist?(identifier, refresh = false, api: JSS.api)
682
+ !valid_id(identifier, refresh, api: api).nil?
302
683
  end
303
684
 
304
685
  # Convert an Array of Hashes of API object data to a
@@ -383,83 +764,90 @@ module JSS
383
764
  end
384
765
  end
385
766
 
386
- # What are all the lookup keys available for this class?
767
+ # Retrieve an object from the API and return an instance of this APIObject
768
+ # subclass.
387
769
  #
388
- # @return [Array<Symbol>] the DEFAULT_LOOKUP_KEYS plus any OTHER_LOOKUP_KEYS
389
- # defined for this class
770
+ # @example
771
+ # # computer where 'xyxyxyxy' is in any of the lookup key fields
772
+ # JSS::Computer.fetch 'xyxyxyxy'
390
773
  #
391
- def self.lookup_keys
392
- return DEFAULT_LOOKUP_KEYS.keys unless defined? self::OTHER_LOOKUP_KEYS
393
- DEFAULT_LOOKUP_KEYS.keys + self::OTHER_LOOKUP_KEYS.keys
394
- end
395
-
396
- # @return [Hash] the available lookup keys mapped to the appropriate
397
- # resource key for building a REST url to retrieve an object.
774
+ # # computer where 'xyxyxyxy' is the serial number
775
+ # JSS::Computer.fetch serial_number: 'xyxyxyxy'
398
776
  #
399
- def self.rsrc_keys
400
- hash = {}
401
- all_lookup_keys.each { |key, deets| hash[key] = deets[:rsrc_key] }
402
- hash
403
- end
404
-
405
- # the available list methods for an APIObject sublcass
777
+ # Fetching is faster when specifying a lookup key, and that key has a
778
+ # fetch_rsrc_key defined in its OTHER_LOOKUP_KEYS constant, as in the second
779
+ # example above.
406
780
  #
407
- # @return [Array<Symbol>] The list methods (e.g. .all_serial_numbers) for
408
- # this APIObject subclass
781
+ # When no lookup key is given, as in the first example above, or when that
782
+ # key doesn't have a defined fetch_rsrc_key, ruby-jss uses the currently cached
783
+ # list resource data to find the id matching the value given, and that id
784
+ # is used to fetch the object. (see 'List Resources and Lookup Keys' in the
785
+ # APIObject comments/docs above)
409
786
  #
410
-
411
- # The combined DEFAULT_LOOKUP_KEYS and OTHER_LOOKUP_KEYS
412
- # (which may be defined in subclasses)
413
- #
414
- # @return [Hash] See DEFAULT_LOOKUP_KEYS constant
787
+ # Since that cached list data may be out of date, you can provide the param
788
+ # `refrsh: true`, to reload the list from the server. This will cause the
789
+ # fetch to be slower still, so use with caution.
415
790
  #
416
- def self.all_lookup_keys
417
- return DEFAULT_LOOKUP_KEYS.merge(self::OTHER_LOOKUP_KEYS) if defined? self::OTHER_LOOKUP_KEYS
418
- DEFAULT_LOOKUP_KEYS
419
- end
420
-
421
- # @return [Hash] the available lookup keys mapped to the appropriate
422
- # list class method (e.g. id: :all_ids )
791
+ # For creating new objects in the JSS, use {APIObject.make}
423
792
  #
424
- def self.lookup_key_list_methods
425
- hash = {}
426
- all_lookup_keys.each { |key, deets| hash[key] = deets[:list] }
427
- hash
428
- end
429
-
430
- # Retrieve an object from the API.
793
+ # @param searchterm[String, Integer] An single value to
794
+ # search for in all the lookup keys for this clsss. This is slower
795
+ # than specifying a lookup key
431
796
  #
432
- # This is the preferred way to retrieve existing objects from the JSS.
433
- # It's a wrapper for using APIObject.new and avoids the confusion of using
434
- # ruby's .new class method when you're not creating a new object in the JSS
797
+ # @param args[Hash] the remaining options for fetching an object.
798
+ # If no searchterm is provided, one of the args must be a valid
799
+ # lookup key and value to find in that key, e.g. `serial_number: '1234567'`
435
800
  #
436
- # For creating new objects in the JSS, use {APIObject.make}
801
+ # @option args api[JSS::APIConnection] an API connection to use for the query.
802
+ # Defaults to the corrently active API. See {JSS::APIConnection}
437
803
  #
438
- # @param args[Hash] The data for fetching an object, such as id: or name:
439
- # Each APIObject subclass can define additional lookup keys for fetching.
804
+ # @option args refresh[Boolean] should the summary list of all objects be
805
+ # reloaded from the API before being used to look for this object.
440
806
  #
441
807
  # @return [APIObject] The ruby-instance of a JSS object
442
808
  #
443
- def self.fetch(arg, api: JSS.api)
444
- raise JSS::UnsupportedError, 'JSS::APIObject cannot be instantiated' if self.class == JSS::APIObject
809
+ def self.fetch(searchterm = nil, **args)
810
+ validate_not_metaclass(self)
811
+
812
+ # which connection?
813
+ api = args.delete :api
814
+ api ||= JSS.api
815
+
816
+ # refresh the .all list if needed
817
+ all(:refresh, api: api) if args.delete :refresh
818
+
819
+ # a random object?
820
+ if searchterm == :random
821
+ return new id: all_ids.sample, api: api
822
+ end
823
+
824
+ # get the lookup key and value, if given
825
+ fetch_key, fetch_val = args.to_a.first
445
826
 
446
- # if given a hash (or a colletion of named params)
447
- # pass to .new
448
- if arg.is_a? Hash
449
- raise ArgumentError, 'Use .make to create new JSS objects' if arg[:id] == :new
450
- arg[:api] ||= api
451
- return new arg
827
+ if fetch_key
828
+ validate_unique_name(fetch_val) if fetch_key == :name
829
+
830
+ # does this lookup key have a fetch_rsrc_key?
831
+ fetch_rsrc_key = fetch_rsrc_key(fetch_key)
832
+ return new fetch_rsrc: "#{self::RSRC_BASE}/#{fetch_rsrc_key}/#{fetch_val}", api: api if fetch_rsrc_key
452
833
  end
453
834
 
454
- # loop thru the lookup_key list methods for this class
455
- # and if it's result includes the desired value,
456
- # the pass they key and arg to .new
457
- lookup_key_list_methods.each do |key, method_name|
458
- return new(key => arg, :api => api) if method_name && send(method_name).include?(arg)
459
- end # each key
835
+ # if we'ere here, we need to get the id from either the lookup key/val or
836
+ # the searchterm
837
+ if fetch_key
838
+ # it has an OTHER_LOOKUP_KEY but that key doesn't have a fetch_rsrc
839
+ # so we look in the .map_all_ids_to_* hash for it.
840
+ id = id_for_identifier fetch_key, fetch_val, api: api
841
+ err_detail = "where #{fetch_key} = #{fetch_val}"
842
+ elsif searchterm
843
+ id = valid_id searchterm, api: api
844
+ err_detail = "matching #{searchterm}"
845
+ else
846
+ raise ArgumentError, 'Missing searchterm or fetch key'
847
+ end
848
+ raise JSS::NoSuchItemError, "No #{self::RSRC_OBJECT_KEY} found #{err_detail}" unless id
460
849
 
461
- # if we're here, we couldn't find a matching object
462
- raise NoSuchItemError, "No matching #{self::RSRC_OBJECT_KEY} found"
850
+ new id: id, api: api
463
851
  end # fetch
464
852
 
465
853
  # Make a ruby instance of a not-yet-existing APIObject.
@@ -479,9 +867,10 @@ module JSS
479
867
  # @return [APIObject] The un-created ruby-instance of a JSS object
480
868
  #
481
869
  def self.make(**args)
482
- args[:api] ||= JSS.api
483
- raise JSS::UnsupportedError, 'JSS::APIObject cannot be instantiated' if self.class == JSS::APIObject
870
+ validate_not_metaclass(self)
484
871
  raise ArgumentError, "Use '#{self.class}.fetch id: xx' to retrieve existing JSS objects" if args[:id]
872
+
873
+ args[:api] ||= JSS.api
485
874
  args[:id] = :new
486
875
  new args
487
876
  end
@@ -489,9 +878,13 @@ module JSS
489
878
  # Disallow direct use of ruby's .new class method for creating instances.
490
879
  # Require use of .fetch or .make
491
880
  def self.new(**args)
881
+ validate_not_metaclass(self)
882
+
492
883
  calling_method = caller_locations(1..1).first.label
493
- # puts "Called By: #{calling_method}"
494
- raise JSS::UnsupportedError, 'Use .fetch or .make to instantiate APIObject classes' unless OK_INSTANTIATORS.include? calling_method
884
+ unless OK_INSTANTIATORS.include? calling_method
885
+ raise JSS::UnsupportedError, 'Use .fetch or .make to instantiate APIObject classes'
886
+ end
887
+
495
888
  super
496
889
  end
497
890
 
@@ -509,13 +902,12 @@ module JSS
509
902
  # @return [Array<Integer>] The id's that didn't exist when we tried to
510
903
  # delete them.
511
904
  #
512
- def self.delete(victims, api: JSS.api)
513
- raise JSS::UnsupportedError, '.delete can only be called on subclasses of JSS::APIObject' if self == JSS::APIObject
905
+ def self.delete(victims, refresh = true, api: JSS.api)
906
+ validate_not_metaclass(self)
907
+
514
908
  raise JSS::InvalidDataError, 'Parameter must be an Integer ID or an Array of them' unless victims.is_a?(Integer) || victims.is_a?(Array)
515
909
 
516
910
  case victims
517
- when Integer
518
- victims = [victims]
519
911
  when Integer
520
912
  victims = [victims]
521
913
  when Array
@@ -523,7 +915,7 @@ module JSS
523
915
  end
524
916
 
525
917
  skipped = []
526
- current_ids = all_ids :refresh, api: api
918
+ current_ids = all_ids refresh, api: api
527
919
  victims.each do |vid|
528
920
  if current_ids.include? vid
529
921
  api.delete_rsrc "#{self::RSRC_BASE}/id/#{vid}"
@@ -535,51 +927,21 @@ module JSS
535
927
  skipped
536
928
  end # self.delete
537
929
 
538
- ### Class Constants
539
- #####################################
930
+ # Can't use APIObject directly.
931
+ def self.validate_not_metaclass(klass)
932
+ raise JSS::UnsupportedError, 'JSS::APIObject is a metaclass. Do not use it directly' if klass == JSS::APIObject
933
+ end
540
934
 
541
- # These Symbols are added to VALID_DATA_KEYS for performing the
542
- # :data validity test described above.
543
- #
544
- REQUIRED_DATA_KEYS = %i[id name].freeze
935
+ # Raise an exception if a name is being used for fetching and it isn't
936
+ # unique. Case Insensitive
937
+ def self.validate_unique_name(name, refresh = false)
938
+ return unless defined? self::NON_UNIQUE_NAMES
545
939
 
546
- # All API objects have an id and a name. As such By these keys are available
547
- # for object lookups.
548
- #
549
- # Others can be defined by subclasses in their OTHER_LOOKUP_KEYS constant
550
- # which has the same format, described here:
551
- #
552
- # The merged Hashes DEFAULT_LOOKUP_KEYS and OTHER_LOOKUP_KEYS
553
- # (as provided by the .all_lookup_keys Class method)
554
- # define what unique identifiers can be passed as parameters to the
555
- # fetch method for retrieving an object from the API.
556
- # They also define the class methods that return a list (Array) of all such
557
- # identifiers for the class (e.g. the :all_ids class method returns an array
558
- # of all id's for an APIObject subclass)
559
- #
560
- # Since there's often a discrepency between the name of the identifier as
561
- # an attribute (e.g. serial_number) and the REST resource key for
562
- # retrieving that object (e.g. ../computers/serialnumber/xxxxx) this hash
563
- # also explicitly provides the REST resource key for a given lookup key, so
564
- # e.g. both serialnumber and serial_number can be used, and both will have
565
- # the resource key 'serialnumber' and the list method ':all_serial_numbers'
566
- #
567
- # Here's how the Hash is structured, using serialnumber as an example:
568
- #
569
- # LOOKUP_KEYS = {
570
- # serialnumber: {rsrc_key: :serialnumber, list: :all_serial_numbers},
571
- # serial_number: {rsrc_key: :serialnumber, list: :all_serial_numbers}
572
- # }
573
- #
574
- DEFAULT_LOOKUP_KEYS = {
575
- id: { rsrc_key: :id, list: :all_ids },
576
- name: { rsrc_key: :name, list: :all_names }
577
- }.freeze
940
+ name.downcase!
941
+ matches = all_names(refresh).map(&:downcase).select { |n| n == name }
942
+ raise JSS::AmbiguousError, "Name '#{name}' is not unique for #{self}" if matches.size > 1
943
+ end
578
944
 
579
- # This table holds the object history for JSS objects.
580
- # Object history is not available via the API,
581
- # only MySQL.
582
- OBJECT_HISTORY_TABLE = 'object_history'.freeze
583
945
 
584
946
  # Attributes
585
947
  #####################################
@@ -604,6 +966,11 @@ module JSS
604
966
  # @return [String] the Rest resource for API access (the part after "JSSResource/" )
605
967
  attr_reader :rest_rsrc
606
968
 
969
+ # Attibute Aliases
970
+ #####################################
971
+
972
+ alias in_jss? in_jss
973
+
607
974
  # Constructor
608
975
  #####################################
609
976
 
@@ -625,10 +992,9 @@ module JSS
625
992
  # API data e.g. to limit the data returned
626
993
  #
627
994
  #
628
- def initialize(args = {})
629
- args[:api] ||= JSS.api
995
+ def initialize(**args)
630
996
  @api = args[:api]
631
- raise JSS::UnsupportedError, 'JSS::APIObject is a metaclass and cannot be instantiated' if self.class == JSS::APIObject
997
+ @api ||= JSS.api
632
998
 
633
999
  # we're making a new one in the JSS
634
1000
  if args[:id] == :new
@@ -892,32 +1258,6 @@ module JSS
892
1258
  raise JSS::UnsupportedError, "Object History access is not supported for #{self.class} objects at this time" unless defined? self.class::OBJECT_HISTORY_OBJECT_TYPE
893
1259
  end
894
1260
 
895
- # If we were passed pre-lookedup API data, validate it,
896
- # raising exceptions if not valid.
897
- #
898
- # DEPRECATED: pre-lookedup data is never used
899
- # and support for it will be going away.
900
- #
901
- # TODO: delete this and all defined VALID_DATA_KEYS
902
- #
903
- # @return [void]
904
- #
905
- def validate_external_init_data
906
- # data must include all they keys in REQUIRED_DATA_KEYS + VALID_DATA_KEYS
907
- # in either the main hash keys or the :general sub-hash, if it exists
908
- hash_to_check = @init_data[:general] ? @init_data[:general] : @init_data
909
- combined_valid_keys = self.class::REQUIRED_DATA_KEYS + self.class::VALID_DATA_KEYS
910
- keys_ok = (hash_to_check.keys & combined_valid_keys).count == combined_valid_keys.count
911
- unless keys_ok
912
- raise(
913
- JSS::InvalidDataError,
914
- ":data is not valid JSON for a #{self.class::RSRC_OBJECT_KEY} from the API. It needs at least the keys :#{combined_valid_keys.join ', :'}"
915
- )
916
- end
917
- # and the id must be in the jss
918
- raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} with JSS id: #{@init_data[:id]}" unless \
919
- self.class.all_ids(api: @api).include? hash_to_check[:id]
920
- end # validate_init_data
921
1261
 
922
1262
  # If we're making a new object in the JSS, make sure we were given
923
1263
  # valid data to do so, raise exceptions otherwise.
@@ -940,20 +1280,14 @@ module JSS
940
1280
 
941
1281
  # Given initialization args, perform an API lookup for an object.
942
1282
  #
943
- # @param args[Hash] The args passed to #initialize
1283
+ # @param args[Hash] The args passed to #initialize, which must have either
1284
+ # key :id or key :fetch_rsrc
944
1285
  #
945
1286
  # @return [Hash] The parsed JSON data for the object from the API
946
1287
  #
947
1288
  def look_up_object_data(args)
948
- rsrc =
949
- if args[:fetch_rsrc]
950
- args[:fetch_rsrc]
951
- else
952
- # what lookup key are we using?
953
- # TODO: simplify this, see the notes at #find_rsrc_keys
954
- rsrc_key, lookup_value = find_rsrc_keys(args)
955
- "#{self.class::RSRC_BASE}/#{rsrc_key}/#{lookup_value}"
956
- end
1289
+ rsrc = args[:fetch_rsrc]
1290
+ rsrc ||= "#{self.class::RSRC_BASE}/id/#{args[:id]}"
957
1291
 
958
1292
  # if needed, a non-standard object key can be passed by a subclass.
959
1293
  # e.g. User when loookup is by email.
@@ -967,44 +1301,12 @@ module JSS
967
1301
  # otherwise
968
1302
  @api.get_rsrc(rsrc)
969
1303
  end
1304
+
970
1305
  raw_json[args[:rsrc_object_key]]
971
1306
  rescue RestClient::ResourceNotFound
972
1307
  raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} found matching resource #{rsrc}"
973
1308
  end
974
1309
 
975
- # Given initialization args, determine the rsrc key and
976
- # lookup value to be used in building the GET resource.
977
- # E.g. for looking up something with id 345,
978
- # return the rsrc_key :id, and the value 345, which
979
- # can be used to create the resrouce
980
- # '/things/id/345'
981
- #
982
- # CHANGE: some the new patch-related objects don't have
983
- # GET resources by name, only id. So this method now always
984
- # returns the id-based resource.
985
- #
986
- # TODO: clean up this and the above methods, since the
987
- # id-only get rsrcs actually should simplify the code.
988
- #
989
- # @param args[Hash] The args passed to #initialize
990
- #
991
- # @return [Array] Two item array: [ rsrc_key, lookup_value]
992
- #
993
- def find_rsrc_keys(args)
994
- lookup_keys = self.class.lookup_keys
995
- lookup_key = (self.class.lookup_keys & args.keys)[0]
996
-
997
- raise JSS::MissingDataError, "Args must include a lookup key, one of: :#{lookup_keys.join(', :')}" unless lookup_key
998
-
999
- vid = self.class.valid_id args[lookup_key], :refresh, api: args[:api]
1000
-
1001
- raise NoSuchItemError, "No #{self.class::RSRC_OBJECT_KEY} found with #{lookup_key} '#{args[lookup_key]}'" unless vid
1002
-
1003
- [:id, vid]
1004
- # rsrc_key = self.class.rsrc_keys[lookup_key]
1005
- # [rsrc_key, args[lookup_key]]
1006
- end
1007
-
1008
1310
  # Start examining the @init_data recieved from the API
1009
1311
  #
1010
1312
  # @return [void]
@@ -1028,11 +1330,6 @@ module JSS
1028
1330
 
1029
1331
  @rest_rsrc = "#{self.class::RSRC_BASE}/id/#{@id}"
1030
1332
 
1031
- # many things have a :site
1032
- # TODO: Implement a Sitable mixin module
1033
- #
1034
- # @site = JSS::APIObject.get_name(@main_subset[:site]) if @main_subset[:site]
1035
-
1036
1333
  ##### Handle Mix-ins
1037
1334
  initialize_category
1038
1335
  initialize_site
@@ -1164,9 +1461,42 @@ module JSS
1164
1461
  doc.to_s
1165
1462
  end
1166
1463
 
1167
- # Aliases
1464
+ # Meta Programming
1168
1465
 
1169
- alias in_jss? in_jss
1466
+ # Loop through the defined lookup keys and make
1467
+ # .all_<key>s methods for each one, with
1468
+ # alises as needed.
1469
+ #
1470
+ # This is called automatically in api_object.rb
1471
+ # after all subclasses are loaded.
1472
+ #
1473
+ def self.define_identifier_list_methods
1474
+ return unless @subclasses
1475
+
1476
+ @subclasses.each do |subclass|
1477
+ subclass.lookup_keys.each do |als, key|
1478
+ meth_name = "all_#{key}s"
1479
+
1480
+ if als == key
1481
+ # the all_ method - skip if defined in the class
1482
+ next if subclass.instance_methods.include? meth_name
1483
+
1484
+ subclass.define_singleton_method meth_name do |refresh = false, api: JSS.api|
1485
+ all(refresh, api: api).map { |i| i[key] }
1486
+ end
1487
+
1488
+ else
1489
+ # an alias - skip if defined in the class
1490
+ als_name = "all_#{als}s"
1491
+ next if subclass.instance_methods.include? als_name
1492
+
1493
+ subclass.define_singleton_method als_name do |refresh = false, api: JSS.api|
1494
+ send meth_name, refresh, api: api
1495
+ end
1496
+ end # if
1497
+ end # lookup_keys.each
1498
+ end # @subclasses.each
1499
+ end # self.define_identifier_list_methods
1170
1500
 
1171
1501
  end # class APIObject
1172
1502
 
@@ -1226,3 +1556,5 @@ require 'jss/api_object/site'
1226
1556
  require 'jss/api_object/software_update_server'
1227
1557
  require 'jss/api_object/user'
1228
1558
  require 'jss/api_object/webhook'
1559
+
1560
+ JSS::APIObject.define_identifier_list_methods