sansom 0.2.0 → 0.3.1
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/.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
|