signpost 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/lib/signpost.rb +29 -0
- data/lib/signpost/builder.rb +416 -0
- data/lib/signpost/builder/namespace.rb +13 -0
- data/lib/signpost/builder/nested.rb +34 -0
- data/lib/signpost/endpoint/action.rb +54 -0
- data/lib/signpost/endpoint/builder.rb +22 -0
- data/lib/signpost/endpoint/dynamic.rb +24 -0
- data/lib/signpost/endpoint/redirect.rb +65 -0
- data/lib/signpost/endpoint/resolver.rb +144 -0
- data/lib/signpost/middleware.rb +17 -0
- data/lib/signpost/route/nested.rb +20 -0
- data/lib/signpost/route/simple.rb +89 -0
- data/lib/signpost/router.rb +59 -0
- data/lib/signpost/sign/flat.rb +79 -0
- data/lib/signpost/sign/flat/path.rb +114 -0
- data/lib/signpost/sign/flat/redirect.rb +60 -0
- data/lib/signpost/sign/namespace.rb +11 -0
- data/lib/signpost/sign/nested.rb +39 -0
- data/lib/signpost/version.rb +13 -0
- metadata +92 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
class Signpost
|
2
|
+
class Builder
|
3
|
+
class Nested < self
|
4
|
+
attr_reader :builders
|
5
|
+
|
6
|
+
##
|
7
|
+
# Add middleware to routes
|
8
|
+
#
|
9
|
+
# Params:
|
10
|
+
# - middleware {String|Class} middleware or middleware name
|
11
|
+
# - *args middleware arguments which will be used for instantiating
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# use Rack::Errors
|
16
|
+
# use AuthMiddleware, 'admin', 'seCrEt'
|
17
|
+
#
|
18
|
+
def use(middleware, *args, &block)
|
19
|
+
@options[:middlewares] << Middleware.new(middleware, args, block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build
|
23
|
+
Router.new(@builders, @options)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def root_name
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Signpost
|
2
|
+
module Endpoint
|
3
|
+
class Action
|
4
|
+
|
5
|
+
class Context
|
6
|
+
|
7
|
+
attr_reader :params, :env, :headers
|
8
|
+
|
9
|
+
def body(text)
|
10
|
+
@body = [text]
|
11
|
+
end
|
12
|
+
|
13
|
+
def status(code)
|
14
|
+
@status = code.to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
def expand(path, data={})
|
18
|
+
@router.expand(path, data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def result
|
22
|
+
[@status, @headers, @body]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize(block, router, env, params)
|
28
|
+
@router = router
|
29
|
+
@env = env
|
30
|
+
@params = params
|
31
|
+
|
32
|
+
@status = 200
|
33
|
+
@headers = {}
|
34
|
+
@body = []
|
35
|
+
|
36
|
+
instance_eval(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
Context.new(@block, @router, env, env[@params_key]).result
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def initialize(block, router)
|
47
|
+
@block = block
|
48
|
+
@router = router
|
49
|
+
@params_key = router.params_key
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Signpost
|
2
|
+
module Endpoint
|
3
|
+
class Builder
|
4
|
+
|
5
|
+
##
|
6
|
+
# Build middleware stack
|
7
|
+
#
|
8
|
+
# Returns: {Proc}
|
9
|
+
#
|
10
|
+
def build
|
11
|
+
@middlewares.reverse_each.inject(@endpoint) { |app, m| m.middleware.new(app, *m.args, &m.block) }
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def initialize(endpoint, middlewares)
|
17
|
+
@endpoint = endpoint
|
18
|
+
@middlewares = middlewares
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Signpost
|
2
|
+
module Endpoint
|
3
|
+
class Dynamic
|
4
|
+
def call(env)
|
5
|
+
controller, action = env[@params_key].values_at('controller', 'action')
|
6
|
+
|
7
|
+
resolver = Resolver.new({
|
8
|
+
controller: @spec[:controller] || controller,
|
9
|
+
action: @spec[:action] || action
|
10
|
+
}, @options).resolve
|
11
|
+
|
12
|
+
resolver.endpoint.call(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize(options, spec={})
|
18
|
+
@options = options
|
19
|
+
@spec = spec
|
20
|
+
@params_key = options[:params_key]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Signpost
|
2
|
+
module Endpoint
|
3
|
+
class Redirect
|
4
|
+
|
5
|
+
class Context
|
6
|
+
|
7
|
+
attr_reader :params, :env
|
8
|
+
|
9
|
+
def expand(path, data={})
|
10
|
+
@router.expand(path, data)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(router, env, params)
|
16
|
+
@router = router
|
17
|
+
@env = env
|
18
|
+
@params = params
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :status
|
23
|
+
|
24
|
+
CODES = {
|
25
|
+
permanent: 301,
|
26
|
+
temporary: 303
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
[status, headers(env), body]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def initialize(to, router, status=303)
|
36
|
+
@to = to
|
37
|
+
@status = status.is_a?(Fixnum) ? status : CODES[status.to_sym]
|
38
|
+
@router = router
|
39
|
+
@params_key = router.params_key
|
40
|
+
end
|
41
|
+
|
42
|
+
def headers(env)
|
43
|
+
{ 'Location' => to(env) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def to(env)
|
47
|
+
params = env[@params_key]
|
48
|
+
|
49
|
+
case @to
|
50
|
+
when Proc
|
51
|
+
Context.new(@router, env, params).instance_exec(&@to)
|
52
|
+
when Symbol
|
53
|
+
params = @router.expand(@to, params)
|
54
|
+
else
|
55
|
+
@to.expand(params)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def body
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
class Signpost
|
2
|
+
module Endpoint
|
3
|
+
UnresolvedError = Class.new(StandardError)
|
4
|
+
|
5
|
+
class Resolver
|
6
|
+
##
|
7
|
+
# Class: result of resolving
|
8
|
+
#
|
9
|
+
# Here is `#endpoint` is an endpoint class and `#params` is a hash
|
10
|
+
# with controller and action names
|
11
|
+
#
|
12
|
+
Result = Struct.new(:endpoint, :params)
|
13
|
+
|
14
|
+
##
|
15
|
+
# Resolve endpoint and params
|
16
|
+
#
|
17
|
+
# Returns: {Signpost::Endpoint::Resolver::Result}
|
18
|
+
#
|
19
|
+
def resolve
|
20
|
+
case spec
|
21
|
+
when Hash
|
22
|
+
params = resolve_hash(spec[:controller], spec[:action])
|
23
|
+
when String
|
24
|
+
params = resolve_string
|
25
|
+
else
|
26
|
+
endpoint = spec
|
27
|
+
params = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
unless endpoint
|
31
|
+
if params[:action].kind_of?(Class)
|
32
|
+
endpoint = params[:action]
|
33
|
+
elsif params[:controller]
|
34
|
+
endpoint = params[:controller]
|
35
|
+
else
|
36
|
+
if params[:action]
|
37
|
+
# Try namespace as endpoint
|
38
|
+
# this must be refactored in new year
|
39
|
+
if options[:namespace]
|
40
|
+
specc = { action: params[:action], controller: options[:namespace] }
|
41
|
+
opts = options.dup
|
42
|
+
opts.delete(:namespace)
|
43
|
+
|
44
|
+
result = self.class.new(specc, opts).resolve
|
45
|
+
|
46
|
+
return result if result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
endpoint = Dynamic.new(@options, params)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Result.new(endpoint, params)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :spec, :options
|
60
|
+
|
61
|
+
##
|
62
|
+
# Constructor:
|
63
|
+
#
|
64
|
+
# Params:
|
65
|
+
# - spec {String|Hash|Proc}
|
66
|
+
# - options {Hash}
|
67
|
+
# - :namespace {String} namespace
|
68
|
+
# - :controller_format {String} string with controller name pattern
|
69
|
+
#
|
70
|
+
def initialize(spec, options={})
|
71
|
+
@spec = spec
|
72
|
+
@options = options
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Private: resolve controller and action
|
77
|
+
#
|
78
|
+
# Params:
|
79
|
+
# - controller {String|Class} controller name or constant
|
80
|
+
# - action {String} action name
|
81
|
+
#
|
82
|
+
# Returns: {Hash}
|
83
|
+
# - :controller {Module|Class} controller constant
|
84
|
+
# - :action {String|Class} action name or constant
|
85
|
+
#
|
86
|
+
def resolve_hash(controller, action)
|
87
|
+
if controller.kind_of?(Module)
|
88
|
+
if action
|
89
|
+
pattern = "#{controller}::#{Inflecto.camelize(action)}"
|
90
|
+
action = Inflecto.constantize(pattern) if Object.const_defined?(pattern)
|
91
|
+
end
|
92
|
+
elsif controller
|
93
|
+
name = controller_name(controller)
|
94
|
+
|
95
|
+
return resolve_hash(Inflecto.constantize(Inflecto.camelize(name)), action)
|
96
|
+
end
|
97
|
+
|
98
|
+
{ controller: controller, action: action }
|
99
|
+
rescue NameError
|
100
|
+
raise(UnresolvedError)
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Private: resolve controller and action from string
|
105
|
+
#
|
106
|
+
# Returns: {Hash}
|
107
|
+
# - :controller {Module|Class} controller constant
|
108
|
+
# - :action {String|Class} action name or constant
|
109
|
+
#
|
110
|
+
def resolve_string
|
111
|
+
controller, action = spec.split('#')
|
112
|
+
|
113
|
+
resolve_hash(controller, action)
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Private: get controller name from pattern and namespace
|
118
|
+
#
|
119
|
+
# Params:
|
120
|
+
# - controller {String} raw controller name
|
121
|
+
#
|
122
|
+
# Returns: {String} namespaced controller name from pattern
|
123
|
+
#
|
124
|
+
def controller_name(controller)
|
125
|
+
names = {
|
126
|
+
name: controller,
|
127
|
+
plural_name: Inflecto.pluralize(controller),
|
128
|
+
singular_name: Inflecto.singularize(controller)
|
129
|
+
}
|
130
|
+
|
131
|
+
pattern = options[:controller_format] || '%{name}'
|
132
|
+
|
133
|
+
name = pattern % names
|
134
|
+
|
135
|
+
if options[:namespace]
|
136
|
+
namespace = Array(options[:namespace]).join('/')
|
137
|
+
name = "#{Inflecto.camelize(namespace)}::#{Inflecto.camelize(name)}"
|
138
|
+
end
|
139
|
+
|
140
|
+
name
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Signpost
|
2
|
+
class Route
|
3
|
+
class Nested
|
4
|
+
attr_reader :subpath
|
5
|
+
|
6
|
+
def match(path, env)
|
7
|
+
return unless path.start_with?(subpath)
|
8
|
+
|
9
|
+
@router.call(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def initialize(subpath, router)
|
15
|
+
@subpath = subpath
|
16
|
+
@router = router
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class Signpost
|
2
|
+
class Route
|
3
|
+
class Simple
|
4
|
+
##
|
5
|
+
# Route matcher/expander
|
6
|
+
#
|
7
|
+
# Returns: {Mustermann}
|
8
|
+
#
|
9
|
+
attr_reader :matcher
|
10
|
+
|
11
|
+
##
|
12
|
+
# Rack-compatible endpoint
|
13
|
+
#
|
14
|
+
# Returns: {Proc|Object}
|
15
|
+
#
|
16
|
+
attr_reader :endpoint
|
17
|
+
|
18
|
+
##
|
19
|
+
# Specific route params
|
20
|
+
#
|
21
|
+
# Returns: {Hash}
|
22
|
+
#
|
23
|
+
attr_reader :params
|
24
|
+
|
25
|
+
##
|
26
|
+
# Environment constraints
|
27
|
+
#
|
28
|
+
# Returns: {Array}
|
29
|
+
#
|
30
|
+
attr_reader :constraints
|
31
|
+
|
32
|
+
##
|
33
|
+
# Route name
|
34
|
+
#
|
35
|
+
# Returns: {Symbol}
|
36
|
+
#
|
37
|
+
attr_reader :name
|
38
|
+
|
39
|
+
##
|
40
|
+
# Match path and return matched data or nil if path doesn't match
|
41
|
+
#
|
42
|
+
# Params:
|
43
|
+
# - path {String} uri path
|
44
|
+
#
|
45
|
+
# Example:
|
46
|
+
#
|
47
|
+
# route.match('/users/42') #=> { 'controller' => 'Users', 'action' => 'Show', 'id' => '42' }
|
48
|
+
# route.match('/foo/bar') #=> nil
|
49
|
+
#
|
50
|
+
# Returns: {Hash|NilClass}
|
51
|
+
#
|
52
|
+
def match(path, env)
|
53
|
+
return unless data = matcher.match(path)
|
54
|
+
|
55
|
+
return unless constraints.all? { |c| c.call(env) }
|
56
|
+
|
57
|
+
params.merge(Hash[data.names.zip(data.captures)])
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Generate a string from path pattern
|
62
|
+
#
|
63
|
+
# Params:
|
64
|
+
# - data {Hash} key-values for expanding
|
65
|
+
#
|
66
|
+
# Example:
|
67
|
+
#
|
68
|
+
# route.expand({ id: 42 }) #=> '/users/42'
|
69
|
+
#
|
70
|
+
# Returns: {String}
|
71
|
+
#
|
72
|
+
def expand(data={})
|
73
|
+
matcher.expand(:append, data)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def initialize(matcher, endpoint, params={}, constraints=[], name=nil)
|
79
|
+
@matcher = matcher
|
80
|
+
@endpoint = endpoint
|
81
|
+
@params = params.each_with_object({}) { |(k, v), h| h[k.to_s] = v }.freeze
|
82
|
+
@constraints = constraints
|
83
|
+
@name = name ? name.to_sym : nil
|
84
|
+
|
85
|
+
freeze
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|