File: helpers.rb

package info (click to toggle)
ruby-multi-xml 0.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 472 kB
  • sloc: ruby: 2,822; sh: 4; makefile: 2
file content (228 lines) | stat: -rw-r--r-- 7,988 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
module MultiXml
  # Methods for transforming parsed XML hash structures
  #
  # These helper methods handle key transformation and type casting
  # of parsed XML data structures.
  #
  # @api public
  module Helpers
    module_function

    # Recursively convert all hash keys to symbols
    #
    # @api private
    # @param data [Hash, Array, Object] Data to transform
    # @return [Hash, Array, Object] Transformed data with symbolized keys
    # @example Symbolize hash keys
    #   symbolize_keys({"name" => "John"}) #=> {name: "John"}
    def symbolize_keys(data)
      transform_keys(data, &:to_sym)
    end

    # Recursively convert dashes in hash keys to underscores
    #
    # @api private
    # @param data [Hash, Array, Object] Data to transform
    # @return [Hash, Array, Object] Transformed data with undasherized keys
    # @example Convert dashed keys
    #   undasherize_keys({"first-name" => "John"}) #=> {"first_name" => "John"}
    def undasherize_keys(data)
      transform_keys(data) { |key| key.tr("-", "_") }
    end

    # Recursively typecast XML values based on type attributes
    #
    # @api private
    # @param value [Hash, Array, Object] Value to typecast
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Object] Typecasted value
    # @raise [DisallowedTypeError] if a disallowed type is encountered
    # @example Typecast integer value
    #   typecast_xml_value({"__content__" => "42", "type" => "integer"})
    #   #=> 42
    def typecast_xml_value(value, disallowed_types = DISALLOWED_TYPES)
      case value
      when Hash then typecast_hash(value, disallowed_types)
      when Array then typecast_array(value, disallowed_types)
      else value
      end
    end

    # Typecast array elements and unwrap single-element arrays
    #
    # @api private
    # @param array [Array] Array to typecast
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Object, Array] Typecasted array or single element
    def typecast_array(array, disallowed_types)
      array.map! { |item| typecast_xml_value(item, disallowed_types) }
      (array.size == 1) ? array.first : array
    end

    # Typecast a hash based on its type attribute
    #
    # @api private
    # @param hash [Hash] Hash to typecast
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Object] Typecasted value
    # @raise [DisallowedTypeError] if type is disallowed
    def typecast_hash(hash, disallowed_types)
      type = hash["type"]
      raise DisallowedTypeError, type if disallowed_type?(type, disallowed_types)

      convert_hash(hash, type, disallowed_types)
    end

    # Check if a type is in the disallowed list
    #
    # @api private
    # @param type [String, nil] Type to check
    # @param disallowed_types [Array<String>] Disallowed type list
    # @return [Boolean] true if type is disallowed
    def disallowed_type?(type, disallowed_types)
      type && !type.is_a?(Hash) && disallowed_types.include?(type)
    end

    # Convert a hash based on its type and content
    #
    # @api private
    # @param hash [Hash] Hash to convert
    # @param type [String, nil] Type attribute value
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Object] Converted value
    def convert_hash(hash, type, disallowed_types)
      return extract_array_entries(hash, disallowed_types) if type == "array"
      return convert_text_content(hash) if hash.key?(TEXT_CONTENT_KEY)
      return "" if type == "string" && !hash["nil"].eql?("true")
      return nil if empty_value?(hash, type)

      typecast_children(hash, disallowed_types)
    end

    # Typecast all child values in a hash
    #
    # @api private
    # @param hash [Hash] Hash with children to typecast
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Hash, StringIO] Typecasted hash or unwrapped file
    def typecast_children(hash, disallowed_types)
      result = hash.transform_values { |v| typecast_xml_value(v, disallowed_types) }
      unwrap_file_if_present(result)
    end

    # Extract array entries from element with type="array"
    #
    # @api private
    # @param hash [Hash] Hash containing array entries
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Array] Extracted and typecasted entries
    # @see https://github.com/jnunemaker/httparty/issues/102
    def extract_array_entries(hash, disallowed_types)
      entries = find_array_entries(hash)
      return [] unless entries

      wrap_and_typecast(entries, disallowed_types)
    end

    # Find array or hash entries in a hash, excluding the type key
    #
    # @api private
    # @param hash [Hash] Hash to search
    # @return [Array, Hash, nil] Found entries or nil
    def find_array_entries(hash)
      hash.each do |key, value|
        return value if !key.eql?("type") && (value.is_a?(Array) || value.is_a?(Hash))
      end
      nil
    end

    # Wrap hash in array if needed and typecast all entries
    #
    # @api private
    # @param entries [Array, Hash] Entries to process
    # @param disallowed_types [Array<String>] Types to reject
    # @return [Array] Typecasted entries
    def wrap_and_typecast(entries, disallowed_types)
      entries = [entries] if entries.is_a?(Hash)
      entries.map { |entry| typecast_xml_value(entry, disallowed_types) }
    end

    # Convert text content using type converters
    #
    # @api private
    # @param hash [Hash] Hash containing text content and type
    # @return [Object] Converted value
    def convert_text_content(hash)
      content = hash.fetch(TEXT_CONTENT_KEY)
      converter = TYPE_CONVERTERS[hash["type"]]

      return unwrap_if_simple(hash, content) unless converter

      apply_converter(hash, content, converter)
    end

    # Unwrap value if hash has no other significant keys
    #
    # @api private
    # @param hash [Hash] Original hash
    # @param value [Object] Converted value
    # @return [Object, Hash] Value or hash with merged content
    def unwrap_if_simple(hash, value)
      (hash.size > 1) ? hash.merge(TEXT_CONTENT_KEY => value) : value
    end

    # Check if a hash represents an empty value
    #
    # @api private
    # @param hash [Hash] Hash to check
    # @param type [String, nil] Type attribute value
    # @return [Boolean] true if value should be nil
    def empty_value?(hash, type)
      hash.empty? ||
        hash["nil"] == "true" ||
        (type && hash.size == 1 && !type.is_a?(Hash))
    end

    private

    # Recursively transform hash keys using a block
    #
    # @api private
    # @param data [Hash, Array, Object] Data to transform
    # @return [Hash, Array, Object] Transformed data
    def transform_keys(data, &block)
      case data
      when Hash then data.each_with_object(
        {} #: Hash[Symbol, MultiXml::xmlValue] # rubocop:disable Layout/LeadingCommentSpace
      ) { |(key, value), acc| acc[yield(key)] = transform_keys(value, &block) }
      when Array then data.map { |item| transform_keys(item, &block) }
      else data
      end
    end

    # Unwrap a file object from the result hash if present
    #
    # @api private
    # @param result [Hash] Hash that may contain a file
    # @return [Hash, StringIO] The file if present, otherwise the hash
    def unwrap_file_if_present(result)
      file = result["file"]
      file.is_a?(StringIO) ? file : result
    end

    # Apply a type converter to content
    #
    # @api private
    # @param hash [Hash] Original hash with type info
    # @param content [String] Content to convert
    # @param converter [Proc] Converter to apply
    # @return [Object] Converted value
    def apply_converter(hash, content, converter)
      # Binary converters need access to entity attributes (e.g., encoding, name)
      return converter.call(content, hash) if converter.arity == 2

      hash.delete("type")
      unwrap_if_simple(hash, converter.call(content))
    end
  end
end