sinatras-hat 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,5 @@
1
1
  $LOAD_PATH << File.join(File.dirname(__FILE__))
2
2
 
3
- require 'rubygems'
4
3
  require 'benchmark'
5
4
  require 'sinatra/base'
6
5
  require 'extlib'
@@ -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) || responder.not_found(request)
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) || responder.not_found(request)
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) || responder.not_found(request)
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) || responder.not_found(request)
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
- use Rack::MethodOverride unless kind_of?(Sinatra::Hat::Maker)
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)
@@ -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 ? Array(parent) + parent.parents : []
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.find_by_id(params[:id]) },
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
- def inspect
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 ">> " + msg.join(' | ')
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 " Request finished in #{t} sec."
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
@@ -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
- suffix.gsub!('/:id', "/#{record.id}") if record
18
-
19
- clean(path + suffix)
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}/" + interpolate(maker, record)
39
+ "/#{maker.prefix}/" + key(maker)
28
40
  end
29
41
 
30
- def interpolate(maker, record)
31
- foreign_key = maker.model.foreign_key
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.erb action.to_sym, :views_directory => views
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
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatras-hat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat Nakajima