hanami-router 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ module Hanami
2
+ module Routing
3
+ class Resource
4
+ # Helper class to calculate nested path
5
+ #
6
+ # @api private
7
+ # @since 0.4.0
8
+ class Nested
9
+ # @api private
10
+ # @since 0.4.0
11
+ SEPARATOR = '/'.freeze
12
+
13
+ # @api private
14
+ # @since 0.4.0
15
+ def initialize(resource_name, resource)
16
+ @resource_name = resource_name.to_s.split(SEPARATOR)
17
+ @resource = resource
18
+ @path = []
19
+ _calculate(@resource_name.dup, @resource)
20
+ end
21
+
22
+ # @api private
23
+ # @since 0.4.0
24
+ def to_path
25
+ @path.reverse!.pop
26
+ @resource_name.zip(@path).flatten.join
27
+ end
28
+
29
+ private
30
+
31
+ def _calculate(param_wildcard, resource = nil)
32
+ return if resource.nil?
33
+ @path << resource.wildcard_param(param_wildcard.pop)
34
+ _calculate(param_wildcard, resource.parent)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ module Hanami
2
+ module Routing
3
+ class Resource
4
+ # Options for RESTFul resource(s)
5
+ #
6
+ # @api private
7
+ # @since 0.1.0
8
+ #
9
+ # @see Hanami::Router#resource
10
+ # @see Hanami::Router#resources
11
+ class Options
12
+ # @api private
13
+ # @since 0.1.0
14
+ attr_reader :actions
15
+
16
+ # Initialize the options for:
17
+ # * Hanami::Router#resource
18
+ # * Hanami::Router#resources
19
+ #
20
+ # @param actions [Array<Symbol>] the name of the actions
21
+ # @param options [Hash]
22
+ # @option options [Hash] :only white list of the default actions
23
+ # @option options [Hash] :except black list of the default actions
24
+ # @option options [String] :controller namespace for an actions
25
+ #
26
+ # @api private
27
+ # @since 0.1.0
28
+ #
29
+ # @see Hanami::Routing::Resource
30
+ # @see Hanami::Routing::Resources
31
+ #
32
+ # @example
33
+ # require 'hanami/router'
34
+ #
35
+ # Hanami::Router.new do
36
+ # resources 'articles', only: [:index]
37
+ # resource 'profile', except: [:new, :create, :destroy]
38
+ # end
39
+ def initialize(actions, options = {})
40
+ only = Array(options.delete(:only) || actions)
41
+ except = Array(options.delete(:except))
42
+ @actions = ( actions & only ) - except
43
+
44
+ @options = options
45
+ end
46
+
47
+ # Return the option for the given key
48
+ #
49
+ # @param key [Symbol] the key that should be searched
50
+ #
51
+ # @return [Object,nil] returns the object associated to the given key
52
+ # or nil, if missing.
53
+ #
54
+ # @api private
55
+ # @since 0.1.1
56
+ def [](key)
57
+ @options[key]
58
+ end
59
+
60
+ # Merge the current options with the given hash, without mutating self.
61
+ #
62
+ # @param hash [Hash] the hash to be merged
63
+ #
64
+ # @return [Hash] the result of the merging operation
65
+ #
66
+ # @api private
67
+ # @since 0.1.1
68
+ def merge(hash)
69
+ @options.merge(hash)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ require 'hanami/routing/resource'
2
+ require 'hanami/routing/resources/action'
3
+
4
+ module Hanami
5
+ module Routing
6
+ # Set of RESTful resources routes
7
+ # Implementation of Hanami::Router#resources
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @api private
12
+ #
13
+ # @see Hanami::Router#resources
14
+ class Resources < Resource
15
+ # Set of default routes
16
+ #
17
+ # @api private
18
+ # @since 0.1.0
19
+ self.actions = [:index, :new, :create, :show, :edit, :update, :destroy]
20
+
21
+ # Action class
22
+ #
23
+ # @api private
24
+ # @since 0.1.0
25
+ self.action = Resources::Action
26
+
27
+ # Member action class
28
+ #
29
+ # @api private
30
+ # @since 0.1.0
31
+ self.member = Resources::MemberAction
32
+
33
+ # Collection action class
34
+ #
35
+ # @api private
36
+ # @since 0.1.0
37
+ self.collection = Resources::CollectionAction
38
+
39
+ # Return wildcard param between separators
40
+ #
41
+ # @api private
42
+ # @since 0.4.0
43
+ def wildcard_param(route_param = nil)
44
+ "/:#{ Hanami::Utils::String.new(route_param).singularize }_id/"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,150 @@
1
+ require 'hanami/utils/string'
2
+ require 'hanami/utils/path_prefix'
3
+ require 'hanami/routing/resource'
4
+
5
+ module Hanami
6
+ module Routing
7
+ class Resources < Resource
8
+ # Action for RESTful resources
9
+ #
10
+ # @since 0.1.0
11
+ #
12
+ # @api private
13
+ #
14
+ # @see Hanami::Router#resources
15
+ class Action < Resource::Action
16
+ # Ruby namespace where lookup for default subclasses.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ self.namespace = Resources
21
+
22
+ # Id route variable
23
+ #
24
+ # @since 0.2.0
25
+ # @api private
26
+ class_attribute :identifier
27
+ self.identifier = ':id'.freeze
28
+ end
29
+
30
+ # Pluralize concrete actions
31
+ #
32
+ # @api private
33
+ # @since 0.4.0
34
+ module PluralizedAction
35
+ private
36
+ # The name of the RESTful action.
37
+ #
38
+ # @api private
39
+ # @since 0.4.0
40
+ def as
41
+ Hanami::Utils::String.new(super).pluralize
42
+ end
43
+ end
44
+
45
+ # Collection action
46
+ # It implements #collection within a #resources block.
47
+ #
48
+ # @api private
49
+ # @since 0.1.0
50
+ # @see Hanami::Router#resources
51
+ class CollectionAction < Resource::CollectionAction
52
+ def as(action_name)
53
+ Hanami::Utils::String.new(super(action_name)).pluralize
54
+ end
55
+ end
56
+
57
+ # Member action
58
+ # It implements #member within a #resources block.
59
+ #
60
+ # @api private
61
+ # @since 0.1.0
62
+ # @see Hanami::Router#resources
63
+ class MemberAction < Resource::MemberAction
64
+ private
65
+ def path(action_name)
66
+ rest_path.join(Action.identifier, action_name)
67
+ end
68
+ end
69
+
70
+ # Implementation of common methods for concrete member actions
71
+ #
72
+ # @api private
73
+ # @since 0.1.0
74
+ module DefaultMemberAction
75
+ private
76
+ def path
77
+ rest_path.join(Action.identifier)
78
+ end
79
+ end
80
+
81
+ # Index action
82
+ #
83
+ # @api private
84
+ # @since 0.1.0
85
+ # @see Hanami::Router#resources
86
+ class Index < Action
87
+ include PluralizedAction
88
+ self.verb = :get
89
+ end
90
+
91
+ # New action
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ # @see Hanami::Router#resources
96
+ class New < Resource::New
97
+ end
98
+
99
+ # Create action
100
+ #
101
+ # @api private
102
+ # @since 0.1.0
103
+ # @see Hanami::Router#resources
104
+ class Create < Resource::Create
105
+ include PluralizedAction
106
+ end
107
+
108
+ # Show action
109
+ #
110
+ # @api private
111
+ # @since 0.1.0
112
+ # @see Hanami::Router#resources
113
+ class Show < Resource::Show
114
+ include DefaultMemberAction
115
+ end
116
+
117
+ # Edit action
118
+ #
119
+ # @api private
120
+ # @since 0.1.0
121
+ # @see Hanami::Router#resources
122
+ class Edit < Resource::Edit
123
+ include DefaultMemberAction
124
+
125
+ private
126
+ def path
127
+ super.join(action_name)
128
+ end
129
+ end
130
+
131
+ # Update action
132
+ #
133
+ # @api private
134
+ # @since 0.1.0
135
+ # @see Hanami::Router#resources
136
+ class Update < Resource::Update
137
+ include DefaultMemberAction
138
+ end
139
+
140
+ # Destroy action
141
+ #
142
+ # @api private
143
+ # @since 0.1.0
144
+ # @see Hanami::Router#resources
145
+ class Destroy < Resource::Destroy
146
+ include DefaultMemberAction
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,62 @@
1
+ require 'http_router/route'
2
+
3
+ module Hanami
4
+ module Routing
5
+ # Entry of the routing system
6
+ #
7
+ # @api private
8
+ #
9
+ # @since 0.1.0
10
+ #
11
+ # @see http://rdoc.info/gems/http_router/HttpRouter/Route
12
+ #
13
+ # @example
14
+ # require 'hanami/router'
15
+ #
16
+ # router = Hanami::Router.new
17
+ # router.get('/', to: endpoint) # => #<Hanami::Routing::Route:0x007f83083ba028 ...>
18
+ class Route < HttpRouter::Route
19
+ # Asks the given resolver to return an endpoint that will be associated
20
+ # with the other options.
21
+ #
22
+ # @param resolver [Hanami::Routing::EndpointResolver, #resolve] this may change
23
+ # according to the :resolve option passed to Hanami::Router#initialize.
24
+ #
25
+ # @param options [Hash] options to customize the route
26
+ # @option options [Symbol] :as the name we want to use for the route
27
+ #
28
+ # @since 0.1.0
29
+ #
30
+ # @api private
31
+ #
32
+ # @see Hanami::Router#initialize
33
+ #
34
+ # @example
35
+ # require 'hanami/router'
36
+ #
37
+ # router = Hanami::Router.new
38
+ # router.get('/', to: endpoint, as: :home_page).name # => :home_page
39
+ #
40
+ # router.path(:home_page) # => '/'
41
+ def generate(resolver, options = {}, &endpoint)
42
+ self.to = resolver.resolve(options, &endpoint)
43
+ self.name = options[:as].to_sym if options[:as]
44
+ self
45
+ end
46
+
47
+ # Introspect the given route to understand if there is a wrapped
48
+ # Hanami::Router
49
+ #
50
+ # @since 0.2.0
51
+ # @api private
52
+ def nested_router
53
+ dest.routes if dest.respond_to?(:routes)
54
+ end
55
+
56
+ private
57
+ def to=(dest = nil, &blk)
58
+ self.to dest, &blk
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,215 @@
1
+ require 'hanami/utils/path_prefix'
2
+
3
+ module Hanami
4
+ module Routing
5
+ # Routes inspector
6
+ #
7
+ # @since 0.2.0
8
+ class RoutesInspector
9
+ # Default route formatter
10
+ #
11
+ # @since 0.2.0
12
+ # @api private
13
+ FORMATTER = "%<name>20s %<methods>-10s %<path>-30s %<endpoint>-30s\n".freeze
14
+
15
+ # Default HTTP methods separator
16
+ #
17
+ # @since 0.2.0
18
+ # @api private
19
+ HTTP_METHODS_SEPARATOR = ', '.freeze
20
+
21
+ # Default inspector header hash values
22
+ #
23
+ # @since 0.5.0
24
+ # @api private
25
+ INSPECTOR_HEADER_HASH = Hash[
26
+ name: 'Name',
27
+ methods: 'Method',
28
+ path: 'Path',
29
+ endpoint: 'Action'
30
+ ].freeze
31
+
32
+ # Default inspector header name values
33
+ #
34
+ # @since 0.5.0
35
+ # @api private
36
+ INSPECTOR_HEADER_NAME = 'Name'.freeze
37
+
38
+ # Empty line string
39
+ #
40
+ # @since 0.5.0
41
+ # @api private
42
+ EMPTY_LINE = "\n".freeze
43
+
44
+ # Instantiate a new inspector
45
+ #
46
+ # @return [Hanami::Routing::RoutesInspector] the new instance
47
+ #
48
+ # @since 0.2.0
49
+ # @api private
50
+ def initialize(routes)
51
+ @routes = routes
52
+ end
53
+
54
+ # Return a formatted string that describes all the routes
55
+ #
56
+ # @param formatter [String] the optional formatter for a route
57
+ # @param base_path [String] the base path of a nested route
58
+ #
59
+ # @return [String] routes pretty print
60
+ #
61
+ # @since 0.2.0
62
+ #
63
+ # @see Hanami::Routing::RoutesInspector::FORMATTER
64
+ #
65
+ # @example Default formatter
66
+ # require 'hanami/router'
67
+ #
68
+ # router = Hanami::Router.new do
69
+ # get '/', to: 'home#index'
70
+ # get '/login', to: 'sessions#new', as: :login
71
+ # post '/login', to: 'sessions#create'
72
+ # delete '/logout', to: 'sessions#destroy', as: :logout
73
+ # end
74
+ #
75
+ # puts router.inspector.to_s
76
+ # # => Name Method Path Action
77
+ #
78
+ # GET, HEAD / Home::Index
79
+ # login GET, HEAD /login Sessions::New
80
+ # POST /login Sessions::Create
81
+ # logout GET, HEAD /logout Sessions::Destroy
82
+ #
83
+ # @example Custom formatter
84
+ # require 'hanami/router'
85
+ #
86
+ # router = Hanami::Router.new do
87
+ # get '/', to: 'home#index'
88
+ # get '/login', to: 'sessions#new', as: :login
89
+ # post '/login', to: 'sessions#create'
90
+ # delete '/logout', to: 'sessions#destroy', as: :logout
91
+ # end
92
+ #
93
+ # formatter = "| %{methods} | %{name} | %{path} | %{endpoint} |\n"
94
+ #
95
+ # puts router.inspector.to_s(formatter)
96
+ # # => | Method | Name | Path | Action |
97
+ #
98
+ # | GET, HEAD | | / | Home::Index |
99
+ # | GET, HEAD | login | /login | Sessions::New |
100
+ # | POST | | /login | Sessions::Create |
101
+ # | GET, HEAD | logout | /logout | Sessions::Destroy |
102
+ #
103
+ # @example Nested routes
104
+ # require 'hanami/router'
105
+ #
106
+ # class AdminHanamiApp
107
+ # def call(env)
108
+ # end
109
+ # def routes
110
+ # Hanami::Router.new {
111
+ # get '/home', to: 'home#index'
112
+ # }
113
+ # end
114
+ # end
115
+ #
116
+ # router = Hanami::Router.new {
117
+ # get '/fakeroute', to: 'fake#index'
118
+ # mount AdminHanamiApp, at: '/admin'
119
+ # mount Hanami::Router.new {
120
+ # get '/posts', to: 'posts#index'
121
+ # mount Hanami::Router.new {
122
+ # get '/comments', to: 'comments#index'
123
+ # }, at: '/second_mount'
124
+ # }, at: '/api'
125
+ # }
126
+ #
127
+ # formatter = "| %{methods} | %{name} | %{path} | %{endpoint} |\n"
128
+ #
129
+ # puts router.inspector.to_s(formatter)
130
+ # # => | Method | Name | Path | Action |
131
+ #
132
+ # | GET, HEAD | | /fakeroute | Fake::Index |
133
+ # | GET, HEAD | | /admin/home | Home::Index |
134
+ # | GET, HEAD | | /api/posts | Posts::Index |
135
+ # | GET, HEAD | | /api/second_mount/comments | Comments::Index |
136
+ def to_s(formatter = FORMATTER, base_path = nil)
137
+ base_path = Utils::PathPrefix.new(base_path)
138
+
139
+ inspect_routes(formatter, base_path)
140
+ .insert(0, formatter % INSPECTOR_HEADER_HASH + EMPTY_LINE)
141
+ end
142
+
143
+ # Returns a string representation of routes
144
+ #
145
+ # @param formatter [String] the template for the output
146
+ # @param base_path [Hanami::Utils::PathPrefix] the base path
147
+ #
148
+ # @return [String] serialized routes from router
149
+ #
150
+ # @since 0.5.1
151
+ # @api private
152
+ #
153
+ # @see Hanami::Routing::RoutesInspector#FORMATTER
154
+ # @see Hanami::Routing::RoutesInspector#to_s
155
+ def inspect_routes(formatter, base_path)
156
+ result = ''
157
+
158
+ # TODO refactoring: replace conditional with polymorphism
159
+ # We're exposing too much knowledge from Routing::Route:
160
+ # #path_for_generation and #base_path
161
+ @routes.each do |route|
162
+ result << if router = route.nested_router
163
+ inspect_router(formatter, router, route, base_path)
164
+ else
165
+ inspect_route(formatter, route, base_path)
166
+ end
167
+ end
168
+
169
+ result
170
+ end
171
+
172
+ private
173
+
174
+ # Returns a string representation of the given route
175
+ #
176
+ # @param formatter [String] the template for the output
177
+ # @param route [Hanami::Routing::Route] a route
178
+ # @param base_path [Hanami::Utils::PathPrefix] the base path
179
+ #
180
+ # @return [String] serialized route
181
+ #
182
+ # @since 0.2.0
183
+ # @api private
184
+ #
185
+ # @see Hanami::Routing::RoutesInspector#FORMATTER
186
+ # @see Hanami::Routing::RoutesInspector#to_s
187
+ def inspect_route(formatter, route, base_path)
188
+ formatter % Hash[
189
+ name: route.name,
190
+ methods: route.request_methods.to_a.join(HTTP_METHODS_SEPARATOR),
191
+ path: base_path.join(route.path_for_generation),
192
+ endpoint: route.dest.inspect
193
+ ]
194
+ end
195
+
196
+ # Returns a string representation of the given router
197
+ #
198
+ # @param formatter [String] the template for the output
199
+ # @param router [Hanami::Router] a router
200
+ # @param route [Hanami::Routing::Route] a route
201
+ # @param base_path [Hanami::Utils::PathPrefix] the base path
202
+ #
203
+ # @return [String] serialized routes from router
204
+ #
205
+ # @since 0.2.0
206
+ # @api private
207
+ #
208
+ # @see Hanami::Routing::RoutesInspector#FORMATTER
209
+ # @see Hanami::Routing::RoutesInspector#to_s
210
+ def inspect_router(formatter, router, route, base_path)
211
+ router.inspector.inspect_routes(formatter, base_path.join(route.path_for_generation))
212
+ end
213
+ end
214
+ end
215
+ end