sinatra-trails 0.0.1
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/.gitignore +7 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Rakefile +7 -0
- data/lib/sinatra/trails.rb +302 -0
- data/lib/sinatra/trails/version.rb +5 -0
- data/spec/route_generation_spec.rb +595 -0
- data/spec/spec_helper.rb +17 -0
- data/trails.gemspec +27 -0
- metadata +122 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "active_support/inflector"
|
3
|
+
require "active_support/inflections"
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
|
6
|
+
libdir = File.dirname( __FILE__)
|
7
|
+
$:.unshift(libdir) unless $:.include?(libdir)
|
8
|
+
require "trails/version"
|
9
|
+
|
10
|
+
module Sinatra
|
11
|
+
module Trails
|
12
|
+
class RouteNotDefined < Exception; end
|
13
|
+
|
14
|
+
class Route
|
15
|
+
attr_reader :name, :full_name, :scope, :keys, :to_route, :to_regexp
|
16
|
+
Match = Struct.new(:captures)
|
17
|
+
|
18
|
+
def initialize route, name, ancestors, scope
|
19
|
+
@name = name.to_s
|
20
|
+
@full_name = ancestors.map { |ancestor| Scope === ancestor ? ancestor.name : ancestor }.push(name).select{ |name| Symbol === name }.join('_')
|
21
|
+
@components = Array === route ? route.compact : route.to_s.scan(/[^\/]+/)
|
22
|
+
@components.unshift *ancestors.map { |ancestor| ancestor.path if Scope === ancestor }.compact
|
23
|
+
@scope = scope
|
24
|
+
@captures = []
|
25
|
+
@to_route = "/#{@components.join('/')}"
|
26
|
+
namespace = ancestors.reverse.find { |ancestor| ancestor.class == Scope && ancestor.name }
|
27
|
+
|
28
|
+
@to_regexp, @keys = Sinatra::Base.send(:compile, to_route)
|
29
|
+
add_param 'resource', scope.name if [Resource, Resources].include?(scope.class)
|
30
|
+
add_param 'namespace', namespace.name if namespace
|
31
|
+
add_param 'action', name
|
32
|
+
scope.routes << self
|
33
|
+
end
|
34
|
+
|
35
|
+
def match str
|
36
|
+
if match_data = to_regexp.match(str)
|
37
|
+
Match.new(match_data.captures + @captures)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_path *args
|
42
|
+
query = args.pop if Hash === args.last
|
43
|
+
components = @components.dup
|
44
|
+
components.each_with_index do |component, index|
|
45
|
+
next unless /^:/ === component
|
46
|
+
val = args.pop or raise ArgumentError.new("Please provide `#{component}`")
|
47
|
+
components[index] =
|
48
|
+
case val
|
49
|
+
when Numeric, String, Symbol then val
|
50
|
+
else val.to_param end
|
51
|
+
end
|
52
|
+
raise ArgumentError.new("Too many params where passed") unless args.empty?
|
53
|
+
"/#{components.join('/')}#{'?' + Rack::Utils.build_nested_query(query) if query}"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def add_param key, capture
|
58
|
+
unless keys.include? key
|
59
|
+
@keys << key
|
60
|
+
@captures << capture
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class ScopeMatcher
|
66
|
+
def initialize scope, matchers
|
67
|
+
@scope = scope
|
68
|
+
@names, @matchers = matchers.partition { |el| Symbol === el }
|
69
|
+
end
|
70
|
+
|
71
|
+
def match str
|
72
|
+
if @matchers.empty? && @names.empty?
|
73
|
+
Regexp.union(@scope.routes).match str
|
74
|
+
else
|
75
|
+
Regexp.union(*@matchers, *@names.map{ |name| @scope[name] }).match str
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Scope
|
81
|
+
attr_reader :name, :path, :ancestors, :routes
|
82
|
+
|
83
|
+
def initialize app, path, ancestors = []
|
84
|
+
@ancestors, @routes = ancestors, []
|
85
|
+
@path = path.to_s.sub(/^\//, '') if path
|
86
|
+
@name = path if Symbol === path
|
87
|
+
@sinatra_app = app
|
88
|
+
end
|
89
|
+
|
90
|
+
def map name, opts = {}, &block
|
91
|
+
path = opts.delete(:to) || name
|
92
|
+
route = Route.new(path, name, [*ancestors, self], self)
|
93
|
+
instance_eval &block if block_given?
|
94
|
+
route
|
95
|
+
end
|
96
|
+
|
97
|
+
def namespace path, &block
|
98
|
+
@routes += Scope.new(@sinatra_app, path, [*ancestors, self]).generate_routes!(&block)
|
99
|
+
end
|
100
|
+
|
101
|
+
def resource *names, &block
|
102
|
+
restful_routes Resource, names, &block
|
103
|
+
end
|
104
|
+
|
105
|
+
def resources *names, &block
|
106
|
+
restful_routes Resources, names, &block
|
107
|
+
end
|
108
|
+
|
109
|
+
def before *args, &block
|
110
|
+
opts = Hash === args.last ? args.pop : {}
|
111
|
+
@sinatra_app.before ScopeMatcher.new(self, args), opts, &block
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_routes! &block
|
115
|
+
instance_eval &block if block_given?
|
116
|
+
@routes
|
117
|
+
end
|
118
|
+
|
119
|
+
def routes_hash &block
|
120
|
+
Hash[*generate_routes!(&block).map{ |route| [route.full_name, route]}.flatten]
|
121
|
+
end
|
122
|
+
|
123
|
+
def route_for name
|
124
|
+
name = name.to_s
|
125
|
+
@routes.find{ |route| route.full_name == name || route.scope == self && route.name == name }
|
126
|
+
end
|
127
|
+
alias :[] :route_for
|
128
|
+
|
129
|
+
private
|
130
|
+
def method_missing name, *args, &block
|
131
|
+
return @sinatra_app.send(name, *args, &block) if @sinatra_app.respond_to?(name)
|
132
|
+
if route = route_for(name)
|
133
|
+
return route unless block_given?
|
134
|
+
@routes = @routes | route.scope.generate_routes!(&block)
|
135
|
+
else
|
136
|
+
super
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def restful_routes builder, names, &block
|
141
|
+
opts = {}
|
142
|
+
hash = Hash === names.last ? names.pop : {}
|
143
|
+
hash.delete_if { |key, val| opts[key] = val if !(Symbol === val || Enumerable === val) }
|
144
|
+
|
145
|
+
nested = []
|
146
|
+
mash = Proc.new do |hash, acc|
|
147
|
+
hash.map do |key, val|
|
148
|
+
case val
|
149
|
+
when Hash
|
150
|
+
[*acc, key, *mash.call(val, key).flatten]
|
151
|
+
when Array
|
152
|
+
nested += val.map{ |r| [*acc, key, r] } and next
|
153
|
+
else
|
154
|
+
[key, val]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
hash = (mash.call(hash) + nested).compact.map do |array|
|
160
|
+
array.reverse.inject(Proc.new{}) do |proc, name|
|
161
|
+
Proc.new{ send(builder == Resource ? :resource : :resources, name, opts, &proc) }
|
162
|
+
end.call
|
163
|
+
end
|
164
|
+
|
165
|
+
make = lambda do |name|
|
166
|
+
builder.new(@sinatra_app, name, [*ancestors, self], (@opts || {}).merge(opts))
|
167
|
+
end
|
168
|
+
|
169
|
+
if names.size == 1 && hash.empty?
|
170
|
+
@routes += make.call(names.first).generate_routes!(&block)
|
171
|
+
else
|
172
|
+
names.each { |name| @routes += make.call(name).generate_routes! }
|
173
|
+
instance_eval &block if block_given?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class Resource < Scope
|
179
|
+
attr_reader :opts
|
180
|
+
def initialize app, name, ancestors, opts
|
181
|
+
super app, name.to_sym, ancestors
|
182
|
+
@opts = opts
|
183
|
+
end
|
184
|
+
|
185
|
+
def member action
|
186
|
+
ancestors = [*self.ancestors, name]
|
187
|
+
path = @plural_name ? [plural_name, ':id'] : [name]
|
188
|
+
|
189
|
+
ancestors.unshift(action) and path.push(action) unless action == :show
|
190
|
+
ancestors.reject!{ |ancestor| Resource === ancestor } if opts[:shallow]
|
191
|
+
Route.new(path, action.to_s, ancestors, self)
|
192
|
+
end
|
193
|
+
|
194
|
+
def generate_routes! &block
|
195
|
+
define_routes and super
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
def define_routes
|
200
|
+
member(:show) and member(:new) and member(:edit)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class Resources < Resource
|
205
|
+
attr_reader :plural_name, :name_prefix
|
206
|
+
|
207
|
+
def initialize app, raw_name, ancestors, opts
|
208
|
+
super
|
209
|
+
@plural_name = name
|
210
|
+
@name = name.to_s.singularize.to_sym
|
211
|
+
@path = [plural_name, ":#{name}_id"]
|
212
|
+
end
|
213
|
+
|
214
|
+
def collection action
|
215
|
+
ancestors = [*self.ancestors, action == :new ? name : plural_name]
|
216
|
+
path = [plural_name]
|
217
|
+
ancestors.unshift(action) and path.push(action) unless action == :index
|
218
|
+
|
219
|
+
ancestors[0, ancestors.size - 2] = ancestors[0..-3].reject{ |ancestor| Resource === ancestor } if opts[:shallow]
|
220
|
+
Route.new(path, action.to_s, ancestors, self)
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
def define_routes
|
225
|
+
collection(:index) and collection(:new)
|
226
|
+
member(:show) and member(:edit)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def namespace name, &block
|
231
|
+
named_routes.merge! Scope.new(self, name).routes_hash(&block)
|
232
|
+
end
|
233
|
+
|
234
|
+
def map name, opts = {}, &block
|
235
|
+
namespace(nil) { map name, opts, &block }
|
236
|
+
route_for name
|
237
|
+
end
|
238
|
+
|
239
|
+
def resource *args, &block
|
240
|
+
namespace(nil) { resource *args, &block }
|
241
|
+
end
|
242
|
+
|
243
|
+
def resources *args, &block
|
244
|
+
namespace(nil) { resources *args, &block }
|
245
|
+
end
|
246
|
+
|
247
|
+
def route_for name
|
248
|
+
Trails.route_for(self, name).to_route
|
249
|
+
end
|
250
|
+
|
251
|
+
def path_for name, *args
|
252
|
+
Trails.route_for(self, name).to_path(*args)
|
253
|
+
end
|
254
|
+
|
255
|
+
def print_routes
|
256
|
+
trails = named_routes.map { |name, route| [name, route.to_route] }
|
257
|
+
name_padding = trails.sort_by{ |e| e.first.size }.last.first.size + 3
|
258
|
+
trails.each do |name, route|
|
259
|
+
puts sprintf("%#{name_padding}s => %s", name, route)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
def named_routes
|
265
|
+
self::Routes.instance_variable_get :@routes
|
266
|
+
end
|
267
|
+
|
268
|
+
module Helpers
|
269
|
+
def path_for name, *args
|
270
|
+
Trails.route_for(self.class, name).to_path(*args)
|
271
|
+
end
|
272
|
+
|
273
|
+
def url_for *args
|
274
|
+
url path_for(*args)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class << self
|
279
|
+
def route_for klass, name
|
280
|
+
klass.ancestors.each do |ancestor|
|
281
|
+
next unless ancestor.class == Module && ancestor.include?(Helpers)
|
282
|
+
if route = ancestor.instance_variable_get(:@routes)[name]
|
283
|
+
return route
|
284
|
+
end
|
285
|
+
end
|
286
|
+
raise RouteNotDefined.new("The route `#{name}` is not defined")
|
287
|
+
end
|
288
|
+
|
289
|
+
def registered app
|
290
|
+
routes_module = Module.new do
|
291
|
+
include Helpers
|
292
|
+
@routes = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
|
293
|
+
end
|
294
|
+
|
295
|
+
app.const_set :Routes, routes_module
|
296
|
+
app.send :include, routes_module
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
Sinatra.register Trails
|
302
|
+
end
|
@@ -0,0 +1,595 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'trails' do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:app) do
|
7
|
+
app = Class.new(Sinatra::Base)
|
8
|
+
app.register Sinatra::Trails
|
9
|
+
app.set :environment, :test
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'map' do
|
13
|
+
describe 'basic' do
|
14
|
+
before :all do
|
15
|
+
app.map :home, :to => '/'
|
16
|
+
app.map :dashboard
|
17
|
+
app.map :edit_user, :to => '/users/:id/edit'
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'routes' do
|
21
|
+
it { app.route_for(:home).should == '/' }
|
22
|
+
it { app.route_for(:dashboard).should == '/dashboard' }
|
23
|
+
it { app.route_for('dashboard').should == '/dashboard' }
|
24
|
+
it { lambda{ app.route_for(:missing) }.should raise_error Sinatra::Trails::RouteNotDefined }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'paths' do
|
28
|
+
it { app.path_for(:home).should == '/' }
|
29
|
+
it { app.path_for(:home, :q => 'q', :a => 'a').should == '/?q=q&a=a' }
|
30
|
+
|
31
|
+
describe 'with placeholders' do
|
32
|
+
before { @mock_user = mock(:user, :to_param => 1)}
|
33
|
+
it { app.path_for(:edit_user, 1).should == '/users/1/edit' }
|
34
|
+
it { app.path_for(:edit_user, @mock_user).should == '/users/1/edit' }
|
35
|
+
|
36
|
+
describe 'wrong arg count' do
|
37
|
+
it 'should raise error when not passing :id' do
|
38
|
+
lambda { app.path_for(:edit_user) }.should raise_error(ArgumentError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should raise error when too many params are passed' do
|
42
|
+
lambda { app.path_for(:edit_user, 1, 2) }.should raise_error(ArgumentError)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'with namespace' do
|
50
|
+
before :all do
|
51
|
+
app.namespace '/admin' do
|
52
|
+
map :dashboard
|
53
|
+
map :logout
|
54
|
+
end
|
55
|
+
end
|
56
|
+
it { app.route_for(:dashboard).should == '/admin/dashboard' }
|
57
|
+
it { app.route_for(:logout).should == '/admin/logout' }
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'with named namespace' do
|
61
|
+
before :all do
|
62
|
+
app.namespace :admin do
|
63
|
+
map :dashboard
|
64
|
+
end
|
65
|
+
end
|
66
|
+
it { app.route_for(:admin_dashboard).should == '/admin/dashboard' }
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'with nested namespace' do
|
70
|
+
before :all do
|
71
|
+
app.namespace '/blog' do
|
72
|
+
map :users
|
73
|
+
namespace '/admin' do
|
74
|
+
map :dashboard
|
75
|
+
namespace '/auth' do
|
76
|
+
map :logout
|
77
|
+
map :login
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
it { app.route_for(:users).should == '/blog/users' }
|
83
|
+
it { app.route_for(:dashboard).should == '/blog/admin/dashboard' }
|
84
|
+
it { app.route_for(:logout).should == '/blog/admin/auth/logout' }
|
85
|
+
it { app.route_for(:login).should == '/blog/admin/auth/login' }
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'with named nested namespace' do
|
89
|
+
before :all do
|
90
|
+
app.namespace :blog do
|
91
|
+
map :users
|
92
|
+
namespace :admin do
|
93
|
+
map :dashboard
|
94
|
+
namespace :auth do
|
95
|
+
map :logout
|
96
|
+
map :login
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
it { app.route_for(:blog_users).should == '/blog/users' }
|
102
|
+
it { app.route_for(:blog_admin_dashboard).should == '/blog/admin/dashboard' }
|
103
|
+
it { app.route_for(:blog_admin_auth_logout).should == '/blog/admin/auth/logout' }
|
104
|
+
it { app.route_for(:blog_admin_auth_login).should == '/blog/admin/auth/login' }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'resources' do
|
109
|
+
shared_examples_for 'generates routes for users' do
|
110
|
+
it { app.route_for(:users).should == '/users' }
|
111
|
+
it { app.route_for(:new_user).should == '/users/new' }
|
112
|
+
it { app.route_for(:user).should == '/users/:id' }
|
113
|
+
it { app.route_for(:edit_user).should == '/users/:id/edit' }
|
114
|
+
end
|
115
|
+
|
116
|
+
shared_examples_for 'generates routes for posts' do
|
117
|
+
it { app.route_for(:posts).should == '/posts' }
|
118
|
+
it { app.route_for(:new_post).should == '/posts/new' }
|
119
|
+
it { app.route_for(:post).should == '/posts/:id' }
|
120
|
+
it { app.route_for(:edit_post).should == '/posts/:id/edit' }
|
121
|
+
end
|
122
|
+
|
123
|
+
shared_examples_for 'generates routes for nested user posts collections' do
|
124
|
+
it { app.route_for(:user_posts).should == '/users/:user_id/posts' }
|
125
|
+
it { app.route_for(:new_user_post).should == '/users/:user_id/posts/new' }
|
126
|
+
end
|
127
|
+
|
128
|
+
shared_examples_for 'generates routes for nested user posts members' do
|
129
|
+
it { app.route_for(:user_post).should == '/users/:user_id/posts/:id' }
|
130
|
+
it { app.route_for(:edit_user_post).should == '/users/:user_id/posts/:id/edit' }
|
131
|
+
end
|
132
|
+
|
133
|
+
shared_examples_for 'generates routes for nested user posts' do
|
134
|
+
it_should_behave_like 'generates routes for nested user posts collections'
|
135
|
+
it_should_behave_like 'generates routes for nested user posts members'
|
136
|
+
end
|
137
|
+
|
138
|
+
shared_examples_for 'generates routes for shallow user posts' do
|
139
|
+
it_should_behave_like 'generates routes for nested user posts collections'
|
140
|
+
it { app.route_for(:post).should == '/posts/:id' }
|
141
|
+
it { app.route_for(:edit_post).should == '/posts/:id/edit' }
|
142
|
+
end
|
143
|
+
|
144
|
+
shared_examples_for 'generates routes for nested post comments collections' do
|
145
|
+
it { app.route_for(:post_comments).should == '/posts/:post_id/comments' }
|
146
|
+
it { app.route_for(:new_post_comment).should == '/posts/:post_id/comments/new' }
|
147
|
+
end
|
148
|
+
|
149
|
+
shared_examples_for 'generates routes for shallow post comments' do
|
150
|
+
it_should_behave_like 'generates routes for nested post comments collections'
|
151
|
+
it { app.route_for(:comment).should == '/comments/:id' }
|
152
|
+
it { app.route_for(:edit_comment).should == '/comments/:id/edit' }
|
153
|
+
end
|
154
|
+
|
155
|
+
describe 'basic' do
|
156
|
+
before :all do
|
157
|
+
app.resources :users, 'posts' do
|
158
|
+
# map :flag
|
159
|
+
end
|
160
|
+
end
|
161
|
+
it_should_behave_like 'generates routes for users'
|
162
|
+
it_should_behave_like 'generates routes for posts'
|
163
|
+
# it { app.route_for(:flag).should == '/flag' }
|
164
|
+
end
|
165
|
+
|
166
|
+
describe 'nested with block' do
|
167
|
+
before :all do
|
168
|
+
app.resources :users do
|
169
|
+
resources :posts
|
170
|
+
end
|
171
|
+
end
|
172
|
+
it_should_behave_like 'generates routes for users'
|
173
|
+
it_should_behave_like 'generates routes for nested user posts'
|
174
|
+
end
|
175
|
+
|
176
|
+
describe 'with namespace' do
|
177
|
+
before :all do
|
178
|
+
app.namespace :admin do
|
179
|
+
resources :users
|
180
|
+
end
|
181
|
+
end
|
182
|
+
it { app.route_for(:admin_users).should == '/admin/users' }
|
183
|
+
it { app.route_for(:new_admin_user).should == '/admin/users/new' }
|
184
|
+
it { app.route_for(:admin_user).should == '/admin/users/:id' }
|
185
|
+
it { app.route_for(:edit_admin_user).should == '/admin/users/:id/edit' }
|
186
|
+
end
|
187
|
+
|
188
|
+
describe 'nested with block and namespace' do
|
189
|
+
before :all do
|
190
|
+
app.resources :users do
|
191
|
+
namespace :admin do
|
192
|
+
resources :posts
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
it_should_behave_like 'generates routes for users'
|
197
|
+
it { app.route_for(:user_admin_posts).should == '/users/:user_id/admin/posts' }
|
198
|
+
it { app.route_for(:new_user_admin_post).should == '/users/:user_id/admin/posts/new' }
|
199
|
+
it { app.route_for(:user_admin_post).should == '/users/:user_id/admin/posts/:id' }
|
200
|
+
it { app.route_for(:edit_user_admin_post).should == '/users/:user_id/admin/posts/:id/edit' }
|
201
|
+
end
|
202
|
+
|
203
|
+
describe 'deep nested' do
|
204
|
+
before :all do
|
205
|
+
app.resources :users do
|
206
|
+
resources :posts do
|
207
|
+
resources :comments
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
it_should_behave_like 'generates routes for users'
|
212
|
+
it_should_behave_like 'generates routes for nested user posts'
|
213
|
+
describe 'exageration' do
|
214
|
+
it { app.route_for(:user_post_comments).should == '/users/:user_id/posts/:post_id/comments' }
|
215
|
+
it { app.route_for(:new_user_post_comment).should == '/users/:user_id/posts/:post_id/comments/new' }
|
216
|
+
it { app.route_for(:user_post_comment).should == '/users/:user_id/posts/:post_id/comments/:id' }
|
217
|
+
it { app.route_for(:edit_user_post_comment).should == '/users/:user_id/posts/:post_id/comments/:id/edit' }
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe 'shallow deep nested' do
|
222
|
+
before :all do
|
223
|
+
app.resources :users, :shallow => true do
|
224
|
+
resources :posts do
|
225
|
+
resources :comments
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
it_should_behave_like 'generates routes for users'
|
230
|
+
it_should_behave_like 'generates routes for shallow user posts'
|
231
|
+
it_should_behave_like 'generates routes for shallow post comments'
|
232
|
+
end
|
233
|
+
|
234
|
+
describe 'nested shallow' do
|
235
|
+
before :all do
|
236
|
+
app.resources :users, :shallow => true do
|
237
|
+
resources :posts
|
238
|
+
end
|
239
|
+
end
|
240
|
+
it_should_behave_like 'generates routes for users'
|
241
|
+
it_should_behave_like 'generates routes for shallow user posts'
|
242
|
+
end
|
243
|
+
|
244
|
+
describe 'hash nested' do
|
245
|
+
before :all do
|
246
|
+
app.resources :users => :posts
|
247
|
+
end
|
248
|
+
it_should_behave_like 'generates routes for users'
|
249
|
+
it_should_behave_like 'generates routes for nested user posts'
|
250
|
+
end
|
251
|
+
|
252
|
+
describe 'hash nested with block' do
|
253
|
+
before :all do
|
254
|
+
app.resources :users => :posts do
|
255
|
+
map :flag
|
256
|
+
end
|
257
|
+
end
|
258
|
+
it_should_behave_like 'generates routes for users'
|
259
|
+
it_should_behave_like 'generates routes for nested user posts'
|
260
|
+
it { app.route_for(:flag).should == '/flag' }
|
261
|
+
end
|
262
|
+
|
263
|
+
describe 'nested shallow with hash' do
|
264
|
+
before :all do
|
265
|
+
app.resources :users => :posts, :shallow => true
|
266
|
+
end
|
267
|
+
it_should_behave_like 'generates routes for users'
|
268
|
+
it_should_behave_like 'generates routes for shallow user posts'
|
269
|
+
end
|
270
|
+
|
271
|
+
describe 'deep nested with hash' do
|
272
|
+
before :all do
|
273
|
+
app.resources :users => {:posts => :comments}
|
274
|
+
end
|
275
|
+
it_should_behave_like 'generates routes for users'
|
276
|
+
it_should_behave_like 'generates routes for nested user posts'
|
277
|
+
describe 'exageration' do
|
278
|
+
it { app.route_for(:user_post_comments).should == '/users/:user_id/posts/:post_id/comments' }
|
279
|
+
it { app.route_for(:new_user_post_comment).should == '/users/:user_id/posts/:post_id/comments/new' }
|
280
|
+
it { app.route_for(:user_post_comment).should == '/users/:user_id/posts/:post_id/comments/:id' }
|
281
|
+
it { app.route_for(:edit_user_post_comment).should == '/users/:user_id/posts/:post_id/comments/:id/edit' }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe 'deep nested shallow with hash' do
|
286
|
+
before :all do
|
287
|
+
app.resources :users => {:posts => :comments}, :shallow => true
|
288
|
+
end
|
289
|
+
it_should_behave_like 'generates routes for users'
|
290
|
+
it_should_behave_like 'generates routes for shallow user posts'
|
291
|
+
it { app.route_for(:post_comments).should == '/posts/:post_id/comments' }
|
292
|
+
it { app.route_for(:new_post_comment).should == '/posts/:post_id/comments/new' }
|
293
|
+
it { app.route_for(:comment).should == '/comments/:id' }
|
294
|
+
it { app.route_for(:edit_comment).should == '/comments/:id/edit' }
|
295
|
+
end
|
296
|
+
|
297
|
+
describe 'nested with array' do
|
298
|
+
before :all do
|
299
|
+
app.resources :users => [:posts, :comments]
|
300
|
+
end
|
301
|
+
it { app.route_for(:users).should == '/users' }
|
302
|
+
it { app.route_for(:user_comments).should == '/users/:user_id/comments' }
|
303
|
+
it { app.route_for(:user_posts).should == '/users/:user_id/posts' }
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
describe 'single resource' do
|
308
|
+
before :all do
|
309
|
+
app.resource :user => :profile
|
310
|
+
end
|
311
|
+
it { app.route_for(:user).should == '/user' }
|
312
|
+
it { app.route_for(:new_user).should == '/user/new' }
|
313
|
+
it { app.route_for(:edit_user).should == '/user/edit' }
|
314
|
+
it { app.route_for(:user_profile).should == '/user/profile' }
|
315
|
+
it { app.route_for(:new_user_profile).should == '/user/profile/new' }
|
316
|
+
it { app.route_for(:edit_user_profile).should == '/user/profile/edit' }
|
317
|
+
end
|
318
|
+
|
319
|
+
describe 'finding route for scope' do
|
320
|
+
before :all do
|
321
|
+
@scope = Sinatra::Trails::Scope.new(app, :admin)
|
322
|
+
@scope.generate_routes!{ map(:index) }
|
323
|
+
end
|
324
|
+
|
325
|
+
it { @scope.route_for(:admin_index).should_not be_nil }
|
326
|
+
it { @scope.route_for(:index).should_not be_nil }
|
327
|
+
it "should find resources index without full name"
|
328
|
+
end
|
329
|
+
|
330
|
+
describe 'finding route for resources' do
|
331
|
+
before :all do
|
332
|
+
@scope = Sinatra::Trails::Resources.new(app, :users, [], {})
|
333
|
+
@scope.generate_routes!
|
334
|
+
end
|
335
|
+
it { @scope.route_for(:users).should_not be_nil }
|
336
|
+
it { @scope.route_for(:new_user).should_not be_nil }
|
337
|
+
it { @scope.route_for(:edit_user).should_not be_nil }
|
338
|
+
it { @scope.route_for(:user).should_not be_nil }
|
339
|
+
end
|
340
|
+
|
341
|
+
describe 'finding route for resources' do
|
342
|
+
before :all do
|
343
|
+
@scope = Sinatra::Trails::Resources.new(app, :users, [], {})
|
344
|
+
@scope.generate_routes! do
|
345
|
+
resources :posts
|
346
|
+
end
|
347
|
+
end
|
348
|
+
it { @scope.route_for(:user_posts).should_not be_nil }
|
349
|
+
it { @scope.route_for(:new_user_post).should_not be_nil }
|
350
|
+
it { @scope.route_for(:edit_user_post).should_not be_nil }
|
351
|
+
it { @scope.route_for(:user_post).should_not be_nil }
|
352
|
+
end
|
353
|
+
|
354
|
+
describe 'sinatra integration' do
|
355
|
+
describe 'delegation to sinatra and helpers' do
|
356
|
+
before :all do
|
357
|
+
app.map(:home) { get(home){ path_for(:home) } }
|
358
|
+
app.instance_eval { get(map(:about)){ url_for(:about) } }
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should get path for route" do
|
362
|
+
get '/home'
|
363
|
+
last_response.body.should == '/home'
|
364
|
+
end
|
365
|
+
|
366
|
+
it "should get url for route" do
|
367
|
+
get '/about'
|
368
|
+
last_response.body.should == 'http://example.org/about'
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
describe 'using route as scope' do
|
373
|
+
before :all do
|
374
|
+
app.resources(:users => :posts, :shallow => true) do
|
375
|
+
users do
|
376
|
+
get(member(:aprove)) { path_for(:aprove_user, params[:id]) }
|
377
|
+
end
|
378
|
+
|
379
|
+
user_posts do
|
380
|
+
get(member(:aprove)) { path_for(:aprove_post, params[:id]) }
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
it 'should define action for users member' do
|
386
|
+
get '/users/1/aprove'
|
387
|
+
last_response.body.should == '/users/1/aprove'
|
388
|
+
end
|
389
|
+
|
390
|
+
it 'should define action for user_posts member' do
|
391
|
+
get '/posts/1/aprove'
|
392
|
+
last_response.body.should == '/posts/1/aprove'
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
describe 'before without args' do
|
397
|
+
before :all do
|
398
|
+
app.instance_eval do
|
399
|
+
namespace(:admin) do
|
400
|
+
before { @admin = true }
|
401
|
+
get map(:index, :to => '/') do
|
402
|
+
@admin.to_s
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
namespace(:not_admin) do
|
407
|
+
get map(:index, :to => '/') do
|
408
|
+
@admin.to_s
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
it 'should set before filter within scope' do
|
415
|
+
get '/admin'
|
416
|
+
last_response.body.should == 'true'
|
417
|
+
end
|
418
|
+
|
419
|
+
it 'should not set before filter outside scope' do
|
420
|
+
get '/not_admin'
|
421
|
+
last_response.body.should == ''
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
describe 'before passing routes' do
|
426
|
+
before :all do
|
427
|
+
app.instance_eval do
|
428
|
+
namespace(:admin) do
|
429
|
+
get map(:index, :to => '/') do
|
430
|
+
@auth.to_s
|
431
|
+
end
|
432
|
+
|
433
|
+
get map(:sign_in, :to => '/sign_in') do
|
434
|
+
@auth.to_s
|
435
|
+
end
|
436
|
+
|
437
|
+
get map(:sign_up, :to => '/sign_up') do
|
438
|
+
@auth.to_s
|
439
|
+
end
|
440
|
+
|
441
|
+
before(admin_sign_in, admin_sign_up) { @auth = true }
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
it 'should set before filter for passed routes' do
|
447
|
+
get '/admin/sign_in'
|
448
|
+
last_response.body.should == 'true'
|
449
|
+
get '/admin/sign_up'
|
450
|
+
last_response.body.should == 'true'
|
451
|
+
end
|
452
|
+
|
453
|
+
it 'should not set before filter for not passed routes' do
|
454
|
+
get '/admin'
|
455
|
+
last_response.body.should == ''
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
describe 'before filter lazy match passing symbols' do
|
460
|
+
before :all do
|
461
|
+
app.instance_eval do
|
462
|
+
namespace(:admin) do
|
463
|
+
before(:admin_sign_in, :admin_sign_up) { @auth = true }
|
464
|
+
|
465
|
+
get map(:index, :to => '/') do
|
466
|
+
@auth.to_s
|
467
|
+
end
|
468
|
+
|
469
|
+
get map(:sign_in, :to => '/sign_in') do
|
470
|
+
@auth.to_s
|
471
|
+
end
|
472
|
+
|
473
|
+
get map(:sign_up, :to => '/sign_up') do
|
474
|
+
@auth.to_s
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'should set before filter for passed routes' do
|
481
|
+
get '/admin/sign_in'
|
482
|
+
last_response.body.should == 'true'
|
483
|
+
get '/admin/sign_up'
|
484
|
+
last_response.body.should == 'true'
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'should not set before filter for not passed routes' do
|
488
|
+
get '/admin'
|
489
|
+
last_response.body.should == ''
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
describe 'having access to resource name' do
|
494
|
+
before :all do
|
495
|
+
app.resources(:users => :posts) do
|
496
|
+
get(users){ params[:resource].to_s }
|
497
|
+
get(user_posts){ params[:resource].to_s }
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
it 'should access resource name for users' do
|
502
|
+
get '/users'
|
503
|
+
last_response.body.should == 'user'
|
504
|
+
end
|
505
|
+
|
506
|
+
it 'should access resource name for nested posts' do
|
507
|
+
get '/users/1/posts'
|
508
|
+
last_response.body.should == 'post'
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
describe 'having access to namespace and action' do
|
513
|
+
before :all do
|
514
|
+
app.namespace(:admin) do
|
515
|
+
get(map(:index, :to => '/')){ params[:namespace].to_s }
|
516
|
+
get(map(:sign_in)){ params[:action].to_s }
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
it 'should access namespace' do
|
521
|
+
get '/admin'
|
522
|
+
last_response.body.should == 'admin'
|
523
|
+
end
|
524
|
+
|
525
|
+
it 'should access action' do
|
526
|
+
get '/admin/sign_in'
|
527
|
+
last_response.body.should == 'sign_in'
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
describe 'finding the right route for resources' do
|
532
|
+
it 'should find the right route' do
|
533
|
+
app.resources(:users) do
|
534
|
+
users.to_route.should == '/users'
|
535
|
+
new_user.to_route.should == '/users/new'
|
536
|
+
user.to_route.should == '/users/:id'
|
537
|
+
edit_user.to_route.should == '/users/:id/edit'
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
describe 'having access to action for resources' do
|
543
|
+
before :all do
|
544
|
+
app.resources(:users) do
|
545
|
+
get(users){ params[:action].to_s }
|
546
|
+
get(new_user){ params[:action].to_s }
|
547
|
+
get(user){ params[:action].to_s }
|
548
|
+
get(edit_user){ params[:action].to_s }
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
it 'should set index as action' do
|
553
|
+
get '/users'
|
554
|
+
last_response.body.should == 'index'
|
555
|
+
end
|
556
|
+
|
557
|
+
it 'should set index as show' do
|
558
|
+
get '/users/1'
|
559
|
+
last_response.body.should == 'show'
|
560
|
+
end
|
561
|
+
|
562
|
+
it 'should set index as new' do
|
563
|
+
get '/users/new'
|
564
|
+
last_response.body.should == 'new'
|
565
|
+
end
|
566
|
+
|
567
|
+
it 'should set index as edit' do
|
568
|
+
get '/users/1/edit'
|
569
|
+
last_response.body.should == 'edit'
|
570
|
+
end
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
describe 'accessing routes from outside the app by module inclusion' do
|
575
|
+
let(:other_app) do
|
576
|
+
app = Class.new(Sinatra::Base)
|
577
|
+
app.register Sinatra::Trails
|
578
|
+
app.set :environment, :test
|
579
|
+
end
|
580
|
+
|
581
|
+
let(:klass) { Class.new }
|
582
|
+
|
583
|
+
before :all do
|
584
|
+
app.map :heaven
|
585
|
+
other_app.map :hell
|
586
|
+
klass.send :include, app::Routes
|
587
|
+
klass.send :include, other_app::Routes
|
588
|
+
@obj = klass.new
|
589
|
+
end
|
590
|
+
|
591
|
+
it { @obj.path_for(:heaven).should == '/heaven' }
|
592
|
+
it { @obj.path_for(:hell).should == '/hell' }
|
593
|
+
it { lambda{ @obj.path_for(:purgatory) }.should raise_error Sinatra::Trails::RouteNotDefined }
|
594
|
+
end
|
595
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec'
|
3
|
+
require 'rack/test'
|
4
|
+
|
5
|
+
$:.unshift File.join(File.dirname( __FILE__), '..', 'lib')
|
6
|
+
|
7
|
+
require 'sinatra/trails'
|
8
|
+
|
9
|
+
RSpec::Matchers.define :match_route do |expected|
|
10
|
+
match do |actual|
|
11
|
+
expected.match actual
|
12
|
+
end
|
13
|
+
|
14
|
+
description do
|
15
|
+
"should match route #{expected}"
|
16
|
+
end
|
17
|
+
end
|
data/trails.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "sinatra/trails/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "sinatra-trails"
|
7
|
+
s.version = Sinatra::Trails::VERSION
|
8
|
+
s.authors = ["Macario"]
|
9
|
+
s.email = ["macarui@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/maca/trails"
|
11
|
+
s.summary = %q{A named routes Sinatra DSL inspired by Rails routing}
|
12
|
+
s.description = %q{A named routes Sinatra DSL inspired by Rails routing}
|
13
|
+
|
14
|
+
s.rubyforge_project = "sinatra-trails"
|
15
|
+
s.add_dependency 'sinatra'
|
16
|
+
s.add_dependency 'i18n'
|
17
|
+
s.add_dependency 'activesupport', '>= 3.0'
|
18
|
+
|
19
|
+
s.add_development_dependency 'rake'
|
20
|
+
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'rack-test'
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-trails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Macario
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
16
|
+
requirement: &70283828463680 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70283828463680
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: i18n
|
27
|
+
requirement: &70283828463260 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70283828463260
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: activesupport
|
38
|
+
requirement: &70283828486540 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70283828486540
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70283828486120 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70283828486120
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &70283828485660 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70283828485660
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: &70283828485240 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70283828485240
|
80
|
+
description: A named routes Sinatra DSL inspired by Rails routing
|
81
|
+
email:
|
82
|
+
- macarui@gmail.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- .rspec
|
89
|
+
- Gemfile
|
90
|
+
- Rakefile
|
91
|
+
- lib/sinatra/trails.rb
|
92
|
+
- lib/sinatra/trails/version.rb
|
93
|
+
- spec/route_generation_spec.rb
|
94
|
+
- spec/spec_helper.rb
|
95
|
+
- trails.gemspec
|
96
|
+
homepage: http://github.com/maca/trails
|
97
|
+
licenses: []
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project: sinatra-trails
|
116
|
+
rubygems_version: 1.8.8
|
117
|
+
signing_key:
|
118
|
+
specification_version: 3
|
119
|
+
summary: A named routes Sinatra DSL inspired by Rails routing
|
120
|
+
test_files:
|
121
|
+
- spec/route_generation_spec.rb
|
122
|
+
- spec/spec_helper.rb
|