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
@@ -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