# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. require 'base64' require 'date' require 'time' module AWS module Core module XML class Frame TRANSLATE_DIGITS = ['0123456789'.freeze, ('X'*10).freeze] EASY_FORMAT = "XXXX-XX-XXTXX:XX:XX.XXXZ".freeze DATE_PUNCTUATION = ['-:.TZ'.freeze, (' '*5).freeze] def initialize root_frame, parent_frame, element_name, rules @root_frame = root_frame @parent_frame = parent_frame @element_name = element_name @rules = rules @rules[:children] ||= {} @data = {}.merge(rules[:defaults] || {}) @text = nil # initialize values for child frames of special types (e.g. # lists, maps, and forced elements) known_child_frames.each do |child_frame| context = data_context_for(child_frame) if child_frame.list? context[child_frame.ruby_name] = [] elsif child_frame.map? context[child_frame.ruby_name] = {} elsif child_frame.forced? context[child_frame.ruby_name] = child_frame.value end end end attr_reader :root_frame attr_reader :parent_frame attr_reader :element_name attr_reader :rules def data ignored? ? parent_frame.data : @data end def ruby_name rules[:rename] || root_frame.inflect(element_name) end def rules_for child_element_name rules[:children][child_element_name] || {} end # The list of child frames that have customizations (rules), all # other children will be parsed using standard rules def known_child_frames rules[:children].keys.map {|name| build_child_frame(name) } end def build_child_frame element_name # if element_name should be wrapped # build a frame for the wrapper # build a child frame from the wrapper # else Frame.new(root_frame, self, element_name, rules_for(element_name)) end def consume_child_frame child_frame child_frame.close return if child_frame.ignored? ruby_name = child_frame.ruby_name value = child_frame.value context = data_context_for(child_frame) if child_frame.list? context[ruby_name] << value elsif map = child_frame.map? context[ruby_name][child_frame.map_key] = child_frame.map_value else context[ruby_name] = value end end def close # some xml elements should be indexed at the root level # The :index rule determines the name of this index # and what keys the data should be indexed as (one element # can be indexed under multiple keys). The index value # is always the element itself. if index = @rules[:index] index_keys_for(index) do |key| root_frame.add_to_index(index[:name], key, data) end end end def index_keys_for index_opts, &block # simple (single) key if key = index_opts[:key] yield(data[key]) return end # composite key, joined by ":" if parts = index_opts[:keys] composite_key = parts.map{|part| data[part] }.join(":") yield(composite_key) return end # multiple keys, collected from the given path if path = index_opts[:key_path] keys_from_path(data, path.dup, &block) return end raise "missing require index rule option, :key, :keys or :key_path" end def keys_from_path data, path, &block step = path.shift value = data[step] if path.empty? yield(value) return end if value.is_a?(Array) value.each do |v| keys_from_path(v, path.dup, &block) end else keys_from_path(value, path.dup, &block) end end def add_text chars @text ||= '' @text << chars end def value if !data.empty? data[:encoding] == 'base64' ? Base64.decode64(@text.strip) : data elsif @text.nil? rules[:type] == :boolean ? false : nil else case rules[:type] when nil, :string then @text when :datetime then datetime_like_value(DateTime, :civil) when :time then datetime_like_value(Time, :utc) when :integer then @text.to_i when :float then @text.to_f when :boolean then @text == 'true' when :blob then Base64.decode64(@text) when :symbol then Core::Inflection.ruby_name(@text).to_sym else raise "unhandled type" end end end def ignored? @rules[:ignore] end def forced? @rules[:force] end def list? @rules[:list] end def map? @rules[:map] end def wrapped? @rules[:wrap] end alias_method :wrapper, :wrapped? protected def map_key data[root_frame.inflect(@rules[:map].first)] end def map_value data[root_frame.inflect(@rules[:map].last)] end def data_context_for child_frame if child_frame.wrapped? data[child_frame.wrapper] ||= {} data[child_frame.wrapper] else data end end def datetime_like_value klass, parts_constructor # it's way faster to parse this specific format manually # vs. DateTime#parse, and this happens to be the format # that AWS uses almost (??) everywhere. if @text.tr(*TRANSLATE_DIGITS) == EASY_FORMAT parts = @text.tr(*DATE_PUNCTUATION).chop.split.map {|p| p.to_i } klass.send(parts_constructor, *parts) else # fallback in case we have to handle another date format klass.parse(@text) end end end end end end