sansom 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +93 -84
- data/ext/sansom/pine/extconf.rb +6 -0
- data/lib/rack/fastlint.rb +29 -26
- data/lib/sansom/pine/node.rb +40 -122
- data/lib/sansom/pine.rb +18 -21
- data/lib/sansom/sansomable.rb +20 -37
- data/lib/sansom.rb +0 -3
- data/sansom.gemspec +2 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b5864e32e2af6d42813deac99f743aa5f709377
|
4
|
+
data.tar.gz: 8f7778c22852629a41aa2056c957d68fd295e8b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb966e9564c909969af9b96aa1e62e7b0eedaf345b573ee25090bf229e654bc0a978f29c0f3b736948261d0f8abf9090d84f11da3958ce83e996318b0ab10e02
|
7
|
+
data.tar.gz: d5fee84cb9b078c953f72a15ad227adb9d05aad20c801aab84640b8921f2cc06f7cd8b40f68ee21eda9cead4481faf901106c62c51fef82f5bd390f085576795
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,26 +1,24 @@
|
|
1
1
|
Sansom
|
2
2
|
===
|
3
3
|
|
4
|
-
|
4
|
+
No-nonsense web 'picowork' named after Sansom street in Philly, near where it was made.
|
5
5
|
|
6
6
|
Philosophy
|
7
7
|
-
|
8
8
|
|
9
|
-
***A
|
9
|
+
***A framework should not limit you to one way of thinking.***
|
10
10
|
|
11
|
-
You can write a `Sansomable` for each logical unit of your API, but you also don't have to.
|
11
|
+
- You can write a `Sansomable` for each logical unit of your API, but you also don't have to.
|
12
12
|
|
13
|
-
You can also mount existing Rails/Sinatra/Rack apps in your `Sansomable`. But you also don't have to.
|
13
|
+
- You can also mount existing Rails/Sinatra/Rack apps in your `Sansomable`. But you also don't have to.
|
14
14
|
|
15
|
-
You can write one `Sansomable` for your entire API.
|
15
|
+
- You can write one `Sansomable` for your entire API.
|
16
16
|
|
17
17
|
Fuck it.
|
18
18
|
|
19
19
|
***A tool should do one thing, and do it well. (Unix philosophy)***
|
20
20
|
|
21
|
-
A web framework is, fundamentally, a tool to connect code to a URL's path.
|
22
|
-
|
23
|
-
A web framework doesn't provide an ORM, template rendering, shortcuts, nor security patches.
|
21
|
+
A web framework is, fundamentally, a tool to connect code to a URL's path. Therefore, a web framework doesn't provide an ORM, template rendering, shortcuts, nor security patches.
|
24
22
|
|
25
23
|
***A web framework shall remain a framework***
|
26
24
|
|
@@ -31,96 +29,84 @@ Installation
|
|
31
29
|
|
32
30
|
`gem install sansom`
|
33
31
|
|
34
|
-
|
35
|
-
-
|
36
|
-
Traditional approach:
|
37
|
-
|
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:
|
47
|
-
|
48
|
-
# app.rb
|
49
|
-
|
50
|
-
require "sansom"
|
51
|
-
|
52
|
-
s = Sansom.new
|
53
|
-
# define routes on s
|
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.
|
32
|
+
Or, you can clone this repo and use `gem build sansom.gemspec` to build the gem.
|
57
33
|
|
58
|
-
Writing
|
34
|
+
Writing a Sansom app
|
59
35
|
-
|
36
|
+
Writing a one-file application is trivial with Sansom:
|
60
37
|
|
61
|
-
|
62
|
-
|
63
|
-
####There is more footwork for a traditional-style webapp:
|
64
|
-
|
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.
|
38
|
+
# config.ru
|
71
39
|
|
72
|
-
|
73
|
-
|
74
|
-
require "sansom"
|
40
|
+
require "sansom"
|
75
41
|
|
76
42
|
class MyAPI
|
77
43
|
include Sansomable
|
78
|
-
def
|
44
|
+
def routes
|
79
45
|
# define routes here
|
80
46
|
end
|
81
47
|
end
|
82
48
|
|
83
|
-
And your `config.ru` file
|
84
|
-
|
85
|
-
# config.ru
|
86
|
-
|
87
|
-
require "./myapi"
|
88
|
-
|
89
49
|
run MyAPI.new
|
90
50
|
|
91
51
|
Defining Routes
|
92
52
|
-
|
93
|
-
Routes
|
53
|
+
Routes are defined through (dynamically resolved) instance methods that correspond to HTTP verbs. They take a path and a block. The block must be able to accept (at least) **one** argument.
|
94
54
|
|
95
|
-
|
55
|
+
You can either write
|
56
|
+
|
57
|
+
require "sansom"
|
58
|
+
|
59
|
+
class MyAPI
|
60
|
+
include Sansomable
|
61
|
+
def routes
|
62
|
+
get "/" do |r|
|
63
|
+
# r is a Rack::Request object
|
64
|
+
[200, {}, ["hello world!"]]
|
65
|
+
end
|
66
|
+
|
67
|
+
post "/form" do |r|
|
68
|
+
# return a Rack response
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
Routes can also be defined like so:
|
75
|
+
|
76
|
+
s = MyAPI.new
|
96
77
|
s.get "/" do |r| # r is a Rack::Request
|
97
78
|
[200, {}, ["Return a Rack response."]]
|
98
79
|
end
|
99
80
|
|
100
|
-
|
81
|
+
But let's say you have an existing Sinatra/Rails/Sansom (Rack) app. It's simple: mount them. For example, mounting existing applications can be used to easily version an app:
|
101
82
|
|
102
|
-
#
|
83
|
+
# config.ru
|
103
84
|
|
104
85
|
require "sansom"
|
105
86
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
s.start
|
87
|
+
class Versioning
|
88
|
+
include Sansomable
|
89
|
+
end
|
110
90
|
|
111
|
-
|
91
|
+
s = Versioning.new
|
92
|
+
s.mount "/v1", MyAPI.new
|
93
|
+
s.mount "/v2", MyNewAPI.new
|
94
|
+
|
95
|
+
run s
|
96
|
+
|
97
|
+
Sansom routes vs Sinatra routes
|
112
98
|
-
|
113
99
|
|
114
|
-
Sansom
|
100
|
+
**Sansom routes remain true 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
101
|
|
116
|
-
Sinatra
|
102
|
+
**Sinatra routes become methods behind the scenes**: When a route is matched, Sinatra looks up the method and calls it.
|
117
103
|
|
118
|
-
|
104
|
+
It's a common idiom in Sinatra to use `return` to terminate execution of a route prematurely (since Sinatra routes aren't blocks). **You must use `next` instead** (you can relplace all instances of `return` with `next`).
|
119
105
|
|
120
106
|
Before filters
|
121
107
|
-
|
122
108
|
|
123
|
-
You can write before filters to try to preëmpt request processing. If the block returns
|
109
|
+
You can write before filters to try to preëmpt request processing. If the block returns anything (other than nil) **the request is preëmpted**. In that case, the response from the before block is the response for the request.
|
124
110
|
|
125
111
|
# app.rb
|
126
112
|
|
@@ -128,7 +114,7 @@ You can write before filters to try to preëmpt request processing. If the block
|
|
128
114
|
|
129
115
|
s = Sansom.new
|
130
116
|
s.before do |r|
|
131
|
-
|
117
|
+
[200, {}, ["Preëmpted."]] if some_condition
|
132
118
|
end
|
133
119
|
|
134
120
|
You could use this for request statistics, caching, auth, etc.
|
@@ -136,7 +122,7 @@ You could use this for request statistics, caching, auth, etc.
|
|
136
122
|
After filters
|
137
123
|
-
|
138
124
|
|
139
|
-
|
125
|
+
Called after a route is called. If they return a non-nil response, that response is used instead of the response from a route. After filters are not called if a before filter preëmpted route execution.
|
140
126
|
|
141
127
|
# app.rb
|
142
128
|
|
@@ -156,8 +142,8 @@ Error blocks allow for the app to return something parseable when an error is ra
|
|
156
142
|
require "json"
|
157
143
|
|
158
144
|
s = Sansom.new
|
159
|
-
s.error do |
|
160
|
-
[500, {"yo" => "
|
145
|
+
s.error do |r, err| # err is the error, r is a Rack::Request
|
146
|
+
[500, {"yo" => "headers"}, [{ :message => err.message }.to_json]]
|
161
147
|
end
|
162
148
|
|
163
149
|
There is also a unique error 404 handler:
|
@@ -175,31 +161,51 @@ Matching
|
|
175
161
|
|
176
162
|
`Sansom` uses trees to match routes. It follows a certain set of rules:
|
177
163
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
164
|
+
1. The route matching the path and verb. Routes have a sub-order:
|
165
|
+
1. "Static" paths
|
166
|
+
2. Wildcards (see below)
|
167
|
+
1. Full mappings (kinda a non-compete)
|
168
|
+
2. Partial mappings
|
169
|
+
3. Splats
|
170
|
+
3. The first Subsansom that matches the route & verb
|
171
|
+
4. The first mounted non-`Sansom` rack app matching the route
|
172
|
+
|
183
173
|
|
184
|
-
|
185
|
-
|
186
|
-
|
174
|
+
Wildcards
|
175
|
+
-
|
176
|
+
|
177
|
+
Sansom supports multiple wildcards:
|
178
|
+
|
179
|
+
`/path/to/:resource/:action` - Full mapping
|
180
|
+
`/path/to/resource.<format>` - Partial mapping
|
181
|
+
`/path/to/*.json` - Splat
|
182
|
+
`/path/to/*.<format>.<compression>` - You can mix them.
|
183
|
+
|
184
|
+
Mappings map part of the route (for example `format` above) to the corresponding part of the matched path (for `/resource.<format>` and `/resource.json` yields a mapping of `format`:`json`).
|
185
|
+
|
186
|
+
Mappings (full and partial) are available in `Rack::Request#params` **by name**, and splats are available under the key `splats` in `Rack::Request#params`.
|
187
|
+
|
188
|
+
*See the Matching section of this readme for wildcard precedence.*
|
189
|
+
|
187
190
|
|
188
191
|
Notes
|
189
192
|
-
|
190
193
|
|
191
194
|
- `Sansom` does not pollute _any_ `Object` methods, including `initialize`
|
192
|
-
-
|
193
|
-
|
194
|
-
|
195
|
+
- No regexes are used in the entire project.
|
196
|
+
- Has one dependency: `rack`
|
197
|
+
- `Sansom` is under **400** lines of code at the time of writing. This includes
|
198
|
+
* Rack conformity & the DSL (`sansom.rb`) (~90 lines)
|
199
|
+
* Custom tree-based routing (`sanom/pine.rb`) (~150 lines)
|
200
|
+
* libpatternmatch (~150 lines of C++)
|
195
201
|
|
196
202
|
Speed
|
197
203
|
-
|
198
204
|
|
199
205
|
Well, that's great and all, but how fast is "hello world" example in comparision to Rack or Sinatra?
|
200
206
|
|
201
|
-
Rack: **
|
202
|
-
Sansom: **
|
207
|
+
Rack: **11ms**<br />
|
208
|
+
Sansom: **14ms**\*†<br />
|
203
209
|
Sinatra: **28ms**<br />
|
204
210
|
Rails: **34ms****
|
205
211
|
|
@@ -207,7 +213,10 @@ Rails: **34ms****
|
|
207
213
|
|
208
214
|
Hey [Konstantine](https://github.com/rkh), *put that in your pipe and smoke it*.
|
209
215
|
|
210
|
-
\* Uncached. If a tree lookup is cached, it
|
216
|
+
\* Uncached. If a tree lookup is cached, it takes the same time as Rack.
|
217
|
+
|
218
|
+
† Sansom's speed (compared to Sinatra) may be because it doesn't load any middleware by default.
|
219
|
+
|
211
220
|
\** Rails loads a rich welcome page which may contribute to its slowness
|
212
221
|
|
213
222
|
Todo
|
@@ -220,4 +229,4 @@ If you have any ideas, let me know!
|
|
220
229
|
Contributing
|
221
230
|
-
|
222
231
|
|
223
|
-
You know the drill. But ** make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is
|
232
|
+
You know the drill. But ** make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is its brevity.**
|
data/lib/rack/fastlint.rb
CHANGED
@@ -5,36 +5,39 @@ require "rack"
|
|
5
5
|
module Rack
|
6
6
|
class Lint
|
7
7
|
def self.fastlint res
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
begin
|
9
|
+
return false unless res.respond_to?(:to_a) && res.count == 3
|
10
|
+
|
11
|
+
status, headers, body = res.to_a
|
12
|
+
return false if status.nil?
|
13
|
+
return false if headers.nil?
|
14
|
+
return false if body.nil?
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
return false unless status.to_i >= 100 || status.to_i == -1
|
17
|
+
return false unless headers.respond_to? :each
|
18
|
+
return false unless body.respond_to? :each
|
19
|
+
return false if body.respond_to?(:to_path) && !File.exist?(body.to_path)
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
if status.to_i < 200 || [204, 205, 304].include?(status.to_i)
|
22
|
+
return false if headers.member? "Content-Length"
|
23
|
+
return false if headers.member? "Content-Type"
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
headers.each { |k,v|
|
27
|
+
next if k.start_with? "rack."
|
28
|
+
return false unless k.kind_of? String
|
29
|
+
return false unless v.kind_of? String
|
30
|
+
return false if k == "Status"
|
31
|
+
return false unless k !~ /[:\n]/
|
32
|
+
return false unless k !~ /[-_]\z/
|
33
|
+
return false unless k =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
|
34
|
+
}
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
body.each { |p| return false unless p.respond_to? :to_str } # to_str is implemented by classes that act like strigs
|
37
|
+
true
|
38
|
+
rescue => e
|
39
|
+
false
|
40
|
+
end
|
38
41
|
end
|
39
42
|
end
|
40
43
|
end
|
data/lib/sansom/pine/node.rb
CHANGED
@@ -1,102 +1,44 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
# represents a node on the routing tree
|
3
|
+
# represents a node on the routing tree.
|
4
|
+
# does not use any regexes. Rather, it uses
|
5
|
+
# a custom pattern matching library
|
4
6
|
|
5
|
-
|
7
|
+
require "sansom/pine/matcher"
|
6
8
|
|
7
9
|
class Pine
|
8
10
|
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
11
|
attr_reader :name # node "payload" data
|
15
12
|
attr_accessor :parent # node reference system
|
16
|
-
attr_reader :
|
13
|
+
attr_reader :children # hash of non-patterned children
|
14
|
+
attr_reader :dynamic_children # array of patterned chilren
|
17
15
|
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
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
17
|
+
def initialize n='/'
|
18
|
+
@name = n.freeze
|
19
|
+
@matcher = Pine::Matcher.new name
|
30
20
|
@children = {}
|
31
|
-
@
|
21
|
+
@dynamic_children = []
|
32
22
|
@blocks = {}
|
33
23
|
@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
24
|
end
|
58
25
|
|
59
|
-
def inspect
|
60
|
-
"#<#{self.class}: #{name.inspect}, #{dynamic? ? "Wildcard: '" + wildcard + "' #{wildcard_range.inspect}, " : "" }#{@children.count} children, #{leaf? ? "leaf" : "internal node"}>"
|
61
|
-
end
|
26
|
+
def inspect; "#<#{self.class}: #{children.count+dynamic_children.count} children, #{leaf? ? "leaf" : "internal node"}>"; end
|
62
27
|
|
63
28
|
def == another
|
64
29
|
parent == another.parent &&
|
65
30
|
name == another.name
|
66
31
|
end
|
67
|
-
|
68
|
-
# TODO: check correctness of return values
|
32
|
+
|
69
33
|
def <=> another
|
70
|
-
return
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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."
|
34
|
+
return nil unless another.is_a? Pine::Node
|
35
|
+
return 0 if self == another
|
36
|
+
return -1 if another.ancestor? self
|
37
|
+
return 1 if another.child? self
|
38
|
+
nil
|
81
39
|
end
|
82
40
|
|
83
|
-
def
|
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
|
41
|
+
def child? anothrer
|
100
42
|
another.ancestor? self
|
101
43
|
end
|
102
44
|
|
@@ -107,46 +49,25 @@ class Pine
|
|
107
49
|
end
|
108
50
|
|
109
51
|
def ancestors
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
52
|
+
a = [self]
|
53
|
+
a << a.last.parent until a.last.root?
|
54
|
+
a[1..-1]
|
125
55
|
end
|
126
56
|
|
127
|
-
def
|
128
|
-
|
129
|
-
end
|
57
|
+
def root?; name == '/'; end
|
58
|
+
def leaf?; children.empty? && dynamic_children.empty? && subsansoms.empty? && rack_app.nil?; end
|
130
59
|
|
131
|
-
|
132
|
-
def
|
133
|
-
|
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
|
60
|
+
def dynamic?; @matcher.dynamic? || name.start_with?(':'); end
|
61
|
+
def splats comp; @matcher.splats comp; end
|
62
|
+
def mappings comp; @matcher.mappings comp; end
|
141
63
|
|
142
64
|
# WARNING: Sansom's biggest bottleneck
|
143
65
|
# Partially chainable: No guarantee the returned value responds to :child or :[]
|
144
66
|
def child comp
|
145
67
|
raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
else @wildcard_children.values.detect { |c| c.matches? comp } end
|
68
|
+
res = @children[comp]
|
69
|
+
res ||= dynamic_children.detect { |c| c.instance_variable_get("@matcher").matches? comp }
|
70
|
+
res
|
150
71
|
end
|
151
72
|
|
152
73
|
alias_method :[], :child
|
@@ -155,32 +76,29 @@ class Pine
|
|
155
76
|
def add_child! comp
|
156
77
|
raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
|
157
78
|
c = self[comp] || self.class.new(comp)
|
158
|
-
c.
|
79
|
+
c.parent = self
|
159
80
|
c
|
160
81
|
end
|
161
82
|
|
162
83
|
alias_method :<<, :add_child!
|
163
84
|
|
164
|
-
def
|
165
|
-
def _hwcchildren; @wildcard_children; end
|
166
|
-
|
167
|
-
# returns new parent so its chainable
|
168
|
-
def _set_parent p
|
85
|
+
def parent= p
|
169
86
|
return if @parent == p
|
170
87
|
|
171
88
|
# remove from old parent's children structure
|
172
89
|
unless @parent.nil?
|
173
|
-
@parent.
|
174
|
-
@parent.
|
90
|
+
@parent.children.delete name
|
91
|
+
@parent.dynamic_children.reject! { |c| c.name == name }
|
175
92
|
end
|
176
93
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
94
|
+
unless p.nil?
|
95
|
+
if dynamic?
|
96
|
+
p.dynamic_children << self # add to new parent's children structure
|
97
|
+
else
|
98
|
+
p.children[name] = self
|
99
|
+
end
|
181
100
|
end
|
182
|
-
|
183
|
-
|
101
|
+
|
184
102
|
@parent = p # set new parent
|
185
103
|
end
|
186
104
|
end
|
data/lib/sansom/pine.rb
CHANGED
@@ -1,22 +1,17 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
# Tree data structure designed specifically for
|
4
|
-
# routing. It
|
5
|
-
# and
|
4
|
+
# routing. It uses libpatternmatch (google it) to
|
5
|
+
# match paths with splats and mappings
|
6
6
|
#
|
7
7
|
# While other path routing software optimizes path parsing,
|
8
|
-
# Pine optimizes lookup
|
9
|
-
#
|
10
|
-
#
|
8
|
+
# Pine optimizes lookup and pattern matching. Pine takes
|
9
|
+
# logarithmic time in path matching and linear time in
|
10
|
+
# path matching (libpatternmatch)
|
11
11
|
|
12
12
|
require_relative "./pine/node"
|
13
13
|
|
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
|
-
|
14
|
+
class Pine
|
20
15
|
def initialize
|
21
16
|
@root = Pine::Node.new
|
22
17
|
@cache = {}
|
@@ -30,18 +25,18 @@ class Pine
|
|
30
25
|
# path_comps("/my/path/")
|
31
26
|
# => ["my", "path"]
|
32
27
|
def path_comps path
|
33
|
-
path[1..(path[-1] == "/" ? -2 : -1)].split
|
28
|
+
path.nil? || path.empty? ? [] : path[1..(path[-1] == "/" ? -2 : -1)].split('/')
|
34
29
|
end
|
35
30
|
|
36
31
|
# map_path "/food", Subsansom.new, :map
|
37
|
-
# map_path "/",
|
32
|
+
# map_path "/", ObjectThatRespondsToCall.new, :get
|
38
33
|
# it's also chainable
|
39
34
|
def map_path path, handler, key
|
40
35
|
@cache.clear
|
41
36
|
|
42
|
-
node = (path == "/") ? @root : path_comps(path).inject(@root) { |n, comp| n << comp }
|
37
|
+
node = (path == "/") ? @root : path_comps(path).inject(@root) { |n, comp| n << comp }
|
43
38
|
|
44
|
-
if key == :
|
39
|
+
if key == :mount && !handler.is_a?(Proc)
|
45
40
|
if handler.singleton_class.include? Sansomable
|
46
41
|
node.subsansoms << handler
|
47
42
|
else
|
@@ -62,15 +57,17 @@ class Pine
|
|
62
57
|
return @cache[k] if @cache.has_key? k
|
63
58
|
|
64
59
|
matched_length = 0
|
65
|
-
matched_params = {}
|
60
|
+
matched_params = { :splat => [] }
|
66
61
|
matched_wildcard = false
|
67
62
|
|
63
|
+
# find a matching node
|
68
64
|
walk = path_comps(path).inject @root do |n, comp|
|
69
65
|
c = n[comp]
|
70
66
|
break n if c.nil?
|
71
67
|
matched_length += comp.length+1
|
72
68
|
if c.dynamic?
|
73
|
-
matched_params
|
69
|
+
matched_params.merge! c.mappings(comp)
|
70
|
+
matched_params[:splat].push *c.splats(comp)
|
74
71
|
matched_wildcard = true
|
75
72
|
end
|
76
73
|
c
|
@@ -78,15 +75,15 @@ class Pine
|
|
78
75
|
|
79
76
|
return nil if walk.nil?
|
80
77
|
|
81
|
-
|
78
|
+
remaining_path = path[matched_length..-1]
|
82
79
|
match = walk.blocks[verb.downcase.to_sym]
|
83
|
-
match ||= walk.subsansoms.detect { |i| i._pine.match
|
80
|
+
match ||= walk.subsansoms.detect { |i| i._pine.match remaining_path, verb }
|
84
81
|
match ||= walk.rack_app
|
85
82
|
|
86
83
|
return nil if match.nil?
|
87
84
|
|
88
|
-
r =
|
89
|
-
@cache[k] = r unless matched_wildcard # Only cache static lookups
|
85
|
+
r = [match, remaining_path, path[0..matched_length-1], matched_params]
|
86
|
+
@cache[k] = r unless matched_wildcard # Only cache static lookups (avoid huge memory usage)
|
90
87
|
r
|
91
88
|
end
|
92
89
|
end
|
data/lib/sansom/sansomable.rb
CHANGED
@@ -8,24 +8,20 @@ module Sansomable
|
|
8
8
|
RouteError = Class.new StandardError
|
9
9
|
ResponseError = Class.new StandardError
|
10
10
|
HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options, :link, :unlink, :trace].freeze
|
11
|
-
ACTION_VERBS = [:
|
11
|
+
ACTION_VERBS = [:mount].freeze
|
12
12
|
VALID_VERBS = (HTTP_VERBS+ACTION_VERBS).freeze
|
13
13
|
RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
|
14
|
-
|
14
|
+
NOT_FOUND_RESP = [404, {}, ["Not found."]].freeze
|
15
15
|
|
16
16
|
def _pine
|
17
17
|
if @_pine.nil?
|
18
18
|
@_pine = Pine.new
|
19
|
-
template if respond_to? :template
|
20
19
|
routes if respond_to? :routes
|
21
20
|
end
|
22
21
|
@_pine
|
23
22
|
end
|
24
23
|
|
25
24
|
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
25
|
res = handler.call *args
|
30
26
|
res = res.finish if res.is_a? Rack::Response
|
31
27
|
raise ResponseError, "Response must either be a rack response, string, or object" unless Rack::Lint.fastlint res # custom method
|
@@ -33,31 +29,28 @@ module Sansomable
|
|
33
29
|
res
|
34
30
|
end
|
35
31
|
|
36
|
-
def _not_found
|
37
|
-
return _call_route @_not_found, r unless @_not_found.nil?
|
38
|
-
[404, {}, [NOTFOUND_TEXT]]
|
39
|
-
end
|
40
|
-
|
41
32
|
def call env
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
raise RouteError, "No routes." if _pine.empty?
|
34
|
+
|
35
|
+
handler, remaining_path, _, route_params = _pine.match env["PATH_INFO"], env["REQUEST_METHOD"]
|
36
|
+
return NOT_FOUND_RESP if handler.nil?
|
45
37
|
|
46
38
|
r = Rack::Request.new env
|
47
39
|
|
48
40
|
begin
|
49
|
-
r.path_info =
|
41
|
+
r.path_info = remaining_path unless Proc === handler
|
50
42
|
|
51
|
-
unless
|
43
|
+
unless route_params.empty?
|
52
44
|
r.env["rack.request.query_string"] = r.query_string # now Rack::Request#GET will return r.env["rack.request.query_hash"]
|
53
|
-
|
45
|
+
r.env["rack.request.query_hash"] = Rack::Utils.parse_nested_query(r.query_string).merge(route_params) # add route params r.env["rack.request.query_hash"]
|
54
46
|
r.instance_variable_set "@params", nil # tell Rack::Request to recalc Rack::Request#params
|
55
47
|
end
|
56
48
|
|
57
|
-
res
|
58
|
-
res ||= _call_handler
|
59
|
-
res ||= _call_handler
|
60
|
-
res ||= _not_found
|
49
|
+
res = _call_handler @_before, r if @_before # call before block
|
50
|
+
res ||= _call_handler handler, (Proc === handler ? r : r.env) # call route handler block
|
51
|
+
res ||= _call_handler @_after, r, res if @_after && res # call after block
|
52
|
+
res ||= _call_handler @_not_found, r if @_not_found # call error block
|
53
|
+
res ||= NOT_FOUND_RESP # fallback error message
|
61
54
|
res
|
62
55
|
rescue => e
|
63
56
|
_call_handler @_error_blocks[e.class], e, r rescue raise e
|
@@ -75,24 +68,14 @@ module Sansomable
|
|
75
68
|
end
|
76
69
|
end
|
77
70
|
|
78
|
-
def error
|
79
|
-
|
71
|
+
def error error_class=:default, &block
|
72
|
+
raise ArgumentError, "Invalid error: #{error_class}" unless Class === error_class || error_class == :default
|
73
|
+
(@_error_blocks ||= Hash.new { |h| h[:default] })[error_class] = block
|
80
74
|
end
|
81
75
|
|
82
|
-
def before &block
|
83
|
-
|
84
|
-
|
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
|
76
|
+
def before █ @_before = block; end # 1 arg
|
77
|
+
def after █ @_after = block; end # 2 args
|
78
|
+
def not_found █ @_not_found = block; end # 1 arg
|
96
79
|
|
97
80
|
def method_missing meth, *args, &block
|
98
81
|
path, item = *args.dup.push(block)
|
data/lib/sansom.rb
CHANGED
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.3.1"
|
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."
|
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
allfiles = `git ls-files -z`.split("\x0")
|
16
16
|
s.files = allfiles.grep(%r{(^[^\/]*$|^lib\/)}) # Match all lib files AND files in the root
|
17
|
+
s.extensions = ["ext/sansom/pine/extconf.rb"]
|
17
18
|
s.executables = allfiles.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
s.test_files = allfiles.grep(%r{^(test|spec|features)/})
|
19
20
|
s.require_paths = ["lib"]
|
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.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathaniel Symer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -44,7 +44,8 @@ description: Scientific, philosophical, abstract web 'picowork' named after Sans
|
|
44
44
|
email:
|
45
45
|
- nate@natesymer.com
|
46
46
|
executables: []
|
47
|
-
extensions:
|
47
|
+
extensions:
|
48
|
+
- ext/sansom/pine/extconf.rb
|
48
49
|
extra_rdoc_files: []
|
49
50
|
files:
|
50
51
|
- ".gitignore"
|
@@ -52,6 +53,7 @@ files:
|
|
52
53
|
- LICENSE.txt
|
53
54
|
- README.md
|
54
55
|
- changelog.md
|
56
|
+
- ext/sansom/pine/extconf.rb
|
55
57
|
- lib/rack/fastlint.rb
|
56
58
|
- lib/sansom.rb
|
57
59
|
- lib/sansom/pine.rb
|
@@ -78,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
80
|
version: '0'
|
79
81
|
requirements: []
|
80
82
|
rubyforge_project:
|
81
|
-
rubygems_version: 2.
|
83
|
+
rubygems_version: 2.4.5
|
82
84
|
signing_key:
|
83
85
|
specification_version: 4
|
84
86
|
summary: Scientific, philosophical, abstract web 'picowork' named after Sansom street
|