sansom 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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:
|