bradphelan-sinatras-hat 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENSE +22 -0
- data/README.md +235 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/bradphelan-sinatras-hat.gemspec +145 -0
- data/ci.rb +9 -0
- data/example/app-with-auth.rb +14 -0
- data/example/app-with-cache.rb +30 -0
- data/example/app.rb +23 -0
- data/example/lib/comment.rb +13 -0
- data/example/lib/common.rb +19 -0
- data/example/lib/post.rb +16 -0
- data/example/simple-app.rb +4 -0
- data/example/views/comments/index.erb +6 -0
- data/example/views/comments/show.erb +1 -0
- data/example/views/posts/index.erb +8 -0
- data/example/views/posts/new.erb +11 -0
- data/example/views/posts/show.erb +28 -0
- data/features/authenticated.feature +12 -0
- data/features/create.feature +16 -0
- data/features/destroy.feature +18 -0
- data/features/edit.feature +17 -0
- data/features/formats.feature +19 -0
- data/features/headers.feature +28 -0
- data/features/index.feature +23 -0
- data/features/layouts.feature +11 -0
- data/features/nested.feature +20 -0
- data/features/new.feature +20 -0
- data/features/only.feature +13 -0
- data/features/show.feature +31 -0
- data/features/steps/authenticated_steps.rb +10 -0
- data/features/steps/common_steps.rb +77 -0
- data/features/steps/create_steps.rb +21 -0
- data/features/steps/destroy_steps.rb +16 -0
- data/features/steps/edit_steps.rb +7 -0
- data/features/steps/format_steps.rb +11 -0
- data/features/steps/header_steps.rb +7 -0
- data/features/steps/index_steps.rb +26 -0
- data/features/steps/nested_steps.rb +11 -0
- data/features/steps/new_steps.rb +15 -0
- data/features/steps/only_steps.rb +10 -0
- data/features/steps/show_steps.rb +24 -0
- data/features/steps/update_steps.rb +22 -0
- data/features/support/env.rb +17 -0
- data/features/support/views/comments/index.erb +5 -0
- data/features/support/views/layout.erb +9 -0
- data/features/support/views/people/edit.erb +1 -0
- data/features/support/views/people/index.erb +1 -0
- data/features/support/views/people/layout.erb +9 -0
- data/features/support/views/people/new.erb +1 -0
- data/features/support/views/people/show.erb +1 -0
- data/features/update.feature +25 -0
- data/lib/core_ext/array.rb +5 -0
- data/lib/core_ext/hash.rb +23 -0
- data/lib/core_ext/module.rb +14 -0
- data/lib/core_ext/object.rb +45 -0
- data/lib/sinatras-hat.rb +22 -0
- data/lib/sinatras-hat/actions.rb +81 -0
- data/lib/sinatras-hat/authentication.rb +55 -0
- data/lib/sinatras-hat/extendor.rb +24 -0
- data/lib/sinatras-hat/hash_mutator.rb +18 -0
- data/lib/sinatras-hat/logger.rb +36 -0
- data/lib/sinatras-hat/maker.rb +187 -0
- data/lib/sinatras-hat/model.rb +110 -0
- data/lib/sinatras-hat/resource.rb +57 -0
- data/lib/sinatras-hat/responder.rb +106 -0
- data/lib/sinatras-hat/response.rb +60 -0
- data/lib/sinatras-hat/router.rb +46 -0
- data/sinatras-hat.gemspec +34 -0
- data/spec/actions/create_spec.rb +68 -0
- data/spec/actions/destroy_spec.rb +58 -0
- data/spec/actions/edit_spec.rb +52 -0
- data/spec/actions/index_spec.rb +72 -0
- data/spec/actions/new_spec.rb +39 -0
- data/spec/actions/show_spec.rb +85 -0
- data/spec/actions/update_spec.rb +83 -0
- data/spec/extendor_spec.rb +78 -0
- data/spec/fixtures/views/articles/edit.erb +1 -0
- data/spec/fixtures/views/articles/index.erb +1 -0
- data/spec/fixtures/views/articles/new.erb +1 -0
- data/spec/fixtures/views/articles/show.erb +1 -0
- data/spec/hash_mutator_spec.rb +23 -0
- data/spec/maker_spec.rb +411 -0
- data/spec/model_spec.rb +152 -0
- data/spec/resource_spec.rb +74 -0
- data/spec/responder_spec.rb +139 -0
- data/spec/response_spec.rb +120 -0
- data/spec/router_spec.rb +105 -0
- data/spec/spec_helper.rb +80 -0
- metadata +161 -0
data/lib/sinatras-hat.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'extlib'
|
6
|
+
|
7
|
+
require 'core_ext/array'
|
8
|
+
require 'core_ext/hash'
|
9
|
+
require 'core_ext/object'
|
10
|
+
require 'core_ext/module'
|
11
|
+
|
12
|
+
require 'sinatras-hat/logger'
|
13
|
+
require 'sinatras-hat/extendor'
|
14
|
+
require 'sinatras-hat/authentication'
|
15
|
+
require 'sinatras-hat/hash_mutator'
|
16
|
+
require 'sinatras-hat/resource'
|
17
|
+
require 'sinatras-hat/response'
|
18
|
+
require 'sinatras-hat/responder'
|
19
|
+
require 'sinatras-hat/model'
|
20
|
+
require 'sinatras-hat/router'
|
21
|
+
require 'sinatras-hat/actions'
|
22
|
+
require 'sinatras-hat/maker'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# Contains all of the actions that Sinatra's Hat supports.
|
4
|
+
# Each action states a name, a path, optionally, the HTTP
|
5
|
+
# verb, then a block which takes a request object, optionally
|
6
|
+
# loads data using the :finder or :record options, then
|
7
|
+
# responds, based on whether or not the action was a success
|
8
|
+
#
|
9
|
+
# NOTE: only the :create action renders a different :failure
|
10
|
+
module Actions
|
11
|
+
def self.included(map)
|
12
|
+
map.action :destroy, '/:id', :verb => :delete do |request|
|
13
|
+
record = model.find(request.params) || request.not_found
|
14
|
+
record.destroy
|
15
|
+
responder.success(:destroy, request, record)
|
16
|
+
end
|
17
|
+
|
18
|
+
map.action :new, '/new' do |request|
|
19
|
+
new_record = model.new(request.params)
|
20
|
+
responder.success(:new, request, new_record)
|
21
|
+
end
|
22
|
+
|
23
|
+
map.action :update, '/:id', :verb => :put do |request|
|
24
|
+
record = model.update(request.params) || request.not_found
|
25
|
+
result = record.save ? :success : :failure
|
26
|
+
responder.send(result, :update, request, record)
|
27
|
+
end
|
28
|
+
|
29
|
+
map.action :edit, '/:id/edit' do |request|
|
30
|
+
record = model.find(request.params) || request.not_found
|
31
|
+
responder.success(:edit, request, record)
|
32
|
+
end
|
33
|
+
|
34
|
+
map.action :show, '/:id' do |request|
|
35
|
+
record = model.find(request.params) || request.not_found
|
36
|
+
set_cache_headers(request, record) unless protected?(:show)
|
37
|
+
responder.success(:show, request, record)
|
38
|
+
end
|
39
|
+
|
40
|
+
map.action :create, '/', :verb => :post do |request|
|
41
|
+
record = model.new(request.params)
|
42
|
+
if record
|
43
|
+
puts "RECORD #{record.to_csv}"
|
44
|
+
result = record.save ? :success : :failure
|
45
|
+
puts "RESULT #{result}"
|
46
|
+
responder.send(result, :create, request, record)
|
47
|
+
else
|
48
|
+
responder.send(:failure, request, request.not_found)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
map.action :index, '/' do |request|
|
54
|
+
records = model.all(request.params) || request.not_found
|
55
|
+
set_cache_headers(request, records) unless protected?(:index)
|
56
|
+
responder.success(:index, request, records)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_cache_headers(request, data)
|
62
|
+
|
63
|
+
set_etag(request, data)
|
64
|
+
set_last_modified(request, data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_etag(request, data)
|
68
|
+
record = model.find_last_modified(Array(data))
|
69
|
+
return unless record.respond_to?(:updated_at)
|
70
|
+
request.etag("#{record.id}-#{record.updated_at}-#{data.is_a?(Array)}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_last_modified(request, data)
|
74
|
+
record = model.find_last_modified(Array(data))
|
75
|
+
return unless record.respond_to?(:updated_at)
|
76
|
+
request.last_modified(record.updated_at)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# from http://www.gittr.com/index.php/archive/sinatra-basic-authentication-selectively-applied
|
2
|
+
# adapted by pat nakajima for sinatra's hat
|
3
|
+
module Sinatra
|
4
|
+
module Authorization
|
5
|
+
class ProtectedAction
|
6
|
+
attr_reader :credentials, :request, :block
|
7
|
+
|
8
|
+
def initialize(request, credentials={}, &block)
|
9
|
+
@credentials, @request, @block = credentials, request, block
|
10
|
+
end
|
11
|
+
|
12
|
+
def check!
|
13
|
+
unauthorized! unless auth.provided?
|
14
|
+
bad_request! unless auth.basic?
|
15
|
+
unauthorized! unless authorize(*auth.credentials)
|
16
|
+
end
|
17
|
+
|
18
|
+
def remote_user
|
19
|
+
auth.username
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def authorize(username, password)
|
25
|
+
block.call(username, password)
|
26
|
+
end
|
27
|
+
|
28
|
+
def unauthorized!
|
29
|
+
request.response.headers['WWW-Authenticate'] = %(Basic realm="#{credentials[:realm]}")
|
30
|
+
throw :halt, [ 401, 'Authorization Required' ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def bad_request!
|
34
|
+
throw :halt, [ 400, 'Bad Request' ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def auth
|
38
|
+
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module Helpers
|
43
|
+
def protect!(request)
|
44
|
+
return if authorized?(request)
|
45
|
+
guard = ProtectedAction.new(request, credentials, &authenticator)
|
46
|
+
guard.check!
|
47
|
+
request.env['REMOTE_USER'] = guard.remote_user
|
48
|
+
end
|
49
|
+
|
50
|
+
def authorized?(request)
|
51
|
+
request.env['REMOTE_USER']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# This module gives both Sinatra::Base and Sinatra::Hat::Maker
|
4
|
+
# the #mount method, which is used to mount resources. When
|
5
|
+
# mount is called in an instance of Maker, it sets the new
|
6
|
+
# instance's parent.
|
7
|
+
module Extendor
|
8
|
+
def mount(klass, options={}, &block)
|
9
|
+
unless kind_of?(Sinatra::Hat::Maker)
|
10
|
+
use Rack::MethodOverride
|
11
|
+
end
|
12
|
+
|
13
|
+
Maker.new(klass, options).tap do |maker|
|
14
|
+
maker.parent = self if kind_of?(Sinatra::Hat::Maker)
|
15
|
+
maker.setup(@app || self)
|
16
|
+
maker.instance_eval(&block) if block_given?
|
17
|
+
maker.generate_routes!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Sinatra::Base.extend(Sinatra::Hat::Extendor)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# Used for specifying custom responses using a corny DSL.
|
4
|
+
class HashMutator
|
5
|
+
def initialize(hash)
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def success(&block)
|
10
|
+
@hash[:success] = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def failure(&block)
|
14
|
+
@hash[:failure] = block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# TODO This needs to be using Rack::CommonLogger
|
4
|
+
class Logger
|
5
|
+
def initialize(maker)
|
6
|
+
@maker = maker
|
7
|
+
end
|
8
|
+
|
9
|
+
def info(msg)
|
10
|
+
say msg
|
11
|
+
end
|
12
|
+
|
13
|
+
def debug(msg)
|
14
|
+
say msg
|
15
|
+
end
|
16
|
+
|
17
|
+
def warn(msg)
|
18
|
+
say msg
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(msg)
|
22
|
+
say msg
|
23
|
+
end
|
24
|
+
|
25
|
+
def fatal(msg)
|
26
|
+
say msg
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def say(msg)
|
32
|
+
puts msg if @maker.app and @maker.app.logging
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# This is where it all comes together
|
4
|
+
class Maker
|
5
|
+
include Sinatra::Hat::Extendor
|
6
|
+
include Sinatra::Authorization::Helpers
|
7
|
+
|
8
|
+
attr_reader :klass, :app
|
9
|
+
|
10
|
+
def self.actions
|
11
|
+
@actions ||= { }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.action(name, path, options={}, &block)
|
15
|
+
verb = options[:verb] || :get
|
16
|
+
Router.cache << [verb, name, path]
|
17
|
+
actions[name] = { :path => path, :verb => verb, :fn => block }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.option_setter(name, options={})
|
21
|
+
setter = options[:collection] ? 'Set.new(args)' : 'args.first'
|
22
|
+
class_eval(<<-END, __FILE__, __LINE__)
|
23
|
+
def #{name}(*args, &block)
|
24
|
+
return options[#{name.inspect}] = block if block_given?
|
25
|
+
return options[#{name.inspect}] = #{setter} unless args.empty?
|
26
|
+
return options[#{name.inspect}]
|
27
|
+
end
|
28
|
+
END
|
29
|
+
end
|
30
|
+
|
31
|
+
include Sinatra::Hat::Actions
|
32
|
+
|
33
|
+
# ======================================================
|
34
|
+
|
35
|
+
# The finder block is used when loading all records for the index
|
36
|
+
# action. It gets passed the model proxy and the request params hash.
|
37
|
+
option_setter :finder
|
38
|
+
|
39
|
+
# The finder block is used when loading a single record, which
|
40
|
+
# is the case for most actions. It gets passed the model proxy
|
41
|
+
# and the request params hash.
|
42
|
+
option_setter :record
|
43
|
+
|
44
|
+
# The authenticator block gets called before protected actions. It
|
45
|
+
# gets passed the basic auth username and password.
|
46
|
+
option_setter :authenticator
|
47
|
+
|
48
|
+
# A list of actions that get generated by this maker instance. By
|
49
|
+
# default it's all of the actions specified in actions.rb
|
50
|
+
option_setter :only, :collection => true
|
51
|
+
|
52
|
+
# A way to determine a record's representation in the database
|
53
|
+
option_setter :to_param
|
54
|
+
|
55
|
+
# The path prefix to use for generating groutes.
|
56
|
+
option_setter :prefix
|
57
|
+
|
58
|
+
option_setter :format
|
59
|
+
|
60
|
+
def initialize(klass, overrides={})
|
61
|
+
@klass = klass
|
62
|
+
options.merge!(overrides)
|
63
|
+
with(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Simply stores the app instance when #mount is called.
|
67
|
+
def setup(app)
|
68
|
+
@app = app
|
69
|
+
end
|
70
|
+
|
71
|
+
# Processes a request, using the action specified in actions.rb
|
72
|
+
#
|
73
|
+
# TODO The work of handling a request should probably be wrapped
|
74
|
+
# up in a class.
|
75
|
+
def handle(action, request)
|
76
|
+
request.error(404) unless only.include?(action)
|
77
|
+
protect!(request) if protect.include?(action)
|
78
|
+
|
79
|
+
log_with_benchmark(request, action) do
|
80
|
+
instance_exec(request, &self.class.actions[action][:fn])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Allows the DSL for specifying custom flow controls in a #mount
|
85
|
+
# block by altering the responder's defaults hash.
|
86
|
+
def after(action)
|
87
|
+
yield HashMutator.new(responder.defaults[action])
|
88
|
+
end
|
89
|
+
|
90
|
+
# A list of actions to protect via basic auth. Protected actions
|
91
|
+
# will have the authenticator block called before they are handled.
|
92
|
+
def protect(*actions)
|
93
|
+
credentials.merge!(actions.extract_options!)
|
94
|
+
|
95
|
+
if actions.empty?
|
96
|
+
options[:protect] ||= Set.new([])
|
97
|
+
else
|
98
|
+
actions == [:all] ?
|
99
|
+
Set.new(options[:protect] = only) :
|
100
|
+
Set.new(options[:protect] = actions)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# An array of parent Maker instances under which this instance
|
105
|
+
# was nested.
|
106
|
+
def parents
|
107
|
+
@parents ||= parent ? parent.parents + Array(parent) : []
|
108
|
+
end
|
109
|
+
|
110
|
+
# Looks up the resource path for the specified arguments using this
|
111
|
+
# maker's Resource instance.
|
112
|
+
def resource_path(*args)
|
113
|
+
resource.path(*args)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Default options
|
117
|
+
def options
|
118
|
+
@options ||= {
|
119
|
+
:only => Set.new(Maker.actions.keys),
|
120
|
+
:parent => nil,
|
121
|
+
:format => nil,
|
122
|
+
:prefix => model.plural,
|
123
|
+
:finder => proc { |model, params| model.all },
|
124
|
+
:record => proc { |model, params| model.send("find_by_#{to_param}", params[:id]) },
|
125
|
+
:protect => [ ],
|
126
|
+
:formats => { },
|
127
|
+
:to_param => :id,
|
128
|
+
:credentials => { :username => 'username', :password => 'password', :realm => "The App" },
|
129
|
+
:authenticator => proc { |username, password| [username, password] == [:username, :password].map(&credentials.method(:[])) }
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generates routes in the context of the given app.
|
134
|
+
def generate_routes!
|
135
|
+
Router.new(self).generate(@app)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The responder determines what kind of response should used for
|
139
|
+
# a given action.
|
140
|
+
#
|
141
|
+
# TODO It might be better off to instantiate a new one of these per
|
142
|
+
# request, instead of having one per maker instance.
|
143
|
+
def responder
|
144
|
+
@responder ||= Responder.new(self)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Handles ORM/model related logic.
|
148
|
+
def model
|
149
|
+
@model ||= Model.new(self)
|
150
|
+
end
|
151
|
+
|
152
|
+
# TODO Hook this into Rack::CommonLogger
|
153
|
+
def logger
|
154
|
+
@logger ||= Logger.new(self)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Generates paths for this maker instance.
|
160
|
+
def resource
|
161
|
+
@resource ||= Resource.new(self)
|
162
|
+
end
|
163
|
+
|
164
|
+
def protected?(action)
|
165
|
+
protect.include?(action)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Handles a request with logging and benchmarking.
|
169
|
+
def log_with_benchmark(request, action)
|
170
|
+
msg = [ ]
|
171
|
+
msg << "#{request.env['REQUEST_METHOD']} #{request.env['PATH_INFO']}"
|
172
|
+
msg << "Params: #{request.params.inspect}"
|
173
|
+
msg << "Action: #{action.to_s.upcase}"
|
174
|
+
|
175
|
+
logger.info "[sinatras-hat] " + msg.join(' | ')
|
176
|
+
|
177
|
+
result = nil
|
178
|
+
|
179
|
+
t = Benchmark.realtime { result = yield }
|
180
|
+
|
181
|
+
logger.info " Request finished in #{t} sec."
|
182
|
+
|
183
|
+
result
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# A wrapper around the model class that we're mounting
|
4
|
+
class Model
|
5
|
+
attr_reader :maker
|
6
|
+
|
7
|
+
delegate :options, :klass, :prefix, :to => :maker
|
8
|
+
|
9
|
+
def initialize(maker)
|
10
|
+
@maker = maker
|
11
|
+
end
|
12
|
+
|
13
|
+
# Loads all records using the maker's :finder option.
|
14
|
+
def all(params)
|
15
|
+
params.make_indifferent!
|
16
|
+
p = proxy(params)
|
17
|
+
if p
|
18
|
+
options[:finder].call(p, params)
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Loads one record using the maker's :record option.
|
25
|
+
def find(params)
|
26
|
+
params.make_indifferent!
|
27
|
+
options[:record].call(proxy(params), params)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Finds the owner record of a nested resource.
|
31
|
+
def find_owner(params)
|
32
|
+
params = parent_params(params)
|
33
|
+
options[:record].call(proxy(params), params)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Updates a record with the given params.
|
37
|
+
def update(params)
|
38
|
+
if record = find(params)
|
39
|
+
params.nest!
|
40
|
+
record.attributes = (params[singular] || { })
|
41
|
+
record
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a new instance of the mounted model.
|
46
|
+
def new(params={})
|
47
|
+
params.nest!
|
48
|
+
p = proxy(params)
|
49
|
+
if p
|
50
|
+
p.new(params[singular] || { })
|
51
|
+
else
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the pluralized name for the model.
|
57
|
+
def plural
|
58
|
+
klass.name.snake_case.plural
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the singularized name for the model.
|
62
|
+
def singular
|
63
|
+
klass.name.snake_case.singular
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the foreign_key to be used for this model.
|
67
|
+
def foreign_key
|
68
|
+
"#{singular}_id".to_sym
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the last modified record from the array of records
|
72
|
+
# passed in. It's thorougly inefficient, since it requires all
|
73
|
+
# of the cacheable data to be loaded anyway.
|
74
|
+
def find_last_modified(records)
|
75
|
+
if records.all? { |r| r.respond_to?(:updated_at) }
|
76
|
+
records.sort_by { |r| r.updated_at }.last
|
77
|
+
else
|
78
|
+
records.last
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Returns an association proxy for a nested resource if available,
|
85
|
+
# otherwise it just returns nil
|
86
|
+
def proxy(params)
|
87
|
+
return klass unless parent
|
88
|
+
owner = parent.find_owner(params)
|
89
|
+
if owner and owner.respond_to?(plural)
|
90
|
+
owner.send(plural)
|
91
|
+
else
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Dups and modifies params so that they can be used to find a parent.
|
97
|
+
def parent_params(params)
|
98
|
+
_params = params.dup.to_mash
|
99
|
+
_params.merge! :id => _params.delete(foreign_key)
|
100
|
+
_params
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the parent model if there is one, otherwise nil.
|
104
|
+
def parent
|
105
|
+
return nil unless maker.parent
|
106
|
+
maker.parent.model
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|