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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fececc46ad6a785f2e8cc4be0035fff8f7a7d998
4
- data.tar.gz: 725f8e1256721a69d085cbec36591a3596483245
3
+ metadata.gz: 7b5864e32e2af6d42813deac99f743aa5f709377
4
+ data.tar.gz: 8f7778c22852629a41aa2056c957d68fd295e8b5
5
5
  SHA512:
6
- metadata.gz: 9dbbc5555a69229e68474ce0be6e662cd30fe8bffbaee6a3685b061802735deb72eb3e59db65cd3cc69c9df6a11aae794874e3d92596882d09ff3cf0924410f0
7
- data.tar.gz: 06c215518e828dd5c0da1b20b6cbaa0dd0d5d80ae374ac60ac24d7d584881e95e4e70b5353082db8675b27d9e0d2b692530513b998d590b3d4d2785322169052
6
+ metadata.gz: cb966e9564c909969af9b96aa1e62e7b0eedaf345b573ee25090bf229e654bc0a978f29c0f3b736948261d0f8abf9090d84f11da3958ce83e996318b0ab10e02
7
+ data.tar.gz: d5fee84cb9b078c953f72a15ad227adb9d05aad20c801aab84640b8921f2cc06f7cd8b40f68ee21eda9cead4481faf901106c62c51fef82f5bd390f085576795
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ ext/sansom/pine/Makefile
1
2
  *.gem
2
3
  *.rbc
3
4
  .bundle
data/README.md CHANGED
@@ -1,26 +1,24 @@
1
1
  Sansom
2
2
  ===
3
3
 
4
- Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, where it was made.
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 piece of software should not limit you to one way of thinking.***
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
- General Usage
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 your own traditional-style webapp
34
+ Writing a Sansom app
59
35
  -
36
+ Writing a one-file application is trivial with Sansom:
60
37
 
61
- Writing a one-file webapp is as simple as creating a `Sansomable`, defining routes on it, and calling start on it.
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
- # myapi.rb
73
-
74
- require "sansom"
40
+ require "sansom"
75
41
 
76
42
  class MyAPI
77
43
  include Sansomable
78
- def template
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 can be defined like so:
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
- s = Sansom.new
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
- 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:
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
- # app.rb
83
+ # config.ru
103
84
 
104
85
  require "sansom"
105
86
 
106
- s = Sansom.new
107
- s.map "/v1", MyAPI.new
108
- s.map "/v2", MyNewAPI.new
109
- s.start
87
+ class Versioning
88
+ include Sansomable
89
+ end
110
90
 
111
- Sansom blocks vs Sinatra blocks
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 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.
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 blocks become methods behind the scenes. When a route is matched, Sinatra looks up the method and calls it.
102
+ **Sinatra routes become methods behind the scenes**: When a route is matched, Sinatra looks up the method and calls it.
117
103
 
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.
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 a valid response, the request is preëmpted & it returns that response.
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
- next [200, {}, ["Preëmpted."]] if some_condition
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
- 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.
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 |err, r| # err is the error, r is a Rack::Request
160
- [500, {"yo" => "shit"}, [{ :message => err.message }.to_json]]
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
- - Wildcard routes can't have any siblings
179
- - A matching order is enforced:
180
- 1. The route matching the path and verb
181
- 2. The first Subsansom that matches the route & verb
182
- 3. The first mounted non-`Sansom` rack app matching the route
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
- Some examples of routes Sansom recognizes:
185
- `/path/to/resource` - Standard path
186
- `/users/:id/show` - Parameterized path
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
- - `Sansom` is under **250** lines of code at the time of writing. This includes
193
- * Rack conformity & the DSL (`sansom.rb`)
194
- * Custom tree-based routing (`pine.rb`)
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: **12ms**<br />
202
- Sansom: **15ms**\*<br />
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 will be pretty much as fast as Rack.
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 is brevity.**
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.**
@@ -0,0 +1,6 @@
1
+ require "mkmf"
2
+
3
+ $libs += "-lstdc++"
4
+ with_cppflags("-std=c++0x") { true }
5
+
6
+ create_makefile "sansom/pine/matcher"
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
- return false unless res.respond_to?(:to_a) && res.count == 3
9
-
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?
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
- return false unless status.to_i >= 100 || status.to_i == -1
16
- return false unless headers.respond_to? :each
17
- return false unless body.respond_to? :each
18
- return false if body.respond_to?(:to_path) && !File.exist?(body.to_path)
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
- 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"
23
- end
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
- 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
- }
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
- body.each { |p| return false unless p.respond_to? :to_str } # to_str is implemented by classes that act like strigs
36
-
37
- true
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
@@ -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
- # No regexes are used once a node is initialized
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 :wildcard, :wildcard_range # wildcard data
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
- # 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
17
+ def initialize n='/'
18
+ @name = n.freeze
19
+ @matcher = Pine::Matcher.new name
30
20
  @children = {}
31
- @wildcard_children = {}
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 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."
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 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
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
- 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
52
+ a = [self]
53
+ a << a.last.parent until a.last.root?
54
+ a[1..-1]
125
55
  end
126
56
 
127
- def wildcard?
128
- !wildcard_range.nil? && wildcard_range.size == 0
129
- end
57
+ def root?; name == '/'; end
58
+ def leaf?; children.empty? && dynamic_children.empty? && subsansoms.empty? && rack_app.nil?; end
130
59
 
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
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
- 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
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._set_parent self
79
+ c.parent = self
159
80
  c
160
81
  end
161
82
 
162
83
  alias_method :<<, :add_child!
163
84
 
164
- def _hchildren; @children; end
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._hchildren.delete name unless dynamic?
174
- @parent._hwcchildren.delete name if dynamic?
90
+ @parent.children.delete name
91
+ @parent.dynamic_children.reject! { |c| c.name == name }
175
92
  end
176
93
 
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? }
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
- p._hwcchildren[name] = self
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 is capable of matching both wildcards
5
- # and semiwildcards.
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. 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)
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 "/", my_block, :get
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 } # Fucking ruby interpreter
37
+ node = (path == "/") ? @root : path_comps(path).inject(@root) { |n, comp| n << comp }
43
38
 
44
- if key == :map && !handler.is_a?(Proc) # fucking ruby interpreter
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[c.wildcard] = comp[c.wildcard_range]
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
- remaining = path[matched_length..-1]
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 remaining, verb }
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 = 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
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
@@ -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 = [:map].freeze
11
+ ACTION_VERBS = [:mount].freeze
12
12
  VALID_VERBS = (HTTP_VERBS+ACTION_VERBS).freeze
13
13
  RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
14
- NOTFOUND_TEXT = "Not found.".freeze
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
- 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?
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 = m.remaining_path unless Proc === m.handler
41
+ r.path_info = remaining_path unless Proc === handler
50
42
 
51
- unless m.params.empty?
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
- (r.env["rack.request.query_hash"] ||= {}).merge! m.params # update the necessary field in the hash
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 = _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
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 error_key=nil, &block
79
- (@_error_blocks ||= Hash.new { |h| h[:default] })[error_key || :default] = block
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
- 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
76
+ def before &block; @_before = block; end # 1 arg
77
+ def after &block; @_after = block; end # 2 args
78
+ def not_found &block; @_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
@@ -1,6 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require_relative "./sansom/sansomable"
4
-
5
- Sansom = Class.new Object
6
- Sansom.send :include, Sansomable
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.2.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.2.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: 2014-10-30 00:00:00.000000000 Z
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.2.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