Skip to content

Commit

Permalink
Add support form custom primary keys
Browse files Browse the repository at this point in the history
  • Loading branch information
mesozoic authored and remi committed Apr 6, 2013
1 parent 058a7b3 commit a961557
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 17 deletions.
3 changes: 3 additions & 0 deletions lib/her/model.rb
Expand Up @@ -53,6 +53,9 @@ module Model
# Assign the default API
uses_api Her::API.default_api

# Define the default primary key
primary_key :id

# Configure ActiveModel callbacks
extend ActiveModel::Callbacks
define_model_callbacks :create, :update, :save, :find, :destroy
Expand Down
6 changes: 3 additions & 3 deletions lib/her/model/associations.rb
Expand Up @@ -94,7 +94,7 @@ def has_many(name, attrs={})

if @attributes[name].blank? || method_attrs.any?
path = begin
self.class.build_request_path(@attributes.merge(method_attrs))
request_path(method_attrs)
rescue Her::Errors::PathError
return nil
end
Expand Down Expand Up @@ -147,7 +147,7 @@ def has_one(name, attrs={})

if @attributes[name].blank? || method_attrs.any?
path = begin
self.class.build_request_path(@attributes.merge(method_attrs))
request_path(method_attrs)
rescue Her::Errors::PathError
return nil
end
Expand Down Expand Up @@ -195,7 +195,7 @@ def belongs_to(name, attrs={})

if @attributes[name].blank? || method_attrs.any?
path = begin
klass.build_request_path(@attributes.merge(method_attrs.merge(:id => @attributes[attrs[:foreign_key].to_sym])))
klass.build_request_path(@attributes.merge(method_attrs.merge(klass.primary_key => @attributes[attrs[:foreign_key].to_sym])))
rescue Her::Errors::PathError
return nil
end
Expand Down
4 changes: 2 additions & 2 deletions lib/her/model/attributes.rb
Expand Up @@ -103,10 +103,10 @@ def get_attribute(attribute_name)
end
alias :get_data :get_attribute

# Override the method to prevent from returning the object ID (in ruby-1.8.7)
# Override the method to prevent from returning the object ID
# @private
def id
attributes[:id] || super
attributes[self.class.primary_key] || super
end

# Return `true` if the other object is also a Her::Model and has matching data
Expand Down
16 changes: 8 additions & 8 deletions lib/her/model/orm.rb
Expand Up @@ -6,7 +6,7 @@ module ORM

# Return `true` if a resource was not saved yet
def new?
attributes[:id].nil?
attributes[self.class.primary_key].nil?
end

# Return `true` if a resource is not `#new?`
Expand Down Expand Up @@ -36,12 +36,12 @@ def save
params = to_params
resource = self

if attributes[:id]
callback = :update
method = :put
else
if new?
callback = :create
method = :post
else
callback = :update
method = :put
end

run_callbacks callback do
Expand Down Expand Up @@ -93,7 +93,7 @@ def find(*ids)
params = ids.last.is_a?(Hash) ? ids.pop : {}
results = ids.flatten.compact.uniq.map do |id|
resource = nil
request(params.merge(:_method => :get, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data, response|
request(params.merge(:_method => :get, :_path => "#{build_request_path(params.merge(primary_key => id))}")) do |parsed_data, response|
if response.success?
resource = new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
resource.run_callbacks :find
Expand Down Expand Up @@ -151,7 +151,7 @@ def create(params={})
# @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
# # Called via PUT "/users/1"
def save_existing(id, params)
resource = new(params.merge(:id => id))
resource = new(params.merge(primary_key => id))
resource.save
resource
end
Expand All @@ -162,7 +162,7 @@ def save_existing(id, params)
# User.destroy_existing(1)
# # Called via DELETE "/users/1"
def destroy_existing(id, params={})
request(params.merge(:_method => :delete, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data, response|
request(params.merge(:_method => :delete, :_path => "#{build_request_path(params.merge(primary_key => id))}")) do |parsed_data, response|
new(parse(parsed_data[:data]).merge(:_destroyed => true))
end
end
Expand Down
47 changes: 44 additions & 3 deletions lib/her/model/paths.rb
Expand Up @@ -11,12 +11,29 @@ module Paths
# end
#
# User.find(1) # Fetched via GET /utilisateurs/1
def request_path
self.class.build_request_path(attributes.dup)
#
# @param [Hash] params An optional set of additional parameters for
# path construction. These will not override attributes of the resource.
def request_path(params = {})
self.class.build_request_path(params.merge(attributes.dup))
end

module ClassMethods

# Define the primary key field that will be used to find and save records
#
# @example
# class User
# include Her::Model
# primary_key 'UserId'
# end
#
# @param [Symbol] field
def primary_key(field = nil)
return @her_primary_key if field.nil?
@her_primary_key = field.to_sym
end

# Defines a custom collection path for the resource
#
# @example
Expand All @@ -41,6 +58,22 @@ def collection_path(path=nil)
# include Her::Model
# resource_path "/users/:id"
# end
#
# Note that, if used in combination with resource_path, you may specify
# either the real primary key or the string ':id'. For example:
#
# @example
# class User
# include Her::Model
# primary_key 'user_id'
#
# # This works because we'll have a user_id attribute
# resource_path '/users/:user_id'
#
# # This works because we replace :id with :user_id
# resource_path '/users/:id'
# end
#
def resource_path(path=nil)
@_her_resource_path ||= begin
superclass.resource_path.dup if superclass.respond_to?(:resource_path)
Expand All @@ -62,7 +95,15 @@ def resource_path(path=nil)
def build_request_path(path=nil, parameters={})
unless path.is_a?(String)
parameters = path || {}
path = parameters.include?(:id) && !parameters[:id].nil? ? resource_path : collection_path
path =
if parameters.include?(primary_key) && parameters[primary_key]
resource_path.dup
else
collection_path.dup
end

# Replace :id with our actual primary key
path.gsub!(/(\A|\/):id(\Z|\/)/, "\\1:#{primary_key}\\2")
end

path.gsub(/:([\w_]+)/) do
Expand Down
16 changes: 15 additions & 1 deletion spec/model/orm_spec.rb
Expand Up @@ -11,7 +11,8 @@
builder.adapter :test do |stub|
stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke" }.to_json] }
stub.get("/users") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
stub.get("/admin_users") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
stub.get("/admin_users") { |env| [200, {}, [{ :admin_id => 1, :name => "Tobias Fünke" }, { :admin_id => 2, :name => "Lindsay Fünke" }].to_json] }
stub.get("/admin_users/1") { |env| [200, {}, { :admin_id => 1, :name => "Tobias Fünke" }.to_json] }
end
end

Expand All @@ -21,13 +22,18 @@

spawn_model "Foo::AdminUser" do
uses_api api
primary_key :admin_id
end
end

it "maps a single resource to a Ruby object" do
@user = Foo::User.find(1)
@user.id.should == 1
@user.name.should == "Tobias Fünke"

@admin = Foo::AdminUser.find(1)
@admin.id.should == 1
@admin.name.should == "Tobias Fünke"
end

it "maps a collection of resources to an array of Ruby objects" do
Expand All @@ -48,6 +54,14 @@
@existing_user = Foo::User.find(1)
@existing_user.new?.should be_false
end

it 'handles new resource with custom primary key' do
@new_user = Foo::AdminUser.new(:fullname => 'Lindsay Fünke', :id => -1)
@new_user.should be_new

@existing_user = Foo::AdminUser.find(1)
@existing_user.should_not be_new
end
end

context "mapping data, metadata and error data to Ruby objects" do
Expand Down
26 changes: 26 additions & 0 deletions spec/model/paths_spec.rb
Expand Up @@ -137,6 +137,32 @@
end
end
end

context 'custom primary key' do
before do
spawn_model 'User' do
primary_key 'UserId'
resource_path 'users/:UserId'
end

spawn_model 'Customer' do
primary_key :customer_id
resource_path 'customers/:id'
end
end

describe '#build_request_path' do
it 'uses the correct primary key attribute' do
User.build_request_path(:UserId => 'foo').should == 'users/foo'
User.build_request_path(:id => 'foo').should == 'users'
end

it 'replaces :id with the appropriate primary key' do
Customer.build_request_path(:customer_id => 'joe').should == 'customers/joe'
Customer.build_request_path(:id => 'joe').should == 'customers'
end
end
end
end

context "making subdomain HTTP requests" do
Expand Down

0 comments on commit a961557

Please sign in to comment.