ruby-jss 0.14.0 → 1.0.0b2

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.

@@ -179,6 +179,18 @@ module JSS
179
179
  path.jss_save @data
180
180
  end
181
181
 
182
+ # Remove the data object from
183
+ # the instance_variables used to create
184
+ # pretty-print (pp) output.
185
+ #
186
+ # @return [Array] the desired instance_variables
187
+ #
188
+ def pretty_print_instance_variables
189
+ vars = instance_variables.sort
190
+ vars.delete :@data
191
+ vars
192
+ end
193
+
182
194
  end # class icon
183
195
 
184
196
  end # module
@@ -80,7 +80,7 @@ module JSS
80
80
  # @return [String] The name of the site for this object.
81
81
  #
82
82
  def site_name
83
- @site_name
83
+ @site_name || NO_SITE_NAME
84
84
  end # cat name
85
85
  alias site site_name
86
86
 
@@ -89,7 +89,7 @@ module JSS
89
89
  # @return [Integer] The id of the site for this object.
90
90
  #
91
91
  def site_id
92
- @site_id
92
+ @site_id || NO_SITE_ID
93
93
  end # cat id
94
94
 
95
95
  # The JSS::Site instance for this object's site
@@ -165,7 +165,7 @@ module JSS
165
165
  elsif @init_data[self.class::SITE_SUBSET]
166
166
  @init_data[self.class::SITE_SUBSET][:site]
167
167
  end
168
- site_data ||= {}
168
+ site_data ||= { name: NO_SITE_NAME, id: NO_SITE_ID }
169
169
 
170
170
  @site_name = site_data[:name]
171
171
  @site_id = site_data[:id]
@@ -187,7 +187,7 @@ module JSS
187
187
  parent_elem ||= root.add_element(self.class::SITE_SUBSET.to_s)
188
188
  parent_elem.add_element 'site'
189
189
  end
190
- site_elem.add_element('name').text = @site_name.to_s
190
+ site_elem.add_element('name').text = site_name.to_s
191
191
  end # add_site_to_xml
192
192
 
193
193
  end # module categorizable
@@ -76,7 +76,7 @@ module JSS
76
76
  #
77
77
  def name=(newname)
78
78
  return nil if @name == newname
79
- raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless UPDATABLE
79
+ raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless updatable?
80
80
  raise JSS::InvalidDataError, "Names can't be empty!" if newname.to_s.empty?
81
81
  raise JSS::AlreadyExistsError, "A #{self.class::RSRC_OBJECT_KEY} named '#{newname}' already exsists in the JSS" \
82
82
  if self.class.all_names(:refresh, api: @api).include? newname
@@ -91,7 +91,7 @@ module JSS
91
91
  #
92
92
  def update
93
93
  return nil unless @need_to_update
94
- raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless UPDATABLE
94
+ raise JSS::UnsupportedError, "Editing #{self.class::RSRC_LIST_KEY} isn't yet supported. Please use other Casper workflows." unless updatable?
95
95
  raise JSS::NoSuchItemError, "Not In JSS! Use #create to create this #{self.class::RSRC_OBJECT_KEY} in the JSS before updating it." unless @in_jss
96
96
  @api.put_rsrc @rest_rsrc, rest_xml
97
97
  @need_to_update = false
@@ -26,79 +26,82 @@
26
26
  ###
27
27
  module JSS
28
28
 
29
- #####################################
30
29
  ### Exceptions
31
30
  #####################################
32
31
 
33
- ###
34
32
  ### MissingDataError - raise this error when we
35
33
  ### are missing args, or other simliar stuff.
36
34
  ###
37
35
  class MissingDataError < RuntimeError; end
38
36
 
39
- ###
40
37
  ### InvalidDataError - raise this error when
41
38
  ### a data item isn't what we expected.
42
39
  ###
43
40
  class InvalidDataError < RuntimeError; end
44
41
 
45
- ###
46
42
  ### InvalidConnectionError - raise this error when we
47
43
  ### don't have a usable connection to a network service, or
48
44
  ### don't have proper authentication/authorization.
49
45
  ###
50
46
  class InvalidConnectionError < RuntimeError; end
51
47
 
52
- ###
53
48
  ### NoSuchItemError - raise this error when
54
49
  ### a desired item doesn't exist.
55
50
  ###
56
51
  class NoSuchItemError < RuntimeError; end
57
52
 
58
- ###
59
53
  ### AlreadyExistsError - raise this error when
60
54
  ### trying to create something that already exists.
61
55
  ###
62
56
  class AlreadyExistsError < RuntimeError; end
63
57
 
64
- ###
65
58
  ### FileServiceError - raise this error when
66
59
  ### there's a problem accessing file service on a
67
60
  ### distribution point.
68
61
  ###
69
62
  class FileServiceError < RuntimeError; end
70
63
 
71
- ###
72
64
  ### UnmanagedError - raise this when we
73
65
  ### try to do something managerial to
74
66
  ### an unmanaged object
75
67
  ###
76
- class UnmanagedError < RuntimeError; end
68
+ class UnmanagedError < RuntimeError; end
77
69
 
78
- ###
79
70
  ### UnsupportedError - raise this when we
80
71
  ### try to do something not yet supported
81
72
  ###
82
- class UnsupportedError < RuntimeError; end
73
+ class UnsupportedError < RuntimeError; end
83
74
 
84
- ###
85
75
  ### TimeoutError - raise this when we
86
76
  ### try to do and it times out
87
77
  ###
88
- class TimeoutError < RuntimeError; end
78
+ class TimeoutError < RuntimeError; end
89
79
 
90
- ###
91
80
  ### AuthenticationError - raise this when
92
81
  ### a name/pw are wrong
93
82
  ###
94
- class AuthenticationError < RuntimeError; end
83
+ class AuthenticationError < RuntimeError; end
95
84
 
96
- ###
97
85
  ### ConflictError - raise this when
98
86
  ### attempts to PUT or PUSH to the API
99
87
  ### result in a 409 Conflict http error.
100
- ### See JSS::APIConnect.instance.raise_conflict_error
88
+ ### See {JSS::APIConnection#raise_conflict_error}
89
+ ###
90
+ class ConflictError < RuntimeError; end
91
+
92
+ ### BadRequestError - raise this when
93
+ ### attempts to PUT or PUSH or DELETE to the API
94
+ ### result in a 400 Bad Request http error.
95
+ ### See {JSS::APIConnection.raise_bad_request_error}
96
+ ###
97
+ class BadRequestError < RuntimeError; end
98
+
99
+ ### APIRequestError - raise this when
100
+ ### attempts API actions generate an error not dealt with
101
+ ### by ConflictError or BadRequestError
102
+ ### result in a 400 Bad Request http error.
103
+ ### See {JSS::APIConnection.raise_api_error}
101
104
  ###
102
- class ConflictError < RuntimeError; end
105
+ class APIRequestError < RuntimeError; end
103
106
 
104
- end # module JSS
107
+ end # module JSS
@@ -78,7 +78,7 @@ class Hash
78
78
 
79
79
  # Since a lot of JSON data from the API comes as deeply-nested structures
80
80
  # of Hashes and Arrays, it can be a pain to reference some of the deeper
81
- # data inside, and it isn't worth coding them out into Class attributes.
81
+ # data inside, and it isn't worth coding them out into instance attributes.
82
82
  #
83
83
  # For example see the 'hardware' subset of a JSS::Computer's API data,
84
84
  # which is stored as a Hash in the {JSS::Computer.hardware} attribute.
@@ -94,7 +94,7 @@ class Hash
94
94
  # But, there are two problems with just storing #hardware as an OpenStruct:
95
95
  # 1) we'd lose some important Hash methods, like #keys and #values, breaking
96
96
  # backward compatibility. 2) OpenStructs only work on the Hash itself, not
97
- # not it's contents.
97
+ # its contents.
98
98
  #
99
99
  # So to get the best of both worlds, we use the RecursiveOpenStruct gem
100
100
  #
@@ -112,20 +112,25 @@ class Hash
112
112
  # CAVEAT: Treat these as read-only.
113
113
  #
114
114
  # While the Hashes themselves may be mutable, their use in ruby-jss Classes
115
- # should be usually be considered read-only - and the RecursiveOpenStruct
116
- # object created by this method should not be changed. Changes to the Hash
117
- # or the RecursiveOpenStruct are NOT synced between them
115
+ # should usually be considered read-only - neither the Hash, nor the
116
+ # RecursiveOpenStruct object created by this method should be changed.
117
+ # Changes to the Hash or the RecursiveOpenStruct are NOT synced between them,
118
+ # and ruby-jss won't know to send such changes back to the API when #update
119
+ # is called.
118
120
  #
119
121
  # This should be fine for the intended uses. Data like Computer#hardware
120
122
  # isn't sent back to the JSS via Computer#update, since it must come
121
- # from a 'recon' anyway. Data that is sent back to the JSS will have
122
- # setter methods defined in the class or a mixin module (e.g. the
123
- # Locatable module).
124
- #
125
- # Since the data is read-only, why not use the ImmutableStruct gem, used
126
- # elsewhere in ruby-jss? Because ImmutableStruct is really for creating
127
- # fully-fleshed-out read-only classes, with a known set of attributes rather
128
- # than just giving us a nicer way to access Hash data with arbitrary keys.
123
+ # from a 'recon' anyway.
124
+ #
125
+ # Data that is sent back to the JSS will have setter methods defined in the
126
+ # class or a mixin module (e.g. the Locatable module).
127
+ #
128
+ # Since the data is functionally read-only, why not use the ImmutableStruct
129
+ # gem, used elsewhere in ruby-jss?
130
+ #
131
+ # Because ImmutableStruct is really for creating fully-fleshed-out read-only
132
+ # classes, with a known set of attributes rather than just giving us a nicer
133
+ # way to access Hash data with arbitrary keys.
129
134
  #
130
135
  def jss_recursive_ostruct
131
136
  @jss_ros ||= RecursiveOpenStruct.new(self, recurse_over_arrays: true)
data/lib/jss/utility.rb CHANGED
@@ -415,35 +415,17 @@ module JSS
415
415
 
416
416
  # Parse a JSS Version number into something comparable.
417
417
  #
418
- # With Jamf Pro 9.99, "Semantic Versioning" is used, see http://semver.org/
419
- #
420
- # For versions less than 9.99 parsing is like this:
421
- # - Digits before the first dot are the Major Version
422
- # - The first digit after the first dot is the Minor Version
423
- # - Any other digits after the first dot but before a non-digit
424
- # are the Revision
425
- # - Anything after a second dot is the build identifier
426
- # - Any non-digit anywhere means that it and everything after it
427
- # are the build identifier
428
- #
429
- # So 9.32 becomes major-9, minor-3, rev-2, build-''
430
- # and 9.32.3764 becomes major-9, minor-3, rev-2, build-3764
431
- # and 9.32a3764 becomes major-9, minor-3, rev-2, build-a3764
432
- # and 9.32a1234.t234 becomes major-9, minor-3, rev-2, build-a1234.t234
433
- #
434
- # This old style method of parsing will break if digits between the first
435
- # dot and the second (or the end) ever gets above 99, since '100' will
436
- # become minor-1, rev-0
437
- #
438
418
  # This method returns a Hash with these keys:
439
419
  # * :major => the major version, Integer
440
420
  # * :minor => the minor version, Integor
441
- # * :maint => the revision, Integer
442
- # (this is also available with the keys :patch and :revision)
421
+ # * :maint => the revision, Integer (also available as :patch and :revision)
443
422
  # * :build => the revision, String
444
423
  # * :version => a Gem::Version object built from :major, :minor, :revision
445
424
  # which can be easily compared with other Gem::Version objects.
446
425
  #
426
+ # NOTE: the :version value ignores build numbers, so comparisons
427
+ # only compare major.minor.maint
428
+ #
447
429
  # @param version[String] a JSS version number from the API
448
430
  #
449
431
  # @return [Hash{Symbol => String, Gem::Version}] the parsed version data.
@@ -452,17 +434,8 @@ module JSS
452
434
  major, second_part, *_rest = version.split('.')
453
435
  raise JSS::InvalidDataError, 'JSS Versions must start with "x.x" where x is one or more digits' unless major =~ /\d$/ && second_part =~ /^\d/
454
436
 
455
- # since ruby-jss requires 9.4 and up, this check works fine.
456
- if major == '9' && (second_part.to_i < 99)
457
- parse_jss_version_oldstyle version
458
- else
459
- parse_jss_version_newstyle version
460
- end
461
- end
437
+ release, build = version.split(/-/)
462
438
 
463
- # (see parse_jss_version)
464
- def self.parse_jss_version_newstyle(version)
465
- release, build = version.split '-'
466
439
  major, minor, revision = release.split '.'
467
440
  minor ||= 0
468
441
  revision ||= 0
@@ -474,46 +447,9 @@ module JSS
474
447
  maint: revision.to_i,
475
448
  patch: revision.to_i,
476
449
  build: build,
477
- # version: Gem::Version.new(version)
478
- version: Gem::Version.new("#{major}.#{minor}.#{revision}#{build}")
479
- }
480
- end # parse_jss_version_oldstyle
481
-
482
- # (see parse_jss_version)
483
- def self.parse_jss_version_oldstyle(version)
484
- version =~ /^(\d+?)\.(.*)$/
485
- major = Regexp.last_match[1]
486
- second_part = Regexp.last_match[2].to_s
487
-
488
- minor = second_part[0]
489
- revision = second_part[1..-1]
490
-
491
- # if there's a non-digit anywhere in any part, it and everything after
492
- # is the build.
493
- if revision.to_s =~ /^(\d*)(\D.*)$/
494
- revision = Regexp.last_match[1]
495
- build = Regexp.last_match[2]
496
- # but remove a leading dot
497
- build = build[1..-1] if build.start_with? '.'
498
- end
499
- minor ||= ''
500
- revision ||= ''
501
-
502
- version_string = major.to_s
503
- unless minor.empty?
504
- version_string << ".#{minor}"
505
- version_string << ".#{revision}" unless revision.empty?
506
- end
507
- {
508
- major: major.to_i,
509
- minor: minor.to_i,
510
- revision: revision.to_i,
511
- maint: revision.to_i,
512
- patch: revision.to_i,
513
- build: build.to_s,
514
- version: Gem::Version.new(version_string)
450
+ version: Gem::Version.new("#{major}.#{minor}.#{revision}")
515
451
  }
516
- end # parse_jss_version_oldstyle
452
+ end
517
453
 
518
454
  # @return [Boolean] is this code running as root?
519
455
  #
data/lib/jss/validate.rb CHANGED
@@ -22,7 +22,6 @@
22
22
  #
23
23
  #
24
24
 
25
- #
26
25
  module JSS
27
26
 
28
27
  # A collection of methods for validating values. Mostly for
@@ -60,13 +59,18 @@ module JSS
60
59
  ok = true
61
60
  parts = val.strip.split '.'
62
61
  ok = false unless parts.size == 4
63
- parts.each { |p| ok = false unless p.jss_integer? && p.to_i < 256 }
62
+ parts.each { |p| ok = false unless p.jss_integer? && p.to_i < 256 && p.to_i >= 0 }
64
63
  raise JSS::InvalidDataError, "Not a valid IPv4 address: '#{val}'" unless ok
65
64
  val
66
65
  end
67
66
 
68
67
  # Validate that a value doesn't already exist for a given identifier of a given class
69
68
  #
69
+ # e.g. when klass = JSS::Computer, identifier = :name, and val = 'foo'
70
+ # will raise an error when a computer named 'foo' exists
71
+ #
72
+ # Otherwise returns val.
73
+ #
70
74
  # @param klass[JSS::APIObject] A subclass of JSS::APIObject, e.g. JSS::Computer
71
75
  #
72
76
  # @param identifier[Symbol] One of the keys of an Item of the class's #all Array
@@ -76,10 +80,71 @@ module JSS
76
80
  # @return [Object] the validated unique value
77
81
  #
78
82
  def self.unique_identifier(klass, identifier, val, api: JSS.api)
79
- raise JSS::AlreadyExistsError, "A #{klass} already exists with #{identifier} '#{val}'" if klass.all(:refresh, api: api).map { |i| i[identifier] }.include? val
83
+ return val unless klass.all(:refresh, api: api).map { |i| i[identifier] }.include? val
84
+ raise JSS::AlreadyExistsError, "A #{klass} already exists with #{identifier} '#{val}'"
85
+ end
86
+
87
+ # Confirm that the given value is a boolean value, accepting
88
+ # strings and symbols and returning real booleans as needed
89
+ # Accepts: true, false, 'true', 'false', :true, :false, 'yes', 'no', :yes,
90
+ # or :no (all Strings and Symbols are case insensitive)
91
+ #
92
+ # TODO: use this throughout ruby-jss
93
+ #
94
+ # @param bool [Boolean,String,Symbol] The value to validate
95
+ #
96
+ # @return [Boolean] the valid boolean
97
+ #
98
+ def self.boolean(bool)
99
+ return bool if JSS::TRUE_FALSE.include? bool
100
+ return true if bool.to_s =~ /^(true|yes)$/i
101
+ return false if bool.to_s =~ /^(false|no)$/i
102
+ raise JSS::InvalidDataError, 'Value must be boolean true or false'
103
+ end
104
+
105
+ # Confirm that a value is an integer or a string representation of an
106
+ # integer. Return the integer, or raise an error
107
+ #
108
+ # TODO: use this throughout ruby-jss
109
+ #
110
+ # @param val[Object] the value to validate
111
+ #
112
+ # @return [void]
113
+ #
114
+ def self.integer(val)
115
+ val = val.to_i if val.is_a?(String) && val.jss_integer?
116
+ raise JSS::InvalidDataError, 'Value must be an integer' unless val.is_a? Integer
117
+ val
118
+ end
119
+
120
+ # validate that the given value is a non-empty string
121
+ #
122
+ # @param val [Object] the thing to validate
123
+ #
124
+ # @return [String] the valid non-empty string
125
+ #
126
+ def self.non_empty_string(val)
127
+ raise JSS::InvalidDataError, 'value must be a non-empty String' unless val.is_a?(String) && !val.empty?
80
128
  val
81
129
  end
82
130
 
131
+ # Confirm that the given value is a boolean value, accepting
132
+ # strings and symbols and returning real booleans as needed
133
+ # Accepts: true, false, 'true', 'false', :true, :false, 'yes', 'no', :yes,
134
+ # or :no (all Strings and Symbols are case insensitive)
135
+ #
136
+ #
137
+ # @param bool [Boolean,String,Symbol] The value to validate
138
+ #
139
+ # @return [Boolean] the valid boolean
140
+ #
141
+ def self.boolean(bool)
142
+ return bool if JSS::TRUE_FALSE.include? bool
143
+ return true if bool.to_s =~ /^(true|yes)$/i
144
+ return false if bool.to_s =~ /^(false|no)$/i
145
+ raise JSS::InvalidDataError, 'Value must be boolean true or false'
146
+ end
147
+
83
148
  end # module validate
84
149
 
85
150
  end # module JSS
data/lib/jss/version.rb CHANGED
@@ -27,6 +27,6 @@
27
27
  module JSS
28
28
 
29
29
  ### The version of the JSS ruby gem
30
- VERSION = '0.14.0'.freeze
30
+ VERSION = '1.0.0b2'.freeze
31
31
 
32
32
  end # module
@@ -0,0 +1,208 @@
1
+ ### Copyright 2018 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ ###
27
+ module JSS
28
+
29
+ # Since a non-trivial amounts of the JSON data from the API are borked, the
30
+ # methods here can be used to parse the XML data into usable JSON, which we
31
+ # can then treat normally.
32
+ #
33
+ # For classes with borked JSON, set the constant USE_XML_WORKAROUND to a Hash
34
+ # with a single key that maps the structure of the XML and resultant Ruby data.
35
+ #
36
+ # As an example, here's the data map from JSS::PatchTitle
37
+ #
38
+ # USE_XML_WORKAROUND = {
39
+ # patch_software_title: {
40
+ # id: -1,
41
+ # name: JSS::BLANK,
42
+ # name_id: JSS::BLANK,
43
+ # source_id: -1,
44
+ # notifications: {
45
+ # email_notification: nil,
46
+ # web_notification: nil
47
+ # },
48
+ # category: {
49
+ # id: -1,
50
+ # name: JSS::BLANK
51
+ # },
52
+ # versions: [
53
+ # {
54
+ # software_version: JSS::BLANK,
55
+ # package: -1,
56
+ # name: JSS::BLANK
57
+ # }
58
+ # }
59
+ # ]
60
+ # }
61
+ # }.freeze
62
+ #
63
+ # The constant must always be a hash that represents the data structure
64
+ # of the object. The keys match the names of the XML elements, and the
65
+ # values indicate how to handle the element values.
66
+ #
67
+ # Single-value attributes will be converted based on the provided map example
68
+ # The class of the map example is the class of the desired data, and the value
69
+ # of the map example is the value to use when the XML data is nil or empty.
70
+ #
71
+ # So a map example of '' (an empty string, a.k.a. JSS::BLANK) indicates
72
+ # that the value should be a String and if the XML element is nil or empty,
73
+ # use '' in the Ruby data. If its -1, that means the value should be an
74
+ # Integer, and if its empty or nil, use -1 in Ruby.
75
+ #
76
+ # Booleans are special: the map example must be nil, and nil is used when the
77
+ # xml is empty, since you want to be able to know that the XML value was
78
+ # neither true nor false.
79
+ #
80
+ # Allowed single value classes and common default examples are:
81
+ # String, common default: '' or JSS::BLANK
82
+ # Integer, common default: -1
83
+ # Float, common default: -1.0
84
+ # Boolean, required default: nil
85
+ #
86
+ # Arrays and Hashes will be recognized as such, and their contents will be
87
+ # converted recursively using the same process.
88
+ #
89
+ # For Arrays, provide one example in the map of an Array
90
+ # item, and all sub elements will be processd like the example. See
91
+ # the ':versions' array defiend in the example above
92
+ #
93
+ # For sub-hashes, use the same technique as for the main hash.
94
+ # see the :category value above.
95
+ #
96
+ # IMPORTANT NOTE: Lots of Arrays in the XML have a matching 'size' element
97
+ # containing an integer indicating how many items are in the array. Unfortunately
98
+ # there is zero consistency about their existence or location. If they exist at
99
+ # all, sometimes the are adjacent to the Array element, sometimes within it.
100
+ #
101
+ # Fortunately in Ruby, all container/enumerable classes have a 'size' or 'count'
102
+ # method to easily get that number.
103
+ # As such, when parsing XML elements, any 'size' element that exists with no
104
+ # other 'size' elements, and contains only an integer value and no sub-
105
+ # elements, are ignored. I haven't yet found any cases of a 'size' element
106
+ # that is used for anything else.
107
+ #
108
+ module XMLWorkaround
109
+
110
+ BOOLEAN_STRINGS = %w[true false].freeze
111
+ TRUE_STRING = BOOLEAN_STRINGS.first
112
+ SIZE_ELEM_NAME = 'size'.freeze
113
+
114
+ # When APIObject classes are fetched, API JSON data is retrieved by the
115
+ # APIObject#lookup_object_data method, which parses the JSON into Ruby data.
116
+ #
117
+ # If the APIObject class has the constant USE_XML_WORKAROUND defined, that
118
+ # means the JSON data from the API is invalid, incorrect, or otherwise
119
+ # borked. So instead, the XML is retrieved from the API here.
120
+ #
121
+ # It is then parsed by using the methods in this module and returned
122
+ # to the APIObject#lookup_object_data method, which then
123
+ # treats it normally.
124
+ #
125
+ def self.data_via_xml(rsrc, map, api)
126
+ raw_xml = api.get_rsrc(rsrc, :xml)
127
+ xmlroot = REXML::Document.new(raw_xml).root
128
+ hash_from_xml = {}
129
+ map.each do |key, model|
130
+ hash_from_xml[key] = process_map_item model, xmlroot
131
+ end
132
+ hash_from_xml
133
+ end
134
+
135
+ # given a REXML element, return its ruby value
136
+ #
137
+ # This method is then called recursively as needed when traversing XML
138
+ # elements that contain sub-elements.
139
+ #
140
+ # XML Elements that do not contain other elements are converted to a
141
+ # single ruby value.
142
+ #
143
+ def self.process_map_item(model, element)
144
+ case model
145
+ when String
146
+ element ? element.text : model
147
+ when Integer
148
+ element ? element.text.to_i : model
149
+ when Float
150
+ element ? element.text.to_f : model
151
+ when nil
152
+ return nil unless element
153
+ element.text.downcase == TRUE_STRING ? true : false
154
+ when Array
155
+ element ? elem_as_array(model.first, element) : []
156
+ when Hash
157
+ element ? elem_as_hash(model, element) : {}
158
+ end # case type
159
+ end
160
+
161
+ # remove the 'size' sub element from a given element as long as:
162
+ # - a sub element named 'size' exists
163
+ # - there's only one sub element named 'size'
164
+ # - it doesn't have sub elements itself
165
+ # - and it contains an integer value
166
+ # Such elements are extraneous for the most part, and are not consistently
167
+ # located - sometimes they are in the Array-ish elements they reference,
168
+ # sometimes they are alongside them. In any case they confuse the logic
169
+ # when deciding if an element with sub-elements should become an
170
+ # Array or a Hash.
171
+ #
172
+ def self.remove_size_sub_elem(elem)
173
+ size_elems = elem.elements.to_a.select { |subel| subel.name == SIZE_ELEM_NAME }
174
+ size_elem = size_elems.first
175
+ return unless size_elem
176
+ return unless size_elems.count == 1
177
+ return if size_elem.has_elements?
178
+ return unless size_elem.text.jss_integer?
179
+ elem.delete_element size_elem
180
+ end
181
+
182
+ # convert an XML element into an Array
183
+ def self.elem_as_array(model, elem)
184
+ remove_size_sub_elem elem
185
+ arr = []
186
+ elem.each do |subelem|
187
+ # Recursion for the win!
188
+ arr << process_map_item(model, subelem)
189
+ end # each subelem
190
+ arr.compact
191
+ end
192
+
193
+ # convert an XML element into a Hash
194
+ def self.elem_as_hash(model, elem)
195
+ remove_size_sub_elem elem
196
+ hsh = {}
197
+ model.each do |key, mod|
198
+ val = process_map_item(mod, elem.elements[key.to_s])
199
+ val = [] if mod.is_a?(Array) && val.to_s.empty?
200
+ val = {} if mod.is_a?(Hash) && val.to_s.empty?
201
+ hsh[key] = val
202
+ end
203
+ hsh
204
+ end
205
+
206
+ end # module XMLWorkarounds
207
+
208
+ end # module