Module: Jamf::XMLWorkaround
- Defined in:
- lib/jamf/api/classic/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 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.
Constant Summary collapse
- BOOLEAN_STRINGS =
%w[true false].freeze
- TRUE_STRING =
BOOLEAN_STRINGS.first
- SIZE_ELEM_NAME =
'size'.freeze
Class Method Summary collapse
-
.data_via_xml(rsrc, map, cnx) ⇒ 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.
-
.elem_as_array(model, elem) ⇒ Object
convert an XML element into an Array.
-
.elem_as_hash(model, elem) ⇒ Object
convert an XML element into a Hash.
-
.process_map_item(model, element) ⇒ Object
given a REXML element, return its ruby value.
-
.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.
Class Method Details
.data_via_xml(rsrc, map, cnx) ⇒ 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/jamf/api/classic/xml_workaround.rb', line 125 def self.data_via_xml(rsrc, map, cnx) raw_xml = cnx.c_get(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
185 186 187 188 189 190 191 192 193 |
# File 'lib/jamf/api/classic/xml_workaround.rb', line 185 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
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/jamf/api/classic/xml_workaround.rb', line 196 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 160 |
# File 'lib/jamf/api/classic/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 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.
173 174 175 176 177 178 179 180 181 182 |
# File 'lib/jamf/api/classic/xml_workaround.rb', line 173 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 |