sansom 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33777ff470f9889d21b8854b1dea67b731a38a32
4
- data.tar.gz: 3c0baa22623738bc1668c91328f057e862c9e1e1
3
+ metadata.gz: fececc46ad6a785f2e8cc4be0035fff8f7a7d998
4
+ data.tar.gz: 725f8e1256721a69d085cbec36591a3596483245
5
5
  SHA512:
6
- metadata.gz: e83bcf72135895c959bc11282caab7780243338afdeddd9e0c7c6e6c1150d8251b01191c4f03666f1d9a7cf66f3a5dca04bcd733d5c8da0e29acf52ef2308299
7
- data.tar.gz: 1ba7cf9530ea405f9b7c582b664a4f65b7f18942f1b3077de3b7c00e6df339847b55acf14fd6fb3f4f71bdd596b8c0afadbbcc263dc8a92328cef7a250e50bd6
6
+ metadata.gz: 9dbbc5555a69229e68474ce0be6e662cd30fe8bffbaee6a3685b061802735deb72eb3e59db65cd3cc69c9df6a11aae794874e3d92596882d09ff3cf0924410f0
7
+ data.tar.gz: 06c215518e828dd5c0da1b20b6cbaa0dd0d5d80ae374ac60ac24d7d584881e95e4e70b5353082db8675b27d9e0d2b692530513b998d590b3d4d2785322169052
data/README.md CHANGED
@@ -16,100 +16,88 @@ You can write one `Sansomable` for your entire API.
16
16
 
17
17
  Fuck it.
18
18
 
19
- ***A piece of software should not be a magic box.***
19
+ ***A tool should do one thing, and do it well. (Unix philosophy)***
20
20
 
21
- A web framework is, however simplistically, a tool to connect code to a URL's path.
21
+ A web framework is, fundamentally, a tool to connect code to a URL's path.
22
22
 
23
- A web framework doesn't provide an ORM, template rendering, nor change how ruby works to the point where you're not writing Ruby code but instead `Rails` or `Sinatra` code.
23
+ A web framework doesn't provide an ORM, template rendering, shortcuts, nor security patches.
24
+
25
+ ***A web framework shall remain a framework***
26
+
27
+ No single tool should get so powerful that it can overcome its master. Rails and Sinatra have been doing this: modifying the language beyond recognition. Rails has activerecord and Sinatra's blocks aren't really blocks.
24
28
 
25
29
  Installation
26
30
  -
27
31
 
28
32
  `gem install sansom`
29
33
 
30
- Usage
34
+ General Usage
31
35
  -
36
+ Traditional approach:
32
37
 
33
- It's pretty simple. Instead of `Class`s storing routes, `Object`s store routes.
34
-
35
- There are two ways you can deploy `Sansom`:
38
+ # config.ru
39
+
40
+ require "sansom"
41
+
42
+ s = Sansom.new
43
+ # define routes on s
44
+ run s
45
+
46
+ One-file approach:
36
47
 
37
48
  # app.rb
38
-
39
- #!/usr/bin/env ruby
40
49
 
41
50
  require "sansom"
42
51
 
43
52
  s = Sansom.new
44
- s.get "/" do |r|
45
- # r is a Rack::Request
46
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
47
- end
53
+ # define routes on s
48
54
  s.start
55
+
56
+ They're basically the same, except the rack server evaluates config.ru in its own context. The config.ru approach allows for the config to be separated from the application code.
49
57
 
50
- Or, the more production-ready version:
58
+ Writing your own traditional-style webapp
59
+ -
51
60
 
52
- # config.ru
53
-
54
- require "sansom"
55
-
56
- s = Sansom.new
57
-
58
- s.get "/" do |r|
59
- # r is a Rack::Request
60
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
61
- end
62
-
63
- run s
64
-
65
- But `Sansom` can do more than just that:
61
+ Writing a one-file webapp is as simple as creating a `Sansomable`, defining routes on it, and calling start on it.
66
62
 
67
- It can be used in a similar fashion to Sinatra:
63
+ ####There is more footwork for a traditional-style webapp:
68
64
 
69
- # myapi.rb
70
-
71
- #!/usr/bin/env ruby
72
-
73
- require "sansom"
74
-
75
- class MyAPI < Sansom
76
- # This method is used to define Sansom routes
77
- def template
78
- get "/" do |r|
79
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
80
- # r is a Rack::Request
81
- end
82
- end
83
- end
65
+ Sansom is defined like this:
66
+
67
+ Sansom = Class.new Object
68
+ Sansom.send :include, Sansomable
69
+
70
+ So you'll want your app to either `include Sansomable` or be a subclass of `Sansom`, so that a basic declaration looks like this.
71
+
72
+ # myapi.rb
73
+
74
+ require "sansom"
75
+
76
+ class MyAPI
77
+ include Sansomable
78
+ def template
79
+ # define routes here
80
+ end
81
+ end
84
82
 
85
83
  And your `config.ru` file
86
84
 
87
85
  # config.ru
88
86
 
89
- require "sansom"
90
87
  require "./myapi"
91
88
 
92
89
  run MyAPI.new
93
90
 
94
- Sansom can also map other instances of Sansom to a route. Check this:
95
-
96
- # myapi.rb
97
-
98
- #!/usr/bin/env ruby
99
-
100
- require "sansom"
101
-
102
- class MyAPI < Sansom
103
- # This method is used to define Sansom routes
104
- def template
105
- get "/" do |r|
106
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
107
- # r is a Rack::Request
108
- end
109
- end
91
+ Defining Routes
92
+ -
93
+ Routes can be defined like so:
94
+
95
+ s = Sansom.new
96
+ s.get "/" do |r| # r is a Rack::Request
97
+ [200, {}, ["Return a Rack response."]]
110
98
  end
111
-
112
- Let's say you've written a new version of your api. No problem.
99
+
100
+ You can replace `get` with any http verb. Or `map`, if you want to map a subsansom. Let's say you've written a new version of your api. No problem:
113
101
 
114
102
  # app.rb
115
103
 
@@ -120,38 +108,68 @@ Let's say you've written a new version of your api. No problem.
120
108
  s.map "/v2", MyNewAPI.new
121
109
  s.start
122
110
 
123
- Or maybe you want to mount your Rails/Sinatra/whatever app
111
+ Sansom blocks vs Sinatra blocks
112
+ -
113
+
114
+ Sansom blocks remain blocks: When a route is mapped, the same block you use is called when a route is matched. It's the same object every time.
115
+
116
+ Sinatra blocks become methods behind the scenes. When a route is matched, Sinatra looks up the method and calls it.
117
+
118
+ Sinatra's mechanism allows for the use of `return` inside blocks. Sansom doesn't do this, so you must use the `next` directive in the same way you'd use return.
119
+
120
+ Before filters
121
+ -
122
+
123
+ You can write before filters to try to preëmpt request processing. If the block returns a valid response, the request is preëmpted & it returns that response.
124
124
 
125
125
  # app.rb
126
126
 
127
127
  require "sansom"
128
128
 
129
129
  s = Sansom.new
130
- s.map "/api", SinatraApp.new
131
- s.map "/", RailsApp.new
132
- s.start
130
+ s.before do |r|
131
+ next [200, {}, ["Preëmpted."]] if some_condition
132
+ end
133
133
 
134
- Lastly, any object can become a "`Sansom`" through a mixin:
134
+ You could use this for request statistics, caching, auth, etc.
135
+
136
+ After filters
137
+ -
138
+
139
+ You can also write after filters to tie up the loose ends of a response. If they return a valid response, that response is used instead of the response from a route. After blocks are not called if a before filter was ever called.
135
140
 
136
- # mixin.ru
141
+ # app.rb
142
+
143
+ require "sansom"
137
144
 
138
- class Mixin < Hash
139
- include Sansomable
140
-
141
- def template
142
- get "/sansomable" do |r|
143
- [200, { "Content-Type" => "text/plain"}, ["Sansomable Hash"]]
144
- end
145
- end
145
+ s = Sansom.new
146
+ s.after do |req,res| # req is a Rack::Request and res is the response generated by a route.
147
+ next [200, {}, ["Postëmpted."]] if some_condition
146
148
  end
149
+
150
+ Errors
151
+ -
152
+
153
+ Error blocks allow for the app to return something parseable when an error is raised.
154
+
155
+ require "sansom"
156
+ require "json"
147
157
 
148
- run Mixin.new
158
+ s = Sansom.new
159
+ s.error do |err, r| # err is the error, r is a Rack::Request
160
+ [500, {"yo" => "shit"}, [{ :message => err.message }.to_json]]
161
+ end
149
162
 
150
- If you look at how `Sansom` is defined, it makes sense:
163
+ There is also a unique error 404 handler:
151
164
 
152
- Sansom = Class.new Object
153
- Sansom.include Sansomable
165
+ require "sansom"
166
+ require "json"
154
167
 
168
+ s = Sansom.new
169
+ s.not_found do |r| # r is a Rack::Request
170
+ [404, {"yo" => "shit"}, [{ :message => "not found" }.to_json]]
171
+ end
172
+
155
173
  Matching
156
174
  -
157
175
 
@@ -162,7 +180,10 @@ Matching
162
180
  1. The route matching the path and verb
163
181
  2. The first Subsansom that matches the route & verb
164
182
  3. The first mounted non-`Sansom` rack app matching the route
165
-
183
+
184
+ Some examples of routes Sansom recognizes:
185
+ `/path/to/resource` - Standard path
186
+ `/users/:id/show` - Parameterized path
166
187
 
167
188
  Notes
168
189
  -
@@ -177,32 +198,26 @@ Speed
177
198
 
178
199
  Well, that's great and all, but how fast is "hello world" example in comparision to Rack or Sinatra?
179
200
 
180
- Rack: **15ms**<br />
181
- Sansom: **15ms**<br />
201
+ Rack: **12ms**<br />
202
+ Sansom: **15ms**\*<br />
182
203
  Sinatra: **28ms**<br />
183
- Rails: **34ms**
204
+ Rails: **34ms****
184
205
 
185
- (results are rounded down)
206
+ (results are measured locally using Puma and are rounded down)
186
207
 
187
208
  Hey [Konstantine](https://github.com/rkh), *put that in your pipe and smoke it*.
188
209
 
210
+ \* Uncached. If a tree lookup is cached, it will be pretty much as fast as Rack.
211
+ \** Rails loads a rich welcome page which may contribute to its slowness
212
+
189
213
  Todo
190
214
  -
191
215
 
192
- 1. Returning more than just rack responses:
193
- * Strings
194
- * Objects
195
- * Maybe more syntactic sugar
196
- 2. (Even) more stability
197
- 3. \<Your idea here\>
216
+ * Multiple return types for routes
217
+
218
+ If you have any ideas, let me know!
198
219
 
199
220
  Contributing
200
221
  -
201
222
 
202
- 1. Fork it
203
- 2. Create your feature branch (`git checkout -b my-new-feature`)
204
- 3. Commit your changes (`git commit -am 'Add some feature'`)
205
- 4. Push to the branch (`git push origin my-new-feature`)
206
- 5. Create a new Pull Request
207
-
208
- **Please make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is is brevity.**
223
+ You know the drill. But ** make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is is brevity.**
data/changelog.md CHANGED
@@ -61,4 +61,16 @@ Here's an example
61
61
 
62
62
  0.1.2
63
63
 
64
- - Fixed issue with `include` in the `Sansom` class
64
+ - Fixed issue with `include` in the `Sansom` class
65
+
66
+ 0.2.0
67
+
68
+ - Rewrite internals to:
69
+ 1. Avoid collisions with the including class
70
+ 2. Improve performance
71
+ 3. Look better
72
+ 4. **Avoid bugs**
73
+
74
+ - Route match caching by path and HTTP method
75
+ &nbsp;&nbsp;->Should improve performance for static paths dramatically
76
+
data/lib/rack/fastlint.rb CHANGED
@@ -3,36 +3,37 @@
3
3
  require "rack"
4
4
 
5
5
  module Rack
6
- class Fastlint
7
- def self.response res
8
- return false unless res.kind_of?(Array) && res.count == 3
6
+ class Lint
7
+ def self.fastlint res
8
+ return false unless res.respond_to?(:to_a) && res.count == 3
9
9
 
10
- status, headers, body = res
10
+ status, headers, body = res.to_a
11
+ return false if status.nil?
12
+ return false if headers.nil?
13
+ return false if body.nil?
11
14
 
12
15
  return false unless status.to_i >= 100 || status.to_i == -1
13
16
  return false unless headers.respond_to? :each
14
17
  return false unless body.respond_to? :each
18
+ return false if body.respond_to?(:to_path) && !File.exist?(body.to_path)
15
19
 
16
- if body.respond_to? :to_path
17
- return false unless File.exist? body.to_path
20
+ if status.to_i < 200 || [204, 205, 304].include?(status.to_i)
21
+ return false if headers.member? "Content-Length"
22
+ return false if headers.member? "Content-Type"
18
23
  end
19
24
 
20
- begin
21
- headers.each { |k,v|
22
- next if key =~ /^rack\..+$/
23
- throw StandardError unless k.kind_of? String
24
- throw StandardError unless v.kind_of? String
25
- throw StandardError if k.downcase == "status"
26
- throw StandardError unless k !~ /[:\n]/
27
- throw StandardError unless k !~ /[-_]\z/
28
- throw StandardError unless k =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
29
- }
25
+ headers.each { |k,v|
26
+ next if k.start_with? "rack."
27
+ return false unless k.kind_of? String
28
+ return false unless v.kind_of? String
29
+ return false if k == "Status"
30
+ return false unless k !~ /[:\n]/
31
+ return false unless k !~ /[-_]\z/
32
+ return false unless k =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
33
+ }
34
+
35
+ body.each { |p| return false unless p.respond_to? :to_str } # to_str is implemented by classes that act like strigs
30
36
 
31
- body.each { |part| throw StandardError unless part.kind_of? String }
32
- rescue StandardError
33
- return false
34
- end
35
-
36
37
  true
37
38
  end
38
39
  end
data/lib/sansom.rb CHANGED
@@ -1,94 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "rack"
4
- require_relative "./sansom/pine"
5
- require_relative "./rack/fastlint.rb"
6
-
7
- module Sansomable
8
- InvalidRouteError = Class.new StandardError
9
- HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options, :link, :unlink, :trace].freeze
10
- ROUTE_METHODS = HTTP_VERBS+[:map]
11
- RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
12
- NOT_FOUND = [404, {}, ["Not found."]].freeze
13
-
14
- def tree
15
- if @tree.nil?
16
- @tree = Pine::Node.new "ROOT"
17
- template if respond_to? :template
18
- end
19
- @tree
20
- end
21
-
22
- def call env
23
- return NOT_FOUND if tree.leaf? && tree.root?
24
-
25
- r = Rack::Request.new env
26
- m = tree.match r.path_info, r.request_method
27
-
28
- return NOT_FOUND if m.nil?
29
-
30
- begin
31
- if @before_block && @before_block.arity == 1
32
- bres = @before_block.call r
33
- return bres if Rack::Fastlint.response bres
34
- end
35
-
36
- if m.url_params.count > 0
37
- q = r.params.merge m.url_params
38
- s = q.map { |p| p.join '=' }.join '&'
39
- r.env["rack.request.query_hash"] = q
40
- r.env["rack.request.query_string"] = s
41
- r.env["QUERY_STRING"] = s
42
- r.instance_variable_set "@params", r.POST.merge(q)
43
- end
44
-
45
- case m.item
46
- when Proc then res = m.item.call r
47
- else
48
- raise InvalidRouteError, "Route handlers must be blocks or valid rack apps." unless m.item.respond_to? :call
49
- r.env["PATH_INFO"] = m.remaining_path
50
- res = m.item.call r.env
51
- end
52
-
53
- if @after_block && @after_block.arity == 2
54
- ares = @after_block.call r, res
55
- return ares if Rack::Fastlint.response ares
56
- end
57
-
58
- res
59
- rescue StandardError => e
60
- b = (@error_blocks[e.class] || @error_blocks[:default]) rescue nil
61
- raise e if b.nil?
62
- b.call e, r
63
- end
64
- end
65
-
66
- def start port=3001
67
- raise NoRoutesError if tree.leaf?
68
- Rack::Handler.pick(RACK_HANDLERS).run self, :Port => port
69
- end
70
-
71
- def error error_key=:default, &block
72
- @error_blocks ||= {}
73
- @error_blocks[error_key] = block
74
- end
75
-
76
- def before &block
77
- @before_block = block
78
- end
79
-
80
- def after &block
81
- @after_block = block
82
- end
83
-
84
- def method_missing meth, *args, &block
85
- path, item = *args.dup.push(block)
86
- return super unless path && item
87
- return super unless item != self
88
- return super unless ROUTE_METHODS.include? meth
89
- tree.map_path path, item, meth
90
- end
91
- end
3
+ require_relative "./sansom/sansomable"
92
4
 
93
5
  Sansom = Class.new Object
94
6
  Sansom.send :include, Sansomable
data/lib/sansom/pine.rb CHANGED
@@ -1,108 +1,92 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Path routing tree
3
+ # Tree data structure designed specifically for
4
+ # routing. It is capable of matching both wildcards
5
+ # and semiwildcards.
6
+ #
7
+ # While other path routing software optimizes path parsing,
8
+ # Pine optimizes lookup. You could say it matches a route in
9
+ # something resembling logarithmic time, but really is linear time
10
+ # due to child lookups (children are just iterated over)
4
11
 
5
- module Pine
6
- Result = Struct.new :item, :remaining_path, :url_params
12
+ require_relative "./pine/node"
7
13
 
8
- class Content
9
- attr_reader :items, :map
10
-
11
- def initialize
12
- @items = []
13
- @map = {}
14
- end
15
-
16
- def set k,v
17
- @items << v if k == :map
18
- @map[k] = v unless k == :map
19
- end
14
+ class Pine
15
+ Match = Struct.new :handler, # Proc/Subsansom/Rack App
16
+ :remaining_path, # Part of path that wasn't matched, applies to subsansoms
17
+ :matched_path, # The matched part of a path
18
+ :params # Wildcard params
19
+
20
+ def initialize
21
+ @root = Pine::Node.new
22
+ @cache = {}
20
23
  end
21
24
 
22
- class Node
23
- attr_reader :name, :parent, :content
24
-
25
- def initialize name
26
- @name = name
27
- @content = Content.new
28
- @children = {}
29
- end
25
+ def empty?
26
+ @root.leaf?
27
+ end
30
28
 
31
- def root?
32
- @parent.nil?
33
- end
29
+ # returns all non-root path components
30
+ # path_comps("/my/path/")
31
+ # => ["my", "path"]
32
+ def path_comps path
33
+ path[1..(path[-1] == "/" ? -2 : -1)].split "/"
34
+ end
34
35
 
35
- def leaf?
36
- @children.count == 0
36
+ # map_path "/food", Subsansom.new, :map
37
+ # map_path "/", my_block, :get
38
+ # it's also chainable
39
+ def map_path path, handler, key
40
+ @cache.clear
41
+
42
+ node = (path == "/") ? @root : path_comps(path).inject(@root) { |n, comp| n << comp } # Fucking ruby interpreter
43
+
44
+ if key == :map && !handler.is_a?(Proc) # fucking ruby interpreter
45
+ if handler.singleton_class.include? Sansomable
46
+ node.subsansoms << handler
47
+ else
48
+ node.rack_app = handler
49
+ end
50
+ else
51
+ node.blocks[key] = handler
37
52
  end
38
53
 
39
- def wildcard?
40
- @name.start_with? ":"
41
- end
54
+ self
55
+ end
56
+
57
+ # match "/", :get
58
+ def match path, verb
59
+ return nil if empty?
42
60
 
43
- def [] k
44
- return @children[k] if @children.member? k
45
- c = @children.values.first
46
- return c if (c.wildcard? rescue false)
47
- end
61
+ k = verb.to_s + path.to_s
62
+ return @cache[k] if @cache.has_key? k
48
63
 
49
- def << comp
50
- child = self[comp]
51
-
52
- if child.nil?
53
- child = self.class.new comp
54
- child.instance_variable_set "@parent", self
55
- @children.reject!(&:leaf?) if child.wildcard?
56
- @children[comp] = child
57
- end
64
+ matched_length = 0
65
+ matched_params = {}
66
+ matched_wildcard = false
58
67
 
59
- child
60
- end
61
-
62
- def parse_path path
63
- c = path.split "/"
64
- c[0] = '/'
65
- c.delete_at(-1) if c[-1].empty?
68
+ walk = path_comps(path).inject @root do |n, comp|
69
+ c = n[comp]
70
+ break n if c.nil?
71
+ matched_length += comp.length+1
72
+ if c.dynamic?
73
+ matched_params[c.wildcard] = comp[c.wildcard_range]
74
+ matched_wildcard = true
75
+ end
66
76
  c
67
77
  end
68
78
 
69
- def map_path path, item, key
70
- parse_path(path).inject(self) { |node, comp| node << comp }.content.set key, item
71
- path
72
- end
73
-
74
- def match path, verb
75
- matched_comps = []
76
- matched_params = {}
77
-
78
- walk = parse_path(path).inject self do |node, comp|
79
- break node if node.leaf?
80
- next node[comp] if node.root?
81
-
82
- c = node[comp]
83
- break node if c.nil?
84
- matched_comps << comp
85
- matched_params[c.name[1..-1]] = comp if c.wildcard?
86
- c
87
- end
79
+ return nil if walk.nil?
88
80
 
89
- return nil if walk.nil?
90
- return nil if walk.root?
81
+ remaining = path[matched_length..-1]
82
+ match = walk.blocks[verb.downcase.to_sym]
83
+ match ||= walk.subsansoms.detect { |i| i._pine.match remaining, verb }
84
+ match ||= walk.rack_app
91
85
 
92
- c = walk.content
93
- subpath = path.sub "/#{matched_comps.join("/")}", ""
94
-
95
- match = c.map[verb.downcase.to_sym]
96
- match ||= c.items.detect { |i| sansom?(i) && i.tree.match(subpath, verb) }
97
- match ||= c.items.detect { |i| !sansom?(i) }
98
-
99
- return nil if match.nil?
100
-
101
- Result.new match, subpath, matched_params
102
- end
86
+ return nil if match.nil?
103
87
 
104
- def sansom? obj
105
- obj.singleton_class.include? Sansomable
106
- end
88
+ r = Match.new match, remaining, path[0..matched_length-1], matched_params
89
+ @cache[k] = r unless matched_wildcard # Only cache static lookups, avoid huge memory usage
90
+ r
107
91
  end
108
- end
92
+ end
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # represents a node on the routing tree
4
+
5
+ # No regexes are used once a node is initialized
6
+
7
+ class Pine
8
+ class Node
9
+ LineageError = Class.new StandardError
10
+ WILDCARD_REGEX = /<(\w*)\b[^>]*>/.freeze
11
+ URLPATHSAFE_REGEX = /[^a-zA-Z0-9_-]/.freeze
12
+ ROOT = "/".freeze
13
+
14
+ attr_reader :name # node "payload" data
15
+ attr_accessor :parent # node reference system
16
+ attr_reader :wildcard, :wildcard_range # wildcard data
17
+ attr_reader :rack_app, :subsansoms, :blocks # mapping
18
+ attr_reader :end_seq, :start_seq, :min_length # stored information used to match wildcards
19
+ attr_reader :wildcard_delimeter, :semiwildcard_delimeter # delimiter for wildcard syntax
20
+
21
+ # Pine::Node.new "asdf", "$", "?" # creates a node with $ as the wildcard delimiter and ? as the semiwildcard delimiter
22
+ # Pine::Node.new "asdf", "#" # creates a node with # as the wildcard delimiter and the default semiwildcard delimiter
23
+ # Pine::Node.new "asdf" # creates a node with the default delimiters
24
+ # Pine::Node.new # creates root node
25
+ # Delimiters can be any length
26
+ def initialize name=ROOT, wc_delim=":", swc_delim="<"
27
+ raise ArgumentError, "Delimiters must not be safe characters in a URL path." if wc_delim.match URLPATHSAFE_REGEX rescue false
28
+ raise ArgumentError, "Delimiters must not be safe characters in a URL path." if swc_delim.match URLPATHSAFE_REGEX rescue false
29
+ @name = name.freeze
30
+ @children = {}
31
+ @wildcard_children = {}
32
+ @blocks = {}
33
+ @subsansoms = []
34
+ @wildcard_delimeter = wc_delim
35
+ @semiwildcard_delimeter = swc_delim
36
+
37
+ unless root?
38
+ if @name.start_with? wildcard_delimeter
39
+ @wildcard_range = Range.new(0, -1).freeze
40
+ @wildcard = @name[wildcard_delimeter.length..-1].freeze
41
+ @start_seq = "".freeze
42
+ @end_seq = "".freeze
43
+ else
44
+ r = ['<','>'].include?(semiwildcard_delimeter) ? WILDCARD_REGEX : /#{swc_delim}(\w*)\b[^#{swc_delim}]*#{swc_delim}/
45
+ m = @name.match r
46
+ unless m.nil?
47
+ o = m.offset 1
48
+ @wildcard_range = Range.new(o.first-1, (-1*(m.string.length-o.last+1))+1).freeze # calc `last` rel to the last char idx
49
+ @wildcard = @name[wildcard_range.first+semiwildcard_delimeter.length..wildcard_range.last-semiwildcard_delimeter.length].freeze
50
+ @start_seq = @name[0..wildcard_range.first-1].freeze
51
+ @end_seq = wildcard_range.last == -1 ? "" : @name[wildcard_range.last+1..-1].freeze
52
+ end
53
+ end
54
+ end
55
+
56
+ @min_length = dynamic? ? start_seq.length + end_seq.length : name.length
57
+ end
58
+
59
+ def inspect
60
+ "#<#{self.class}: #{name.inspect}, #{dynamic? ? "Wildcard: '" + wildcard + "' #{wildcard_range.inspect}, " : "" }#{@children.count} children, #{leaf? ? "leaf" : "internal node"}>"
61
+ end
62
+
63
+ def == another
64
+ parent == another.parent &&
65
+ name == another.name
66
+ end
67
+
68
+ # TODO: check correctness of return values
69
+ def <=> another
70
+ return 0 if n == another
71
+
72
+ n = self
73
+ n = n.parent until n == another || n.root?
74
+ return 1 if n == another
75
+
76
+ n = another
77
+ n = n.parent until n == self || n.root?
78
+ return -1 if n == self
79
+
80
+ raise LinneageError, "Node not in tree."
81
+ end
82
+
83
+ def detach!
84
+ _set_parent nil
85
+ end
86
+
87
+ def siblings
88
+ parent.children.dup - self
89
+ end
90
+
91
+ def children
92
+ hash_children.values
93
+ end
94
+
95
+ def hash_children
96
+ Hash[@children.to_a + @wildcard_children.to_a]
97
+ end
98
+
99
+ def child? another
100
+ another.ancestor? self
101
+ end
102
+
103
+ def ancestor? another
104
+ n = self
105
+ n = n.parent until n == another || n.root?
106
+ n == another
107
+ end
108
+
109
+ def ancestors
110
+ n = self
111
+ n = n.parent until n.root?
112
+ n
113
+ end
114
+
115
+ def root?
116
+ name == ROOT
117
+ end
118
+
119
+ def leaf?
120
+ children.empty? && subsansoms.empty? && rack_app.nil?
121
+ end
122
+
123
+ def semiwildcard?
124
+ !wildcard_range.nil? && wildcard_range.size != 0
125
+ end
126
+
127
+ def wildcard?
128
+ !wildcard_range.nil? && wildcard_range.size == 0
129
+ end
130
+
131
+ # returns true if self is either a wildcard or a semiwildcard
132
+ def dynamic?
133
+ !wildcard_range.nil?
134
+ end
135
+
136
+ # Bottleneck for wildcard-heavy apps
137
+ def matches? comp
138
+ return comp == name unless dynamic?
139
+ comp.length >= min_length && comp.start_with?(start_seq) && comp.end_with?(end_seq)
140
+ end
141
+
142
+ # WARNING: Sansom's biggest bottleneck
143
+ # Partially chainable: No guarantee the returned value responds to :child or :[]
144
+ def child comp
145
+ raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
146
+ case
147
+ when @children.empty? && @wildcard_children.empty? then nil
148
+ when @children.member?(comp) then @children[comp]
149
+ else @wildcard_children.values.detect { |c| c.matches? comp } end
150
+ end
151
+
152
+ alias_method :[], :child
153
+
154
+ # chainable
155
+ def add_child! comp
156
+ raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
157
+ c = self[comp] || self.class.new(comp)
158
+ c._set_parent self
159
+ c
160
+ end
161
+
162
+ alias_method :<<, :add_child!
163
+
164
+ def _hchildren; @children; end
165
+ def _hwcchildren; @wildcard_children; end
166
+
167
+ # returns new parent so its chainable
168
+ def _set_parent p
169
+ return if @parent == p
170
+
171
+ # remove from old parent's children structure
172
+ unless @parent.nil?
173
+ @parent._hchildren.delete name unless dynamic?
174
+ @parent._hwcchildren.delete name if dynamic?
175
+ end
176
+
177
+ # add to new parent's children structure
178
+ if wildcard?
179
+ p._hchildren.reject! { |_,c| c.leaf? }
180
+ p._hwcchildren.reject! { |_,c| c.leaf? }
181
+ end
182
+ p._hwcchildren[name] = self
183
+
184
+ @parent = p # set new parent
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rack"
4
+ require_relative "./pine"
5
+ require_relative "../rack/fastlint"
6
+
7
+ module Sansomable
8
+ RouteError = Class.new StandardError
9
+ ResponseError = Class.new StandardError
10
+ HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options, :link, :unlink, :trace].freeze
11
+ ACTION_VERBS = [:map].freeze
12
+ VALID_VERBS = (HTTP_VERBS+ACTION_VERBS).freeze
13
+ RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
14
+ NOTFOUND_TEXT = "Not found.".freeze
15
+
16
+ def _pine
17
+ if @_pine.nil?
18
+ @_pine = Pine.new
19
+ template if respond_to? :template
20
+ routes if respond_to? :routes
21
+ end
22
+ @_pine
23
+ end
24
+
25
+ def _call_handler handler, *args
26
+ raise ArgumentError, "Handler must not be nil." if handler.nil?
27
+ raise ArgumentError, "Handler must be a valid rack app." unless handler.respond_to? :call
28
+ raise ArgumentError, "Handler cannot take all passed args." if handler.respond_to?(:arity) && args.count != handler.arity
29
+ res = handler.call *args
30
+ res = res.finish if res.is_a? Rack::Response
31
+ raise ResponseError, "Response must either be a rack response, string, or object" unless Rack::Lint.fastlint res # custom method
32
+ res = [200, {}, [res.to_str]] if res.respond_to? :to_str
33
+ res
34
+ end
35
+
36
+ def _not_found
37
+ return _call_route @_not_found, r unless @_not_found.nil?
38
+ [404, {}, [NOTFOUND_TEXT]]
39
+ end
40
+
41
+ def call env
42
+ return _not_found if _pine.empty? # no routes
43
+ m = _pine.match env["PATH_INFO"], env["REQUEST_METHOD"]
44
+ return _not_found if m.nil?
45
+
46
+ r = Rack::Request.new env
47
+
48
+ begin
49
+ r.path_info = m.remaining_path unless Proc === m.handler
50
+
51
+ unless m.params.empty?
52
+ r.env["rack.request.query_string"] = r.query_string # now Rack::Request#GET will return r.env["rack.request.query_hash"]
53
+ (r.env["rack.request.query_hash"] ||= {}).merge! m.params # update the necessary field in the hash
54
+ r.instance_variable_set "@params", nil # tell Rack::Request to recalc Rack::Request#params
55
+ end
56
+
57
+ res = _call_handler @_before, r if @_before # call before block
58
+ res ||= _call_handler m.handler, (Proc === m.handler ? r : r.env) # call route handler block
59
+ res ||= _call_handler @_after, r, res if @_after # call after block
60
+ res ||= _not_found
61
+ res
62
+ rescue => e
63
+ _call_handler @_error_blocks[e.class], e, r rescue raise e
64
+ end
65
+ end
66
+
67
+ def start port=3001, handler=""
68
+ raise RouteError, "No routes." if _pine.empty?
69
+ begin
70
+ h = Rack::Handler.get handler.to_s
71
+ rescue LoadError, NameError
72
+ h = Rack::Handler.pick(RACK_HANDLERS)
73
+ ensure
74
+ h.run self, :Port => port
75
+ end
76
+ end
77
+
78
+ def error error_key=nil, &block
79
+ (@_error_blocks ||= Hash.new { |h| h[:default] })[error_key || :default] = block
80
+ end
81
+
82
+ def before &block
83
+ raise ArgumentError, "Before filter blocks must take one argument." if block && block.arity != 1
84
+ @_before = block
85
+ end
86
+
87
+ def after &block
88
+ raise ArgumentError, "After filter blocks must take two arguments." if block && block.arity != 2
89
+ @_after = block
90
+ end
91
+
92
+ def not_found &block
93
+ raise ArgumentError, "Not found blocks must take one argument." if block && block.arity != 1
94
+ @_not_found = block
95
+ end
96
+
97
+ def method_missing meth, *args, &block
98
+ path, item = *args.dup.push(block)
99
+ return super unless path && item && item != self
100
+ return super unless VALID_VERBS.include? meth
101
+ return super unless item.respond_to? :call
102
+ _pine.map_path path, item, meth
103
+ end
104
+ end
data/sansom.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "sansom"
7
- s.version = "0.1.2"
7
+ s.version = "0.2.0"
8
8
  s.authors = ["Nathaniel Symer"]
9
9
  s.email = ["nate@natesymer.com"]
10
10
  s.summary = "Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, near where it was made."
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sansom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathaniel Symer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-28 00:00:00.000000000 Z
11
+ date: 2014-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -55,6 +55,8 @@ files:
55
55
  - lib/rack/fastlint.rb
56
56
  - lib/sansom.rb
57
57
  - lib/sansom/pine.rb
58
+ - lib/sansom/pine/node.rb
59
+ - lib/sansom/sansomable.rb
58
60
  - sansom.gemspec
59
61
  homepage: http://github.com/fhsjaagshs/sansom
60
62
  licenses: