addressive 0.1.0.alpha

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.
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'addressive'
3
+ s.version = '0.1.0.alpha'
4
+ s.date = '2011-11-23'
5
+ s.authors = ["HannesG"]
6
+ s.email = %q{hannes.georg@googlemail.com}
7
+ s.summary = 'A system which should help bringing different Rack applications together.'
8
+ s.homepage = 'https://github.com/hannesg/addressive'
9
+ s.description = ''
10
+
11
+ s.require_paths = ['lib']
12
+
13
+ s.files = Dir.glob('lib/**/**/*.rb') + ['addressive.gemspec']
14
+
15
+ s.add_dependency 'uri_template', '~> 0.1.4'
16
+
17
+ s.add_development_dependency 'simplecov'
18
+ s.add_development_dependency 'rspec'
19
+ s.add_development_dependency 'yard'
20
+ s.add_development_dependency 'rack'
21
+
22
+ end
@@ -0,0 +1,229 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ module Addressive
19
+
20
+ # A graph is basically a hash of nodes and some builder methods.
21
+ # Graphs are only used to generate nodes and their relations.
22
+ # They will be GCed when they are done.
23
+ class Graph
24
+
25
+ # An app builder is used to add uris for a certain app to a node.
26
+ class AppBuilder
27
+
28
+ # @private
29
+ def initialize(node, factory, app)
30
+ @node = node
31
+ @app = app
32
+ @spec_factory = factory
33
+ end
34
+
35
+ # Sets a default value for an option.
36
+ def default(name, value)
37
+ @spec_factory.defaults[name] = value
38
+ end
39
+
40
+ # Adds one or more uri specs for a given name. It uses the current app as the default app for all specs.
41
+ def uri(name,*args)
42
+ specs = @node.uri_spec(name)
43
+ specs << @spec_factory.convert(*args)
44
+ return specs
45
+ end
46
+
47
+ end
48
+
49
+ # A NodeBuilder is used to build a Node inside a Graph.
50
+ # This class should not be generated directly, it's created for you by {Builder#node}.
51
+ class NodeBuilder
52
+
53
+ attr_reader :node
54
+
55
+ # @private
56
+ def initialize(network,node)
57
+ @network = network
58
+ @node = node
59
+ @spec_factory = network.spec_factory.derive({})
60
+ end
61
+
62
+ # Adds an edge from the current node to a node with the given name.
63
+ #
64
+ # @example
65
+ # nw = Addressive::Graph.new{
66
+ # #create a node named :app_a
67
+ # node :app_a do
68
+ # # Creates an edge to the node named :app_b. The name of the edge will be :app_b, too.
69
+ # edge :app_b
70
+ #
71
+ # # Creates another edge to the node named :app_b with edge name :app_c.
72
+ # edge :app_c, :app_b
73
+ #
74
+ # # Edge takes a block. Same behavior as Builder#node.
75
+ # edge :app_d do
76
+ #
77
+ # edge :app_a
78
+ #
79
+ # end
80
+ # end
81
+ # }
82
+ # # :app_a now references :app_b twice, :app_d once and is referenced only by :app_d.
83
+ #
84
+ # @yield BlockBuilder
85
+ #
86
+ def edge(as , name = as, &block)
87
+ @node.edges[as] = @network.build.node(name,&block).node
88
+ return self
89
+ end
90
+
91
+ alias ref edge
92
+
93
+ # Sets a default value for an option.
94
+ def default(name, value)
95
+ @spec_factory.defaults[name] = value
96
+ end
97
+
98
+ # Adds one or more uri specs for a given name.
99
+ def uri(name,*args)
100
+ @node.uri_spec(name) << @spec_factory.convert(args)
101
+ return @node.uri_spec(name)
102
+ end
103
+
104
+ # Adds an rack-application to this node.
105
+ #
106
+ # @example
107
+ #
108
+ # Addressive::Graph.new{
109
+ #
110
+ # node :a_node do
111
+ #
112
+ # app lambda{|env| [200,{},['App 1']]} do
113
+ # uri :show, '/a_node/app1'
114
+ # end
115
+ #
116
+ # app lambda{|env| [200,{},['App 2']]} do
117
+ # uri :show, '/a_node/app2'
118
+ # end
119
+ #
120
+ # end
121
+ #
122
+ # }
123
+ #
124
+ # @yield {AppBuilder}
125
+ # @return {AppBuilder}
126
+ def app(app, options = {}, &block)
127
+ app = app.to_app if app.respond_to? :to_app
128
+ sf = @spec_factory.derive(options.merge(:app=>app))
129
+ builder = AppBuilder.new(@node,sf,app)
130
+ if block
131
+ if block.arity == 1
132
+ yield builder
133
+ else
134
+ builder.instance_eval(&block)
135
+ end
136
+ end
137
+ unless @node.apps.include? app
138
+ @node.apps << app
139
+ if app.respond_to? :generate_uri_specs
140
+ app.generate_uri_specs(builder)
141
+ end
142
+ end
143
+ return builder
144
+ end
145
+
146
+ end
147
+
148
+ # A Builder is used to construct a network.
149
+ # It's here so that a {Graph} can have only read methods, while a {Builder} does the heavy lifting.
150
+ # This class should not be generated directly, it's created for you by {Graph#build} and {Graph#initialize}.
151
+ class Builder
152
+
153
+ # @private
154
+ def initialize(network)
155
+ @network = network
156
+ end
157
+
158
+ # Creates or edits the node with the given name in the current network and yields and returns a NodeBuilder.
159
+ # This NodeBuilder can then be use to actually describe the node.
160
+ #
161
+ # @yield {NodeBuilder}
162
+ # @return {NodeBuilder}
163
+ def node(name, &block)
164
+ n = @network.node(name)
165
+ if block
166
+ if block.arity == 1
167
+ yield @network.node_builder_class.new(@network, n)
168
+ else
169
+ @network.node_builder_class.new(@network, n).instance_eval(&block)
170
+ end
171
+ end
172
+ return @network.node_builder_class.new(@network, n)
173
+ end
174
+
175
+ # Sets a network-wide default.
176
+ # @note
177
+ # Only nodes created after this default has been set will receive this value.
178
+ #
179
+ def default(name,value)
180
+ @network.spec_factory.defaults[name]=value
181
+ end
182
+
183
+ end
184
+
185
+ # @private
186
+ def configure(node)
187
+ return node
188
+ end
189
+
190
+ # @private
191
+ attr_reader :node_class, :node_builder_class, :spec_factory
192
+
193
+ # Creates a new Graph.
194
+ # @yield {Builder}
195
+ def initialize(&block)
196
+ @defaults = {}
197
+ @node_class = Class.new(Node)
198
+ @node_builder_class = Class.new(NodeBuilder)
199
+ @spec_factory = URISpecFactory.new({})
200
+ @nodes = Hash.new{|hsh, name| hsh[name] = configure(node_class.new) }
201
+ build(&block)
202
+ end
203
+
204
+ # Creates, yields and returns a {Builder} for this network.
205
+ #
206
+ # @yield {Builder}
207
+ # @return {Builder}
208
+ def build(&block)
209
+ if block
210
+ if block.arity == 1
211
+ yield Builder.new(self)
212
+ else
213
+ Builder.new(self).instance_eval(&block)
214
+ end
215
+ end
216
+ return Builder.new(self)
217
+ end
218
+
219
+ # Return the node with the given name.
220
+ # @note
221
+ # A Graph will automatically create nodes if they don't exists.
222
+ def [](name)
223
+ return @nodes[name]
224
+ end
225
+
226
+ alias node []
227
+
228
+ end
229
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ module Addressive
19
+
20
+ # A module to visualize
21
+ module Graphviz
@@ -0,0 +1,43 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ require 'rack/request'
19
+
20
+ module Addressive
21
+
22
+ # A patched rack-request, which uses the addressive data if for the GET and uri methods.
23
+ class Request < Rack::Request
24
+
25
+ def GET
26
+ if @env[ADDRESSIVE_ENV_KEY]
27
+ return @env[ADDRESSIVE_ENV_KEY].variables
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def uri(*args)
34
+ if @env[ADDRESSIVE_ENV_KEY]
35
+ @env[ADDRESSIVE_ENV_KEY].uri(*args)
36
+ else
37
+ self.url
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,337 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ require 'rack/request'
19
+ require 'thread'
20
+
21
+ module Addressive
22
+
23
+ class Match < Struct.new(:node,:action,:variables, :spec, :data)
24
+
25
+ include URIBuilder
26
+
27
+ end
28
+
29
+ # A router which can be used as a rack application and routes requests to nodes and their apps.
30
+ #
31
+ class Router
32
+
33
+ # @private
34
+ class Tree
35
+
36
+ # @private
37
+ class Direct < self
38
+
39
+ class EachingProc < Proc
40
+
41
+ def initialize(routes)
42
+ @routes = routes
43
+ end
44
+
45
+ alias each call
46
+
47
+ def size
48
+ @routes.size
49
+ end
50
+
51
+ end
52
+
53
+ def initialize
54
+ @routes = []
55
+ end
56
+
57
+ def preroute(path, uri)
58
+ return self
59
+ end
60
+
61
+ def <<(spec)
62
+ @routes << spec
63
+ end
64
+
65
+ def clear!
66
+ @routes = []
67
+ end
68
+
69
+ def size
70
+ @routes.size
71
+ end
72
+
73
+ def done!
74
+ # compile *gnihihi*
75
+ code = ['def each(path,uri); routes = Enumerator.new(@routes);',
76
+ *@routes.each_with_index.map{|r,i|
77
+ "route = routes.next
78
+ vars = route.template.extract(#{r.template.absolute? ? 'uri' : 'path'})
79
+ if vars
80
+ yield( route, vars, #{i} )
81
+ end
82
+ "
83
+ } ,'; end'].join
84
+ instance_eval(code)
85
+ end
86
+
87
+ def each(path,uri)
88
+ @routes.each_with_index do |route,i|
89
+ vars = route.template.extract(route.template.absolute? ? uri : path)
90
+ if vars
91
+ yield( route, vars, i )
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ # @private
99
+ class SubstringSplit < self
100
+
101
+ def initialize(substring, *partitions)
102
+ @substring = substring
103
+ @partitions = partitions
104
+ end
105
+
106
+ def preroute(path,uri)
107
+ @partitions[ uri.include?(@substring) ? 0 : 1 ].preroute(uri,path)
108
+ end
109
+
110
+ def <<(spec)
111
+ if spec.template.send(:tokens).select(&:literal?).none?{|tk| tk.string.include? @substring }
112
+ # okay, this template does not require the substring.
113
+ # so it can be matched, even when the substring is missing
114
+ @partitions[1] << spec
115
+ end
116
+ @partitions[0] << spec
117
+ end
118
+
119
+
120
+ def clear!
121
+ @partitions[0].clear!
122
+ @partitions[1].clear!
123
+ end
124
+
125
+ def done!
126
+ @partitions[0].done!
127
+ @partitions[1].done!
128
+ end
129
+
130
+ end
131
+
132
+ # @private
133
+ class PrefixSplit < self
134
+
135
+ def initialize(prefix, *partitions)
136
+ @prefix = prefix
137
+ @partitions = partitions
138
+ end
139
+
140
+ def preroute(path,uri)
141
+ @partitions[ path.start_with?(@prefix) ? 0 : 1 ].preroute(uri,path)
142
+ end
143
+
144
+ def <<(spec)
145
+ if spec.template.absolute?
146
+ @partitions[0] << spec
147
+ @partitions[1] << spec
148
+ else
149
+ tkns = spec.template.send(:tokens)
150
+ if tkns.empty?
151
+ # emtpy uri, no problem?!
152
+ @partitions[0] << spec
153
+ @partitions[1] << spec
154
+ return
155
+ end
156
+ tk = tkns.first
157
+ if tk.literal?
158
+ if tk.string.start_with?(@prefix)
159
+ @partitions[0] << spec
160
+ elsif @prefix.start_with?(tk.string) and tkns[1]
161
+ @partitions[0] << spec
162
+ @partitions[1] << spec
163
+ else
164
+ @partitions[1] << spec
165
+ end
166
+ else
167
+ @partitions[0] << spec
168
+ @partitions[1] << spec
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+
175
+ def clear!
176
+ @partitions[0].clear!
177
+ @partitions[1].clear!
178
+ end
179
+
180
+ def done!
181
+ @partitions[0].done!
182
+ @partitions[1].done!
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
189
+ DEBUG_NAME = 'addressive-router'.freeze
190
+ DEBUG_NULL = lambda{|_| nil}
191
+
192
+ attr_reader :routes, :actions
193
+
194
+ def initialize(tree = Tree::Direct.new)
195
+ @routes = []
196
+ @actions = {}
197
+ @tree = tree
198
+ @immaterial = true
199
+ @mutex = Mutex.new
200
+ @sealed = false
201
+ end
202
+
203
+ # Add a nodes specs to this router.
204
+ def add(*nodes)
205
+ @mutex.synchronize do
206
+ raise "Trying to add nodes to a sealed router!" if @sealed
207
+ nodes.each do |node|
208
+ node.uri_specs.each do |action, specs|
209
+ specs.each do |spec|
210
+ if spec.valid? and spec.app and spec.route != false
211
+ @routes << spec
212
+ @actions[spec] = [node, action]
213
+ end
214
+ end
215
+ end
216
+ end
217
+ @immaterial = true
218
+ end
219
+ return self
220
+ end
221
+
222
+ # Routes the env to an app and it's node.
223
+ def call(env)
224
+ rr = Rack::Request.new(env)
225
+ l = env['rack.logger']
226
+ db = l ? l.method(:debug) : DEBUG_NULL
227
+ db.call(DEBUG_NAME) do
228
+ "[ ? ] #{rr.url.inspect}"
229
+ end
230
+ matches = routes_for(rr.path, rr.url)
231
+ result = nil
232
+ matches.each do |addressive|
233
+ env[ADDRESSIVE_ENV_KEY] = addressive
234
+ begin
235
+ result = addressive.spec.app.call(env)
236
+ db.call(DEBUG_NAME) do
237
+ "[#{result[0]}] #{addressive.spec.template.pattern} with #{addressive.variables.inspect} on #{addressive.spec.app} ( route #{addressive.data[:'routes.scanned']} / #{addressive.data[:'routes.total']} ) after #{'%.6f' % addressive.data[:duration]}"
238
+ end
239
+ rescue
240
+ db.call(DEBUG_NAME) do
241
+ "[!!!] #{addressive.spec.template.pattern} with #{addressive.variables.inspect} on #{addressive.spec.app} ( route #{addressive.data[:'routes.scanned']} / #{addressive.data[:'routes.total']} ) after #{'%.6f' % addressive.data[:duration]}"
242
+ end
243
+ raise
244
+ end
245
+ if result[0] != 404
246
+ break
247
+ end
248
+ end
249
+ if result.nil?
250
+ db.call(DEBUG_NAME) do
251
+ "[404] Nothing found"
252
+ end
253
+ return not_found(env)
254
+ end
255
+ return result
256
+ end
257
+
258
+ class RouteEnumerator
259
+
260
+ include Enumerable
261
+
262
+ # @private
263
+ def initialize(routes,path,url,actions)
264
+ @routes,@path,@url,@actions = routes, path, url, actions
265
+ end
266
+
267
+ # @yield {Addressive::Match}
268
+ def each
269
+ total = @routes.size
270
+ scan_time = Time.now
271
+ @routes.each(@path,@url) do |spec, vars, scanned|
272
+ node, action = @actions[spec];
273
+ t = Time.now
274
+ yield Match.new(node, action, vars, spec, {:'routes.scanned'=>scanned,:'routes.total'=>total,:duration => (t - scan_time)})
275
+ # still here?, the passed time should be added
276
+ scan_time += (Time.now - t)
277
+ end
278
+
279
+ end
280
+
281
+ end
282
+
283
+ #
284
+ # @param path String the path to look for
285
+ # @param uri String the full uri
286
+ # @return {RouteEnumerator} an enumerator which yields the requested routes
287
+ def routes_for(path, uri)
288
+ materialize!
289
+ return RouteEnumerator.new(@tree.preroute(path,uri),path,uri,@actions)
290
+ end
291
+
292
+ # @private
293
+ def to_app
294
+ return self
295
+ end
296
+
297
+ # Prevents further editing of this router.
298
+ # This is not mandatory, but encouraged if later changes are not required.
299
+ #
300
+ # @example
301
+ # rtr = Addressive::Router.new
302
+ # rtr.add( Addressive.node ) # okay
303
+ # rtr.seal!
304
+ # rtr.add( Addressive.node ) #!> Exception
305
+ #
306
+ def seal!
307
+ @mutex.synchronize do
308
+ @sealed = true
309
+ end
310
+ materialize!
311
+ end
312
+
313
+ protected
314
+
315
+ # This method is called when no route was found.
316
+ def not_found(env)
317
+ [404,{'Content-Type'=>'text/plain','X-Cascade'=>'pass'},['Ooooopppps 404!']]
318
+ end
319
+
320
+ private
321
+
322
+ def materialize!
323
+ return unless @immaterial
324
+ @mutex.synchronize do
325
+ return unless @immaterial
326
+ @routes.sort_by!{|spec| spec.template.static_characters }.reverse!
327
+ @tree.clear!
328
+ @routes.each do |spec|
329
+ @tree << spec
330
+ end
331
+ @tree.done!
332
+ @immaterial = false
333
+ end
334
+ end
335
+
336
+ end
337
+ end
@@ -0,0 +1,71 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ require 'rack/file'
19
+
20
+ module Addressive
21
+
22
+ # Similiar to Rack::Static but lives happily on a node.
23
+ # Emits just one uri-spec with name ":get" and a simple catch-all uri.
24
+ #
25
+ # @example
26
+ # node = Addressive.node{
27
+ # app Addressive::Static.new(:root=>'/tmp')
28
+ # }
29
+ # node.uri(:get,'file'=>'baz').to_s #=> "/baz"
30
+ #
31
+ # # Create a file, so this won't be a 404:
32
+ # File.new('/tmp/baz','w').close
33
+ # router = Addressive::Router.new.add(node)
34
+ # status,headers,body = router.call('rack.url_scheme'=>'http','PATH_INFO'=>'/baz','HTTP_HOST'=>'example.example')
35
+ # body.class #=> Rack::File
36
+ # body.path #=> "/tmp/baz"
37
+ #
38
+ class Static
39
+
40
+ def generate_uri_specs(builder)
41
+
42
+ builder.uri :get, '/{+file}'
43
+
44
+ end
45
+
46
+ def call(env)
47
+ env['PATH_INFO'] = env['addressive'].variables['file']
48
+ return @file_server.call(env)
49
+ end
50
+
51
+ # @param [Hash] options
52
+ # @option options [#call,Rack::File] :file_server A class compatible to Rack::File. If supplied this will be used instead of the default rack file server
53
+ # @option options [String] :root The directory to search files in. Same as for Rack::File.
54
+ # @option options [Obect] :cache_control Same as for Rack::File.
55
+ def initialize(options={})
56
+ unless options.key? :file_server
57
+ root = options[:root] || Dir.pwd
58
+ cache_control = options[:cache_control]
59
+ @file_server = Rack::File.new(root, cache_control)
60
+ else
61
+ @file_server = options[:file_server]
62
+ end
63
+ end
64
+
65
+ def to_s
66
+ "Static[#{@file_server.inspect}]"
67
+ end
68
+
69
+ end
70
+
71
+ end
data/lib/addressive.rb ADDED
@@ -0,0 +1,404 @@
1
+ # -*- encoding : utf-8 -*-
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the Affero GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ #
15
+ # (c) 2011 by Hannes Georg
16
+ #
17
+
18
+ $LOAD_PATH << File.expand_path('../../uri_template7/lib/',File.dirname(__FILE__))
19
+ require File.expand_path('../../uri_template7/lib/uri_template',File.dirname(__FILE__))
20
+ require 'ostruct'
21
+
22
+ # Addressive is library which should make it possible for different rack-applications to live together on the same server.
23
+ # To accomplish this, addressive supplies you with:
24
+ # - A model for a graph in which each node is one application.
25
+ # - A template-based uri-builder which is aware of the network and can generate uri for any reachable application.
26
+ # - A router which can route rack-requests based on the known templates.
27
+ #
28
+ # What needs to be done by the apps:
29
+ # - Provide uri templates.
30
+ #
31
+ module Addressive
32
+
33
+ autoload :Static, 'addressive/static'
34
+ autoload :Request, 'addressive/request'
35
+ autoload :Router, 'addressive/router'
36
+ autoload :Graph, 'addressive/graph'
37
+
38
+ ADDRESSIVE_ENV_KEY = 'addressive'.freeze
39
+
40
+ class Error < StandardError
41
+
42
+ end
43
+
44
+ # This error is raised whenever no uri spec could be found.
45
+ class NoURISpecFound < Error
46
+
47
+ attr_reader :builder
48
+
49
+ def initialize(builder)
50
+ @builder = builder
51
+ super("No URISpec found for #{builder.inspect}. Only got: #{builder.node.uri_specs.keys.join(', ')}")
52
+ end
53
+
54
+ end
55
+
56
+ class NoEdgeFound < Error
57
+
58
+ attr_reader :node, :edge
59
+
60
+ def initialize(node, edge)
61
+ @node, @edge = node, edge
62
+ super("No Edge '#{edge.inspect}' found, only got '#{node.edges.keys.map(&:inspect).join('\', \'')}'")
63
+ end
64
+
65
+ end
66
+
67
+ # A module for any class, which can create uris.
68
+ module URIBuilder
69
+
70
+ def self.new(*args)
71
+ URIBuilderClass.new(*args)
72
+ end
73
+
74
+ def derive_uri_builder(action, variables, node)
75
+ origin = self.origin rescue nil
76
+ return URIBuilderClass.new( origin, action, variables, node )
77
+ end
78
+
79
+ protected :derive_uri_builder
80
+
81
+ # Creates a new URIBuilder with the given arguments.
82
+ # Given Hashes are merged into the variables. The last symbol is used to determine the action. Everything else is used to traverse the node graph.
83
+ #
84
+ # @example
85
+ # node = Addressive::Node.new
86
+ # node.uri_spec(:show) << Addressive::URISpec.new( URITemplate.new('/an/uri/with/{var}') )
87
+ # bldr = Addressive::URIBuilder.new(node)
88
+ # bldr.uri(:show, 'var'=>'VAR!').to_s #=> '/an/uri/with/VAR%21'
89
+ #
90
+ # @return URIBuilder
91
+ def uri(*args)
92
+ return uri_builder_delegate.uri(*args) if uri_builder_delegate
93
+ hashes, path = args.collect_concat{|a| a.respond_to?(:to_addressive) ? a.to_addressive : [a] }.partition{|x| x.kind_of? Hash}
94
+ node = self.node
95
+ action = self.action
96
+ if path.size >= 1
97
+ node = node.traverse(*path[0..-2])
98
+ action = path.last
99
+ end
100
+ derive_uri_builder(action, hashes.inject(self.variables || {}, &:merge), node)
101
+ end
102
+
103
+ private
104
+
105
+ def uri_builder_delegate
106
+ nil
107
+ end
108
+
109
+ end
110
+
111
+ # A builder which creates uri based on a Node an action and some variables.
112
+ class URIBuilderClass
113
+
114
+ include URIBuilder
115
+
116
+ attr_reader :origin,:node,:variables,:action
117
+
118
+ # @private
119
+ def initialize(origin, action=:default, vars={}, node=origin)
120
+ @origin = origin
121
+ @node = node
122
+ @action = action
123
+ @variables = vars
124
+ end
125
+
126
+ # @private
127
+ def inspect
128
+ '<URIBuilder '+self.action.inspect+' '+self.variables.inspect+'>'
129
+ end
130
+
131
+ # Actually creates the URI as a string
132
+ #
133
+ # @return String
134
+ def to_s
135
+ return uri_builder_delegate.to_s if uri_builder_delegate
136
+ specs = uri_builder_specs
137
+ # if a.none? ????
138
+ if specs.none?
139
+ raise NoURISpecFound.new(self)
140
+ end
141
+ return specs.first.template.expand(self.variables || {}).to_s
142
+ end
143
+
144
+ def humanization_key
145
+ specs = uri_builder_specs
146
+ if specs.none?
147
+ return super
148
+ else
149
+ return specs.first.app._(:action,self.action,self.variables || {} )
150
+ end
151
+ end
152
+ private
153
+ def uri_builder_specs
154
+ return @specs ||= begin
155
+ varnames = (self.variables || {} ).keys
156
+ self.node.uri_spec(self.action).select{|s| s.valid? and (s.variables - varnames).none? }.sort_by{|s| (varnames - s.variables).size }
157
+ end
158
+ end
159
+ end
160
+
161
+ # A specification is a combination of an URI template and some meta-data ( like the app this spec belongs to ).
162
+ class URISpec < OpenStruct
163
+
164
+ attr_accessor :template
165
+
166
+ def valid?
167
+ !!@template
168
+ end
169
+
170
+ def variables
171
+ @template ? @template.variables : []
172
+ end
173
+
174
+ def initialize(template, *args)
175
+ @template = template
176
+ super(*args)
177
+ end
178
+
179
+ def inspect
180
+ ['#<',self.class.name,': ',template.inspect,*@table.map{|k,v| " #{k}=#{v.inspect}"},'>'].join
181
+ end
182
+
183
+ end
184
+
185
+ # A URISpecFactory contains all information necessary to create a URISpec.
186
+ # This class exists because it's annoying to pass around default values by hand and
187
+ # a factory makes it possible to apply post-processing options.
188
+ class URISpecFactory
189
+
190
+ def converter(defaults = self.all_defaults)
191
+ lambda{|spec|
192
+ if spec.kind_of? URISpec
193
+ [ normalize( spec.dup, defaults ) ]
194
+ elsif spec.kind_of? URITemplate
195
+ [ normalize( URISpec.new( spec ) , defaults) ]
196
+ elsif spec.kind_of? String
197
+ [ normalize( URISpec.new( URITemplate.new(spec) ) , defaults) ]
198
+ elsif spec.kind_of? Array
199
+ spec.map(&self.converter(defaults))
200
+ elsif spec.kind_of? Hash
201
+ nd = defaults.merge(spec)
202
+ self.converter(nd).call(nd[:template])
203
+ else
204
+ []
205
+ end
206
+ }
207
+ end
208
+
209
+ protected :converter
210
+
211
+ def normalize( spec, defaults = self.all_defaults )
212
+ if defaults.key? :app
213
+ spec.app = defaults[:app]
214
+ end
215
+ unless spec.template.absolute?
216
+ if defaults[:prefix]
217
+ spec.template = URITemplate.apply( defaults[:prefix] , :/ , spec.template)
218
+ end
219
+ end
220
+ return spec
221
+ end
222
+
223
+ attr_reader :defaults
224
+
225
+ def initialize(defaults, parent = nil)
226
+ @defaults = defaults
227
+ @parent = parent
228
+ end
229
+
230
+ def convert(*args)
231
+ args.map(&converter).flatten
232
+ end
233
+
234
+ alias << convert
235
+
236
+ def all_defaults
237
+ @parent ? @parent.all_defaults.merge(defaults) : defaults
238
+ end
239
+
240
+ def derive(nu_defaults)
241
+ self.class.new(nu_defaults, self)
242
+ end
243
+
244
+ end
245
+
246
+ # A list of {URISpec}s. Useful because it checks the input.
247
+ class URISpecList
248
+
249
+ include Enumerable
250
+
251
+ def initialize(factory, source = [])
252
+ @specs = []
253
+ self.<<(*source)
254
+ end
255
+
256
+ def each(&block)
257
+ @specs.each(&block)
258
+ end
259
+
260
+ def <<(*args)
261
+ args = args.flatten
262
+ fails = args.select{|a| !a.kind_of? URISpec }
263
+ raise ArgumentError, "Expected to receive only URISpecs but, got #{fails.map(&:inspect).join(', ')}" if fails.size != 0
264
+ @specs.push( *args )
265
+ end
266
+
267
+ alias push <<
268
+
269
+ end
270
+
271
+ # The node is the most important class. A node bundles informations about all uri specs for a certain application.
272
+ # Furthermore nodes can reference each other so that one can traverse them.
273
+ #
274
+ # @example
275
+ # node1 = Addressive::Node.new
276
+ # node2 = Addressive::Node.new
277
+ # node1.edges[:another_node] = node2
278
+ # node1.traverse(:another_node) #=> node2
279
+ #
280
+ class Node
281
+
282
+ attr_reader :edges, :meta, :uri_specs
283
+ attr_reader :apps
284
+ attr_accessor :defaults
285
+
286
+ include URIBuilder
287
+
288
+ def initialize
289
+ @edges = {}
290
+ @meta = {}
291
+ @apps = []
292
+ @uri_specs = Hash.new{|hsh,name| hsh[name]=URISpecList.new(@defaults) }
293
+ @uri_builder_delegate = URIBuilder.new(self)
294
+ end
295
+
296
+ # Traverses some edges to another ( or the same ) node.
297
+ #
298
+ # @example
299
+ # node1 = Addressive::Node.new
300
+ # node2 = Addressive::Node.new
301
+ # node3 = Addressive::Node.new
302
+ # node1.edges[:n2] = node2
303
+ # node2.edges[:n3] = node3
304
+ # node3.edges[:n1] = node1
305
+ # node1.traverse() #=> node1
306
+ # node1.traverse(:n2) #=> node2
307
+ # node1.traverse(:n2,:n3,:n1) #=> node1
308
+ #
309
+ def traverse(*args)
310
+ return self if args.none?
311
+ if edges[args.first].kind_of? Addressive::Node
312
+ return edges[args.first].traverse(*args[1..-1])
313
+ else
314
+ raise NoEdgeFound.new(self, args.first)
315
+ end
316
+ end
317
+
318
+ def uri_spec(name)
319
+ @uri_specs[name]
320
+ end
321
+
322
+ def inspect
323
+ if @meta['name']
324
+ return "<#{self.class.name}: #{@meta['name']}>"
325
+ else
326
+ super
327
+ end
328
+ end
329
+
330
+ private
331
+ def uri_builder_delegate
332
+ @uri_builder_delegate
333
+ end
334
+
335
+ end
336
+
337
+ # Creates a new node and yields a builder for it
338
+ #
339
+ # This method makes it easy to create a tree-like structur by implictly creating and returning a root node ( by default called :root ). This should be used in most cases, as you will likely have just one connected component in an addressive graph. For complexer graphs you can use {Addressive#graph}.
340
+ #
341
+ # @example
342
+ # node = Addressive.node do
343
+ # edge( :another ) do
344
+ # default :prefix, '/another'
345
+ # uri :default ,'/'
346
+ # end
347
+ # uri :default ,'/'
348
+ # end
349
+ #
350
+ # node.uri.to_s #=> '/'
351
+ # node.uri(:another,:default).to_s #=> '/another/'
352
+ #
353
+ # @param name [Symbol] a name for this node
354
+ # @yield {NodeBuilder}
355
+ # @return {Graph}
356
+ #
357
+ def self.node(name=:root,&block)
358
+ Graph.new{ |bldr|
359
+ bldr.node name, &block
360
+ }[name]
361
+ end
362
+
363
+ # Creates a new graph and yields a builder for it
364
+ #
365
+ # This is good, if you want to create a graph with disconnected nodes or multiple root-nodes.
366
+ #
367
+ # @example
368
+ # graph = Addressive.graph do
369
+ #
370
+ # node :shared do
371
+ # uri :default, '/shared'
372
+ # end
373
+ #
374
+ # node :foo do
375
+ # uri :default ,'/foo'
376
+ # edge :shared
377
+ # end
378
+ #
379
+ # node :bar do
380
+ # uri :default, '/bar'
381
+ # edge :shared
382
+ # end
383
+ #
384
+ # end
385
+ #
386
+ # # You can then get the nodes with {Addressive::Graph.[]}
387
+ # graph[:foo].uri.to_s #=> '/foo'
388
+ # graph[:bar].uri.to_s #=> '/bar'
389
+ # # :foo and :bar can both reach :shared, but not each other.
390
+ # # Neither can :shared reach any other node.
391
+ # graph[:foo].traverse(:shared) #=> graph[:shared]
392
+ # graph[:bar].traverse(:shared) #=> graph[:shared]
393
+ # graph[:foo].traverse(:bar) #!> Addressive::NoEdgeFound
394
+ # graph[:shared].traverse(:bar) #!> Addressive::NoEdgeFound
395
+ #
396
+ # @yield {Builder}
397
+ # @return {Node}
398
+ #
399
+ def self.graph(&block)
400
+ Graph.new(&block)
401
+ end
402
+
403
+ end
404
+
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: addressive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - HannesG
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: uri_template
16
+ requirement: &7411800 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *7411800
25
+ - !ruby/object:Gem::Dependency
26
+ name: simplecov
27
+ requirement: &7408500 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *7408500
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &7407760 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *7407760
47
+ - !ruby/object:Gem::Dependency
48
+ name: yard
49
+ requirement: &7460580 !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: *7460580
58
+ - !ruby/object:Gem::Dependency
59
+ name: rack
60
+ requirement: &7459920 !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: *7459920
69
+ description: ''
70
+ email: hannes.georg@googlemail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/addressive/request.rb
76
+ - lib/addressive/graphviz.rb
77
+ - lib/addressive/graph.rb
78
+ - lib/addressive/router.rb
79
+ - lib/addressive/static.rb
80
+ - lib/addressive.rb
81
+ - addressive.gemspec
82
+ homepage: https://github.com/hannesg/addressive
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>'
98
+ - !ruby/object:Gem::Version
99
+ version: 1.3.1
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 1.8.10
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: A system which should help bringing different Rack applications together.
106
+ test_files: []
107
+ has_rdoc: