sinatras-hat 0.1.1 → 0.1.2
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.
- data/lib/sinatras-hat.rb +0 -1
- data/lib/sinatras-hat/actions.rb +26 -4
- data/lib/sinatras-hat/extendor.rb +3 -1
- data/lib/sinatras-hat/maker.rb +56 -14
- data/lib/sinatras-hat/model.rb +23 -0
- data/lib/sinatras-hat/resource.rb +18 -8
- data/lib/sinatras-hat/responder.rb +8 -4
- data/lib/sinatras-hat/response.rb +3 -2
- data/lib/sinatras-hat/router.rb +2 -2
- metadata +1 -1
data/lib/sinatras-hat.rb
CHANGED
data/lib/sinatras-hat/actions.rb
CHANGED
@@ -10,7 +10,7 @@ module Sinatra
|
|
10
10
|
module Actions
|
11
11
|
def self.included(map)
|
12
12
|
map.action :destroy, '/:id', :verb => :delete do |request|
|
13
|
-
record = model.find(request.params) ||
|
13
|
+
record = model.find(request.params) || request.not_found
|
14
14
|
record.destroy
|
15
15
|
responder.success(:destroy, request, record)
|
16
16
|
end
|
@@ -21,18 +21,19 @@ module Sinatra
|
|
21
21
|
end
|
22
22
|
|
23
23
|
map.action :update, '/:id', :verb => :put do |request|
|
24
|
-
record = model.update(request.params) ||
|
24
|
+
record = model.update(request.params) || request.not_found
|
25
25
|
result = record.save ? :success : :failure
|
26
26
|
responder.send(result, :update, request, record)
|
27
27
|
end
|
28
28
|
|
29
29
|
map.action :edit, '/:id/edit' do |request|
|
30
|
-
record = model.find(request.params) ||
|
30
|
+
record = model.find(request.params) || request.not_found
|
31
31
|
responder.success(:edit, request, record)
|
32
32
|
end
|
33
33
|
|
34
34
|
map.action :show, '/:id' do |request|
|
35
|
-
record = model.find(request.params) ||
|
35
|
+
record = model.find(request.params) || request.not_found
|
36
|
+
set_cache_headers(request, record) unless protected?(:show)
|
36
37
|
responder.success(:show, request, record)
|
37
38
|
end
|
38
39
|
|
@@ -44,8 +45,29 @@ module Sinatra
|
|
44
45
|
|
45
46
|
map.action :index, '/' do |request|
|
46
47
|
records = model.all(request.params)
|
48
|
+
set_cache_headers(request, records) unless protected?(:index)
|
47
49
|
responder.success(:index, request, records)
|
48
50
|
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def set_cache_headers(request, data)
|
55
|
+
|
56
|
+
set_etag(request, data)
|
57
|
+
set_last_modified(request, data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_etag(request, data)
|
61
|
+
record = model.find_last_modified(Array(data))
|
62
|
+
return unless record.respond_to?(:updated_at)
|
63
|
+
request.etag("#{record.id}-#{record.updated_at}-#{data.is_a?(Array)}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_last_modified(request, data)
|
67
|
+
record = model.find_last_modified(Array(data))
|
68
|
+
return unless record.respond_to?(:updated_at)
|
69
|
+
request.last_modified(record.updated_at)
|
70
|
+
end
|
49
71
|
end
|
50
72
|
end
|
51
73
|
end
|
@@ -6,7 +6,9 @@ module Sinatra
|
|
6
6
|
# instance's parent.
|
7
7
|
module Extendor
|
8
8
|
def mount(klass, options={}, &block)
|
9
|
-
|
9
|
+
unless kind_of?(Sinatra::Hat::Maker)
|
10
|
+
use Rack::MethodOverride
|
11
|
+
end
|
10
12
|
|
11
13
|
Maker.new(klass, options).tap do |maker|
|
12
14
|
maker.parent = self if kind_of?(Sinatra::Hat::Maker)
|
data/lib/sinatras-hat/maker.rb
CHANGED
@@ -11,7 +11,6 @@ module Sinatra
|
|
11
11
|
@actions ||= { }
|
12
12
|
end
|
13
13
|
|
14
|
-
# enables the douche-y DSL you see in actions.rb
|
15
14
|
def self.action(name, path, options={}, &block)
|
16
15
|
verb = options[:verb] || :get
|
17
16
|
Router.cache << [verb, name, path]
|
@@ -28,10 +27,15 @@ module Sinatra
|
|
28
27
|
with(options)
|
29
28
|
end
|
30
29
|
|
30
|
+
# Simply stores the app instance when #mount is called.
|
31
31
|
def setup(app)
|
32
32
|
@app = app
|
33
33
|
end
|
34
34
|
|
35
|
+
# Processes a request, using the action specified in actions.rb
|
36
|
+
#
|
37
|
+
# TODO The work of handling a request should probably be wrapped
|
38
|
+
# up in a class.
|
35
39
|
def handle(action, request)
|
36
40
|
request.error(404) unless only.include?(action)
|
37
41
|
protect!(request) if protect.include?(action)
|
@@ -41,10 +45,14 @@ module Sinatra
|
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
48
|
+
# Allows the DSL for specifying custom flow controls in a #mount
|
49
|
+
# block by altering the responder's defaults hash.
|
44
50
|
def after(action)
|
45
51
|
yield HashMutator.new(responder.defaults[action])
|
46
52
|
end
|
47
53
|
|
54
|
+
# The finder block is used when loading all records for the index
|
55
|
+
# action. It gets passed the model proxy and the request params hash.
|
48
56
|
def finder(&block)
|
49
57
|
if block_given?
|
50
58
|
options[:finder] = block
|
@@ -53,6 +61,9 @@ module Sinatra
|
|
53
61
|
end
|
54
62
|
end
|
55
63
|
|
64
|
+
# The finder block is used when loading a single record, which
|
65
|
+
# is the case for most actions. It gets passed the model proxy
|
66
|
+
# and the request params hash.
|
56
67
|
def record(&block)
|
57
68
|
if block_given?
|
58
69
|
options[:record] = block
|
@@ -61,6 +72,8 @@ module Sinatra
|
|
61
72
|
end
|
62
73
|
end
|
63
74
|
|
75
|
+
# The authenticator block gets called before protected actions. It
|
76
|
+
# gets passed the basic auth username and password.
|
64
77
|
def authenticator(&block)
|
65
78
|
if block_given?
|
66
79
|
options[:authenticator] = block
|
@@ -69,6 +82,8 @@ module Sinatra
|
|
69
82
|
end
|
70
83
|
end
|
71
84
|
|
85
|
+
# A list of actions that get generated by this maker instance. By
|
86
|
+
# default it's all of the actions specified in actions.rb
|
72
87
|
def only(*actions)
|
73
88
|
if actions.empty?
|
74
89
|
options[:only] ||= Set.new(options[:only])
|
@@ -77,6 +92,17 @@ module Sinatra
|
|
77
92
|
end
|
78
93
|
end
|
79
94
|
|
95
|
+
# A way to determine a record's representation in the database
|
96
|
+
def to_param(name=nil)
|
97
|
+
if name
|
98
|
+
options[:to_param] = name
|
99
|
+
else
|
100
|
+
options[:to_param]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# A list of actions to protect via basic auth. Protected actions
|
105
|
+
# will have the authenticator block called before they are handled.
|
80
106
|
def protect(*actions)
|
81
107
|
credentials.merge!(actions.extract_options!)
|
82
108
|
|
@@ -89,43 +115,53 @@ module Sinatra
|
|
89
115
|
end
|
90
116
|
end
|
91
117
|
|
118
|
+
# The path prefix to use for routes and such.
|
92
119
|
def prefix
|
93
120
|
options[:prefix] ||= model.plural
|
94
121
|
end
|
95
122
|
|
123
|
+
# An array of parent Maker instances under which this instance
|
124
|
+
# was nested.
|
96
125
|
def parents
|
97
|
-
@parents ||= parent ?
|
126
|
+
@parents ||= parent ? parent.parents + Array(parent) : []
|
98
127
|
end
|
99
128
|
|
129
|
+
# Looks up the resource path for the specified arguments using this
|
130
|
+
# maker's Resource instance.
|
100
131
|
def resource_path(*args)
|
101
132
|
resource.path(*args)
|
102
133
|
end
|
103
134
|
|
135
|
+
# Default options
|
104
136
|
def options
|
105
137
|
@options ||= {
|
106
138
|
:only => Set.new(Maker.actions.keys),
|
107
139
|
:parent => nil,
|
108
140
|
:finder => proc { |model, params| model.all },
|
109
|
-
:record => proc { |model, params| model.
|
141
|
+
:record => proc { |model, params| model.send("find_by_#{to_param}", params[:id]) },
|
110
142
|
:protect => [ ],
|
111
143
|
:formats => { },
|
144
|
+
:to_param => :id,
|
112
145
|
:credentials => { :username => 'username', :password => 'password', :realm => "The App" },
|
113
146
|
:authenticator => proc { |username, password| [username, password] == [:username, :password].map(&credentials.method(:[])) }
|
114
147
|
}
|
115
148
|
end
|
116
|
-
|
117
|
-
|
118
|
-
"maker: #{klass}"
|
119
|
-
end
|
120
|
-
|
149
|
+
|
150
|
+
# Generates routes in the context of the given app.
|
121
151
|
def generate_routes!
|
122
152
|
Router.new(self).generate(@app)
|
123
153
|
end
|
124
154
|
|
155
|
+
# The responder determines what kind of response should used for
|
156
|
+
# a given action.
|
157
|
+
#
|
158
|
+
# TODO It might be better off to instantiate a new one of these per
|
159
|
+
# request, instead of having one per maker instance.
|
125
160
|
def responder
|
126
161
|
@responder ||= Responder.new(self)
|
127
162
|
end
|
128
163
|
|
164
|
+
# Handles ORM/model related logic.
|
129
165
|
def model
|
130
166
|
@model ||= Model.new(self)
|
131
167
|
end
|
@@ -137,26 +173,32 @@ module Sinatra
|
|
137
173
|
|
138
174
|
private
|
139
175
|
|
176
|
+
# Generates paths for this maker instance.
|
177
|
+
def resource
|
178
|
+
@resource ||= Resource.new(self)
|
179
|
+
end
|
180
|
+
|
181
|
+
def protected?(action)
|
182
|
+
protect.include?(action)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Handles a request with logging and benchmarking.
|
140
186
|
def log_with_benchmark(request, action)
|
141
187
|
msg = [ ]
|
142
188
|
msg << "#{request.env['REQUEST_METHOD']} #{request.env['PATH_INFO']}"
|
143
189
|
msg << "Params: #{request.params.inspect}"
|
144
190
|
msg << "Action: #{action.to_s.upcase}"
|
145
191
|
|
146
|
-
logger.info "
|
192
|
+
logger.info "[sinatras-hat] " + msg.join(' | ')
|
147
193
|
|
148
194
|
result = nil
|
149
195
|
|
150
196
|
t = Benchmark.realtime { result = yield }
|
151
197
|
|
152
|
-
logger.info "
|
198
|
+
logger.info " Request finished in #{t} sec."
|
153
199
|
|
154
200
|
result
|
155
201
|
end
|
156
|
-
|
157
|
-
def resource
|
158
|
-
@resource ||= Resource.new(self)
|
159
|
-
end
|
160
202
|
end
|
161
203
|
end
|
162
204
|
end
|
data/lib/sinatras-hat/model.rb
CHANGED
@@ -10,21 +10,25 @@ module Sinatra
|
|
10
10
|
@maker = maker
|
11
11
|
end
|
12
12
|
|
13
|
+
# Loads all records using the maker's :finder option.
|
13
14
|
def all(params)
|
14
15
|
params.make_indifferent!
|
15
16
|
options[:finder].call(proxy(params), params)
|
16
17
|
end
|
17
18
|
|
19
|
+
# Loads one record using the maker's :record option.
|
18
20
|
def find(params)
|
19
21
|
params.make_indifferent!
|
20
22
|
options[:record].call(proxy(params), params)
|
21
23
|
end
|
22
24
|
|
25
|
+
# Finds the owner record of a nested resource.
|
23
26
|
def find_owner(params)
|
24
27
|
params = parent_params(params)
|
25
28
|
options[:record].call(proxy(params), params)
|
26
29
|
end
|
27
30
|
|
31
|
+
# Updates a record with the given params.
|
28
32
|
def update(params)
|
29
33
|
if record = find(params)
|
30
34
|
params.nest!
|
@@ -33,25 +37,42 @@ module Sinatra
|
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
40
|
+
# Returns a new instance of the mounted model.
|
36
41
|
def new(params={})
|
37
42
|
params.nest!
|
38
43
|
proxy(params).new(params[singular] || { })
|
39
44
|
end
|
40
45
|
|
46
|
+
# Returns the pluralized name for the model.
|
41
47
|
def plural
|
42
48
|
klass.name.snake_case.plural
|
43
49
|
end
|
44
50
|
|
51
|
+
# Returns the singularized name for the model.
|
45
52
|
def singular
|
46
53
|
klass.name.snake_case.singular
|
47
54
|
end
|
48
55
|
|
56
|
+
# Returns the foreign_key to be used for this model.
|
49
57
|
def foreign_key
|
50
58
|
"#{singular}_id".to_sym
|
51
59
|
end
|
52
60
|
|
61
|
+
# Returns the last modified record from the array of records
|
62
|
+
# passed in. It's thorougly inefficient, since it requires all
|
63
|
+
# of the cacheable data to be loaded anyway.
|
64
|
+
def find_last_modified(records)
|
65
|
+
if records.all? { |r| r.respond_to?(:updated_at) }
|
66
|
+
records.sort_by { |r| r.updated_at }.last
|
67
|
+
else
|
68
|
+
records.last
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
53
72
|
private
|
54
73
|
|
74
|
+
# Returns an association proxy for a nested resource if available,
|
75
|
+
# otherwise it just returns the class.
|
55
76
|
def proxy(params)
|
56
77
|
return klass unless parent
|
57
78
|
owner = parent.find_owner(params)
|
@@ -62,12 +83,14 @@ module Sinatra
|
|
62
83
|
end
|
63
84
|
end
|
64
85
|
|
86
|
+
# Dups and modifies params so that they can be used to find a parent.
|
65
87
|
def parent_params(params)
|
66
88
|
_params = params.dup.to_mash
|
67
89
|
_params.merge! :id => _params.delete(foreign_key)
|
68
90
|
_params
|
69
91
|
end
|
70
92
|
|
93
|
+
# Returns the parent model if there is one, otherwise nil.
|
71
94
|
def parent
|
72
95
|
return nil unless maker.parent
|
73
96
|
maker.parent.model
|
@@ -10,27 +10,37 @@ module Sinatra
|
|
10
10
|
def path(suffix, record=nil)
|
11
11
|
suffix = suffix.dup
|
12
12
|
|
13
|
+
parents = path_records_for(record) if record
|
14
|
+
|
13
15
|
path = resources.inject("") do |memo, maker|
|
14
16
|
memo += fragment(maker, record)
|
15
17
|
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
path = clean(path + suffix)
|
20
|
+
path.gsub!(/:(\w+)/) { parents.pop.send(@maker.to_param) } if record
|
21
|
+
path
|
20
22
|
end
|
21
23
|
|
22
24
|
private
|
23
25
|
|
26
|
+
def path_records_for(record)
|
27
|
+
[record].tap do |parents|
|
28
|
+
resources.reverse.each do |resource|
|
29
|
+
parents << resource.model.find_owner(parents.last.attributes)
|
30
|
+
parents.compact!
|
31
|
+
parents.uniq!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
24
36
|
def fragment(maker, record)
|
25
37
|
@maker.eql?(maker) ?
|
26
38
|
"/#{maker.prefix}" :
|
27
|
-
"/#{maker.prefix}/" +
|
39
|
+
"/#{maker.prefix}/" + key(maker)
|
28
40
|
end
|
29
41
|
|
30
|
-
def
|
31
|
-
|
32
|
-
result = record ? record.send(foreign_key) : foreign_key
|
33
|
-
result.inspect
|
42
|
+
def key(maker)
|
43
|
+
maker.model.foreign_key.inspect
|
34
44
|
end
|
35
45
|
|
36
46
|
def clean(s)
|
@@ -49,23 +49,27 @@ module Sinatra
|
|
49
49
|
}
|
50
50
|
end
|
51
51
|
|
52
|
+
# Called when a request is handled successfully. For most GET
|
53
|
+
# requests, this is always the case. For update/create actions,
|
54
|
+
# it is when the record is created/updated successfully.
|
52
55
|
def success(name, request, data)
|
53
56
|
handle(:success, name, request, data)
|
54
57
|
end
|
55
58
|
|
59
|
+
# Called when a request is not able to handled. This could be
|
60
|
+
# because a record could not be created or saved.
|
56
61
|
def failure(name, request, data)
|
57
62
|
handle(:failure, name, request, data)
|
58
63
|
end
|
59
64
|
|
65
|
+
# Serializes the data passed in, first looking for a custom formatter,
|
66
|
+
# then falling back on trying to call to_[format] on the data. If neither
|
67
|
+
# are available, returns an error with the status code 406.
|
60
68
|
def serialize(request, data)
|
61
69
|
name = request.params[:format].to_sym
|
62
70
|
formatter = to_format(name)
|
63
71
|
formatter[data] || request.error(406)
|
64
72
|
end
|
65
|
-
|
66
|
-
def not_found(request)
|
67
|
-
request.not_found
|
68
|
-
end
|
69
73
|
|
70
74
|
private
|
71
75
|
|
@@ -13,9 +13,10 @@ module Sinatra
|
|
13
13
|
@request = request
|
14
14
|
end
|
15
15
|
|
16
|
-
def render(action)
|
16
|
+
def render(action, options={})
|
17
17
|
begin
|
18
|
-
@request.
|
18
|
+
options.each { |sym, value| @request.send(sym, value) }
|
19
|
+
@request.erb "#{maker.prefix}/#{action}".to_sym
|
19
20
|
rescue Errno::ENOENT
|
20
21
|
no_template! "Can't find #{File.expand_path(File.join(views, action.to_s))}.erb"
|
21
22
|
end
|
data/lib/sinatras-hat/router.rb
CHANGED
@@ -34,8 +34,8 @@ module Sinatra
|
|
34
34
|
|
35
35
|
logger.info ">> route for #{maker.klass} #{action}:\t#{method.to_s.upcase}\t#{path}"
|
36
36
|
|
37
|
-
app.send(method, path) { handler[self] }
|
38
|
-
app.send(method, "#{path}.:format") { handler[self] }
|
37
|
+
app.send(method, path + "/?") { handler[self] }
|
38
|
+
app.send(method, "#{path}.:format" + "/?") { handler[self] }
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|