ruby-jss 1.0.4 → 1.1.0b1

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.

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
@@ -37,6 +37,9 @@ module JSS
37
37
 
38
38
  # A User or group in the JSS.
39
39
  #
40
+ # TODO: Split this into 2 classes, with lots of custom code.
41
+ # Thanks Jamf!
42
+ #
40
43
  # @see JSS::APIObject
41
44
  #
42
45
  class Account < JSS::APIObject
@@ -60,17 +63,28 @@ module JSS
60
63
  # It's also used in various error messages
61
64
  RSRC_OBJECT_KEY = :account
62
65
 
63
- # these keys, as well as :id and :name, can be used to look up objects of this class in the JSS
66
+ # these keys, as well as :id and :name, can be used to look up objects of
67
+ # this class in the JSS
64
68
  OTHER_LOOKUP_KEYS = {
65
- userid: {rsrc_key: :userid, list: :all_user_ids},
66
- username: {rsrc_key: :username, list: :all_user_names},
67
- groupid: {rsrc_key: :groupid, list: :all_group_ids},
68
- groupname: {rsrc_key: :groupname, list: :all_group_names}
69
+ userid: { fetch_rsrc_key: :userid },
70
+ username: { fetch_rsrc_key: :username },
71
+ groupid: { fetch_rsrc_key: :groupid },
72
+ groupname: { fetch_rsrc_key: :groupname }
69
73
  }.freeze
70
74
 
71
75
  # Class Methods
72
76
  #####################################
73
77
 
78
+ # override auto-defined method
79
+ def self.all_ids(_refresh = false, **_bunk)
80
+ raise '.all_ids is not valid for JSS::Account, use .all_user_ids or .all_group_ids'
81
+ end
82
+
83
+ # override auto-defined method
84
+ def self.all_names(_refresh = false, **_bunk)
85
+ raise '.all_names is not valid for JSS::Account, use .all_user_names or .all_group_names'
86
+ end
87
+
74
88
  # @return [Array<Hash>] all JSS account users
75
89
  def self.all_users(refresh = false, api: JSS.api)
76
90
  all(refresh, api: api)[:users]
@@ -65,9 +65,6 @@ module JSS
65
65
  # It's also used in various error messages
66
66
  RSRC_OBJECT_KEY = :advanced_computer_search
67
67
 
68
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
69
- VALID_DATA_KEYS = [:sql_text, :display_fields, :computers].freeze
70
-
71
68
  # what kind of thing is returned by this search?
72
69
  RESULT_CLASS = JSS::Computer
73
70
 
@@ -59,9 +59,6 @@ module JSS
59
59
  # It's also used in various error messages
60
60
  RSRC_OBJECT_KEY = :advanced_mobile_device_search
61
61
 
62
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
63
- VALID_DATA_KEYS = [:sql_text, :display_fields, :mobile_devices].freeze
64
-
65
62
  # what kind of thing is returned by this search?
66
63
  RESULT_CLASS = JSS::MobileDevice
67
64
 
@@ -59,9 +59,6 @@ module JSS
59
59
  # It's also used in various error messages
60
60
  RSRC_OBJECT_KEY = :advanced_user_search
61
61
 
62
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
63
- VALID_DATA_KEYS = [:criteria, :display_fields, :users].freeze
64
-
65
62
  # what kind of thing is returned by this search?
66
63
  RESULT_CLASS = JSS::User
67
64
 
@@ -63,9 +63,6 @@ module JSS
63
63
  # It's also used in various error messages
64
64
  RSRC_OBJECT_KEY = :building
65
65
 
66
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
67
- VALID_DATA_KEYS = [].freeze
68
-
69
66
  # the object type for this object in
70
67
  # the object history table.
71
68
  # See {APIObject#add_object_history_entry}
@@ -69,9 +69,6 @@ module JSS
69
69
  # It's also used in various error messages
70
70
  RSRC_OBJECT_KEY = :category
71
71
 
72
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
73
- VALID_DATA_KEYS = [:priority].freeze
74
-
75
72
  # When no category has been assigned, this is the 'name' and id used
76
73
  NO_CATEGORY_NAME = JSS::Categorizable::NO_CATEGORY_NAME
77
74
  NO_CATEGORY_ID = JSS::Categorizable::NO_CATEGORY_ID
@@ -180,19 +180,35 @@ module JSS
180
180
  # Where is the Site data in the API JSON?
181
181
  SITE_SUBSET = :general
182
182
 
183
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
184
- # DEPRECATED, with be removed in a future release.
185
- VALID_DATA_KEYS = %i[sus distribution_point alt_mac_address].freeze
186
-
187
- # these keys, as well as :id and :name, can be used to look up objects of this class in the JSS
183
+ # these keys, as well as :id and :name, can be used to look up objects
184
+ # of this class in the JSS
185
+ #
186
+ # the wierd alises mac_addresse and macaddresse
187
+ # are for proper pluralization of 'mac_address' and such
188
188
  OTHER_LOOKUP_KEYS = {
189
- udid: { rsrc_key: :udid, list: :all_udids },
190
- serialnumber: { rsrc_key: :serialnumber, list: :all_serial_numbers },
191
- serial_number: { rsrc_key: :serialnumber, list: :all_serial_numbers },
192
- macaddress: { rsrc_key: :macaddress, list: :all_mac_addresses },
193
- mac_address: { rsrc_key: :macaddress, list: :all_mac_addresses }
189
+ udid: {
190
+ aliases: [:uuid, :guid],
191
+ fetch_rsrc_key: :udid
192
+ },
193
+ serial_number: {
194
+ aliases: [:serialnumber, :sn],
195
+ fetch_rsrc_key: :serialnumber
196
+ },
197
+ mac_address: {
198
+ aliases: [
199
+ :mac_address,
200
+ :mac_addresse,
201
+ :macaddress,
202
+ :macaddresse,
203
+ :macaddr
204
+ ],
205
+ fetch_rsrc_key: :macaddress
206
+ }
207
+
194
208
  }.freeze
195
209
 
210
+ NON_UNIQUE_NAMES = true
211
+
196
212
  # This class lets us seach for computers
197
213
  SEARCH_CLASS = JSS::AdvancedComputerSearch
198
214
 
@@ -308,25 +324,28 @@ module JSS
308
324
  # @return [Array<Hash{:name=>String, :id=> Integer}>]
309
325
  #
310
326
  def self.all(refresh = false, api: JSS.api)
311
- api.object_list_cache[RSRC_LIST_KEY] = nil if refresh
312
- return api.object_list_cache[RSRC_LIST_KEY] if api.object_list_cache[RSRC_LIST_KEY]
313
- api.object_list_cache[RSRC_LIST_KEY] = api.get_rsrc(self::LIST_RSRC)[self::RSRC_LIST_KEY]
314
- end
315
-
316
- # @return [Array<String>] all computer serial numbers in the jss
317
- def self.all_serial_numbers(refresh = false, api: JSS.api)
318
- all(refresh, api: api).map { |i| i[:serial_number] }
319
- end
327
+ cache = api.object_list_cache
328
+ cache_key = self::RSRC_LIST_KEY
329
+ cache[cache_key] = nil if refresh
330
+ return cache[cache_key] if cache[cache_key]
320
331
 
321
- # @return [Array<String>] all computer mac_addresses in the jss
322
- def self.all_mac_addresses(refresh = false, api: JSS.api)
323
- all(refresh, api: api).map { |i| i[:mac_address] }
332
+ cache[cache_key] = api.get_rsrc(self::LIST_RSRC)[cache_key]
324
333
  end
325
334
 
326
- # @return [Array<String>] all computer udids in the jss
327
- def self.all_udids(refresh = false, api: JSS.api)
328
- all(refresh, api: api).map { |i| i[:udid] }
329
- end
335
+ # # @return [Array<String>] all computer serial numbers in the jss
336
+ # def self.all_serial_numbers(refresh = false, api: JSS.api)
337
+ # all(refresh, api: api).map { |i| i[:serial_number] }
338
+ # end
339
+ #
340
+ # # @return [Array<String>] all computer mac_addresses in the jss
341
+ # def self.all_mac_addresses(refresh = false, api: JSS.api)
342
+ # all(refresh, api: api).map { |i| i[:mac_address] }
343
+ # end
344
+ #
345
+ # # @return [Array<String>] all computer udids in the jss
346
+ # def self.all_udids(refresh = false, api: JSS.api)
347
+ # all(refresh, api: api).map { |i| i[:udid] }
348
+ # end
330
349
 
331
350
  # @return [Array<Hash>] all managed computers in the jss
332
351
  def self.all_managed(refresh = false, api: JSS.api)
@@ -785,7 +804,7 @@ module JSS
785
804
  identity: cert[:identity],
786
805
  name: cert[:name]
787
806
  }
788
- end
807
+ end # map do cert
789
808
 
790
809
  # Freeze immutable things.
791
810
  # These are updated via recon, and aren't sent
@@ -798,6 +817,8 @@ module JSS
798
817
  @software.freeze
799
818
 
800
819
  @management_password = nil
820
+
821
+ # not in jss
801
822
  else
802
823
  @udid = args[:udid]
803
824
  @serial_number = args[:serial_number]
@@ -806,9 +827,43 @@ module JSS
806
827
  @alt_mac_address = args[:alt_mac_address]
807
828
  @barcode1 = args[:barcode_1]
808
829
  @barcode2 = args[:barcode_2]
809
- end
830
+ end # if in jss
810
831
  end # initialize
811
832
 
833
+ # Make all the keys of the @hardware hash available as top-level methods
834
+ # on the Computer instance.
835
+ #
836
+ # This is done by catching method_missing and seeing if the method exists
837
+ # as key of @hardware, and if so, retuning that value, if not, passing on
838
+ # the method_missing call.
839
+ # So:
840
+ # comp.processor_type
841
+ # is now the same as:
842
+ # comp.hardware[:processor_type]
843
+ #
844
+ # The reason for using `method_missing` rather than looping through the
845
+ # @hardware hash during initialization and doing `define_method` is
846
+ # speed. When instantiating lots of computers, defining the methods
847
+ # for each one, when those methods may not be needed, just slows things
848
+ # down. This way, they're only used when needed.
849
+ #
850
+ # This method may be expanded in the future to handle other ad-hoc,
851
+ # top-level methods.
852
+ #
853
+ def method_missing(method, *args, &block)
854
+ if @hardware.key? method
855
+ @hardware[method]
856
+ else
857
+ super
858
+ end # if
859
+ end # def
860
+
861
+ # Companion to method_missing, allows for easier debugging in backtraces
862
+ # that involve missing methods.
863
+ def respond_to_missing?(method, *)
864
+ @hardware.key?(method) || super
865
+ end
866
+
812
867
  # @return [Array] the JSS groups to which thismachine belongs (smart and static)
813
868
  #
814
869
  def computer_groups
@@ -53,13 +53,6 @@ module JSS
53
53
  include JSS::Creatable
54
54
  include JSS::Sitable
55
55
 
56
- # Class Methods
57
- #####################################
58
-
59
- def self.all_invitations(refresh = false, api: JSS.api)
60
- all(refresh, api: api).map { |ci| ci[:invitation] }
61
- end
62
-
63
56
  # Class Constants
64
57
  #####################################
65
58
 
@@ -73,12 +66,9 @@ module JSS
73
66
  # It's also used in various error messages
74
67
  RSRC_OBJECT_KEY = :computer_invitation
75
68
 
76
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
77
- VALID_DATA_KEYS = [:invitation].freeze
78
-
79
69
  # See JSS::APIObject
80
70
  OTHER_LOOKUP_KEYS = {
81
- invitation: {rsrc_key: :invitation, list: :all_invitations}
71
+ invitation: { fetch_rsrc_key: :invitation }
82
72
  }.freeze
83
73
 
84
74
  # the object type for this object in
@@ -51,9 +51,6 @@ module JSS
51
51
  # It's also used in various error messages
52
52
  RSRC_OBJECT_KEY = :os_x_configuration_profile
53
53
 
54
- # these keys, as well as :id and :name, are present in valid API JSON data for this class
55
- VALID_DATA_KEYS = %i[distribution_method scope redeploy_on_update].freeze
56
-
57
54
  # Our scopes deal with computers
58
55
  SCOPE_TARGET_KEY = :computers
59
56
 
@@ -70,9 +70,6 @@ module JSS
70
70
  ### It's also used in various error messages
71
71
  RSRC_OBJECT_KEY = :department
72
72
 
73
- ### these keys, as well as :id and :name, are present in valid API JSON data for this class
74
- VALID_DATA_KEYS = []
75
-
76
73
  # the object type for this object in
77
74
  # the object history table.
78
75
  # See {APIObject#add_object_history_entry}
@@ -71,9 +71,6 @@ module JSS
71
71
  ### It's also used in various error messages
72
72
  RSRC_OBJECT_KEY = :distribution_point
73
73
 
74
- ### these keys, as well as :id and :name, are present in valid API JSON data for this class
75
- VALID_DATA_KEYS = [:read_only_username, :ssh_username, :is_master ]
76
-
77
74
  ### what are the mount options? these are comma-separated, and are passed with -o
78
75
  MOUNT_OPTIONS = 'nobrowse'
79
76
 
@@ -65,11 +65,7 @@ module JSS
65
65
 
66
66
  EXTENDABLE = true
67
67
 
68
- # ExtensionAttributes refer to the numeric data type as "Integer"
69
- # but the ext. attr values that come with extendable objects refer to
70
- # that data type as "Number". Here's an array with both, so we can
71
- # work with ether more easily.
72
- NUMERIC_TYPES = %w[Number Integer].freeze
68
+ INVALID_DATE = '-- INVALIDLY FORMATTED DATE --'.freeze
73
69
 
74
70
  # Attribtues
75
71
  ###################################
@@ -77,9 +73,6 @@ module JSS
77
73
  # @return [Array<Hash>] The extension attribute values for the object
78
74
  attr_reader :extension_attributes
79
75
 
80
- # @return [Hash] A mapping of Ext Attrib names to their values
81
- attr_reader :ext_attrs
82
-
83
76
  # Mixed-in Instance Methods
84
77
  ###################################
85
78
 
@@ -93,79 +86,124 @@ module JSS
93
86
  def parse_ext_attrs
94
87
  @extension_attributes = @init_data[:extension_attributes]
95
88
  @extension_attributes ||= []
96
- @ext_attrs = {}
97
89
 
90
+ # remember changes as they happen so
91
+ # we only send changes back to the server.
92
+ @changed_eas = []
93
+ end
94
+
95
+ # @return [Array<String>] the names of all known EAs
96
+ #
97
+ def ea_names
98
+ ea_types.keys
99
+ end
100
+
101
+ # @return [Hash{String => String}] EA names => data type
102
+ # (one of 'String', 'Number', or 'Date')
103
+ def ea_types
104
+ return @ea_types if @ea_types
105
+
106
+ @ea_types = {}
107
+ extension_attributes.each { |ea| @ea_types[ea[:name]] = ea[:type] }
108
+ @ea_types
109
+ end
110
+
111
+ # An easier-to-use hash of EA name to EA value.
112
+ # This isn't created until its needed, to speed up instantiation.
113
+ #
114
+ def ext_attrs
115
+ return @ext_attrs if @ext_attrs
116
+
117
+ @ext_attrs = {}
98
118
  @extension_attributes.each do |ea|
99
- case ea[:type]
119
+ @ext_attrs[ea[:name]] =
120
+ case ea[:type]
100
121
 
101
- when 'Date'
102
- begin # if there's random non-date data, the parse will fail
103
- ea[:value] = JSS.parse_datetime ea[:value]
104
- rescue
105
- true
106
- end
122
+ when 'Date'
123
+ begin # if there's random non-date data, the parse will fail
124
+ JSS.parse_datetime ea[:value]
125
+ rescue
126
+ INVALID_DATE
127
+ end
107
128
 
108
- when *NUMERIC_TYPES
109
- ea[:value] = ea[:value].to_i unless ea[:value].to_s.empty?
110
- end # case
129
+ when *JSS::ExtensionAttribute::NUMERIC_TYPES
130
+ ea[:value].to_i unless ea[:value].to_s.empty?
111
131
 
112
- @ext_attrs[ea[:name]] = ea[:value]
132
+ else # String
133
+ ea[:value]
134
+ end # case
113
135
  end # each do ea
114
136
 
115
- # remember changes as they happen so
116
- # we only send changes back to the server.
117
- @changed_eas = []
137
+ @ext_attrs
118
138
  end
119
139
 
120
140
  # Set the value of an extension attribute
121
141
  #
122
- # If the extension attribute is defined as a popup menu, the value must be one of the
123
- # defined popup choices, or an empty string
142
+ # The new value is validated based on the data type of the
143
+ # Ext. Attrib:
144
+ #
145
+ # - If the ext. attrib. is defined with a data type of Integer/Number, the
146
+ # value must be an Integer.
147
+ # - If defined with a data type of Date, the value will be parsed as a
148
+ # timestamp, and parsing may raise an exception. Dates can't be blank.
149
+ # - If defined wth data type of String, `to_s` will be called on the value.
124
150
  #
125
- # If the ext. attrib. is defined with a data type of Integer, the value must be an Integer.
151
+ # By default, the full EA definition object is fetched to see if the EA's
152
+ # input type is 'popup menu', and if so, the new value must be one of the
153
+ # defined popup choices, or blank.
126
154
  #
127
- # If the ext. attrib. is defined with a data type of Date, the value will be converted to a Time
155
+ # The EA definitions used for popup validation are cached, so we don't have
156
+ # to reach out to the server every time. If you expect the definition to
157
+ # have changed since it was cached, provide a truthy value to the refresh:
158
+ # parameter
128
159
  #
129
- # Note that while the Jamf Pro Web interface does not allow editing the values of
130
- # Extension Attributes populated by Scripts or LDAP, the API does allow it.
131
- # Bear in mind however that those values will be reset again at the next recon.
160
+ # To bypass popup validation complepletely, provide a falsey value to the
161
+ # validate_popup_choice: parameter.
162
+ # WARNING: beware that your value is the correct type and format, or you might
163
+ # get errors when saving back to the API.
164
+ #
165
+ # Note that while the Jamf Pro Web interface does not allow editing the
166
+ # values of Extension Attributes populated by Scripts or LDAP, the API does
167
+ # allow it. Bear in mind however that those values will be reset again at
168
+ # the next recon.
132
169
  #
133
170
  # @param name[String] the name of the extension attribute to set
134
171
  #
135
- # @param value[String,Time,Time,Integer] the new value for the extension attribute for this user
172
+ # @param value[String,Time,Integer] the new value for the extension
173
+ # attribute for this user
174
+ #
175
+ # @param validate_popup_choice[Boolean] validate the new value against the E.A. definition.
176
+ # Defaults to true.
177
+ #
178
+ # @param refresh[Boolean] Re-read the ext. attrib definition from the API,
179
+ # for popup validation.
136
180
  #
137
181
  # @return [void]
138
182
  #
139
- def set_ext_attr(name, value)
140
- # this will raise an exception if the name doesn't exist
141
- ea_def = self.class::EXT_ATTRIB_CLASS.fetch name: name, api: api
183
+ def set_ext_attr(name, value, validate_popup_choice: true, refresh: false)
184
+ raise ArgumentError, "Unknown Extension Attribute Name: '#{name}'" unless ea_types.key? name
142
185
 
143
- if ea_def.input_type == 'Pop-up Menu' && (!ea_def.popup_choices.include? value.to_s)
144
- raise JSS::UnsupportedError, "The value for #{name} must be one of: '#{ea_def.popup_choices.join("' '")}'"
145
- end
186
+ value ||= JSS::BLANK
187
+ validate_popup_value(name, value, refresh) if validate_popup_choice
146
188
 
147
- unless value == JSS::BLANK
148
- case ea_def.data_type
149
- when 'Date'
150
- value = JSS.parse_datetime value
189
+ case ea_types[name]
190
+ when JSS::ExtensionAttribute::DATA_TYPE_DATE
191
+ raise JSS::InvalidDataError, "The value for #{name} must be a date, cannot be blank" if value == JSS::BLANK
151
192
 
152
- when *NUMERIC_TYPES
153
- raise JSS::InvalidDataError, "The value for #{name} must be an integer" unless value.is_a? Integer
193
+ value = JSS.parse_datetime value
154
194
 
155
- end # case
156
- end # unless blank
195
+ when *JSS::ExtensionAttribute::NUMERIC_TYPES
196
+ raise JSS::InvalidDataError, "The value for #{name} must be an integer" unless value.is_a? Integer
157
197
 
158
- been_set = false
159
- @extension_attributes.each do |ea|
160
- next unless ea[:name] == name
161
- ea[:value] = value
162
- been_set = true
163
- end
164
- unless been_set
165
- @extension_attributes << { id: ea_def.id, name: name, type: ea_def.data_type, value: value }
166
- end
167
-
168
- @ext_attrs[name] = value
198
+ else # String
199
+ value = value.to_s
200
+ end # case
201
+
202
+ # update this ea hash in the @extension_attributes array
203
+ @extension_attributes.each { |ea| ea[:value] = value if ea[:name] == name }
204
+
205
+ # update the shortcut hash too
206
+ @ext_attrs[name] = value if @ext_attrs
169
207
  @changed_eas << name
170
208
  @need_to_update = true
171
209
  end
@@ -210,6 +248,24 @@ module JSS
210
248
  eaxml
211
249
  end
212
250
 
213
- end # module Purchasable
251
+ # Used by set_ext_attr
252
+ def validate_popup_value(name, value, refresh)
253
+ # all popups can take blanks
254
+ return if value == JSS::BLANK
255
+
256
+ # get the ea def. instance from the api cache, or the api
257
+ api.ext_attr_definition_cache[self.class] ||= {}
258
+ api.ext_attr_definition_cache[self.class][name] = nil if refresh
259
+ api.ext_attr_definition_cache[self.class][name] ||= self.class::EXT_ATTRIB_CLASS.fetch name: name, api: api
260
+
261
+ ea_def = api.ext_attr_definition_cache[self.class][name]
262
+ return unless ea_def.from_popup_menu?
263
+
264
+ return if ea_def.popup_choices.include? value.to_s
265
+
266
+ raise JSS::UnsupportedError, "The value for #{name} must be one of: '#{ea_def.popup_choices.join("' '")}'"
267
+ end
268
+
269
+ end # module extendable
214
270
 
215
271
  end # module JSS