Module: JSS::XMLWorkaround

Defined in:
lib/jss/xml_workaround.rb

Overview

Since a non-trivial amounts of the JSON data from the API are borked, the methods here can be used to parse the XML data into usable JSON, which we can then treat normally.

For classes with borked JSON, set the constant USE_XML_WORKAROUND to a Hash with a single key that maps the structure of the XML and resultant Ruby data.

As an example, here's the data map from JSS::PatchTitle

USE_XML_WORKAROUND = {

patch_software_title: {
  id: -1,
  name: JSS::BLANK,
  name_id: JSS::BLANK,
  source_id: -1,
  notifications: {
    email_notification: nil,
    web_notification: nil
  },
  category: {
    id: -1,
    name: JSS::BLANK
  },
  versions: [
    {
      software_version: JSS::BLANK,
      package: -1,
        name: JSS::BLANK
      }
    }
  ]
}

}.freeze

The constant must always be a hash that represents the data structure of the object. The keys match the names of the XML elements, and the values indicate how to handle the element values.

Single-value attributes will be converted based on the provided map example The class of the map example is the class of the desired data, and the value of the map example is the value to use when the XML data is nil or empty.

So a map example of '' (an empty string, a.k.a. JSS::BLANK) indicates that the value should be a String and if the XML element is nil or empty, use '' in the Ruby data. If its -1, that means the value should be an Integer, and if its empty or nil, use -1 in Ruby.

Booleans are special: the map example must be nil, and nil is used when the xml is empty, since you want to be able to know that the XML value was neither true nor false.

Allowed single value classes and common default examples are:

String, common default: '' or JSS::BLANK
Integer, common default: -1
Float, common default: -1.0
Boolean, required default: nil

Arrays and Hashes will be recognized as such, and their contents will be converted recursively using the same process.

For Arrays, provide one example in the map of an Array item, and all sub elements will be processd like the example. See the ':versions' array defiend in the example above

For sub-hashes, use the same technique as for the main hash. see the :category value above.

IMPORTANT NOTE: Lots of Arrays in the XML have a matching 'size' element containing an integer indicating how many items are in the array. Unfortunately there is zero consistency about their existence or location. If they exist at all, sometimes the are adjacent to the Array element, sometimes within it.

Fortunately in Ruby, all container/enumerable classes have a 'size' or 'count' method to easily get that number. As such, when parsing XML elements, any 'size' element that exists with no other 'size' elements, and contains only an integer value and no sub- elements, are ignored. I haven't yet found any cases of a 'size' element that is used for anything else.

Constant Summary collapse

BOOLEAN_STRINGS =
%w[true false].freeze
TRUE_STRING =
BOOLEAN_STRINGS.first
SIZE_ELEM_NAME =
'size'.freeze

Class Method Summary collapse

Class Method Details

.data_via_xml(rsrc, map, api) ⇒ Object

When APIObject classes are fetched, API JSON data is retrieved by the APIObject#lookup_object_data method, which parses the JSON into Ruby data.

If the APIObject class has the constant USE_XML_WORKAROUND defined, that means the JSON data from the API is invalid, incorrect, or otherwise borked. So instead, the XML is retrieved from the API here.

It is then parsed by using the methods in this module and returned to the APIObject#lookup_object_data method, which then treats it normally.


125
126
127
128
129
130
131
132
133
# File 'lib/jss/xml_workaround.rb', line 125

def self.data_via_xml(rsrc, map, api)
  raw_xml = api.get_rsrc(rsrc, :xml)
  xmlroot = REXML::Document.new(raw_xml).root
  hash_from_xml = {}
  map.each do |key, model|
    hash_from_xml[key] = process_map_item model, xmlroot
  end
  hash_from_xml
end

.elem_as_array(model, elem) ⇒ Object

convert an XML element into an Array


183
184
185
186
187
188
189
190
191
# File 'lib/jss/xml_workaround.rb', line 183

def self.elem_as_array(model, elem)
  remove_size_sub_elem elem
  arr = []
  elem.each do |subelem|
    # Recursion for the win!
    arr << process_map_item(model, subelem)
  end # each subelem
  arr.compact
end

.elem_as_hash(model, elem) ⇒ Object

convert an XML element into a Hash


194
195
196
197
198
199
200
201
202
203
204
# File 'lib/jss/xml_workaround.rb', line 194

def self.elem_as_hash(model, elem)
  remove_size_sub_elem elem
  hsh = {}
  model.each do |key, mod|
    val = process_map_item(mod, elem.elements[key.to_s])
    val = [] if  mod.is_a?(Array) && val.to_s.empty?
    val = {} if  mod.is_a?(Hash) && val.to_s.empty?
    hsh[key] = val
  end
  hsh
end

.process_map_item(model, element) ⇒ Object

given a REXML element, return its ruby value

This method is then called recursively as needed when traversing XML elements that contain sub-elements.

XML Elements that do not contain other elements are converted to a single ruby value.


143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/jss/xml_workaround.rb', line 143

def self.process_map_item(model, element)
  case model
  when String
    element ? element.text : model
  when Integer
    element ? element.text.to_i : model
  when Float
    element ? element.text.to_f : model
  when nil
    return nil unless element
    element.text.downcase == TRUE_STRING ? true : false
  when Array
    element ? elem_as_array(model.first, element) : []
  when Hash
    element ? elem_as_hash(model, element) : {}
  end # case type
end

.remove_size_sub_elem(elem) ⇒ Object

remove the 'size' sub element from a given element as long as:

  • a sub element named 'size' exists

  • there's only one sub element named 'size'

  • it doesn't have sub elements itself

  • and it contains an integer value

Such elements are extraneous for the most part, and are not consistently located - sometimes they are in the Array-ish elements they reference, sometimes they are alongside them. In any case they confuse the logic when deciding if an element with sub-elements should become an Array or a Hash.


172
173
174
175
176
177
178
179
180
# File 'lib/jss/xml_workaround.rb', line 172

def self.remove_size_sub_elem(elem)
  size_elems = elem.elements.to_a.select { |subel| subel.name == SIZE_ELEM_NAME }
  size_elem = size_elems.first
  return unless size_elem
  return unless size_elems.count == 1
  return if size_elem.has_elements?
  return unless size_elem.text.jss_integer?
  elem.delete_element size_elem
end