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.
@@ -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