Skip to content

Commit

Permalink
feat(search): Add a search action for models
Browse files Browse the repository at this point in the history
  • Loading branch information
paulRbr committed Aug 9, 2014
1 parent 44eec1f commit 04ddc5c
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -10,4 +10,4 @@ require 'coveralls/rake/task'
Coveralls::RakeTask.new
task :test_with_coveralls => [:spec, :features, 'coveralls:push']

task :default => :spec
task :default => :spec
42 changes: 42 additions & 0 deletions lib/yodatra/models_controller.rb
Expand Up @@ -23,6 +23,12 @@ module Yodatra
# If your model is referenced by another model, nested routes are also created for you. And you don't need to worry about the references/joins, they are done automaticly!
# For example, imagine a <b>Team</b> model that has many <b>User</b>s, the following routes will be exposed:
# GET /team/:team_id/users, GET /team/:team_id/users/:id, POST /team/:team_id/users, PUT /team/:team_id/users/:id and DESTROY /team/:team_id/users/:id
#
# _Note_: You can disable any of these five actions by using the `#disable` class method
# and giving in parameters the list of actions you want to disable
# e.g. `disable :read, :read_all, :create, :update, :delete`
#
# _Note2_: You can enable a special "search" action by using the `#enable_search_on` class method
class ModelsController < Sinatra::Base

before do
Expand All @@ -37,6 +43,10 @@ class ModelsController < Sinatra::Base
ALL_ROUTE =
%r{\A/([\w]+?)(?:/([0-9]+)/([\w]+?)){0,1}\Z}

# Search route
SEARCH_ROUTE =
%r{\A/([\w]+?)(?:/([0-9]+)/([\w]+?)){0,1}/search\Z}

READ_ALL = :read_all
get ALL_ROUTE do
retrieve_resources READ_ALL do |resource|
Expand Down Expand Up @@ -118,6 +128,10 @@ def model_name
self.name.split('::').last.gsub(/sController/, '')
end

def model
model_name.constantize
end

# This helper gives the ability to disable default root by specifying
# a list of routes to disable.
# @param *opts list of routes to disable (e.g. :create, :destroy)
Expand All @@ -128,6 +142,30 @@ def disable(*opts)
define_method method, Proc.new {|| true}
end
end

def enable_search_on(*attributes)
self.instance_eval do
get SEARCH_ROUTE do
retrieve_resources '' do |resource|

pass if !involved? || params[:q].blank? || params[:q].size > 100

terms = params[:q].split(/[\+ ]/)
search_terms = []

# Seperate terms to match
terms.each do |term|
attributes.each do |attr|
search_terms << resource.arel_table[attr.to_sym].matches("%#{term}%")
end
end

resource.where(search_terms.reduce(:or)).limit(100).
flatten.as_json(read_scope).to_json
end
end
end
end
end

private
Expand Down Expand Up @@ -181,6 +219,10 @@ def model_name
self.class.model_name
end

def model
self.class.model
end

def disabled? key
method = ((nested? ? 'nested_' : '')+"#{key}_disabled?").to_sym
self.class.method_defined?(method) && self.send(method)
Expand Down
43 changes: 43 additions & 0 deletions spec/active_record/connection_adapters/fake_adapter.rb
@@ -0,0 +1,43 @@
require "active_record"

module ActiveRecord
class Base
def self.fake_connection(config)
ConnectionAdapters::FakeAdapter.new nil, logger
end
end

module ConnectionAdapters
class FakeAdapter < AbstractAdapter
attr_accessor :tables, :primary_keys

@columns = Hash.new { |h,k| h[k] = [] }
class << self
attr_reader :columns
end

def initialize(connection, logger)
super
@tables = []
@primary_keys = {}
@columns = self.class.columns
end

def primary_key(table)
@primary_keys[table]
end

def merge_column(table_name, name, sql_type = nil, options = {})
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
sql_type.to_s,
options[:null])
end

def columns(table_name, message)
@columns[table_name]
end
end
end
end
7 changes: 7 additions & 0 deletions spec/data/ar_model.rb
@@ -0,0 +1,7 @@
# Mock model constructed for the tests
class ArModel < ActiveRecord::Base
end

ActiveRecord::Base.establish_connection(:adapter => 'fake')
ActiveRecord::Base.connection.merge_column('ar_models', :email, :string)
ActiveRecord::Base.connection.merge_column('ar_models', :name, :string)
2 changes: 2 additions & 0 deletions spec/data/ar_models_controller.rb
@@ -0,0 +1,2 @@
class ArModelsController < Yodatra::ModelsController
end
2 changes: 1 addition & 1 deletion spec/data/model.rb
Expand Up @@ -47,4 +47,4 @@ def models
Model
end
def errors; []; end
end
end
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Expand Up @@ -8,13 +8,15 @@

require File.expand_path '../../lib/yodatra.rb', __FILE__
require File.expand_path '../../lib/yodatra/models_controller.rb', __FILE__
require File.expand_path '../data/ar_models_controller.rb', __FILE__

module RSpecMixin
include Rack::Test::Methods
def app
Sinatra.new {
use Yodatra::Base
use Yodatra::ModelsController
use ArModelsController
}
end
end
Expand Down
77 changes: 58 additions & 19 deletions spec/unit/models_controller_spec.rb
@@ -1,5 +1,7 @@
require File.expand_path '../../spec_helper.rb', __FILE__
require File.expand_path '../../active_record/connection_adapters/fake_adapter.rb', __FILE__
require File.expand_path '../../data/model.rb', __FILE__
require File.expand_path '../../data/ar_model.rb', __FILE__

describe 'Model controller' do

Expand All @@ -13,15 +15,15 @@
it 'should have a GET all route' do
get '/models'

last_response.should be_ok
expect(last_response).to be_ok
expect(last_response.body).to eq(Model::ALL.map{|e| {:data => e} }.to_json)
end
end
context 'nested' do
it 'should have a GET all route' do
get '/models/1/models'

last_response.should be_ok
expect(last_response).to be_ok
expect(last_response.body).to eq(Model::ALL.map{|e| {:data => e} }.to_json)
end
end
Expand All @@ -34,15 +36,15 @@ class Yodatra::ModelsController
it 'should fail with no route available' do
get '/models'

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
end
describe 'getting an specific Model instance' do
it 'should have a GET one route' do
get '/models/2'

last_response.should be_ok
expect(last_response).to be_ok
expect(last_response.body).to eq({ :data => 'c'}.to_json)
end
context 'forced GET one route disabled' do
Expand All @@ -54,7 +56,7 @@ class Yodatra::ModelsController
it 'should fail with no route available' do
get '/models/1'

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
end
Expand All @@ -65,7 +67,7 @@ class Yodatra::ModelsController
post '/models', {:data => 'd'}
}.to change(Model::ALL, :length).by(1)

last_response.should be_ok
expect(last_response).to be_ok
end
end
context 'with incorrect params' do
Expand All @@ -74,7 +76,7 @@ class Yodatra::ModelsController
post '/models', {}
}.to change(Model::ALL, :length).by(0)

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(@errors.to_json)
end
end
Expand All @@ -87,7 +89,7 @@ class Yodatra::ModelsController
it 'should fail with no route available' do
post '/models', {:data => 'd'}

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
end
Expand All @@ -98,7 +100,7 @@ class Yodatra::ModelsController
put '/models/21', {:data => 'e'}
}.to change(Model::ALL, :length).by(0)

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(['record not found'].to_json)
end
end
Expand All @@ -109,7 +111,7 @@ class Yodatra::ModelsController
put '/models/2', {:data => 'e'}
}.to change(Model::ALL, :length).by(0)

last_response.should be_ok
expect(last_response).to be_ok
expect(last_response.body).to eq({ :data => 'e'}.to_json)
expect(Model.find(2).to_json).to eq({ :data => 'e'}.to_json)
end
Expand All @@ -120,7 +122,7 @@ class Yodatra::ModelsController
put '/models/2', {:data => 321}
}.to change(Model::ALL, :length).by(0)

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(@errors.to_json)
end
end
Expand All @@ -133,7 +135,7 @@ class Yodatra::ModelsController
it 'should fail with no route available' do
put '/models', {:data => 'd'}

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
end
Expand All @@ -145,7 +147,7 @@ class Yodatra::ModelsController
delete '/models/1'
}.to change(Model::ALL, :length).by(-1)

last_response.should be_ok
expect(last_response).to be_ok
end
end
context 'targeting an existing instance but deletion fails' do
Expand All @@ -157,7 +159,7 @@ class Yodatra::ModelsController
delete '/models/1/models/1'
}.to change(Model::ALL, :length).by(0)

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(@errors.to_json)
end
end
Expand All @@ -167,7 +169,7 @@ class Yodatra::ModelsController
delete '/models/6'
}.to change(Model::ALL, :length).by(0)

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
context 'when the deletion route is disabled' do
Expand All @@ -179,7 +181,7 @@ class Yodatra::ModelsController
it 'should fail with no route available' do
delete '/models/2'

last_response.should_not be_ok
expect(last_response).to_not be_ok
end
end
end
Expand All @@ -196,20 +198,57 @@ class Yodatra::ModelsController
it 'fails with a record not found message' do
get '/modeels/1/models'

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(['record not found'].to_json)
end
end
context 'with non existant parent model' do
it 'fails with a record not found message' do
get '/models/123/models'

last_response.should_not be_ok
expect(last_response).to_not be_ok
expect(last_response.body).to eq(['record not found'].to_json)
end
end
end

end

end
describe 'Model search' do
describe 'when the search feature is not enabled (default)' do
it 'fails with no route found' do
get '/ar_models/search?q=john4'

expect(last_response).to_not be_ok
end
end

describe 'when the search is enabled of the models controller' do
before do
class ArModelsController
enable_search_on :email
end

@search_term = 'john4'

expect_arel_matches = [ArModel.arel_table[:email].matches("%#{@search_term}%")]
expect(ArModel).to receive(:where).with(expect_arel_matches.reduce(:or)) do |arg|
ActiveRecord::Relation.new ArModel, 'ar_models'
end
allow_any_instance_of(ActiveRecord::Relation).to receive(:limit) do |arg|
FakeModel = Struct.new(:name, :email)
@fake_instance = FakeModel.new('john', 'john4@swcc.com')
[FakeModel.new('john', 'john4@swcc.com')]
end
end

it 'should search in the list of available models' do
get "/ar_models/search?q=#{@search_term}"

expect(last_response).to be_ok
expect(last_response.body).to eq([@fake_instance.as_json({})].to_json)
end
end
end

end

0 comments on commit 04ddc5c

Please sign in to comment.