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 +4 -4
- data/README.md +114 -99
- data/changelog.md +13 -1
- data/lib/rack/fastlint.rb +22 -21
- data/lib/sansom.rb +1 -89
- data/lib/sansom/pine.rb +72 -88
- data/lib/sansom/pine/node.rb +187 -0
- data/lib/sansom/sansomable.rb +104 -0
- data/sansom.gemspec +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fececc46ad6a785f2e8cc4be0035fff8f7a7d998
|
4
|
+
data.tar.gz: 725f8e1256721a69d085cbec36591a3596483245
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
19
|
+
***A tool should do one thing, and do it well. (Unix philosophy)***
|
20
20
|
|
21
|
-
A web framework is,
|
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
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
58
|
+
Writing your own traditional-style webapp
|
59
|
+
-
|
51
60
|
|
52
|
-
|
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
|
-
|
63
|
+
####There is more footwork for a traditional-style webapp:
|
68
64
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
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.
|
131
|
-
|
132
|
-
|
130
|
+
s.before do |r|
|
131
|
+
next [200, {}, ["Preëmpted."]] if some_condition
|
132
|
+
end
|
133
133
|
|
134
|
-
|
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
|
-
#
|
141
|
+
# app.rb
|
142
|
+
|
143
|
+
require "sansom"
|
137
144
|
|
138
|
-
|
139
|
-
|
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
|
-
|
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
|
-
|
163
|
+
There is also a unique error 404 handler:
|
151
164
|
|
152
|
-
|
153
|
-
|
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: **
|
181
|
-
Sansom: **15ms
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
+
->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
|
7
|
-
def self.
|
8
|
-
return false unless res.
|
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
|
17
|
-
return false
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
6
|
-
Result = Struct.new :item, :remaining_path, :url_params
|
12
|
+
require_relative "./pine/node"
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# match "/", :get
|
58
|
+
def match path, verb
|
59
|
+
return nil if empty?
|
42
60
|
|
43
|
-
|
44
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
c
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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.
|
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.
|
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-
|
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:
|