herr 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.gitignore +4 -0
 - data/.rspec +1 -0
 - data/.travis.yml +15 -0
 - data/.yardopts +2 -0
 - data/CONTRIBUTING.md +26 -0
 - data/Gemfile +10 -0
 - data/LICENSE +7 -0
 - data/README.md +990 -0
 - data/Rakefile +11 -0
 - data/UPGRADE.md +81 -0
 - data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
 - data/gemfiles/Gemfile.activemodel-4.0 +7 -0
 - data/gemfiles/Gemfile.activemodel-4.1 +7 -0
 - data/gemfiles/Gemfile.activemodel-4.2 +7 -0
 - data/her.gemspec +30 -0
 - data/lib/her.rb +16 -0
 - data/lib/her/api.rb +115 -0
 - data/lib/her/collection.rb +12 -0
 - data/lib/her/errors.rb +27 -0
 - data/lib/her/middleware.rb +10 -0
 - data/lib/her/middleware/accept_json.rb +17 -0
 - data/lib/her/middleware/first_level_parse_json.rb +36 -0
 - data/lib/her/middleware/parse_json.rb +21 -0
 - data/lib/her/middleware/second_level_parse_json.rb +36 -0
 - data/lib/her/model.rb +72 -0
 - data/lib/her/model/associations.rb +141 -0
 - data/lib/her/model/associations/association.rb +103 -0
 - data/lib/her/model/associations/association_proxy.rb +46 -0
 - data/lib/her/model/associations/belongs_to_association.rb +96 -0
 - data/lib/her/model/associations/has_many_association.rb +100 -0
 - data/lib/her/model/associations/has_one_association.rb +79 -0
 - data/lib/her/model/attributes.rb +266 -0
 - data/lib/her/model/base.rb +33 -0
 - data/lib/her/model/deprecated_methods.rb +61 -0
 - data/lib/her/model/http.rb +114 -0
 - data/lib/her/model/introspection.rb +65 -0
 - data/lib/her/model/nested_attributes.rb +45 -0
 - data/lib/her/model/orm.rb +205 -0
 - data/lib/her/model/parse.rb +227 -0
 - data/lib/her/model/paths.rb +121 -0
 - data/lib/her/model/relation.rb +164 -0
 - data/lib/her/version.rb +3 -0
 - data/spec/api_spec.rb +131 -0
 - data/spec/collection_spec.rb +26 -0
 - data/spec/middleware/accept_json_spec.rb +10 -0
 - data/spec/middleware/first_level_parse_json_spec.rb +62 -0
 - data/spec/middleware/second_level_parse_json_spec.rb +35 -0
 - data/spec/model/associations_spec.rb +416 -0
 - data/spec/model/attributes_spec.rb +268 -0
 - data/spec/model/callbacks_spec.rb +145 -0
 - data/spec/model/dirty_spec.rb +86 -0
 - data/spec/model/http_spec.rb +194 -0
 - data/spec/model/introspection_spec.rb +76 -0
 - data/spec/model/nested_attributes_spec.rb +134 -0
 - data/spec/model/orm_spec.rb +479 -0
 - data/spec/model/parse_spec.rb +373 -0
 - data/spec/model/paths_spec.rb +341 -0
 - data/spec/model/relation_spec.rb +226 -0
 - data/spec/model/validations_spec.rb +42 -0
 - data/spec/model_spec.rb +31 -0
 - data/spec/spec_helper.rb +26 -0
 - data/spec/support/extensions/array.rb +5 -0
 - data/spec/support/extensions/hash.rb +5 -0
 - data/spec/support/macros/her_macros.rb +17 -0
 - data/spec/support/macros/model_macros.rb +29 -0
 - data/spec/support/macros/request_macros.rb +27 -0
 - metadata +280 -0
 
    
        data/lib/her/model.rb
    ADDED
    
    | 
         @@ -0,0 +1,72 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "her/model/base"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "her/model/deprecated_methods"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "her/model/http"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "her/model/attributes"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "her/model/relation"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require "her/model/orm"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require "her/model/parse"
         
     | 
| 
      
 8 
     | 
    
         
            +
            require "her/model/associations"
         
     | 
| 
      
 9 
     | 
    
         
            +
            require "her/model/introspection"
         
     | 
| 
      
 10 
     | 
    
         
            +
            require "her/model/paths"
         
     | 
| 
      
 11 
     | 
    
         
            +
            require "her/model/nested_attributes"
         
     | 
| 
      
 12 
     | 
    
         
            +
            require "active_model"
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            module Her
         
     | 
| 
      
 15 
     | 
    
         
            +
              # This module is the main element of Her. After creating a Her::API object,
         
     | 
| 
      
 16 
     | 
    
         
            +
              # include this module in your models to get a few magic methods defined in them.
         
     | 
| 
      
 17 
     | 
    
         
            +
              #
         
     | 
| 
      
 18 
     | 
    
         
            +
              # @example
         
     | 
| 
      
 19 
     | 
    
         
            +
              #   class User
         
     | 
| 
      
 20 
     | 
    
         
            +
              #     include Her::Model
         
     | 
| 
      
 21 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 22 
     | 
    
         
            +
              #
         
     | 
| 
      
 23 
     | 
    
         
            +
              #   @user = User.new(:name => "Rémi")
         
     | 
| 
      
 24 
     | 
    
         
            +
              #   @user.save
         
     | 
| 
      
 25 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 26 
     | 
    
         
            +
                extend ActiveSupport::Concern
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                # Her modules
         
     | 
| 
      
 29 
     | 
    
         
            +
                include Her::Model::Base
         
     | 
| 
      
 30 
     | 
    
         
            +
                include Her::Model::DeprecatedMethods
         
     | 
| 
      
 31 
     | 
    
         
            +
                include Her::Model::Attributes
         
     | 
| 
      
 32 
     | 
    
         
            +
                include Her::Model::ORM
         
     | 
| 
      
 33 
     | 
    
         
            +
                include Her::Model::HTTP
         
     | 
| 
      
 34 
     | 
    
         
            +
                include Her::Model::Parse
         
     | 
| 
      
 35 
     | 
    
         
            +
                include Her::Model::Introspection
         
     | 
| 
      
 36 
     | 
    
         
            +
                include Her::Model::Paths
         
     | 
| 
      
 37 
     | 
    
         
            +
                include Her::Model::Associations
         
     | 
| 
      
 38 
     | 
    
         
            +
                include Her::Model::NestedAttributes
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                # Supported ActiveModel modules
         
     | 
| 
      
 41 
     | 
    
         
            +
                include ActiveModel::AttributeMethods
         
     | 
| 
      
 42 
     | 
    
         
            +
                include ActiveModel::Validations
         
     | 
| 
      
 43 
     | 
    
         
            +
                include ActiveModel::Validations::Callbacks
         
     | 
| 
      
 44 
     | 
    
         
            +
                include ActiveModel::Conversion
         
     | 
| 
      
 45 
     | 
    
         
            +
                include ActiveModel::Dirty
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                # Class methods
         
     | 
| 
      
 48 
     | 
    
         
            +
                included do
         
     | 
| 
      
 49 
     | 
    
         
            +
                  # Assign the default API
         
     | 
| 
      
 50 
     | 
    
         
            +
                  use_api Her::API.default_api
         
     | 
| 
      
 51 
     | 
    
         
            +
                  method_for :create, :post
         
     | 
| 
      
 52 
     | 
    
         
            +
                  method_for :update, :put
         
     | 
| 
      
 53 
     | 
    
         
            +
                  method_for :find, :get
         
     | 
| 
      
 54 
     | 
    
         
            +
                  method_for :destroy, :delete
         
     | 
| 
      
 55 
     | 
    
         
            +
                  method_for :new, :get
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  # Define the default primary key
         
     | 
| 
      
 58 
     | 
    
         
            +
                  primary_key :id
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  # Define default storage accessors for errors and metadata
         
     | 
| 
      
 61 
     | 
    
         
            +
                  store_response_errors :response_errors
         
     | 
| 
      
 62 
     | 
    
         
            +
                  store_metadata :metadata
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  # Include ActiveModel naming methods
         
     | 
| 
      
 65 
     | 
    
         
            +
                  extend ActiveModel::Translation
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  # Configure ActiveModel callbacks
         
     | 
| 
      
 68 
     | 
    
         
            +
                  extend ActiveModel::Callbacks
         
     | 
| 
      
 69 
     | 
    
         
            +
                  define_model_callbacks :create, :update, :save, :find, :destroy, :initialize
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
              end
         
     | 
| 
      
 72 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,141 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "her/model/associations/association"
         
     | 
| 
      
 2 
     | 
    
         
            +
            require "her/model/associations/association_proxy"
         
     | 
| 
      
 3 
     | 
    
         
            +
            require "her/model/associations/belongs_to_association"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "her/model/associations/has_many_association"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "her/model/associations/has_one_association"
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Her
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 9 
     | 
    
         
            +
                # This module adds associations to models
         
     | 
| 
      
 10 
     | 
    
         
            +
                module Associations
         
     | 
| 
      
 11 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  # Returns true if the model has a association_name association, false otherwise.
         
     | 
| 
      
 14 
     | 
    
         
            +
                  #
         
     | 
| 
      
 15 
     | 
    
         
            +
                  # @private
         
     | 
| 
      
 16 
     | 
    
         
            +
                  def has_association?(association_name)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    associations = self.class.associations.values.flatten.map { |r| r[:name] }
         
     | 
| 
      
 18 
     | 
    
         
            +
                    associations.include?(association_name)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  # Returns the resource/collection corresponding to the association_name association.
         
     | 
| 
      
 22 
     | 
    
         
            +
                  #
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # @private
         
     | 
| 
      
 24 
     | 
    
         
            +
                  def get_association(association_name)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    send(association_name) if has_association?(association_name)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  module ClassMethods
         
     | 
| 
      
 29 
     | 
    
         
            +
                    # Return @_her_associations, lazily initialized with copy of the
         
     | 
| 
      
 30 
     | 
    
         
            +
                    # superclass' her_associations, or an empty hash.
         
     | 
| 
      
 31 
     | 
    
         
            +
                    #
         
     | 
| 
      
 32 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 33 
     | 
    
         
            +
                    def associations
         
     | 
| 
      
 34 
     | 
    
         
            +
                      @_her_associations ||= begin
         
     | 
| 
      
 35 
     | 
    
         
            +
                        superclass.respond_to?(:associations) ? superclass.associations.dup : Hash.new { |h,k| h[k] = [] }
         
     | 
| 
      
 36 
     | 
    
         
            +
                      end
         
     | 
| 
      
 37 
     | 
    
         
            +
                    end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 40 
     | 
    
         
            +
                    def association_names
         
     | 
| 
      
 41 
     | 
    
         
            +
                      associations.inject([]) { |memo, (name, details)| memo << details }.flatten.map { |a| a[:name] }
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 45 
     | 
    
         
            +
                    def association_keys
         
     | 
| 
      
 46 
     | 
    
         
            +
                      associations.inject([]) { |memo, (name, details)| memo << details }.flatten.map { |a| a[:data_key] }
         
     | 
| 
      
 47 
     | 
    
         
            +
                    end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    # Parse associations data after initializing a new object
         
     | 
| 
      
 50 
     | 
    
         
            +
                    #
         
     | 
| 
      
 51 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 52 
     | 
    
         
            +
                    def parse_associations(data)
         
     | 
| 
      
 53 
     | 
    
         
            +
                      associations.each_pair do |type, definitions|
         
     | 
| 
      
 54 
     | 
    
         
            +
                        definitions.each do |association|
         
     | 
| 
      
 55 
     | 
    
         
            +
                          association_class = "her/model/associations/#{type}_association".classify.constantize
         
     | 
| 
      
 56 
     | 
    
         
            +
                          data.merge! association_class.parse(association, self, data)
         
     | 
| 
      
 57 
     | 
    
         
            +
                        end
         
     | 
| 
      
 58 
     | 
    
         
            +
                      end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      data
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                    # Define an *has_many* association.
         
     | 
| 
      
 64 
     | 
    
         
            +
                    #
         
     | 
| 
      
 65 
     | 
    
         
            +
                    # @param [Symbol] name The name of the method added to resources
         
     | 
| 
      
 66 
     | 
    
         
            +
                    # @param [Hash] opts Options
         
     | 
| 
      
 67 
     | 
    
         
            +
                    # @option opts [String] :class_name The name of the class to map objects to
         
     | 
| 
      
 68 
     | 
    
         
            +
                    # @option opts [Symbol] :data_key The attribute where the data is stored
         
     | 
| 
      
 69 
     | 
    
         
            +
                    # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    #
         
     | 
| 
      
 71 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 72 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 73 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 74 
     | 
    
         
            +
                    #     has_many :articles
         
     | 
| 
      
 75 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 76 
     | 
    
         
            +
                    #
         
     | 
| 
      
 77 
     | 
    
         
            +
                    #   class Article
         
     | 
| 
      
 78 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 79 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 80 
     | 
    
         
            +
                    #
         
     | 
| 
      
 81 
     | 
    
         
            +
                    #   @user = User.find(1)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    #   @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
         
     | 
| 
      
 83 
     | 
    
         
            +
                    #   # Fetched via GET "/users/1/articles"
         
     | 
| 
      
 84 
     | 
    
         
            +
                    def has_many(name, opts={})
         
     | 
| 
      
 85 
     | 
    
         
            +
                      Her::Model::Associations::HasManyAssociation.attach(self, name, opts)
         
     | 
| 
      
 86 
     | 
    
         
            +
                    end
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                    # Define an *has_one* association.
         
     | 
| 
      
 89 
     | 
    
         
            +
                    #
         
     | 
| 
      
 90 
     | 
    
         
            +
                    # @param [Symbol] name The name of the method added to resources
         
     | 
| 
      
 91 
     | 
    
         
            +
                    # @param [Hash] opts Options
         
     | 
| 
      
 92 
     | 
    
         
            +
                    # @option opts [String] :class_name The name of the class to map objects to
         
     | 
| 
      
 93 
     | 
    
         
            +
                    # @option opts [Symbol] :data_key The attribute where the data is stored
         
     | 
| 
      
 94 
     | 
    
         
            +
                    # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
         
     | 
| 
      
 95 
     | 
    
         
            +
                    #
         
     | 
| 
      
 96 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 97 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 98 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 99 
     | 
    
         
            +
                    #     has_one :organization
         
     | 
| 
      
 100 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 101 
     | 
    
         
            +
                    #
         
     | 
| 
      
 102 
     | 
    
         
            +
                    #   class Organization
         
     | 
| 
      
 103 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 104 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 105 
     | 
    
         
            +
                    #
         
     | 
| 
      
 106 
     | 
    
         
            +
                    #   @user = User.find(1)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    #   @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
         
     | 
| 
      
 108 
     | 
    
         
            +
                    #   # Fetched via GET "/users/1/organization"
         
     | 
| 
      
 109 
     | 
    
         
            +
                    def has_one(name, opts={})
         
     | 
| 
      
 110 
     | 
    
         
            +
                      Her::Model::Associations::HasOneAssociation.attach(self, name, opts)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    # Define a *belongs_to* association.
         
     | 
| 
      
 114 
     | 
    
         
            +
                    #
         
     | 
| 
      
 115 
     | 
    
         
            +
                    # @param [Symbol] name The name of the method added to resources
         
     | 
| 
      
 116 
     | 
    
         
            +
                    # @param [Hash] opts Options
         
     | 
| 
      
 117 
     | 
    
         
            +
                    # @option opts [String] :class_name The name of the class to map objects to
         
     | 
| 
      
 118 
     | 
    
         
            +
                    # @option opts [Symbol] :data_key The attribute where the data is stored
         
     | 
| 
      
 119 
     | 
    
         
            +
                    # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{class_name}.pluralize/{id}`)
         
     | 
| 
      
 120 
     | 
    
         
            +
                    # @option opts [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    #
         
     | 
| 
      
 122 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 123 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 124 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 125 
     | 
    
         
            +
                    #     belongs_to :team, :class_name => "Group"
         
     | 
| 
      
 126 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 127 
     | 
    
         
            +
                    #
         
     | 
| 
      
 128 
     | 
    
         
            +
                    #   class Group
         
     | 
| 
      
 129 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 130 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 131 
     | 
    
         
            +
                    #
         
     | 
| 
      
 132 
     | 
    
         
            +
                    #   @user = User.find(1) # => #<User(users/1) id=1 team_id=2 name="Tobias">
         
     | 
| 
      
 133 
     | 
    
         
            +
                    #   @user.team # => #<Team(teams/2) id=2 name="Developers">
         
     | 
| 
      
 134 
     | 
    
         
            +
                    #   # Fetched via GET "/teams/2"
         
     | 
| 
      
 135 
     | 
    
         
            +
                    def belongs_to(name, opts={})
         
     | 
| 
      
 136 
     | 
    
         
            +
                      Her::Model::Associations::BelongsToAssociation.attach(self, name, opts)
         
     | 
| 
      
 137 
     | 
    
         
            +
                    end
         
     | 
| 
      
 138 
     | 
    
         
            +
                  end
         
     | 
| 
      
 139 
     | 
    
         
            +
                end
         
     | 
| 
      
 140 
     | 
    
         
            +
              end
         
     | 
| 
      
 141 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,103 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Her
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Associations
         
     | 
| 
      
 4 
     | 
    
         
            +
                  class Association
         
     | 
| 
      
 5 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 6 
     | 
    
         
            +
                    attr_accessor :params
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(parent, opts = {})
         
     | 
| 
      
 10 
     | 
    
         
            +
                      @parent = parent
         
     | 
| 
      
 11 
     | 
    
         
            +
                      @opts = opts
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @params = {}
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                      @klass = @parent.class.her_nearby_class(@opts[:class_name])
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @name = @opts[:name]
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 19 
     | 
    
         
            +
                    def self.proxy(parent, opts = {})
         
     | 
| 
      
 20 
     | 
    
         
            +
                      AssociationProxy.new new(parent, opts)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 24 
     | 
    
         
            +
                    def self.parse_single(association, klass, data)
         
     | 
| 
      
 25 
     | 
    
         
            +
                      data_key = association[:data_key]
         
     | 
| 
      
 26 
     | 
    
         
            +
                      return {} unless data[data_key]
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                      klass = klass.her_nearby_class(association[:class_name])
         
     | 
| 
      
 29 
     | 
    
         
            +
                      if data[data_key].kind_of?(klass)
         
     | 
| 
      
 30 
     | 
    
         
            +
                        { association[:name] => data[data_key] }
         
     | 
| 
      
 31 
     | 
    
         
            +
                      else
         
     | 
| 
      
 32 
     | 
    
         
            +
                        { association[:name] => klass.new(data[data_key]) }
         
     | 
| 
      
 33 
     | 
    
         
            +
                      end
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 37 
     | 
    
         
            +
                    def assign_single_nested_attributes(attributes)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      if @parent.attributes[@name].blank?
         
     | 
| 
      
 39 
     | 
    
         
            +
                        @parent.attributes[@name] = @klass.new(@klass.parse(attributes))
         
     | 
| 
      
 40 
     | 
    
         
            +
                      else
         
     | 
| 
      
 41 
     | 
    
         
            +
                        @parent.attributes[@name].assign_attributes(attributes)
         
     | 
| 
      
 42 
     | 
    
         
            +
                      end
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 46 
     | 
    
         
            +
                    def fetch(opts = {})
         
     | 
| 
      
 47 
     | 
    
         
            +
                      attribute_value = @parent.attributes[@name]
         
     | 
| 
      
 48 
     | 
    
         
            +
                      return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                      return @cached_result unless @params.any? || @cached_result.nil?
         
     | 
| 
      
 51 
     | 
    
         
            +
                      return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                      path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
         
     | 
| 
      
 54 
     | 
    
         
            +
                      @klass.get(path, @params).tap do |result|
         
     | 
| 
      
 55 
     | 
    
         
            +
                        @cached_result = result unless @params.any?
         
     | 
| 
      
 56 
     | 
    
         
            +
                      end
         
     | 
| 
      
 57 
     | 
    
         
            +
                    end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 60 
     | 
    
         
            +
                    def build_association_path(code)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 62 
     | 
    
         
            +
                        instance_exec(&code)
         
     | 
| 
      
 63 
     | 
    
         
            +
                      rescue Her::Errors::PathError
         
     | 
| 
      
 64 
     | 
    
         
            +
                        return nil
         
     | 
| 
      
 65 
     | 
    
         
            +
                      end
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    # Add query parameters to the HTTP request performed to fetch the data
         
     | 
| 
      
 69 
     | 
    
         
            +
                    #
         
     | 
| 
      
 70 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 71 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 72 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 73 
     | 
    
         
            +
                    #     has_many :comments
         
     | 
| 
      
 74 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 75 
     | 
    
         
            +
                    #
         
     | 
| 
      
 76 
     | 
    
         
            +
                    #   user = User.find(1)
         
     | 
| 
      
 77 
     | 
    
         
            +
                    #   user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
         
     | 
| 
      
 78 
     | 
    
         
            +
                    def where(params = {})
         
     | 
| 
      
 79 
     | 
    
         
            +
                      return self if params.blank? && @parent.attributes[@name].blank?
         
     | 
| 
      
 80 
     | 
    
         
            +
                      AssociationProxy.new self.clone.tap { |a| a.params = a.params.merge(params) }
         
     | 
| 
      
 81 
     | 
    
         
            +
                    end
         
     | 
| 
      
 82 
     | 
    
         
            +
                    alias all where
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                    # Fetches the data specified by id
         
     | 
| 
      
 85 
     | 
    
         
            +
                    #
         
     | 
| 
      
 86 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 87 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 88 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 89 
     | 
    
         
            +
                    #     has_many :comments
         
     | 
| 
      
 90 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 91 
     | 
    
         
            +
                    #
         
     | 
| 
      
 92 
     | 
    
         
            +
                    #   user = User.find(1)
         
     | 
| 
      
 93 
     | 
    
         
            +
                    #   user.comments.find(3) # Fetched via GET "/users/1/comments/3
         
     | 
| 
      
 94 
     | 
    
         
            +
                    def find(id)
         
     | 
| 
      
 95 
     | 
    
         
            +
                      return nil if id.blank?
         
     | 
| 
      
 96 
     | 
    
         
            +
                      path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
         
     | 
| 
      
 97 
     | 
    
         
            +
                      @klass.get(path, @params)
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                  end
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,46 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Her
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Associations
         
     | 
| 
      
 4 
     | 
    
         
            +
                  class AssociationProxy < (ActiveSupport.const_defined?('ProxyObject') ? ActiveSupport::ProxyObject : ActiveSupport::BasicObject)
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 7 
     | 
    
         
            +
                    def self.install_proxy_methods(target_name, *names)
         
     | 
| 
      
 8 
     | 
    
         
            +
                      names.each do |name|
         
     | 
| 
      
 9 
     | 
    
         
            +
                        module_eval <<-RUBY, __FILE__, __LINE__ + 1
         
     | 
| 
      
 10 
     | 
    
         
            +
                          def #{name}(*args, &block)
         
     | 
| 
      
 11 
     | 
    
         
            +
                            #{target_name}.#{name}(*args, &block)
         
     | 
| 
      
 12 
     | 
    
         
            +
                          end
         
     | 
| 
      
 13 
     | 
    
         
            +
                        RUBY
         
     | 
| 
      
 14 
     | 
    
         
            +
                      end
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    install_proxy_methods :association,
         
     | 
| 
      
 18 
     | 
    
         
            +
                      :build, :create, :where, :find, :all, :assign_nested_attributes
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 21 
     | 
    
         
            +
                    def initialize(association)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      @_her_association = association
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                    def association
         
     | 
| 
      
 26 
     | 
    
         
            +
                      @_her_association
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 30 
     | 
    
         
            +
                    def method_missing(name, *args, &block)
         
     | 
| 
      
 31 
     | 
    
         
            +
                      if :object_id == name # avoid redefining object_id
         
     | 
| 
      
 32 
     | 
    
         
            +
                        return association.fetch.object_id
         
     | 
| 
      
 33 
     | 
    
         
            +
                      end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                      # create a proxy to the fetched object's method
         
     | 
| 
      
 36 
     | 
    
         
            +
                      metaclass = (class << self; self; end)
         
     | 
| 
      
 37 
     | 
    
         
            +
                      metaclass.install_proxy_methods 'association.fetch', name
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                      # resend message to fetched object
         
     | 
| 
      
 40 
     | 
    
         
            +
                      __send__(name, *args, &block)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,96 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Her
         
     | 
| 
      
 2 
     | 
    
         
            +
              module Model
         
     | 
| 
      
 3 
     | 
    
         
            +
                module Associations
         
     | 
| 
      
 4 
     | 
    
         
            +
                  class BelongsToAssociation < Association
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 7 
     | 
    
         
            +
                    def self.attach(klass, name, opts)
         
     | 
| 
      
 8 
     | 
    
         
            +
                      opts = {
         
     | 
| 
      
 9 
     | 
    
         
            +
                        :class_name => name.to_s.classify,
         
     | 
| 
      
 10 
     | 
    
         
            +
                        :name => name,
         
     | 
| 
      
 11 
     | 
    
         
            +
                        :data_key => name,
         
     | 
| 
      
 12 
     | 
    
         
            +
                        :default => nil,
         
     | 
| 
      
 13 
     | 
    
         
            +
                        :foreign_key => "#{name}_id",
         
     | 
| 
      
 14 
     | 
    
         
            +
                        :path => "/#{name.to_s.pluralize}/:id"
         
     | 
| 
      
 15 
     | 
    
         
            +
                      }.merge(opts)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      klass.associations[:belongs_to] << opts
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                      klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
         
     | 
| 
      
 19 
     | 
    
         
            +
                        def #{name}
         
     | 
| 
      
 20 
     | 
    
         
            +
                          cached_name = :"@_her_association_#{name}"
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                          cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
         
     | 
| 
      
 23 
     | 
    
         
            +
                          cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.proxy(self, #{opts.inspect}))
         
     | 
| 
      
 24 
     | 
    
         
            +
                        end
         
     | 
| 
      
 25 
     | 
    
         
            +
                      RUBY
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 29 
     | 
    
         
            +
                    def self.parse(*args)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      parse_single(*args)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    # Initialize a new object
         
     | 
| 
      
 34 
     | 
    
         
            +
                    #
         
     | 
| 
      
 35 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 36 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 37 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 38 
     | 
    
         
            +
                    #     belongs_to :organization
         
     | 
| 
      
 39 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    #
         
     | 
| 
      
 41 
     | 
    
         
            +
                    #   class Organization
         
     | 
| 
      
 42 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 43 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    #
         
     | 
| 
      
 45 
     | 
    
         
            +
                    #   user = User.find(1)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    #   new_organization = user.organization.build(:name => "Foo Inc.")
         
     | 
| 
      
 47 
     | 
    
         
            +
                    #   new_organization # => #<Organization name="Foo Inc.">
         
     | 
| 
      
 48 
     | 
    
         
            +
                    def build(attributes = {})
         
     | 
| 
      
 49 
     | 
    
         
            +
                      @klass.build(attributes)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    # Create a new object, save it and associate it to the parent
         
     | 
| 
      
 53 
     | 
    
         
            +
                    #
         
     | 
| 
      
 54 
     | 
    
         
            +
                    # @example
         
     | 
| 
      
 55 
     | 
    
         
            +
                    #   class User
         
     | 
| 
      
 56 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 57 
     | 
    
         
            +
                    #     belongs_to :organization
         
     | 
| 
      
 58 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 59 
     | 
    
         
            +
                    #
         
     | 
| 
      
 60 
     | 
    
         
            +
                    #   class Organization
         
     | 
| 
      
 61 
     | 
    
         
            +
                    #     include Her::Model
         
     | 
| 
      
 62 
     | 
    
         
            +
                    #   end
         
     | 
| 
      
 63 
     | 
    
         
            +
                    #
         
     | 
| 
      
 64 
     | 
    
         
            +
                    #   user = User.find(1)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    #   user.organization.create(:name => "Foo Inc.")
         
     | 
| 
      
 66 
     | 
    
         
            +
                    #   user.organization # => #<Organization id=2 name="Foo Inc.">
         
     | 
| 
      
 67 
     | 
    
         
            +
                    def create(attributes = {})
         
     | 
| 
      
 68 
     | 
    
         
            +
                      resource = build(attributes)
         
     | 
| 
      
 69 
     | 
    
         
            +
                      @parent.attributes[@name] = resource if resource.save
         
     | 
| 
      
 70 
     | 
    
         
            +
                      resource
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 74 
     | 
    
         
            +
                    def fetch
         
     | 
| 
      
 75 
     | 
    
         
            +
                      foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
         
     | 
| 
      
 76 
     | 
    
         
            +
                      data_key_value = @parent.attributes[@opts[:data_key].to_sym]
         
     | 
| 
      
 77 
     | 
    
         
            +
                      return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (@parent.persisted? && foreign_key_value.blank? && data_key_value.blank?)
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                      return @cached_result unless @params.any? || @cached_result.nil?
         
     | 
| 
      
 80 
     | 
    
         
            +
                      return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                      path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
         
     | 
| 
      
 83 
     | 
    
         
            +
                      path = build_association_path lambda { @klass.build_request_path(path_params) }
         
     | 
| 
      
 84 
     | 
    
         
            +
                      @klass.get(path, @params).tap do |result|
         
     | 
| 
      
 85 
     | 
    
         
            +
                        @cached_result = result if @params.blank?
         
     | 
| 
      
 86 
     | 
    
         
            +
                      end
         
     | 
| 
      
 87 
     | 
    
         
            +
                    end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                    # @private
         
     | 
| 
      
 90 
     | 
    
         
            +
                    def assign_nested_attributes(attributes)
         
     | 
| 
      
 91 
     | 
    
         
            +
                      assign_single_nested_attributes(attributes)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    end
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
            end
         
     |