signpost 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ class Signpost
2
+ class Builder
3
+ class Namespace < Nested
4
+
5
+ private
6
+
7
+ def root_name
8
+ :root
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -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,17 @@
1
+ class Signpost
2
+ class Middleware
3
+ attr_reader :middleware
4
+
5
+ attr_reader :args
6
+
7
+ attr_reader :block
8
+
9
+ private
10
+
11
+ def initialize(middleware, args, block)
12
+ @middleware = middleware
13
+ @args = args
14
+ @block = block
15
+ end
16
+ end
17
+ 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