diff --git a/lib/her.rb b/lib/her.rb index b8852091..a3ae78b3 100644 --- a/lib/her.rb +++ b/lib/her.rb @@ -9,6 +9,7 @@ require "her/api" require "her/middleware" require "her/errors" +require "her/collection" module Her end diff --git a/lib/her/collection.rb b/lib/her/collection.rb new file mode 100644 index 00000000..fccdee6e --- /dev/null +++ b/lib/her/collection.rb @@ -0,0 +1,12 @@ +module Her + class Collection < ::Array + attr_reader :metadata, :errors + + # @private + def initialize(items=[], metadata={}, errors=[]) # {{{ + super(items) + @metadata = metadata || {} + @errors = errors || [] + end # }}} + end +end diff --git a/lib/her/model/http.rb b/lib/her/model/http.rb index 9862b5f9..bdcaf0f9 100644 --- a/lib/her/model/http.rb +++ b/lib/her/model/http.rb @@ -26,9 +26,9 @@ def get(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) get_raw(path, attrs) do |parsed_data| if parsed_data[:data].is_a?(Array) - new_collection(parsed_data[:data]) + new_collection(parsed_data) else - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end end # }}} @@ -43,7 +43,7 @@ def get_raw(path, attrs={}, &block) # {{{ def get_collection(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) get_raw(path, attrs) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -51,7 +51,7 @@ def get_collection(path, attrs={}) # {{{ def get_resource(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) get_raw(path, attrs) do |parsed_data| - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end # }}} @@ -60,9 +60,9 @@ def post(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) post_raw(path, attrs) do |parsed_data| if parsed_data[:data].is_a?(Array) - new_collection(parsed_data[:data]) + new_collection(parsed_data) else - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end end # }}} @@ -77,7 +77,7 @@ def post_raw(path, attrs={}, &block) # {{{ def post_collection(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) post_raw(path, attrs) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -94,9 +94,9 @@ def put(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) put_raw(path, attrs) do |parsed_data| if parsed_data[:data].is_a?(Array) - new_collection(parsed_data[:data]) + new_collection(parsed_data) else - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end end # }}} @@ -111,7 +111,7 @@ def put_raw(path, attrs={}, &block) # {{{ def put_collection(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) put_raw(path, attrs) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -119,7 +119,7 @@ def put_collection(path, attrs={}) # {{{ def put_resource(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) put_raw(path, attrs) do |parsed_data| - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end # }}} @@ -128,9 +128,9 @@ def patch(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) patch_raw(path, attrs) do |parsed_data| if parsed_data[:data].is_a?(Array) - new_collection(parsed_data[:data]) + new_collection(parsed_data) else - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end end # }}} @@ -145,7 +145,7 @@ def patch_raw(path, attrs={}, &block) # {{{ def patch_collection(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) patch_raw(path, attrs) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -153,7 +153,7 @@ def patch_collection(path, attrs={}) # {{{ def patch_resource(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) patch_raw(path, attrs) do |parsed_data| - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end # }}} @@ -162,9 +162,9 @@ def delete(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) delete_raw(path, attrs) do |parsed_data| if parsed_data[:data].is_a?(Array) - new_collection(parsed_data[:data]) + new_collection(parsed_data) else - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end end # }}} @@ -179,7 +179,7 @@ def delete_raw(path, attrs={}, &block) # {{{ def delete_collection(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) delete_raw(path, attrs) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -187,7 +187,7 @@ def delete_collection(path, attrs={}) # {{{ def delete_resource(path, attrs={}) # {{{ path = "#{build_request_path(attrs)}/#{path}" if path.is_a?(Symbol) delete_raw(path, attrs) do |parsed_data| - new(parsed_data[:data]) + new(parsed_data[:data].merge :_metadata => parsed_data[:data], :_errors => parsed_data[:errors]) end end # }}} diff --git a/lib/her/model/orm.rb b/lib/her/model/orm.rb index 5b0a4d1c..ffd6f0cc 100644 --- a/lib/her/model/orm.rb +++ b/lib/her/model/orm.rb @@ -2,11 +2,15 @@ module Her module Model # This module adds ORM-like capabilities to the model module ORM + attr_reader :metadata, :errors + # Initialize a new object with data received from an HTTP request # @private - def initialize(single_data={}) # {{{ + def initialize(data={}) # {{{ @data = {} - cleaned_data = single_data.inject({}) do |memo, item| + @metadata = data.delete(:_metadata) || {} + @errors = data.delete(:_errors) || {} + cleaned_data = data.inject({}) do |memo, item| key, value = item send "#{key}=".to_sym, value unless value.nil? respond_to?("#{key}=") ? memo : memo.merge({ key => value }) @@ -16,8 +20,11 @@ def initialize(single_data={}) # {{{ # Initialize a collection of resources # @private - def self.initialize_collection(name, collection_data) # {{{ - collection_data.map { |item_data| Object.const_get(name.to_s.classify).new(item_data) } + def self.initialize_collection(name, parsed_data={}) # {{{ + collection_data = parsed_data[:data].map do |item_data| + Object.const_get(name.to_s.classify).new(item_data) + end + Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors]) end # }}} # Handles missing methods by routing them through @data @@ -45,9 +52,9 @@ def id # {{{ # Initialize a collection of resources with raw data from an HTTP request # - # @param [Array] collection_data An array of model hashes - def new_collection(collection_data) # {{{ - Her::Model::ORM.initialize_collection(self.to_s.underscore, collection_data) + # @param [Array] parsed_data + def new_collection(parsed_data) # {{{ + Her::Model::ORM.initialize_collection(self.to_s.underscore, parsed_data) end # }}} # Return `true` if a resource was not saved yet @@ -55,6 +62,16 @@ def new? # {{{ !@data.include?(:id) end # }}} + # Return `true` if a resource does not contain errors + def valid? # {{{ + @errors.empty? + end # }}} + + # Return `true` if a resource contains errors + def invalid? # {{{ + @errors.any? + end # }}} + # Fetch a specific resource based on an ID # # @example @@ -73,7 +90,7 @@ def find(id, params={}) # {{{ # # Fetched via GET "/users" def all(params={}) # {{{ request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data| - new_collection(parsed_data[:data]) + new_collection(parsed_data) end end # }}} @@ -89,6 +106,8 @@ def create(params={}) # {{{ request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data| resource.instance_eval do @data = parsed_data[:data] + @metadata = parsed_data[:metadata] + @errors = parsed_data[:errors] end end end @@ -133,6 +152,8 @@ def save # {{{ self.class.wrap_in_hooks(resource, *hooks) do |resource, klass| klass.request(params.merge(:_method => method, :_path => "#{request_path}")) do |parsed_data| @data = parsed_data[:data] + @metadata = parsed_data[:metadata] + @errors = parsed_data[:errors] end end self @@ -150,6 +171,8 @@ def destroy # {{{ self.class.wrap_in_hooks(resource, :destroy) do |resource, klass| klass.request(params.merge(:_method => :delete, :_path => "#{request_path}")) do |parsed_data| @data = parsed_data[:data] + @metadata = parsed_data[:metadata] + @errors = parsed_data[:errors] end end self diff --git a/lib/her/model/relationships.rb b/lib/her/model/relationships.rb index 25cc438c..3a2bb1f5 100644 --- a/lib/her/model/relationships.rb +++ b/lib/her/model/relationships.rb @@ -19,7 +19,7 @@ def parse_relationships(data) # {{{ next if !data.include?(name) or data[name].nil? data[name] = case type when :has_many - Her::Model::ORM.initialize_collection(class_name, data[name]) + Her::Model::ORM.initialize_collection(class_name, :data => data[name]) when :has_one, :belongs_to Object.const_get(class_name).new(data[name]) else diff --git a/spec/model/orm_spec.rb b/spec/model/orm_spec.rb index f215e957..adb9f6bf 100644 --- a/spec/model/orm_spec.rb +++ b/spec/model/orm_spec.rb @@ -62,6 +62,45 @@ end# }}} end + context "mapping data, metadata and error data to Ruby objects" do + before do # {{{ + api = Her::API.new + api.setup :url => "https://api.example.com" do |builder| + builder.use Her::Middleware::SecondLevelParseJSON + builder.use Faraday::Request::UrlEncoded + builder.adapter :test do |stub| + stub.get("/users") { |env| [200, {}, { :data => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], :metadata => { :total_pages => 10, :next_page => 2 }, :errors => ["Oh", "My", "God"] }.to_json] } + stub.post("/users") { |env| [200, {}, { :data => { :name => "George Michael Bluth" }, :metadata => { :foo => :bar }, :errors => ["Yes", "Sir"] }.to_json] } + end + end + + spawn_model :User do + uses_api api + end + end # }}} + + it "handles metadata on a collection" do # {{{ + @users = User.all + @users.metadata[:total_pages].should == 10 + end # }}} + + it "handles error data on a collection" do # {{{ + @users = User.all + @users.errors.length.should == 3 + end # }}} + + it "handles metadata on a resource" do # {{{ + @user = User.create(:name => "George Michael Bluth") + @user.metadata[:foo].should == "bar" + end # }}} + + it "handles error data on a resource" do # {{{ + @user = User.create(:name => "George Michael Bluth") + @user.errors.should == ["Yes", "Sir"] + @user.should be_invalid + end # }}} + end + context "defining custom getters and setters" do before do # {{{ api = Her::API.new