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 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