lib/aws/core/xml/grammar.rb



# 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.

module AWS
  module Core
    module XML

      # A class that simplifies building XML {Parser} rules.  This is also
      # a compatability layer between the old and new formats of the api
      # config.
      class Grammar

        def initialize rules = {}, options = {}
          @rules = rules
          @context = @rules
          @element_name = 'xml'
          @inflect_rename = options.key?(:inflect_rename) ?
            options[:inflect_rename] : true
        end

        # Parses the XML with the rules provided by the current grammar.
        # This method is meant to provide backwards compatability with
        # the old XmlGrammar class that handled rules and parsing.
        # @param [String] xml
        # @return [Data] Returns a hash-like parsed response.
        def parse xml
          Data.new(Parser.parse(xml, rules))
        end

        # @return [Hash] Returns a hash of rules defined by this grammar.
        attr_reader :rules

        # Returns a new grammar (leaving the current one un-modified) with
        # the given customizations applied.  Customizations can be given in
        # a hash-form or in a block form.
        #
        # @example Block-form customizations
        #
        #   grammar.customize do
        #     element "EnumElement" do
        #       symbol_value
        #       list
        #     end
        #   end
        #
        # @example Hash-form customizations
        #
        #   grammar.customize "EnumElement" => [:symbol_value, :list]
        #
        # @return [Grammar] Returns a grammar with the given customizations
        #   applied.
        #
        def customize customizations = nil, &block
          opts = { :inflect_rename => @inflect_rename }
          self.class.customize(customizations, @rules, opts, &block)
        end

        # Applies customizations to the current grammar, not returning
        # a new grammar.
        def customize! customizations = nil, &block
          apply_customizations(customizations) if customizations
          instance_eval(&block) if block_given?
          self
        end

        def self.customize customizations = nil, rules = {}, opts = {}, &block
          grammar = self.new(deep_copy(rules), opts)
          grammar.send(:apply_customizations, customizations) if customizations
          grammar.instance_eval(&block) if block_given?
          grammar
        end

        def self.parse xml
          self.new.parse(xml)
        end

        protected

        # Performs a deep copy of the rules hash so that it can be
        # customized without chaning the parent grammar.
        def self.deep_copy rules
          rules.inject({}) do |copy,(key,value)|
            copy[key] = value.is_a?(Hash) ? deep_copy(value) : value
            copy
          end
        end

        def apply_customizations customizations
          customizations.each do |item|
            (type, identifier, args) = parse_customization_item(item)
            case type
            when :method
              validate_config_method(identifier)
              validate_args(identifier, args)
              send(identifier, *args)
            when :element
              element(identifier) do
                apply_customizations(args)
              end
            end
          end
        end

        def parse_customization_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

        def validate_config_method(method)
          allow_methods = %w(
            rename attribute_name boolean integer long float list force string
            ignore collect_values symbol_value timestamp map_entry map blob
            position
          )
          unless allow_methods.include?(method.to_s)
            raise "#{method} cannot be used in configuration"
          end
        end

        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

        def inflect value
          Inflection.ruby_name(value.to_s).to_sym
        end

        ##
        ## customization methods
        ##

        def element element_name, &block

          parent_context = @context
          parent_element_name = @element_name

          @context = context_for_child(element_name)

          @element_name = element_name

          begin
            if block_given?
              block.arity == 1 ? yield(parent_element_name) : yield
            end
          ensure
            @context = parent_context
            @element_name = parent_element_name
          end

        end

        def ignore
          @context[:ignore] = true
        end

        def rename new_name
          if @inflect_rename
            @context[:rename] = inflect(new_name)
          else
            @context[:rename] = new_name
          end
        end

        def force
          @context[:force] = true
        end

        def collect_values
          @context[:list] = true
        end

        def index index_name, options = {}
          @context[:index] = options.merge(:name => index_name)
        end

        def default_value name, value
          @context[:defaults] ||= {}
          @context[:defaults][name] = value
        end

        def list child_element_name = nil, &block
          if child_element_name
            ignore
            element(child_element_name) do |parent_element_name|
              rename(parent_element_name)
              collect_values
              yield if block_given?
            end
          else
            collect_values
          end
        end

        def map_entry key_element_name, value_element_name
          @context[:map] = [key_element_name, value_element_name]
        end

        def map map_element_name, key_element_name, value_element_name
          ignore
          element(map_element_name) do |parent_element_name|
            rename(parent_element_name)
            map_entry(key_element_name, value_element_name)
          end
        end

        def wrapper method_name, options = {}, &block
          options[:for].each do |child|
            context_for_child(child)[:wrap] = method_name
          end
        end

        def construct_value &block
          raise 'remove the need for this'
        end

        def boolean_value
          @context[:type] = :boolean
        end
        alias_method :boolean, :boolean_value

        def blob_value
          @context[:type] = :blob
        end
        alias_method :blob, :blob_value

        def datetime_value
          @context[:type] = :datetime
        end
        alias_method :datetime, :datetime_value

        def time_value
          @context[:type] = :time
        end
        alias_method :timestamp, :time_value
        alias_method :time, :time_value

        def string_value
          @context[:type] = :string
        end
        alias_method :string, :string_value

        def integer_value
          @context[:type] = :integer
        end
        alias_method :integer, :integer_value
        alias_method :long, :integer_value

        def float_value
          @context[:type] = :float
        end
        alias_method :float, :float_value

        def symbol_value
          @context[:type] = :symbol
        end
        alias_method :symbol, :symbol_value

        def eql? other
          other.is_a?(Grammar) and self.rules == other.rules
        end
        alias_method :==, :eql?

        def position *args; end
        def http_trait *args; end
        alias_method :http_header, :http_trait
        alias_method :http_uri_label, :http_trait
        alias_method :http_payload, :http_trait
        alias_method :http_status, :http_trait


        protected
        def context_for_child child_element_name
          @context[:children] ||= {}
          @context[:children][child_element_name] ||= {}
          @context[:children][child_element_name]
        end

      end
    end
  end
end