remiprev/her

View on GitHub
lib/her/model/orm.rb

Summary

Maintainability
A
35 mins
Test Coverage
# coding: utf-8

module Her
  module Model
    # This module adds ORM-like capabilities to the model
    module ORM
      extend ActiveSupport::Concern

      # Return `true` if a resource was not saved yet
      def new?
        id.nil?
      end
      alias new_record? new?

      # Return `true` if a resource is not `#new?`
      def persisted?
        !new?
      end

      # Return whether the object has been destroyed
      def destroyed?
        @destroyed == true
      end

      # Save a resource and return `false` if the response is not a successful one or
      # if there are errors in the resource. Otherwise, return the newly updated resource
      #
      # @example Save a resource after fetching it
      #   @user = User.find(1)
      #   # Fetched via GET "/users/1"
      #   @user.fullname = "Tobias Fünke"
      #   @user.save
      #   # Called via PUT "/users/1"
      #
      # @example Save a new resource by creating it
      #   @user = User.new({ :fullname => "Tobias Fünke" })
      #   @user.save
      #   # Called via POST "/users"
      def save
        callback = new? ? :create : :update
        method = self.class.method_for(callback)

        run_callbacks :save do
          run_callbacks callback do
            self.class.request(to_params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
              load_from_parsed_data(parsed_data)
              return false if !response.success? || @response_errors.any?
              changes_applied
            end
          end
        end

        self
      end

      # Similar to save(), except that ResourceInvalid is raised if the save fails
      def save!
        unless save
          raise Her::Errors::ResourceInvalid, self
        end
        self
      end

      # Update a resource and return it
      #
      # @example
      #   @user = User.find(1)
      #   @user.update_attributes(:name => "Tobias Fünke")
      #   # Called via PUT "/users/1"
      def update_attributes(attributes)
        assign_attributes(attributes) && save
      end

      # Destroy a resource
      #
      # @example
      #   @user = User.find(1)
      #   @user.destroy
      #   # Called via DELETE "/users/1"
      def destroy(params = {})
        method = self.class.method_for(:destroy)
        run_callbacks :destroy do
          self.class.request(params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
            load_from_parsed_data(parsed_data)
            @destroyed = response.success?
          end
        end
        self
      end

      # Initializes +attribute+ to zero if +nil+ and adds the value passed as
      # +by+ (default is 1). The increment is performed directly on the
      # underlying attribute, no setter is invoked. Only makes sense for
      # number-based attributes. Returns +self+.
      def increment(attribute, by = 1)
        attributes[attribute] ||= 0
        attributes[attribute] += by
        self
      end

      # Wrapper around #increment that saves the resource. Saving is subjected
      # to validation checks. Returns +self+.
      def increment!(attribute, by = 1)
        increment(attribute, by) && save
        self
      end

      # Initializes +attribute+ to zero if +nil+ and substracts the value passed as
      # +by+ (default is 1). The decrement is performed directly on the
      # underlying attribute, no setter is invoked. Only makes sense for
      # number-based attributes. Returns +self+.
      def decrement(attribute, by = 1)
        increment(attribute, -by)
      end

      # Wrapper around #decrement that saves the resource. Saving is subjected
      # to validation checks. Returns +self+.
      def decrement!(attribute, by = 1)
        increment!(attribute, -by)
      end

      # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
      # if the predicate returns +true+ the attribute will become +false+. This
      # method toggles directly the underlying value without calling any setter.
      # Returns +self+.
      #
      # @example
      #   user = User.first
      #   user.admin? # => false
      #   user.toggle(:admin)
      #   user.admin? # => true
      def toggle(attribute)
        attributes[attribute] = !public_send("#{attribute}?")
        self
      end

      # Wrapper around #toggle that saves the resource. Saving is subjected to
      # validation checks. Returns +true+ if the record could be saved.
      def toggle!(attribute)
        toggle(attribute) && save
      end

      # Refetches the resource
      #
      # This method finds the resource by its primary key (which could be
      # assigned manually) and modifies the object in-place.
      #
      # @example
      #   user = User.find(1)
      #   # => #<User(users/1) id=1 name="Tobias Fünke">
      #   user.name = "Oops"
      #   user.reload # Fetched again via GET "/users/1"
      #   # => #<User(users/1) id=1 name="Tobias Fünke">
      def reload(options = nil)
        fresh_object = self.class.find(id)
        assign_attributes(fresh_object.attributes)
        self
      end

      # Uses parsed response to assign attributes and metadata
      #
      # @private
      def load_from_parsed_data(parsed_data)
        data = parsed_data[:data]
        assign_attributes(self.class.parse(data)) if data.any?
        @metadata = parsed_data[:metadata]
        @response_errors = parsed_data[:errors]
      end

      module ClassMethods
        # Create a new chainable scope
        #
        # @example
        #   class User
        #     include Her::Model
        #
        #     scope :admins, lambda { where(:admin => 1) }
        #     scope :page, lambda { |page| where(:page => page) }
        #   enc
        #
        #   User.admins # Called via GET "/users?admin=1"
        #   User.page(2).all # Called via GET "/users?page=2"
        def scope(name, code)
          # Add the scope method to the class
          (class << self; self end).send(:define_method, name) do |*args|
            instance_exec(*args, &code)
          end

          # Add the scope method to the default/blank relation
          scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
        end

        # @private
        def scoped
          @_her_default_scope || blank_relation
        end

        # Define the default scope for the model
        #
        # @example
        #   class User
        #     include Her::Model
        #
        #     default_scope lambda { where(:admin => 1) }
        #   enc
        #
        #   User.all # Called via GET "/users?admin=1"
        #   User.new.admin # => 1
        def default_scope(block = nil)
          @_her_default_scope ||= (!respond_to?(:default_scope) && superclass.respond_to?(:default_scope)) ? superclass.default_scope : scoped
          @_her_default_scope = @_her_default_scope.instance_exec(&block) unless block.nil?
          @_her_default_scope
        end

        # Delegate the following methods to `scoped`
        [:all, :where, :create, :build, :find, :find_by, :find_or_create_by,
         :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
          class_eval <<-RUBY, __FILE__, __LINE__ + 1
            def #{method}(*params)
              scoped.send(#{method.to_sym.inspect}, *params)
            end
          RUBY
        end

        # Save an existing resource and return it
        #
        # @example
        #   @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
        #   # Called via PUT "/users/1"
        def save_existing(id, params)
          save_existing!(id, params)
        rescue Her::Errors::ResourceInvalid => e
          e.resource
        end

        # Similar to .save_existing but raises ResourceInvalid if save fails
        def save_existing!(id, params)
          resource = new(params.merge(primary_key => id))
          resource.save!
          resource
        end

        # Destroy an existing resource
        #
        # @example
        #   User.destroy_existing(1)
        #   # Called via DELETE "/users/1"
        def destroy_existing(id, params = {})
          request(params.merge(:_method => method_for(:destroy), :_path => build_request_path(params.merge(primary_key => id)))) do |parsed_data, response|
            data = parse(parsed_data[:data])
            metadata = parsed_data[:metadata]
            response_errors = parsed_data[:errors]
            record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
            record.response_errors = response_errors
            record
          end
        end

        # Return or change the HTTP method used to create or update records
        #
        # @param [Symbol, String] action The behavior in question (`:create` or `:update`)
        # @param [Symbol, String] method The HTTP method to use (`'PUT'`, `:post`, etc.)
        def method_for(action = nil, method = nil)
          @method_for ||= (superclass.respond_to?(:method_for) ? superclass.method_for : {})
          return @method_for if action.nil?

          action = action.to_s.downcase.to_sym

          return @method_for[action] if method.nil?
          @method_for[action] = method.to_s.downcase.to_sym
        end

        # Build a new resource with the given attributes.
        # If the request_new_object_on_build flag is set, the new object is requested via API.
        def build(attributes = {})
          params = attributes
          return new(params) unless request_new_object_on_build?

          path = build_request_path(params.merge(primary_key => 'new'))
          method = method_for(:new)

          resource = nil
          request(params.merge(:_method => method, :_path => path)) do |parsed_data, response|
            if response.success?
              resource = new_from_parsed_data(parsed_data)
            end
          end
          resource
        end

        # @private
        def blank_relation
          @blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
          @blank_relation ||= Relation.new(self)
        end
      end
    end
  end
end