# Copyright 2011 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 'aws/meta_utils' require 'aws/inflection' require 'rexml/document' begin require 'nokogiri' rescue LoadError => e end module AWS # @private class XmlGrammar # @private class Context def initialize @data = {} end def id @data[:id] end def method_missing(m, *args) key = m.to_sym return super unless @data.key?(key) @data[key] end def respond_to?(m) @data.key?(m.to_sym) or super end def inspect methods = @data.keys "<Object #{methods.reject{|m| m =~ /=$/ }.join(', ')}>" end # this gets called a LOT during response parsing, and having # it be a public method is the fastest way to call it. # Strictly speaking it should be private. # @private def __set_data__(getter, value) @data[getter.to_sym] = value end end # @private class CustomizationContext < Hash def initialize(element_name = nil) original_store(:children, {}) if element_name original_store(:name, element_name) recompute_accessors end end alias_method :original_store, :[]= def []=(name, value) super if respond_to?("changed_#{name}") send("changed_#{name}", value) end end def changed_boolean(value) recompute_accessors end def changed_renamed(value) recompute_accessors end def deep_copy(hash = self) fields = hash.inject({}) do |copy,(key,value)| if value.is_a?(CustomizationContext) value = value.deep_copy elsif value.is_a?(Hash) value = deep_copy(value) end copy[key] = value copy end hash.merge(fields) end private def recompute_accessors ruby_name = Inflection.ruby_name((self[:renamed] || self[:name]).to_s) self[:getter] = if self[:boolean] && ruby_name != "value" "#{ruby_name}?" else ruby_name end self[:setter] = "#{ruby_name}=" end end # @private class << self def parse xml, options = {} context = options[:context] || Context.new if defined? Nokogiri parser = Parser.new(context, customizations) parser.extend(NokogiriAdapter) xml = "<foo/>" if xml.empty? Nokogiri::XML::SAX::Parser.new(parser).parse(xml.strip) else parser = Parser.new(context, customizations) parser.extend(REXMLSaxParserAdapter) REXML::Parsers::StreamParser.new(REXML::Source.new(xml), parser).parse end context end def simulate context = nil StubResponse.new(customizations, context) end def customize config = nil, &block grammar = Class.new(self) grammar.customizations = customizations.deep_copy grammar.config_eval(config) if config grammar.module_eval(&block) if block_given? grammar end def element element_name, &block eval_customization_context(element_name, &block) end def add_method method_name, &block format_value do |value| MetaUtils.extend_method(value = super(value), method_name, &block) value end # different strategey, slightly different behavior # element(method_name.to_s) do # format_value(&block) # force # end end def ignore @current[:ignored] = true end def rename new_name @current[:renamed] = new_name.to_s end def force @current[:forced] = true end def collect_values @current[:collected] = true @current[:initial_collection] = lambda { [] } @current[:add_to_collection] = lambda { |ary, val| ary << val } force end def index(name, &block) (@customizations[:index_names] ||= []) << name @current[:indexed] = [name, block] end def boolean_value @current[:boolean] = true format_value {|value| super(value) == 'true' } end # required by the hash configuration method alias_method :boolean, :boolean_value TRANSLATE_DIGITS = ['0123456789'.freeze, ('X'*10).freeze] EASY_FORMAT = "XXXX-XX-XXTXX:XX:XX.XXXZ".freeze DATE_PUNCTUATION = ['-:.TZ'.freeze, (' '*5).freeze] def datetime_value datetime_like_value(DateTime, :civil) end def time_value datetime_like_value(Time, :utc) end alias_method :timestamp, :time_value def datetime_like_value(klass, parts_constructor) format_value do |value| value = super(value) if value and value.tr(*TRANSLATE_DIGITS) == EASY_FORMAT # 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. parts = value.tr(*DATE_PUNCTUATION). chop.split.map { |elem| elem.to_i } klass.send(parts_constructor, *parts) elsif value # fallback in case we have to handle another date format klass.parse(value) else nil end end end def integer_value format_value do |value| value = super(value) value.nil? ? nil : value.to_i end end # required by the hash configuration method alias_method :integer, :integer_value alias_method :long, :integer_value def float_value format_value do |value| value = super(value) value.nil? ? nil : value.to_f end end alias_method :float, :float_value def symbol_value format_value do |value| value = super(value) ['', nil].include?(value) ? nil : Inflection.ruby_name(value).to_sym end end def format_value &block @current[:value_formatter] ||= ValueFormatter.new @current[:value_formatter].extend_format_value(&block) end def list child_element_name = nil, &block if child_element_name ignore parent_element_name = @current_name element(child_element_name) do rename(parent_element_name) collect_values yield if block_given? end else collect_values end end def map_entry(key, value) collect_values element(key) { rename :key } element(value) { rename :value } @current[:initial_collection] = lambda { {} } @current[:add_to_collection] = lambda do |hash, entry| hash[entry.key] = entry.value end end def map entry_name, key, value parent_element_name = @current_name ignore element(entry_name) do rename(parent_element_name) map_entry(key, value) end end def wrapper method_name, options = {}, &blk if block_given? customizations = eval_customization_context(method_name, CustomizationContext.new(method_name), &blk) raise NotImplementedError.new("can't customize wrapped " + "elements within the wrapper") unless customizations[:children].empty? @current[:wrapper_frames] ||= {} @current[:wrapper_frames][method_name] = customizations end (options[:for] || []).each do |element_name| element element_name do @current[:wrapper] ||= [] @current[:wrapper] << method_name end end end def construct_value &block @current[:construct_value] = block end def eql? other self.customizations == other.customizations end protected def initial_customizations(element_name = nil) CustomizationContext.new(element_name) end protected def eval_customization_context name, initial = nil, &block current_name = @current_name current = @current parent = @parent begin @current_name = name @parent = @current initial ||= customizations_for(name) @current = initial yield if block_given? ensure @current_name = current_name @current = current @parent = parent end # will be modified to include the customizations defined in # the block initial end protected def config_eval(config) config.each do |item| (type, identifier, args) = parse_config_item(item) case type when :method validate_config_method(identifier) validate_args(identifier, args) send(identifier, *args) when :element element(identifier) do config_eval(args) end end end end protected def validate_args(identifier, args) arity = method(identifier).arity if args.length > 0 raise "#{identifier} does not accept an argument" if arity == 0 else raise "#{identifier} requires an argument" unless arity == 0 || arity == -1 end end protected def parse_config_item(item) case item when Symbol [:method, item, []] when Hash (method, arg) = item.to_a.first if method.kind_of?(Symbol) [:method, method, [arg].flatten] else [:element, method, arg] end end end protected def validate_config_method(method) allow_methods = %w( rename attribute_name boolean integer long float list force ignore collect_values symbol_value timestamp map_entry map ) unless allow_methods.include?(method.to_s) raise "#{method} cannot be used in configuration" end end protected def customizations @customizations ||= CustomizationContext.new end protected def customizations_for element_name if @parent @parent[:children][element_name] ||= CustomizationContext.new(element_name) else customizations[:children][element_name] ||= CustomizationContext.new(element_name) end end protected def customizations= customizations @customizations = customizations @current = customizations end end # @private class ValueFormatter def extend_format_value &block MetaUtils.extend_method(self, :format_value, &block) end def format_value value value end end # @private class Parser def initialize context, customizations @context = context @customizations = customizations end def start_element element_name, attrs if @frame @frame = @frame.build_child_frame(element_name) else @frame = RootFrame.new(@context, @customizations) end # consume attributes the same way we consume nested xml elements attrs.each do |(attr_name, attr_value)| attr_frame = @frame.build_child_frame(attr_name) attr_frame.add_text(attr_value) @frame.consume_child_frame(attr_frame) end end def end_element name @frame.close if @frame.parent_frame child_frame = @frame parent_frame = @frame.parent_frame parent_frame.consume_child_frame(child_frame) end @frame = @frame.parent_frame end def characters chars @frame.add_text(chars) if @frame end end module REXMLSaxParserAdapter require 'rexml/streamlistener' include REXML::StreamListener def tag_start(name, attrs) start_element(name, attrs) end def tag_end(name) end_element(name) end def text(chars) characters(chars) end end module NokogiriAdapter def xmldecl(*args); end def start_document; end def end_document; end def start_element_namespace(name, attrs = [], prefix = nil, uri = nil, ns = []) start_element(name, attrs.map { |att| [att.localname, att.value] }) end def end_element_namespace(name, prefix = nil, uri = nil) end_element(name) end def error(*args); end end # @private class Frame attr_reader :parent_frame attr_reader :root_frame attr_reader :element_name attr_accessor :customizations def initialize element_name, options = {} @element_name = element_name @context = options[:context] @parent_frame = options[:parent_frame] @root_frame = options[:root_frame] @wrapper_frames = {} if @parent_frame @customizations = @parent_frame.customizations_for_child(element_name) else @customizations = options[:customizations] @root_frame ||= self end if @root_frame == self and indexes = @customizations[:index_names] indexes.each do |name| if context.kind_of?(Context) context.__set_data__(name, {}) else add_mutators(name) context.send("#{name}=", {}) end end end # we build and discard child frames here so we can know # which children should always add a method to this # frame's context (forced elements, like collected arrays) @customizations[:children].keys.each do |child_element_name| consume_initial_frame(build_child_frame(child_element_name)) end if @customizations[:wrapper_frames] @customizations[:wrapper_frames].keys.each do |method_name| consume_initial_frame(wrapper_frame_for(method_name)) end end end def build_child_frame(child_element_name) Frame.new(child_element_name, :parent_frame => self, :root_frame => root_frame) end def consume_child_frame child_frame return if child_frame.ignored? if child_frame.wrapped? child_frame.wrapper_methods.each do |method_name| consume_in_wrapper(method_name, child_frame) end else # forced child frames have already added mutators to this context add_mutators_for(child_frame) unless child_frame.forced? if child_frame.collected? child_frame.add_to_collection(context.send(child_frame.getter), child_frame.value) else invoke_setter(child_frame, child_frame.value) end end end def close if indexed = @customizations[:indexed] (name, block) = indexed key = block.call(context) [key].flatten.each do |k| index(name)[k] = context end end end def add_text text @text ||= '' @text << text end def value @customizations[:value_formatter] ? @customizations[:value_formatter].format_value(default_value) : default_value end def context @context ||= (self.ignored? ? parent_frame.context : construct_context) end def setter @customizations[:setter] end def getter @customizations[:getter] end def initial_collection @customizations[:initial_collection].call end def add_to_collection(collection, value) @customizations[:add_to_collection].call(collection, value) end def index(name) return root_frame.index(name) unless root_frame == self context.send(name) end protected def consume_initial_frame(child_frame) if child_frame.forced? add_mutators_for(child_frame) if child_frame.collected? invoke_setter(child_frame, child_frame.initial_collection) else # this allows nested forced elements to appear invoke_setter(child_frame, child_frame.value) end end end protected def construct_context if @customizations[:construct_value] instance_eval(&@customizations[:construct_value]) else Context.new end end protected def consume_in_wrapper method_name, child_frame wrapper_frame = wrapper_frame_for(method_name) add_mutators(method_name) # the wrapper consumes the unwrapped child customizations = child_frame.customizations.merge(:wrapper => nil) child_frame = child_frame.dup child_frame.customizations = customizations wrapper_frame.consume_child_frame(child_frame) consume_child_frame(wrapper_frame) end protected def wrapper_frame_for(method_name) @wrapper_frames[method_name] ||= Frame.new(method_name.to_s, :customizations => wrapper_customizations(method_name)) end protected def wrapper_customizations(method_name) customizations = CustomizationContext.new(method_name) customizations[:children] = @customizations[:children] if wrapper_frames = @customizations[:wrapper_frames] and additional = wrapper_frames[method_name] additional[:children] = @customizations[:children].merge(additional[:children]) if additional[:children] customizations.merge!(additional) end customizations end protected def invoke_setter(child_frame, value) if context.kind_of?(Context) context.__set_data__(child_frame.getter, value) else context.send(child_frame.setter, value) end end protected def add_mutators_for child_frame return if context.kind_of?(Context) add_mutators(child_frame.ruby_name, child_frame.setter, child_frame.getter) end protected def add_mutators(variable_name, setter = nil, getter = nil) return if context.kind_of?(Context) variable_name = variable_name.to_s.gsub(/\?$/, '') setter ||= "#{variable_name}=" getter ||= variable_name return if context.respond_to?(getter) && context.respond_to?(setter) MetaUtils.extend_method(context, setter) do |val| instance_variable_set("@#{variable_name}", val) end MetaUtils.extend_method(context, getter) do instance_variable_get("@#{variable_name}") end end protected def forced? @customizations[:forced] end protected def ignored? @customizations[:ignored] end protected def collected? @customizations[:collected] end protected def wrapped? @customizations[:wrapper] end protected def wrapper_methods @customizations[:wrapper] end protected def default_value if # TODO : move this out of the default value method @context and @context.respond_to?(:encoding) and @context.encoding == 'base64' then Base64.decode64(@text.strip) else @context || @text end end protected def ruby_name Inflection.ruby_name(@customizations[:renamed] || element_name) end protected def customizations_for_child child_element_name @customizations[:children][child_element_name] || CustomizationContext.new(child_element_name) end protected def initial_customizations(element_name = nil) end end # @private class RootFrame < Frame def initialize context, customizations super('ROOT', :context => context, :customizations => customizations) end end # @private class StubResponse def initialize customizations, context = nil @customizations = customizations stub_methods(customizations, context || self) end def inspect methods = public_methods - Object.public_methods "<Stub #{methods.collect{|m| ":#{m}" }.sort.join(', ')}>" end # @private private def stub_methods customizations, context add_wrappers_to_context(customizations, context) add_child_elements_to_context(customizations, context) add_indexes_to_context(customizations, context) end # @private private def add_wrappers_to_context customizations, context wrappers(customizations) do |wrapper_name,wrapper_customizations| MetaUtils.extend_method(context, wrapper_name) do StubResponse.new(wrapper_customizations) end end end # @private private def add_child_elements_to_context customizations, context without_wrapper(customizations) do |child_name,child_rules| ruby_name = Inflection.ruby_name(child_rules[:renamed] || child_name) # we stop at any collected elements if child_rules[:collected] MetaUtils.extend_method(context, ruby_name) { [] } next end if child_rules[:construct_value] MetaUtils.extend_method(context, ruby_name) do child_rules[:construct_value].call end elsif child_rules[:children].empty? # has no child elements unless child_rules[:ignored] method_name = child_rules[:boolean] ? "#{ruby_name}?" : ruby_name MetaUtils.extend_method(context, method_name) do if child_rules[:value_formatter] child_rules[:value_formatter].format_value('') else nil end end end else # it has one or more child elements if child_rules[:ignored] stub_methods(child_rules, context) else MetaUtils.extend_method(context, ruby_name) do StubResponse.new(child_rules) end end end end end # @private def add_indexes_to_context(customizations, context) if indexes = customizations[:index_names] indexes.each do |index| MetaUtils.extend_method(context, index) { {} } end end end # @private def wrappers customizations, &block wrappers = {} customizations[:children].each_pair do |child_name,child_rules| if child_rules[:wrapper] wrapper_name = child_rules[:wrapper].first wrappers[wrapper_name] ||= { :children => {} } wrappers[wrapper_name][:children][child_name] = child_rules.merge(:wrapper => nil) end end wrappers.each_pair do |wrapper_name, wrapper_customizations| yield(wrapper_name, wrapper_customizations) end end # @private private def without_wrapper customizations, &block customizations[:children].each_pair do |child_name,child_rules| unless child_rules[:wrapper] yield(child_name, child_rules) end end end end end end