ruby_routes 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/lib/ruby_routes/route.rb +96 -0
- data/lib/ruby_routes/route_set.rb +80 -0
- data/lib/ruby_routes/router.rb +205 -0
- data/lib/ruby_routes/string_extensions.rb +26 -0
- data/lib/ruby_routes/url_helpers.rb +55 -0
- data/lib/ruby_routes/version.rb +3 -0
- data/lib/ruby_routes.rb +22 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d9f05718781115fc2cc1d8851ddea52d0af3fa46f7a937335398b518ea0116e9
|
4
|
+
data.tar.gz: 12b51d5138dbbdec4b46d66f20489e167a721f67f58ed80652add4a2e9aa606b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 32c7fab4ab209b6c2c1a5d50531d9f248f8413fb1409c6ebec9173eb538149c2a5f358e6bad85c0ed6993ed6b91a62a46f8d3551881ffaba4551b80df79c13a2
|
7
|
+
data.tar.gz: dd96665fc956cf1a76444e75287c2661b3629c0d0cc5546f44ed464533a50b8ff830e354d1caa83782c53855446f6e0043039ae09489fef8b5f33eeaa73329c0
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Router Gem Contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,305 @@
|
|
1
|
+
# Ruby Routes Gem
|
2
|
+
|
3
|
+
A lightweight, flexible routing system for Ruby that provides a Rails-like DSL for defining and matching HTTP routes.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Rails-like DSL**: Familiar syntax for defining routes
|
8
|
+
- **HTTP Method Support**: GET, POST, PUT, PATCH, DELETE, and custom methods
|
9
|
+
- **RESTful Resources**: Automatic generation of RESTful routes
|
10
|
+
- **Nested Routes**: Support for nested resources and namespaces
|
11
|
+
- **Route Constraints**: Add constraints to routes (regex, etc.)
|
12
|
+
- **Named Routes**: Generate URLs from route names
|
13
|
+
- **Path Generation**: Build URLs with parameters
|
14
|
+
- **Scope Support**: Group routes with common options
|
15
|
+
- **Concerns**: Reusable route groups
|
16
|
+
- **Lightweight**: Minimal dependencies, fast performance
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'ruby_routes'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
gem install ruby_routes
|
36
|
+
```
|
37
|
+
|
38
|
+
## Basic Usage
|
39
|
+
|
40
|
+
### Simple Routes
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'ruby_routes'
|
44
|
+
|
45
|
+
router = RubyRoutes.draw do
|
46
|
+
get '/', to: 'home#index'
|
47
|
+
get '/about', to: 'pages#about'
|
48
|
+
post '/users', to: 'users#create'
|
49
|
+
put '/users/:id', to: 'users#update'
|
50
|
+
delete '/users/:id', to: 'users#destroy'
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### RESTful Resources
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
router = RubyRoutes.draw do
|
58
|
+
resources :users
|
59
|
+
resources :posts
|
60
|
+
resources :comments
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
This creates the following routes:
|
65
|
+
- `GET /users` → `users#index`
|
66
|
+
- `GET /users/new` → `users#new`
|
67
|
+
- `POST /users` → `users#create`
|
68
|
+
- `GET /users/:id` → `users#show`
|
69
|
+
- `GET /users/:id/edit` → `users#edit`
|
70
|
+
- `PUT /users/:id` → `users#update`
|
71
|
+
- `PATCH /users/:id` → `users#update`
|
72
|
+
- `DELETE /users/:id` → `users#destroy`
|
73
|
+
|
74
|
+
### Named Routes
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
router = RubyRoutes.draw do
|
78
|
+
get '/users', as: :users, to: 'users#index'
|
79
|
+
get '/users/:id', as: :user, to: 'users#show'
|
80
|
+
post '/users', as: :create_user, to: 'users#create'
|
81
|
+
end
|
82
|
+
|
83
|
+
# Generate paths
|
84
|
+
router.route_set.generate_path(:users) # => "/users"
|
85
|
+
router.route_set.generate_path(:user, id: '123') # => "/users/123"
|
86
|
+
```
|
87
|
+
|
88
|
+
### Namespaces
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
router = RubyRoutes.draw do
|
92
|
+
namespace :admin do
|
93
|
+
resources :users
|
94
|
+
resources :posts
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Creates routes like:
|
99
|
+
# GET /admin/users → admin/users#index
|
100
|
+
# GET /admin/users/:id → admin/users#show
|
101
|
+
# etc.
|
102
|
+
```
|
103
|
+
|
104
|
+
### Nested Resources
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
router = RubyRoutes.draw do
|
108
|
+
resources :categories do
|
109
|
+
resources :products
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates routes like:
|
114
|
+
# GET /categories/:category_id/products → products#index
|
115
|
+
# GET /categories/:category_id/products/:id → products#show
|
116
|
+
# etc.
|
117
|
+
```
|
118
|
+
|
119
|
+
### Scopes and Constraints
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
router = RubyRoutes.draw do
|
123
|
+
scope constraints: { id: /\d+/ } do
|
124
|
+
get '/users/:id', to: 'users#show'
|
125
|
+
end
|
126
|
+
|
127
|
+
scope defaults: { format: 'html' } do
|
128
|
+
get '/posts', to: 'posts#index'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
### Concerns
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
router = RubyRoutes.draw do
|
137
|
+
concern :commentable do
|
138
|
+
resources :comments
|
139
|
+
end
|
140
|
+
|
141
|
+
resources :posts do
|
142
|
+
concerns :commentable
|
143
|
+
end
|
144
|
+
|
145
|
+
resources :articles do
|
146
|
+
concerns :commentable
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
### Root Route
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
router = RubyRoutes.draw do
|
155
|
+
root to: 'home#index'
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
## Route Matching
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
router = RubyRoutes.draw do
|
163
|
+
get '/users/:id', to: 'users#show'
|
164
|
+
post '/users', to: 'users#create'
|
165
|
+
end
|
166
|
+
|
167
|
+
# Match a request
|
168
|
+
result = router.route_set.match('GET', '/users/123')
|
169
|
+
if result
|
170
|
+
puts "Controller: #{result[:controller]}"
|
171
|
+
puts "Action: #{result[:action]}"
|
172
|
+
puts "Params: #{result[:params]}"
|
173
|
+
# => Controller: users
|
174
|
+
# => Action: show
|
175
|
+
# => Params: {"id"=>"123"}
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
## Path Generation
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
router = RubyRoutes.draw do
|
183
|
+
get '/users/:id', as: :user, to: 'users#show'
|
184
|
+
get '/posts/:id/comments/:comment_id', as: :post_comment, to: 'comments#show'
|
185
|
+
end
|
186
|
+
|
187
|
+
# Generate paths
|
188
|
+
router.route_set.generate_path(:user, id: '123')
|
189
|
+
# => "/users/123"
|
190
|
+
|
191
|
+
router.route_set.generate_path(:post_comment, id: '456', comment_id: '789')
|
192
|
+
# => "/posts/456/comments/789"
|
193
|
+
```
|
194
|
+
|
195
|
+
## Integration with Rack
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
require 'rack'
|
199
|
+
require 'ruby_routes'
|
200
|
+
|
201
|
+
# Define routes
|
202
|
+
router = RubyRoutes.draw do
|
203
|
+
get '/', to: 'home#index'
|
204
|
+
get '/users', to: 'users#index'
|
205
|
+
get '/users/:id', to: 'users#show'
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create Rack app
|
209
|
+
class RubyRoutesApp
|
210
|
+
def initialize(router)
|
211
|
+
@router = router
|
212
|
+
end
|
213
|
+
|
214
|
+
def call(env)
|
215
|
+
request_method = env['REQUEST_METHOD']
|
216
|
+
request_path = env['PATH_INFO']
|
217
|
+
|
218
|
+
route_info = @router.route_set.match(request_method, request_path)
|
219
|
+
|
220
|
+
if route_info
|
221
|
+
# Handle the request
|
222
|
+
controller = route_info[:controller]
|
223
|
+
action = route_info[:action]
|
224
|
+
params = route_info[:params]
|
225
|
+
|
226
|
+
# Your controller logic here
|
227
|
+
[200, {'Content-Type' => 'text/html'}, ["Hello from #{controller}##{action}"]]
|
228
|
+
else
|
229
|
+
[404, {'Content-Type' => 'text/html'}, ['Not Found']]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Run the app
|
235
|
+
app = RubyRoutesApp.new(router)
|
236
|
+
Rack::Handler::WEBrick.run app, Port: 9292
|
237
|
+
```
|
238
|
+
|
239
|
+
## API Reference
|
240
|
+
|
241
|
+
### RubyRoutes.draw(&block)
|
242
|
+
|
243
|
+
Creates a new router instance and yields to the block for route definition.
|
244
|
+
|
245
|
+
### HTTP Methods
|
246
|
+
|
247
|
+
- `get(path, options = {})`
|
248
|
+
- `post(path, options = {})`
|
249
|
+
- `put(path, options = {})`
|
250
|
+
- `patch(path, options = {})`
|
251
|
+
- `delete(path, options = {})`
|
252
|
+
- `match(path, options = {})`
|
253
|
+
|
254
|
+
### Resource Methods
|
255
|
+
|
256
|
+
- `resources(name, options = {})` - Creates RESTful routes for a collection
|
257
|
+
- `resource(name, options = {})` - Creates RESTful routes for a singular resource
|
258
|
+
|
259
|
+
### Options
|
260
|
+
|
261
|
+
- `to: 'controller#action'` - Specifies controller and action
|
262
|
+
- `controller: 'name'` - Specifies controller name
|
263
|
+
- `action: 'name'` - Specifies action name
|
264
|
+
- `as: :name` - Names the route for path generation
|
265
|
+
- `via: :method` - Specifies HTTP method(s)
|
266
|
+
- `constraints: {}` - Adds route constraints
|
267
|
+
- `defaults: {}` - Sets default parameters
|
268
|
+
|
269
|
+
### RouteSet Methods
|
270
|
+
|
271
|
+
- `match(method, path)` - Matches a request to a route
|
272
|
+
- `generate_path(name, params = {})` - Generates path from named route
|
273
|
+
- `find_route(method, path)` - Finds a specific route
|
274
|
+
- `find_named_route(name)` - Finds a named route
|
275
|
+
|
276
|
+
## Examples
|
277
|
+
|
278
|
+
See the `examples/` directory for more detailed examples:
|
279
|
+
|
280
|
+
- `examples/basic_usage.rb` - Basic routing examples
|
281
|
+
- `examples/rack_integration.rb` - Full Rack application example
|
282
|
+
|
283
|
+
## Testing
|
284
|
+
|
285
|
+
Run the test suite:
|
286
|
+
|
287
|
+
```bash
|
288
|
+
bundle exec rspec
|
289
|
+
```
|
290
|
+
|
291
|
+
## Contributing
|
292
|
+
|
293
|
+
1. Fork the repository
|
294
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
295
|
+
3. Commit your changes (`git commit -am 'Add some amazing feature'`)
|
296
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
297
|
+
5. Create a new Pull Request
|
298
|
+
|
299
|
+
## License
|
300
|
+
|
301
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE).
|
302
|
+
|
303
|
+
## Acknowledgments
|
304
|
+
|
305
|
+
This gem was inspired by Rails routing and aims to provide a lightweight alternative for Ruby applications that need flexible routing without the full Rails framework.
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
class Route
|
3
|
+
attr_reader :path, :methods, :controller, :action, :name, :constraints, :defaults
|
4
|
+
|
5
|
+
def initialize(path, options = {})
|
6
|
+
@path = normalize_path(path)
|
7
|
+
@methods = Array(options[:via] || :get).map(&:to_s).map(&:upcase)
|
8
|
+
@controller = extract_controller(options)
|
9
|
+
@action = options[:action] || extract_action(options[:to])
|
10
|
+
@name = options[:as]
|
11
|
+
@constraints = options[:constraints] || {}
|
12
|
+
@defaults = options[:defaults] || {}
|
13
|
+
|
14
|
+
validate_route!
|
15
|
+
end
|
16
|
+
|
17
|
+
def match?(request_method, request_path)
|
18
|
+
return false unless methods.include?(request_method.to_s.upcase)
|
19
|
+
|
20
|
+
path_params = extract_path_params(request_path)
|
21
|
+
path_params != nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract_params(request_path)
|
25
|
+
path_params = extract_path_params(request_path)
|
26
|
+
return {} unless path_params
|
27
|
+
|
28
|
+
params = path_params.dup
|
29
|
+
# Convert symbol keys to string keys for consistency
|
30
|
+
string_defaults = defaults.transform_keys(&:to_s)
|
31
|
+
params.merge!(string_defaults)
|
32
|
+
params
|
33
|
+
end
|
34
|
+
|
35
|
+
def named?
|
36
|
+
!name.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def resource?
|
40
|
+
path.match?(/\/:id$/) || path.match?(/\/:id\./)
|
41
|
+
end
|
42
|
+
|
43
|
+
def collection?
|
44
|
+
!resource?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def normalize_path(path)
|
50
|
+
path = "/#{path}" unless path.start_with?('/')
|
51
|
+
# Remove trailing slash unless it's the root path
|
52
|
+
path = path.chomp('/') unless path == '/'
|
53
|
+
path
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_controller(options)
|
57
|
+
if options[:to]
|
58
|
+
options[:to].to_s.split('#').first
|
59
|
+
else
|
60
|
+
options[:controller]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_action(to)
|
65
|
+
return nil unless to
|
66
|
+
to.to_s.split('#').last
|
67
|
+
end
|
68
|
+
|
69
|
+
def extract_path_params(request_path)
|
70
|
+
route_parts = path.split('/')
|
71
|
+
request_parts = request_path.split('/')
|
72
|
+
|
73
|
+
return nil if route_parts.length != request_parts.length
|
74
|
+
|
75
|
+
params = {}
|
76
|
+
route_parts.each_with_index do |route_part, index|
|
77
|
+
request_part = request_parts[index]
|
78
|
+
|
79
|
+
if route_part.start_with?(':')
|
80
|
+
param_name = route_part[1..-1]
|
81
|
+
params[param_name] = request_part
|
82
|
+
elsif route_part != request_part
|
83
|
+
return nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
params
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_route!
|
91
|
+
raise InvalidRoute, "Controller is required" if controller.nil?
|
92
|
+
raise InvalidRoute, "Action is required" if action.nil?
|
93
|
+
raise InvalidRoute, "Invalid HTTP method: #{methods}" if methods.empty?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
class RouteSet
|
3
|
+
attr_reader :routes
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@routes = []
|
7
|
+
@named_routes = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_route(route)
|
11
|
+
@routes << route
|
12
|
+
@named_routes[route.name] = route if route.named?
|
13
|
+
route
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_route(request_method, request_path)
|
17
|
+
@routes.find { |route| route.match?(request_method, request_path) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_named_route(name)
|
21
|
+
@named_routes[name] or raise RouteNotFound, "No route named '#{name}'"
|
22
|
+
end
|
23
|
+
|
24
|
+
def match(request_method, request_path)
|
25
|
+
route = find_route(request_method, request_path)
|
26
|
+
return nil unless route
|
27
|
+
|
28
|
+
{
|
29
|
+
route: route,
|
30
|
+
params: route.extract_params(request_path),
|
31
|
+
controller: route.controller,
|
32
|
+
action: route.action
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def recognize_path(path, method = :get)
|
37
|
+
match(method, path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_path(name, params = {})
|
41
|
+
route = find_named_route(name)
|
42
|
+
generate_path_from_route(route, params)
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_path_from_route(route, params = {})
|
46
|
+
path = route.path.dup
|
47
|
+
|
48
|
+
params.each do |key, value|
|
49
|
+
path.gsub!(":#{key}", value.to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove any remaining :param placeholders
|
53
|
+
path.gsub!(/\/:[^\/]+/, '')
|
54
|
+
path.gsub!(/\/$/, '') if path != '/'
|
55
|
+
|
56
|
+
path
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear!
|
60
|
+
@routes.clear
|
61
|
+
@named_routes.clear
|
62
|
+
end
|
63
|
+
|
64
|
+
def size
|
65
|
+
@routes.size
|
66
|
+
end
|
67
|
+
|
68
|
+
def empty?
|
69
|
+
@routes.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def each(&block)
|
73
|
+
@routes.each(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def include?(route)
|
77
|
+
@routes.include?(route)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
class Router
|
3
|
+
attr_reader :route_set
|
4
|
+
|
5
|
+
def initialize(&block)
|
6
|
+
@route_set = RouteSet.new
|
7
|
+
@scope_stack = []
|
8
|
+
instance_eval(&block) if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Basic route definition
|
12
|
+
def get(path, options = {})
|
13
|
+
add_route(path, options.merge(via: :get))
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(path, options = {})
|
17
|
+
add_route(path, options.merge(via: :post))
|
18
|
+
end
|
19
|
+
|
20
|
+
def put(path, options = {})
|
21
|
+
add_route(path, options.merge(via: :put))
|
22
|
+
end
|
23
|
+
|
24
|
+
def patch(path, options = {})
|
25
|
+
add_route(path, options.merge(via: :patch))
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(path, options = {})
|
29
|
+
add_route(path, options.merge(via: :delete))
|
30
|
+
end
|
31
|
+
|
32
|
+
def match(path, options = {})
|
33
|
+
add_route(path, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Resources routing (Rails-like)
|
37
|
+
def resources(name, options = {}, &block)
|
38
|
+
singular = name.to_s.singularize
|
39
|
+
plural = name.to_s.pluralize
|
40
|
+
|
41
|
+
# Collection routes
|
42
|
+
get "/#{plural}", options.merge(to: "#{plural}#index")
|
43
|
+
get "/#{plural}/new", options.merge(to: "#{plural}#new")
|
44
|
+
post "/#{plural}", options.merge(to: "#{plural}#create")
|
45
|
+
|
46
|
+
# Member routes
|
47
|
+
get "/#{plural}/:id", options.merge(to: "#{plural}#show")
|
48
|
+
get "/#{plural}/:id/edit", options.merge(to: "#{plural}#edit")
|
49
|
+
put "/#{plural}/:id", options.merge(to: "#{plural}#update")
|
50
|
+
patch "/#{plural}/:id", options.merge(to: "#{plural}#update")
|
51
|
+
delete "/#{plural}/:id", options.merge(to: "#{plural}#destroy")
|
52
|
+
|
53
|
+
# Nested resources if specified
|
54
|
+
if options[:nested]
|
55
|
+
nested_name = options[:nested]
|
56
|
+
nested_singular = nested_name.to_s.singularize
|
57
|
+
nested_plural = nested_name.to_s.pluralize
|
58
|
+
|
59
|
+
get "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#index")
|
60
|
+
get "/#{plural}/:id/#{nested_plural}/new", options.merge(to: "#{nested_plural}#new")
|
61
|
+
post "/#{plural}/:id/#{nested_plural}", options.merge(to: "#{nested_plural}#create")
|
62
|
+
get "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#show")
|
63
|
+
get "/#{plural}/:id/#{nested_plural}/:nested_id/edit", options.merge(to: "#{nested_plural}#edit")
|
64
|
+
put "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
65
|
+
patch "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
66
|
+
delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#update")
|
67
|
+
delete "/#{plural}/:id/#{nested_plural}/:nested_id", options.merge(to: "#{nested_plural}#destroy")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Handle concerns if block is given
|
71
|
+
if block_given?
|
72
|
+
# Push a scope for nested resources
|
73
|
+
@scope_stack.push({ path: "/#{plural}/:id" })
|
74
|
+
# Execute the block in the context of this router instance
|
75
|
+
instance_eval(&block)
|
76
|
+
@scope_stack.pop
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def resource(name, options = {})
|
81
|
+
singular = name.to_s.singularize
|
82
|
+
|
83
|
+
get "/#{singular}", options.merge(to: "#{singular}#show")
|
84
|
+
get "/#{singular}/new", options.merge(to: "#{singular}#new")
|
85
|
+
post "/#{singular}", options.merge(to: "#{singular}#create")
|
86
|
+
get "/#{singular}/edit", options.merge(to: "#{singular}#edit")
|
87
|
+
put "/#{singular}", options.merge(to: "#{singular}#update")
|
88
|
+
patch "/#{singular}", options.merge(to: "#{singular}#update")
|
89
|
+
delete "/#{singular}", options.merge(to: "#{singular}#destroy")
|
90
|
+
end
|
91
|
+
|
92
|
+
# Namespace support
|
93
|
+
def namespace(name, options = {}, &block)
|
94
|
+
@scope_stack.push({ path: "/#{name}", module: name })
|
95
|
+
|
96
|
+
if block_given?
|
97
|
+
instance_eval(&block)
|
98
|
+
end
|
99
|
+
|
100
|
+
@scope_stack.pop
|
101
|
+
end
|
102
|
+
|
103
|
+
# Scope support
|
104
|
+
def scope(options = {}, &block)
|
105
|
+
@scope_stack.push(options)
|
106
|
+
|
107
|
+
if block_given?
|
108
|
+
instance_eval(&block)
|
109
|
+
end
|
110
|
+
|
111
|
+
@scope_stack.pop
|
112
|
+
end
|
113
|
+
|
114
|
+
# Root route
|
115
|
+
def root(options = {})
|
116
|
+
add_route("/", options.merge(via: :get))
|
117
|
+
end
|
118
|
+
|
119
|
+
# Concerns (reusable route groups)
|
120
|
+
def concerns(*names, &block)
|
121
|
+
names.each do |name|
|
122
|
+
concern = @concerns[name]
|
123
|
+
raise "Concern '#{name}' not found" unless concern
|
124
|
+
|
125
|
+
instance_eval(&concern)
|
126
|
+
end
|
127
|
+
|
128
|
+
if block_given?
|
129
|
+
instance_eval(&block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def concern(name, &block)
|
134
|
+
@concerns ||= {}
|
135
|
+
@concerns[name] = block
|
136
|
+
end
|
137
|
+
|
138
|
+
# Route constraints
|
139
|
+
def constraints(constraints = {}, &block)
|
140
|
+
@scope_stack.push({ constraints: constraints })
|
141
|
+
|
142
|
+
if block_given?
|
143
|
+
instance_eval(&block)
|
144
|
+
end
|
145
|
+
|
146
|
+
@scope_stack.pop
|
147
|
+
end
|
148
|
+
|
149
|
+
# Defaults
|
150
|
+
def defaults(defaults = {}, &block)
|
151
|
+
@scope_stack.push({ defaults: defaults })
|
152
|
+
|
153
|
+
if block_given?
|
154
|
+
instance_eval(&block)
|
155
|
+
end
|
156
|
+
|
157
|
+
@scope_stack.pop
|
158
|
+
end
|
159
|
+
|
160
|
+
# Mount other applications
|
161
|
+
def mount(app, at: nil)
|
162
|
+
path = at || "/#{app}"
|
163
|
+
add_route("#{path}/*path", to: app, via: :all)
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def add_route(path, options = {})
|
169
|
+
# Apply current scope
|
170
|
+
scoped_options = apply_scope(path, options)
|
171
|
+
|
172
|
+
# Create and add the route
|
173
|
+
route = Route.new(scoped_options[:path], scoped_options)
|
174
|
+
@route_set.add_route(route)
|
175
|
+
route
|
176
|
+
end
|
177
|
+
|
178
|
+
def apply_scope(path, options)
|
179
|
+
scoped_options = options.dup
|
180
|
+
scoped_path = path
|
181
|
+
|
182
|
+
@scope_stack.reverse_each do |scope|
|
183
|
+
if scope[:path]
|
184
|
+
scoped_path = "#{scope[:path]}#{scoped_path}"
|
185
|
+
end
|
186
|
+
|
187
|
+
if scope[:module] && scoped_options[:to]
|
188
|
+
controller = scoped_options[:to].to_s.split('#').first
|
189
|
+
scoped_options[:to] = "#{scope[:module]}/#{controller}##{scoped_options[:to].to_s.split('#').last}"
|
190
|
+
end
|
191
|
+
|
192
|
+
if scope[:constraints]
|
193
|
+
scoped_options[:constraints] = (scoped_options[:constraints] || {}).merge(scope[:constraints])
|
194
|
+
end
|
195
|
+
|
196
|
+
if scope[:defaults]
|
197
|
+
scoped_options[:defaults] = (scoped_options[:defaults] || {}).merge(scope[:defaults])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
scoped_options[:path] = scoped_path
|
202
|
+
scoped_options
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class String
|
2
|
+
def singularize
|
3
|
+
case self
|
4
|
+
when /ies$/
|
5
|
+
self.sub(/ies$/, 'y')
|
6
|
+
when /s$/
|
7
|
+
self.sub(/s$/, '')
|
8
|
+
else
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def pluralize
|
14
|
+
case self
|
15
|
+
when /y$/
|
16
|
+
self.sub(/y$/, 'ies')
|
17
|
+
when /sh$/, /ch$/, /x$/, /z$/
|
18
|
+
self + 'es'
|
19
|
+
when /s$/
|
20
|
+
# Words ending in 's' are already plural
|
21
|
+
self
|
22
|
+
else
|
23
|
+
self + 's'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module RubyRoutes
|
2
|
+
module UrlHelpers
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def url_helpers
|
9
|
+
@url_helpers ||= Module.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_url_helper(name, route)
|
13
|
+
url_helpers.define_method(name) do |*args|
|
14
|
+
params = args.first || {}
|
15
|
+
route_set.generate_path_from_route(route, params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def url_helpers
|
21
|
+
self.class.url_helpers
|
22
|
+
end
|
23
|
+
|
24
|
+
def path_to(name, params = {})
|
25
|
+
route = route_set.find_named_route(name)
|
26
|
+
route_set.generate_path_from_route(route, params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def url_to(name, params = {})
|
30
|
+
path = path_to(name, params)
|
31
|
+
"http://localhost#{path}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def link_to(name, text, params = {})
|
35
|
+
path = path_to(name, params)
|
36
|
+
"<a href=\"#{path}\">#{text}</a>"
|
37
|
+
end
|
38
|
+
|
39
|
+
def button_to(name, text, params = {})
|
40
|
+
path = path_to(name, params)
|
41
|
+
method = params.delete(:method) || :post
|
42
|
+
|
43
|
+
html = "<form action=\"#{path}\" method=\"#{method}\">"
|
44
|
+
html += "<input type=\"hidden\" name=\"_method\" value=\"#{method}\">" if method != :get
|
45
|
+
html += "<button type=\"submit\">#{text}</button>"
|
46
|
+
html += "</form>"
|
47
|
+
html
|
48
|
+
end
|
49
|
+
|
50
|
+
def redirect_to(name, params = {})
|
51
|
+
path = path_to(name, params)
|
52
|
+
{ status: 302, location: path }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/ruby_routes.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative "ruby_routes/version"
|
2
|
+
require_relative "ruby_routes/string_extensions"
|
3
|
+
require_relative "ruby_routes/route"
|
4
|
+
require_relative "ruby_routes/route_set"
|
5
|
+
require_relative "ruby_routes/url_helpers"
|
6
|
+
require_relative "ruby_routes/router"
|
7
|
+
|
8
|
+
module RubyRoutes
|
9
|
+
class Error < StandardError; end
|
10
|
+
class RouteNotFound < Error; end
|
11
|
+
class InvalidRoute < Error; end
|
12
|
+
|
13
|
+
# Create a new router instance
|
14
|
+
def self.new(&block)
|
15
|
+
RubyRoutes::Router.new(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Define the routes using a block
|
19
|
+
def self.draw(&block)
|
20
|
+
RubyRoutes::Router.new(&block)
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_routes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yosef Benny Widyokarsono
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rspec
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '3.12'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '3.12'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rake
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '13.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '13.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: simplecov
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.22'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.22'
|
54
|
+
description: A lightweight, flexible routing system that provides a Rails-like DSL
|
55
|
+
for defining and matching HTTP routes
|
56
|
+
email:
|
57
|
+
- yosefbennywidyo@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- LICENSE
|
63
|
+
- README.md
|
64
|
+
- lib/ruby_routes.rb
|
65
|
+
- lib/ruby_routes/route.rb
|
66
|
+
- lib/ruby_routes/route_set.rb
|
67
|
+
- lib/ruby_routes/router.rb
|
68
|
+
- lib/ruby_routes/string_extensions.rb
|
69
|
+
- lib/ruby_routes/url_helpers.rb
|
70
|
+
- lib/ruby_routes/version.rb
|
71
|
+
homepage: https://github.com/yosefbennywidyo/ruby_routes
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata: {}
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.4.2
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.7.1
|
90
|
+
specification_version: 4
|
91
|
+
summary: A Rails-like routing system for Ruby
|
92
|
+
test_files: []
|