Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ruby_odata / lib / ruby_odata / class_builder.rb
Size: Mime:
module OData
  # Internal helper class for building a dynamic class.  This class shouldn't be called directly.
  class ClassBuilder
    # Creates a new instance of the ClassBuilder class
    #
    # @param [String] klass_name the name/type of the class to create
    # @param [Array] methods the accessor methods to add to the class
    # @param [Array] nav_props the accessor methods to add for navigation properties
    # @param [Service] context the service context that this entity belongs to
    # @param [String, nil] namespace optional namespace to create the classes in
    def initialize(klass_name, methods, nav_props, context, namespace = nil)
      @klass_name = klass_name.camelcase
      @methods = methods
      @nav_props = nav_props
      @context = context
      @namespace = namespace
    end

    # Returns a dynamically generated class definition based on the constructor parameters
    def build
      # return if already built
      return @klass unless @klass.nil?

      # need the class name to build class
      return nil    if @klass_name.nil?

      # return if we can find constant corresponding to class name
      already_defined = eval("defined?(#{@klass_name}) == 'constant' and #{@klass_name}.class == Class")
      if already_defined
        @klass = @klass_name.constantize
        return @klass
      end

      if @namespace
        namespaces = @namespace.split(/\.|::/)

        namespaces.each_with_index do |ns, index|
          if index == 0
            next if Object.const_defined? ns
            Object.const_set(ns, Module.new)
          else
            current_ns = namespaces[0..index-1].join '::'
            next if eval "#{current_ns}.const_defined? '#{ns}'"
            eval "#{current_ns}.const_set('#{ns}', Module.new)"
          end
        end

        klass_constant = @klass_name.split('::').last
        eval "#{namespaces.join '::'}.const_set('#{klass_constant}', Class.new.extend(ActiveSupport::JSON))"
      else
        Object.const_set(@klass_name, Class.new.extend(ActiveSupport::JSON))
      end

      @klass = @klass_name.constantize
      @klass.class_eval do
        include OData
      end

      add_initializer(@klass)
      add_methods(@klass)
      add_nav_props(@klass)
      add_class_methods(@klass)

      return @klass
    end

    private
    def add_initializer(klass)
      klass.send :define_method, :initialize do |*args|
        return if args.blank?
        props = args[0]
        return unless props.is_a? Hash
        props.each do |k,v|
          raise NoMethodError, "undefined method `#{k}'" unless self.respond_to? k.to_sym
          instance_variable_set("@#{k}", v)
        end
      end
    end

    def add_methods(klass)
      # Add metadata methods
      klass.send :define_method, :__metadata do
        instance_variable_get("@__metadata")
      end
      klass.send :define_method, :__metadata= do |value|
        instance_variable_set("@__metadata", value)
      end
      klass.send :define_method, :as_json do |*args|
        meta = RUBY_VERSION < "1.9" ? '@__metadata' : ('@__metadata'.to_sym)

        options = args[0] || {}
        options[:type] ||= :unknown

        vars = self.instance_values

        if options[:type] == :add
          # For adds, we need to get rid of all attributes except __metadata when passing
          # the object to the server
          vars.each_value do |value|
            if value.is_a? OData
              child_vars = value.instance_variables
              if(child_vars.include?(meta))
                child_vars.each do |var|
                  value.send :remove_instance_variable, var if var != meta
                end
              else
                value.send :remove_instance_variable, meta if value.instance_variable_defined? meta
              end
            end
          end
        end

        props = self.class.properties

        # Convert a Int64 to a string for serialization (to match Edm.Int64)
        bigints = vars.find_all { |o| props[o[0]] && props[o[0]].type == "Edm.Int64" } || []
        bigints.each do |i|
          vars[i[0]] = i[1].to_s
        end

        # Convert Arrays into proper Collections
        collections = vars.find_all { |o| o[1].class == Array } || []
        collections.each do |c|
          if options[:type] == :add
            vars[c[0]] = c[1]
          else
            vars[c[0]] = { '__metadata' => { 'type' => props[c[0]].type }, 'results' => c[1] }
          end
        end

        # Convert a BigDecimal to a string for serialization (to match Edm.Decimal)
        decimals = vars.find_all { |o| o[1].class == BigDecimal } || []
        decimals.each do |d|
          vars[d[0]] = d[1].to_s
        end

        # Convert Time to an RFC3339 string for serialization
        times = vars.find_all { |o| o[1].class == Time } || []
        times.each do |t|
          sdate = t[1].xmlschema(3)
          # Remove the ending Z (indicating UTC).
          # If the Z is there when saving, the time is converted to local time on the server
          sdate.chop! if sdate.match(/Z$/)
          vars[t[0]] = sdate
        end

        if options[:type] == :link
          # For links, delete all of the vars and just add a uri
          uri = self.__metadata[:uri]
          vars = { 'uri' => uri }
        end

        vars
      end

      # Add the methods that were passed in
      @methods.each do |method_name|
        klass.send :define_method, method_name do
          instance_variable_get("@#{method_name}")
        end
        klass.send :define_method, "#{method_name}=" do |value|
          instance_variable_set("@#{method_name}", value)
        end
      end

      # Add an id method pulling out the id from the uri (mainly for Pickle support)
      klass.send :define_method, :id do
        metadata = self.__metadata
        id = nil
        if metadata && metadata[:uri]  =~ /\((\d+)L?\)$/
          id = $~[1]
        end
        return (true if Integer(id) rescue false) ? id.to_i : id
      end

      # Override equals
      klass.send :define_method, :== do |other|
        self.class == other.class &&
        self.id == other.id &&
        self.__metadata == other.__metadata
      end
    end

    def add_nav_props(klass)
      @nav_props.each do |method_name|
        klass.send :define_method, method_name do
          instance_variable_get("@#{method_name}")
        end
        klass.send :define_method, "#{method_name}=" do |value|
          instance_variable_set("@#{method_name}", value)
        end
      end
    end

    def add_class_methods(klass)
      context = @context

      # Retrieves a list of properties defined on a type (standard and navigation properties)
      klass.send :define_singleton_method, 'properties' do
        context.class_metadata[klass.to_s] || {}
      end

      # Finds a single model by ID
      klass.send :define_singleton_method, 'first' do |id|
        return nil if id.nil?
        # TODO: Instead of just pluralizing the klass name, use the actual collection name
        collection = klass.to_s.pluralize
        context.send "#{collection}", id
        begin
          results = context.execute
        rescue OData::ServiceError => e
          return nil
        end
        results.count == 0 ? nil : results.first
      end
    end
  end
end # module OData