kaiwren/wrest

View on GitHub
lib/wrest/components/container.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

# Copyright 2009 Sidu Ponnappa

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License
# 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 Wrest
  module Components
    module Container
    end
  end
end

require 'wrest/components/container/typecaster'
require 'wrest/components/container/alias_accessors'

module Wrest
  module Components
    # Adds behaviour allowing a class to
    # contain attributes and providing support
    # for dynamic getters, setters and query methods.
    # These methods are added at runtime, on the first
    # invocation and on a per instance basis.
    # <tt>respond_to?</tt> however will respond as though
    # they are all already present.
    # This means that two different instances of the same
    # Container could well have different attribute
    # getters/setters/query methods.
    #
    # Note that the first call to a particular getter/setter/query
    # method will be slower because the method is defined
    # at that point; subsequent calls will be much faster.
    #
    # Also keep in mind that attribute getter/setter/query methods
    # will _not_ override any existing methods on the class.
    #
    # In situations where this is a problem, such as a client consuming Rails
    # REST services where <tt>id</tt> is a common attribute and clashes with
    # Object#id, it is recommended to create getter/setter/query methods
    # on the class (which affects all instances) using the +always_has+ macro.
    #
    # If you're implementing your own initialize method
    # remember to delegate to the default initialize
    # of Container by invoking <tt>super(attributes)</tt>
    #
    # Example:
    #  class ShenCoin
    #    include Wrest::Components::Container
    #    include Wrest::Components::Container::Typecaster
    #
    #    always_has   :id
    #    typecast         :id   =>  as_integer
    #  end
    #  coin = ShenCoin.new(:id => '5', :chi_count => 500, :owner => 'Kai Wren')
    #  coin.id    # => 5
    #  coin.owner # => 'Kai Wren'
    module Container
      def self.included(klass) # :nodoc:
        klass.extend Container::ClassMethods
        klass.extend Container::Typecaster::Helpers
        klass.class_eval do
          include Container::InstanceMethods
          include Container::AliasAccessors
        end
      end

      def self.build_attribute_getter(attribute_name) # :nodoc:
        "def #{attribute_name};@attributes[:#{attribute_name}];end;"
      end

      def self.build_attribute_setter(attribute_name) # :nodoc:
        "def #{attribute_name}=(value);@attributes[:#{attribute_name}] = value;end;"
      end

      def self.build_attribute_queryer(attribute_name) # :nodoc:
        "def #{attribute_name}?;not @attributes[:#{attribute_name}].nil?;end;"
      end

      module ClassMethods
        # This macro explicitly creates getter, setter and query methods on
        # an Container, overriding any existing methods with the same names.
        # This can be used when attribute names clash with existing method names;
        # an example would be Rails REST resources which frequently make use
        # an attribute named <tt>id</tt> which clashes with Object#id. Also,
        # this can be used as a performance optimisation if the incoming
        # attributes are known beforehand.
        def always_has(*attribute_names)
          attribute_names.each do |attribute_name|
            class_eval(
              Container.build_attribute_getter(attribute_name) +
              Container.build_attribute_setter(attribute_name) +
              Container.build_attribute_queryer(attribute_name)
            )
          end
        end

        # This is a convenience macro which includes
        # Wrest::Components::Container::Typecaster into
        # the class (effectively overwriting this method) before delegating to
        # the actual typecast method that is a part of that module.
        # This saves us the effort of explicitly doing the include. Easy to use API is king.
        #
        # Remember that using typecast carries a performance penalty.
        # See Wrest::Components::Container::Typecaster for the actual docs.
        def typecast(cast_map)
          class_eval { include Wrest::Components::Container::Typecaster }
          typecast cast_map
        end

        # This is the name of the class in snake-case, with any parent
        # module names removed.
        #
        # The class will use as the root element when
        # serialised to xml after replacing underscores with hyphens.
        #
        # This method can be overidden should you need a different name.
        def element_name
          @element_name ||= Utils.string_underscore(Utils.string_demodulize(name))
        end
      end

      module InstanceMethods
        # Sets up any class to act like
        # an attributes container by creating
        # two variables, @attributes and @interface.
        # Remember not to use these two variable names
        # when using Container in your own class.
        def initialize(attributes = {})
          @attributes = HashWithIndifferentAccess.new(attributes)
        end

        # A translator is a anything that knows how to serialise a
        # Hash. It must needs have a method named 'serialise' that
        # accepts a hash and configuration options, and returns the serialised
        # result (leaving the hash unchanged, of course).
        #
        # Examples for JSON and XML can be found under Wrest::Components::Translators.
        # These serialised output of these translators will work out of the box for Rails
        # applications; you may need to roll your own for anything else.
        #
        # Note: When serilising to XML, if you want the name of the class as the name of the root node
        # then you should use the Container#to_xml helper.
        def serialise_using(translator, options = {})
          payload = {
            self.class.element_name => @attributes.dup
          }
          translator.serialise(payload, options)
        end

        def to_xml(options = {})
          serialise_using(Wrest::Components::Translators::Xml, { root: self.class.element_name }.merge(options))
        end

        def [](key)
          @attributes[key.to_sym]
        end

        def []=(key, value)
          @attributes[key.to_sym] = value
        end

        def respond_to_missing?(method_name, *)
          if super.respond_to?(method_name)
            true
          else
            @attributes.include?(method_name.to_s.gsub(/(\?$)|(=$)/,
                                                       '').to_sym)
          end
        end

        # Creates getter, setter and query methods for
        # attributes on the first call.
        def method_missing(method_sym, *arguments)
          method_name = method_sym.to_s
          attribute_name = method_name.gsub(/(\?$)|(=$)/, '')
          if @attributes.include?(attribute_name.to_sym) || method_name[-1] == '=' || method_name[-1] == '?'
            generate_methods!(attribute_name, method_name)
            send(method_sym, *arguments)
          else
            super(method_sym, *arguments)
          end
        end

        private

        def generate_methods!(attribute_name, method_name)
          case method_name[-1]
          when '='
            instance_eval Container.build_attribute_setter(attribute_name)
          when '?'
            instance_eval Container.build_attribute_queryer(attribute_name)
          else
            instance_eval Container.build_attribute_getter(attribute_name)
          end
        end
      end
    end
  end
end