module Jamf::XMLWorkaround

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 Jamf::PatchTitle

USE_XML_WORKAROUND = {

patch_software_title: {
  id: -1,
  name: Jamf::BLANK,
  name_id: Jamf::BLANK,
  source_id: -1,
  notifications: {
    email_notification: nil,
    web_notification: nil
  },
  category: {
    id: -1,
    name: Jamf::BLANK
  },
  versions: [
    {
      software_version: Jamf::BLANK,
      package: -1,
        name: Jamf::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. Jamf::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 Jamf::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.

Constants

BOOLEAN_STRINGS
SIZE_ELEM_NAME
TRUE_STRING

Public Class Methods

data_via_xml(rsrc, map, cnx) click to toggle source

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.

    # File lib/jamf/api/classic/xml_workaround.rb
125 def self.data_via_xml(rsrc, map, cnx)
126   raw_xml = cnx.c_get(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
elem_as_array(model, elem) click to toggle source

convert an XML element into an Array

    # File lib/jamf/api/classic/xml_workaround.rb
185 def self.elem_as_array(model, elem)
186   remove_size_sub_elem elem
187   arr = []
188   elem.each do |subelem|
189     # Recursion for the win!
190     arr << process_map_item(model, subelem)
191   end # each subelem
192   arr.compact
193 end
elem_as_hash(model, elem) click to toggle source

convert an XML element into a Hash

    # File lib/jamf/api/classic/xml_workaround.rb
196 def self.elem_as_hash(model, elem)
197   remove_size_sub_elem elem
198   hsh = {}
199   model.each do |key, mod|
200     val = process_map_item(mod, elem.elements[key.to_s])
201     val = [] if  mod.is_a?(Array) && val.to_s.empty?
202     val = {} if  mod.is_a?(Hash) && val.to_s.empty?
203     hsh[key] = val
204   end
205   hsh
206 end
process_map_item(model, element) click to toggle source

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.

    # File lib/jamf/api/classic/xml_workaround.rb
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 
154     element.text.downcase == TRUE_STRING
155   when Array
156     element ? elem_as_array(model.first, element) : []
157   when Hash
158     element ? elem_as_hash(model, element) : {}
159   end # case type
160 end
remove_size_sub_elem(elem) click to toggle source

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.

    # File lib/jamf/api/classic/xml_workaround.rb
173 def self.remove_size_sub_elem(elem)
174   size_elems = elem.elements.to_a.select { |subel| subel.name == SIZE_ELEM_NAME }
175   size_elem = size_elems.first
176   return unless size_elem
177   return unless size_elems.count == 1
178   return if size_elem.has_elements?
179   return unless size_elem.text.jss_integer?
180 
181   elem.delete_element size_elem
182 end