### Copyright 2018 Pixar ### ### Licensed under the Apache License, Version 2.0 (the "Apache License") ### with the following modification; you may not use this file except in ### compliance with the Apache License and the following modification to it: ### Section 6. Trademarks. is deleted and replaced with: ### ### 6. Trademarks. This License does not grant permission to use the trade ### names, trademarks, service marks, or product names of the Licensor ### and its affiliates, except as required to comply with Section 4(c) of ### the License and to reproduce the content of the NOTICE file. ### ### You may obtain a copy of the Apache License at ### ### http://www.apache.org/licenses/LICENSE-2.0 ### ### Unless required by applicable law or agreed to in writing, software ### distributed under the Apache License with the above modification is ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ### KIND, either express or implied. See the Apache License for the specific ### language governing permissions and limitations under the Apache License. ### ### ### module JSS # 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. # module XMLWorkaround BOOLEAN_STRINGS = %w[true false].freeze TRUE_STRING = BOOLEAN_STRINGS.first SIZE_ELEM_NAME = 'size'.freeze # 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. # 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 # 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. # 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 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. # 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 # convert an XML element into an Array 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 # convert an XML element into a Hash 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 end # module XMLWorkarounds end # module