sansom 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33777ff470f9889d21b8854b1dea67b731a38a32
4
- data.tar.gz: 3c0baa22623738bc1668c91328f057e862c9e1e1
3
+ metadata.gz: fececc46ad6a785f2e8cc4be0035fff8f7a7d998
4
+ data.tar.gz: 725f8e1256721a69d085cbec36591a3596483245
5
5
  SHA512:
6
- metadata.gz: e83bcf72135895c959bc11282caab7780243338afdeddd9e0c7c6e6c1150d8251b01191c4f03666f1d9a7cf66f3a5dca04bcd733d5c8da0e29acf52ef2308299
7
- data.tar.gz: 1ba7cf9530ea405f9b7c582b664a4f65b7f18942f1b3077de3b7c00e6df339847b55acf14fd6fb3f4f71bdd596b8c0afadbbcc263dc8a92328cef7a250e50bd6
6
+ metadata.gz: 9dbbc5555a69229e68474ce0be6e662cd30fe8bffbaee6a3685b061802735deb72eb3e59db65cd3cc69c9df6a11aae794874e3d92596882d09ff3cf0924410f0
7
+ data.tar.gz: 06c215518e828dd5c0da1b20b6cbaa0dd0d5d80ae374ac60ac24d7d584881e95e4e70b5353082db8675b27d9e0d2b692530513b998d590b3d4d2785322169052
data/README.md CHANGED
@@ -16,100 +16,88 @@ You can write one `Sansomable` for your entire API.
16
16
 
17
17
  Fuck it.
18
18
 
19
- ***A piece of software should not be a magic box.***
19
+ ***A tool should do one thing, and do it well. (Unix philosophy)***
20
20
 
21
- A web framework is, however simplistically, a tool to connect code to a URL's path.
21
+ A web framework is, fundamentally, a tool to connect code to a URL's path.
22
22
 
23
- A web framework doesn't provide an ORM, template rendering, nor change how ruby works to the point where you're not writing Ruby code but instead `Rails` or `Sinatra` code.
23
+ A web framework doesn't provide an ORM, template rendering, shortcuts, nor security patches.
24
+
25
+ ***A web framework shall remain a framework***
26
+
27
+ No single tool should get so powerful that it can overcome its master. Rails and Sinatra have been doing this: modifying the language beyond recognition. Rails has activerecord and Sinatra's blocks aren't really blocks.
24
28
 
25
29
  Installation
26
30
  -
27
31
 
28
32
  `gem install sansom`
29
33
 
30
- Usage
34
+ General Usage
31
35
  -
36
+ Traditional approach:
32
37
 
33
- It's pretty simple. Instead of `Class`s storing routes, `Object`s store routes.
34
-
35
- There are two ways you can deploy `Sansom`:
38
+ # config.ru
39
+
40
+ require "sansom"
41
+
42
+ s = Sansom.new
43
+ # define routes on s
44
+ run s
45
+
46
+ One-file approach:
36
47
 
37
48
  # app.rb
38
-
39
- #!/usr/bin/env ruby
40
49
 
41
50
  require "sansom"
42
51
 
43
52
  s = Sansom.new
44
- s.get "/" do |r|
45
- # r is a Rack::Request
46
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
47
- end
53
+ # define routes on s
48
54
  s.start
55
+
56
+ They're basically the same, except the rack server evaluates config.ru in its own context. The config.ru approach allows for the config to be separated from the application code.
49
57
 
50
- Or, the more production-ready version:
58
+ Writing your own traditional-style webapp
59
+ -
51
60
 
52
- # config.ru
53
-
54
- require "sansom"
55
-
56
- s = Sansom.new
57
-
58
- s.get "/" do |r|
59
- # r is a Rack::Request
60
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
61
- end
62
-
63
- run s
64
-
65
- But `Sansom` can do more than just that:
61
+ Writing a one-file webapp is as simple as creating a `Sansomable`, defining routes on it, and calling start on it.
66
62
 
67
- It can be used in a similar fashion to Sinatra:
63
+ ####There is more footwork for a traditional-style webapp:
68
64
 
69
- # myapi.rb
70
-
71
- #!/usr/bin/env ruby
72
-
73
- require "sansom"
74
-
75
- class MyAPI < Sansom
76
- # This method is used to define Sansom routes
77
- def template
78
- get "/" do |r|
79
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
80
- # r is a Rack::Request
81
- end
82
- end
83
- end
65
+ Sansom is defined like this:
66
+
67
+ Sansom = Class.new Object
68
+ Sansom.send :include, Sansomable
69
+
70
+ So you'll want your app to either `include Sansomable` or be a subclass of `Sansom`, so that a basic declaration looks like this.
71
+
72
+ # myapi.rb
73
+
74
+ require "sansom"
75
+
76
+ class MyAPI
77
+ include Sansomable
78
+ def template
79
+ # define routes here
80
+ end
81
+ end
84
82
 
85
83
  And your `config.ru` file
86
84
 
87
85
  # config.ru
88
86
 
89
- require "sansom"
90
87
  require "./myapi"
91
88
 
92
89
  run MyAPI.new
93
90
 
94
- Sansom can also map other instances of Sansom to a route. Check this:
95
-
96
- # myapi.rb
97
-
98
- #!/usr/bin/env ruby
99
-
100
- require "sansom"
101
-
102
- class MyAPI < Sansom
103
- # This method is used to define Sansom routes
104
- def template
105
- get "/" do |r|
106
- [200, { "Content-Type" => "text/plain" }, ["Hello Sansom"]]
107
- # r is a Rack::Request
108
- end
109
- end
91
+ Defining Routes
92
+ -
93
+ Routes can be defined like so:
94
+
95
+ s = Sansom.new
96
+ s.get "/" do |r| # r is a Rack::Request
97
+ [200, {}, ["Return a Rack response."]]
110
98
  end
111
-
112
- Let's say you've written a new version of your api. No problem.
99
+
100
+ You can replace `get` with any http verb. Or `map`, if you want to map a subsansom. Let's say you've written a new version of your api. No problem:
113
101
 
114
102
  # app.rb
115
103
 
@@ -120,38 +108,68 @@ Let's say you've written a new version of your api. No problem.
120
108
  s.map "/v2", MyNewAPI.new
121
109
  s.start
122
110
 
123
- Or maybe you want to mount your Rails/Sinatra/whatever app
111
+ Sansom blocks vs Sinatra blocks
112
+ -
113
+
114
+ Sansom blocks remain blocks: When a route is mapped, the same block you use is called when a route is matched. It's the same object every time.
115
+
116
+ Sinatra blocks become methods behind the scenes. When a route is matched, Sinatra looks up the method and calls it.
117
+
118
+ Sinatra's mechanism allows for the use of `return` inside blocks. Sansom doesn't do this, so you must use the `next` directive in the same way you'd use return.
119
+
120
+ Before filters
121
+ -
122
+
123
+ You can write before filters to try to preëmpt request processing. If the block returns a valid response, the request is preëmpted & it returns that response.
124
124
 
125
125
  # app.rb
126
126
 
127
127
  require "sansom"
128
128
 
129
129
  s = Sansom.new
130
- s.map "/api", SinatraApp.new
131
- s.map "/", RailsApp.new
132
- s.start
130
+ s.before do |r|
131
+ next [200, {}, ["Preëmpted."]] if some_condition
132
+ end
133
133
 
134
- Lastly, any object can become a "`Sansom`" through a mixin:
134
+ You could use this for request statistics, caching, auth, etc.
135
+
136
+ After filters
137
+ -
138
+
139
+ You can also write after filters to tie up the loose ends of a response. If they return a valid response, that response is used instead of the response from a route. After blocks are not called if a before filter was ever called.
135
140
 
136
- # mixin.ru
141
+ # app.rb
142
+
143
+ require "sansom"
137
144
 
138
- class Mixin < Hash
139
- include Sansomable
140
-
141
- def template
142
- get "/sansomable" do |r|
143
- [200, { "Content-Type" => "text/plain"}, ["Sansomable Hash"]]
144
- end
145
- end
145
+ s = Sansom.new
146
+ s.after do |req,res| # req is a Rack::Request and res is the response generated by a route.
147
+ next [200, {}, ["Postëmpted."]] if some_condition
146
148
  end
149
+
150
+ Errors
151
+ -
152
+
153
+ Error blocks allow for the app to return something parseable when an error is raised.
154
+
155
+ require "sansom"
156
+ require "json"
147
157
 
148
- run Mixin.new
158
+ s = Sansom.new
159
+ s.error do |err, r| # err is the error, r is a Rack::Request
160
+ [500, {"yo" => "shit"}, [{ :message => err.message }.to_json]]
161
+ end
149
162
 
150
- If you look at how `Sansom` is defined, it makes sense:
163
+ There is also a unique error 404 handler:
151
164
 
152
- Sansom = Class.new Object
153
- Sansom.include Sansomable
165
+ require "sansom"
166
+ require "json"
154
167
 
168
+ s = Sansom.new
169
+ s.not_found do |r| # r is a Rack::Request
170
+ [404, {"yo" => "shit"}, [{ :message => "not found" }.to_json]]
171
+ end
172
+
155
173
  Matching
156
174
  -
157
175
 
@@ -162,7 +180,10 @@ Matching
162
180
  1. The route matching the path and verb
163
181
  2. The first Subsansom that matches the route & verb
164
182
  3. The first mounted non-`Sansom` rack app matching the route
165
-
183
+
184
+ Some examples of routes Sansom recognizes:
185
+ `/path/to/resource` - Standard path
186
+ `/users/:id/show` - Parameterized path
166
187
 
167
188
  Notes
168
189
  -
@@ -177,32 +198,26 @@ Speed
177
198
 
178
199
  Well, that's great and all, but how fast is "hello world" example in comparision to Rack or Sinatra?
179
200
 
180
- Rack: **15ms**<br />
181
- Sansom: **15ms**<br />
201
+ Rack: **12ms**<br />
202
+ Sansom: **15ms**\*<br />
182
203
  Sinatra: **28ms**<br />
183
- Rails: **34ms**
204
+ Rails: **34ms****
184
205
 
185
- (results are rounded down)
206
+ (results are measured locally using Puma and are rounded down)
186
207
 
187
208
  Hey [Konstantine](https://github.com/rkh), *put that in your pipe and smoke it*.
188
209
 
210
+ \* Uncached. If a tree lookup is cached, it will be pretty much as fast as Rack.
211
+ \** Rails loads a rich welcome page which may contribute to its slowness
212
+
189
213
  Todo
190
214
  -
191
215
 
192
- 1. Returning more than just rack responses:
193
- * Strings
194
- * Objects
195
- * Maybe more syntactic sugar
196
- 2. (Even) more stability
197
- 3. \<Your idea here\>
216
+ * Multiple return types for routes
217
+
218
+ If you have any ideas, let me know!
198
219
 
199
220
  Contributing
200
221
  -
201
222
 
202
- 1. Fork it
203
- 2. Create your feature branch (`git checkout -b my-new-feature`)
204
- 3. Commit your changes (`git commit -am 'Add some feature'`)
205
- 4. Push to the branch (`git push origin my-new-feature`)
206
- 5. Create a new Pull Request
207
-
208
- **Please make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is is brevity.**
223
+ You know the drill. But ** make sure you don't add tons and tons of code. Part of `Sansom`'s beauty is is brevity.**
data/changelog.md CHANGED
@@ -61,4 +61,16 @@ Here's an example
61
61
 
62
62
  0.1.2
63
63
 
64
- - Fixed issue with `include` in the `Sansom` class
64
+ - Fixed issue with `include` in the `Sansom` class
65
+
66
+ 0.2.0
67
+
68
+ - Rewrite internals to:
69
+ 1. Avoid collisions with the including class
70
+ 2. Improve performance
71
+ 3. Look better
72
+ 4. **Avoid bugs**
73
+
74
+ - Route match caching by path and HTTP method
75
+ &nbsp;&nbsp;->Should improve performance for static paths dramatically
76
+
data/lib/rack/fastlint.rb CHANGED
@@ -3,36 +3,37 @@
3
3
  require "rack"
4
4
 
5
5
  module Rack
6
- class Fastlint
7
- def self.response res
8
- return false unless res.kind_of?(Array) && res.count == 3
6
+ class Lint
7
+ def self.fastlint res
8
+ return false unless res.respond_to?(:to_a) && res.count == 3
9
9
 
10
- status, headers, body = res
10
+ status, headers, body = res.to_a
11
+ return false if status.nil?
12
+ return false if headers.nil?
13
+ return false if body.nil?
11
14
 
12
15
  return false unless status.to_i >= 100 || status.to_i == -1
13
16
  return false unless headers.respond_to? :each
14
17
  return false unless body.respond_to? :each
18
+ return false if body.respond_to?(:to_path) && !File.exist?(body.to_path)
15
19
 
16
- if body.respond_to? :to_path
17
- return false unless File.exist? body.to_path
20
+ if status.to_i < 200 || [204, 205, 304].include?(status.to_i)
21
+ return false if headers.member? "Content-Length"
22
+ return false if headers.member? "Content-Type"
18
23
  end
19
24
 
20
- begin
21
- headers.each { |k,v|
22
- next if key =~ /^rack\..+$/
23
- throw StandardError unless k.kind_of? String
24
- throw StandardError unless v.kind_of? String
25
- throw StandardError if k.downcase == "status"
26
- throw StandardError unless k !~ /[:\n]/
27
- throw StandardError unless k !~ /[-_]\z/
28
- throw StandardError unless k =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
29
- }
25
+ headers.each { |k,v|
26
+ next if k.start_with? "rack."
27
+ return false unless k.kind_of? String
28
+ return false unless v.kind_of? String
29
+ return false if k == "Status"
30
+ return false unless k !~ /[:\n]/
31
+ return false unless k !~ /[-_]\z/
32
+ return false unless k =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
33
+ }
34
+
35
+ body.each { |p| return false unless p.respond_to? :to_str } # to_str is implemented by classes that act like strigs
30
36
 
31
- body.each { |part| throw StandardError unless part.kind_of? String }
32
- rescue StandardError
33
- return false
34
- end
35
-
36
37
  true
37
38
  end
38
39
  end
data/lib/sansom.rb CHANGED
@@ -1,94 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "rack"
4
- require_relative "./sansom/pine"
5
- require_relative "./rack/fastlint.rb"
6
-
7
- module Sansomable
8
- InvalidRouteError = Class.new StandardError
9
- HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options, :link, :unlink, :trace].freeze
10
- ROUTE_METHODS = HTTP_VERBS+[:map]
11
- RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
12
- NOT_FOUND = [404, {}, ["Not found."]].freeze
13
-
14
- def tree
15
- if @tree.nil?
16
- @tree = Pine::Node.new "ROOT"
17
- template if respond_to? :template
18
- end
19
- @tree
20
- end
21
-
22
- def call env
23
- return NOT_FOUND if tree.leaf? && tree.root?
24
-
25
- r = Rack::Request.new env
26
- m = tree.match r.path_info, r.request_method
27
-
28
- return NOT_FOUND if m.nil?
29
-
30
- begin
31
- if @before_block && @before_block.arity == 1
32
- bres = @before_block.call r
33
- return bres if Rack::Fastlint.response bres
34
- end
35
-
36
- if m.url_params.count > 0
37
- q = r.params.merge m.url_params
38
- s = q.map { |p| p.join '=' }.join '&'
39
- r.env["rack.request.query_hash"] = q
40
- r.env["rack.request.query_string"] = s
41
- r.env["QUERY_STRING"] = s
42
- r.instance_variable_set "@params", r.POST.merge(q)
43
- end
44
-
45
- case m.item
46
- when Proc then res = m.item.call r
47
- else
48
- raise InvalidRouteError, "Route handlers must be blocks or valid rack apps." unless m.item.respond_to? :call
49
- r.env["PATH_INFO"] = m.remaining_path
50
- res = m.item.call r.env
51
- end
52
-
53
- if @after_block && @after_block.arity == 2
54
- ares = @after_block.call r, res
55
- return ares if Rack::Fastlint.response ares
56
- end
57
-
58
- res
59
- rescue StandardError => e
60
- b = (@error_blocks[e.class] || @error_blocks[:default]) rescue nil
61
- raise e if b.nil?
62
- b.call e, r
63
- end
64
- end
65
-
66
- def start port=3001
67
- raise NoRoutesError if tree.leaf?
68
- Rack::Handler.pick(RACK_HANDLERS).run self, :Port => port
69
- end
70
-
71
- def error error_key=:default, &block
72
- @error_blocks ||= {}
73
- @error_blocks[error_key] = block
74
- end
75
-
76
- def before &block
77
- @before_block = block
78
- end
79
-
80
- def after &block
81
- @after_block = block
82
- end
83
-
84
- def method_missing meth, *args, &block
85
- path, item = *args.dup.push(block)
86
- return super unless path && item
87
- return super unless item != self
88
- return super unless ROUTE_METHODS.include? meth
89
- tree.map_path path, item, meth
90
- end
91
- end
3
+ require_relative "./sansom/sansomable"
92
4
 
93
5
  Sansom = Class.new Object
94
6
  Sansom.send :include, Sansomable
data/lib/sansom/pine.rb CHANGED
@@ -1,108 +1,92 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Path routing tree
3
+ # Tree data structure designed specifically for
4
+ # routing. It is capable of matching both wildcards
5
+ # and semiwildcards.
6
+ #
7
+ # While other path routing software optimizes path parsing,
8
+ # Pine optimizes lookup. You could say it matches a route in
9
+ # something resembling logarithmic time, but really is linear time
10
+ # due to child lookups (children are just iterated over)
4
11
 
5
- module Pine
6
- Result = Struct.new :item, :remaining_path, :url_params
12
+ require_relative "./pine/node"
7
13
 
8
- class Content
9
- attr_reader :items, :map
10
-
11
- def initialize
12
- @items = []
13
- @map = {}
14
- end
15
-
16
- def set k,v
17
- @items << v if k == :map
18
- @map[k] = v unless k == :map
19
- end
14
+ class Pine
15
+ Match = Struct.new :handler, # Proc/Subsansom/Rack App
16
+ :remaining_path, # Part of path that wasn't matched, applies to subsansoms
17
+ :matched_path, # The matched part of a path
18
+ :params # Wildcard params
19
+
20
+ def initialize
21
+ @root = Pine::Node.new
22
+ @cache = {}
20
23
  end
21
24
 
22
- class Node
23
- attr_reader :name, :parent, :content
24
-
25
- def initialize name
26
- @name = name
27
- @content = Content.new
28
- @children = {}
29
- end
25
+ def empty?
26
+ @root.leaf?
27
+ end
30
28
 
31
- def root?
32
- @parent.nil?
33
- end
29
+ # returns all non-root path components
30
+ # path_comps("/my/path/")
31
+ # => ["my", "path"]
32
+ def path_comps path
33
+ path[1..(path[-1] == "/" ? -2 : -1)].split "/"
34
+ end
34
35
 
35
- def leaf?
36
- @children.count == 0
36
+ # map_path "/food", Subsansom.new, :map
37
+ # map_path "/", my_block, :get
38
+ # it's also chainable
39
+ def map_path path, handler, key
40
+ @cache.clear
41
+
42
+ node = (path == "/") ? @root : path_comps(path).inject(@root) { |n, comp| n << comp } # Fucking ruby interpreter
43
+
44
+ if key == :map && !handler.is_a?(Proc) # fucking ruby interpreter
45
+ if handler.singleton_class.include? Sansomable
46
+ node.subsansoms << handler
47
+ else
48
+ node.rack_app = handler
49
+ end
50
+ else
51
+ node.blocks[key] = handler
37
52
  end
38
53
 
39
- def wildcard?
40
- @name.start_with? ":"
41
- end
54
+ self
55
+ end
56
+
57
+ # match "/", :get
58
+ def match path, verb
59
+ return nil if empty?
42
60
 
43
- def [] k
44
- return @children[k] if @children.member? k
45
- c = @children.values.first
46
- return c if (c.wildcard? rescue false)
47
- end
61
+ k = verb.to_s + path.to_s
62
+ return @cache[k] if @cache.has_key? k
48
63
 
49
- def << comp
50
- child = self[comp]
51
-
52
- if child.nil?
53
- child = self.class.new comp
54
- child.instance_variable_set "@parent", self
55
- @children.reject!(&:leaf?) if child.wildcard?
56
- @children[comp] = child
57
- end
64
+ matched_length = 0
65
+ matched_params = {}
66
+ matched_wildcard = false
58
67
 
59
- child
60
- end
61
-
62
- def parse_path path
63
- c = path.split "/"
64
- c[0] = '/'
65
- c.delete_at(-1) if c[-1].empty?
68
+ walk = path_comps(path).inject @root do |n, comp|
69
+ c = n[comp]
70
+ break n if c.nil?
71
+ matched_length += comp.length+1
72
+ if c.dynamic?
73
+ matched_params[c.wildcard] = comp[c.wildcard_range]
74
+ matched_wildcard = true
75
+ end
66
76
  c
67
77
  end
68
78
 
69
- def map_path path, item, key
70
- parse_path(path).inject(self) { |node, comp| node << comp }.content.set key, item
71
- path
72
- end
73
-
74
- def match path, verb
75
- matched_comps = []
76
- matched_params = {}
77
-
78
- walk = parse_path(path).inject self do |node, comp|
79
- break node if node.leaf?
80
- next node[comp] if node.root?
81
-
82
- c = node[comp]
83
- break node if c.nil?
84
- matched_comps << comp
85
- matched_params[c.name[1..-1]] = comp if c.wildcard?
86
- c
87
- end
79
+ return nil if walk.nil?
88
80
 
89
- return nil if walk.nil?
90
- return nil if walk.root?
81
+ remaining = path[matched_length..-1]
82
+ match = walk.blocks[verb.downcase.to_sym]
83
+ match ||= walk.subsansoms.detect { |i| i._pine.match remaining, verb }
84
+ match ||= walk.rack_app
91
85
 
92
- c = walk.content
93
- subpath = path.sub "/#{matched_comps.join("/")}", ""
94
-
95
- match = c.map[verb.downcase.to_sym]
96
- match ||= c.items.detect { |i| sansom?(i) && i.tree.match(subpath, verb) }
97
- match ||= c.items.detect { |i| !sansom?(i) }
98
-
99
- return nil if match.nil?
100
-
101
- Result.new match, subpath, matched_params
102
- end
86
+ return nil if match.nil?
103
87
 
104
- def sansom? obj
105
- obj.singleton_class.include? Sansomable
106
- end
88
+ r = Match.new match, remaining, path[0..matched_length-1], matched_params
89
+ @cache[k] = r unless matched_wildcard # Only cache static lookups, avoid huge memory usage
90
+ r
107
91
  end
108
- end
92
+ end
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # represents a node on the routing tree
4
+
5
+ # No regexes are used once a node is initialized
6
+
7
+ class Pine
8
+ class Node
9
+ LineageError = Class.new StandardError
10
+ WILDCARD_REGEX = /<(\w*)\b[^>]*>/.freeze
11
+ URLPATHSAFE_REGEX = /[^a-zA-Z0-9_-]/.freeze
12
+ ROOT = "/".freeze
13
+
14
+ attr_reader :name # node "payload" data
15
+ attr_accessor :parent # node reference system
16
+ attr_reader :wildcard, :wildcard_range # wildcard data
17
+ attr_reader :rack_app, :subsansoms, :blocks # mapping
18
+ attr_reader :end_seq, :start_seq, :min_length # stored information used to match wildcards
19
+ attr_reader :wildcard_delimeter, :semiwildcard_delimeter # delimiter for wildcard syntax
20
+
21
+ # Pine::Node.new "asdf", "$", "?" # creates a node with $ as the wildcard delimiter and ? as the semiwildcard delimiter
22
+ # Pine::Node.new "asdf", "#" # creates a node with # as the wildcard delimiter and the default semiwildcard delimiter
23
+ # Pine::Node.new "asdf" # creates a node with the default delimiters
24
+ # Pine::Node.new # creates root node
25
+ # Delimiters can be any length
26
+ def initialize name=ROOT, wc_delim=":", swc_delim="<"
27
+ raise ArgumentError, "Delimiters must not be safe characters in a URL path." if wc_delim.match URLPATHSAFE_REGEX rescue false
28
+ raise ArgumentError, "Delimiters must not be safe characters in a URL path." if swc_delim.match URLPATHSAFE_REGEX rescue false
29
+ @name = name.freeze
30
+ @children = {}
31
+ @wildcard_children = {}
32
+ @blocks = {}
33
+ @subsansoms = []
34
+ @wildcard_delimeter = wc_delim
35
+ @semiwildcard_delimeter = swc_delim
36
+
37
+ unless root?
38
+ if @name.start_with? wildcard_delimeter
39
+ @wildcard_range = Range.new(0, -1).freeze
40
+ @wildcard = @name[wildcard_delimeter.length..-1].freeze
41
+ @start_seq = "".freeze
42
+ @end_seq = "".freeze
43
+ else
44
+ r = ['<','>'].include?(semiwildcard_delimeter) ? WILDCARD_REGEX : /#{swc_delim}(\w*)\b[^#{swc_delim}]*#{swc_delim}/
45
+ m = @name.match r
46
+ unless m.nil?
47
+ o = m.offset 1
48
+ @wildcard_range = Range.new(o.first-1, (-1*(m.string.length-o.last+1))+1).freeze # calc `last` rel to the last char idx
49
+ @wildcard = @name[wildcard_range.first+semiwildcard_delimeter.length..wildcard_range.last-semiwildcard_delimeter.length].freeze
50
+ @start_seq = @name[0..wildcard_range.first-1].freeze
51
+ @end_seq = wildcard_range.last == -1 ? "" : @name[wildcard_range.last+1..-1].freeze
52
+ end
53
+ end
54
+ end
55
+
56
+ @min_length = dynamic? ? start_seq.length + end_seq.length : name.length
57
+ end
58
+
59
+ def inspect
60
+ "#<#{self.class}: #{name.inspect}, #{dynamic? ? "Wildcard: '" + wildcard + "' #{wildcard_range.inspect}, " : "" }#{@children.count} children, #{leaf? ? "leaf" : "internal node"}>"
61
+ end
62
+
63
+ def == another
64
+ parent == another.parent &&
65
+ name == another.name
66
+ end
67
+
68
+ # TODO: check correctness of return values
69
+ def <=> another
70
+ return 0 if n == another
71
+
72
+ n = self
73
+ n = n.parent until n == another || n.root?
74
+ return 1 if n == another
75
+
76
+ n = another
77
+ n = n.parent until n == self || n.root?
78
+ return -1 if n == self
79
+
80
+ raise LinneageError, "Node not in tree."
81
+ end
82
+
83
+ def detach!
84
+ _set_parent nil
85
+ end
86
+
87
+ def siblings
88
+ parent.children.dup - self
89
+ end
90
+
91
+ def children
92
+ hash_children.values
93
+ end
94
+
95
+ def hash_children
96
+ Hash[@children.to_a + @wildcard_children.to_a]
97
+ end
98
+
99
+ def child? another
100
+ another.ancestor? self
101
+ end
102
+
103
+ def ancestor? another
104
+ n = self
105
+ n = n.parent until n == another || n.root?
106
+ n == another
107
+ end
108
+
109
+ def ancestors
110
+ n = self
111
+ n = n.parent until n.root?
112
+ n
113
+ end
114
+
115
+ def root?
116
+ name == ROOT
117
+ end
118
+
119
+ def leaf?
120
+ children.empty? && subsansoms.empty? && rack_app.nil?
121
+ end
122
+
123
+ def semiwildcard?
124
+ !wildcard_range.nil? && wildcard_range.size != 0
125
+ end
126
+
127
+ def wildcard?
128
+ !wildcard_range.nil? && wildcard_range.size == 0
129
+ end
130
+
131
+ # returns true if self is either a wildcard or a semiwildcard
132
+ def dynamic?
133
+ !wildcard_range.nil?
134
+ end
135
+
136
+ # Bottleneck for wildcard-heavy apps
137
+ def matches? comp
138
+ return comp == name unless dynamic?
139
+ comp.length >= min_length && comp.start_with?(start_seq) && comp.end_with?(end_seq)
140
+ end
141
+
142
+ # WARNING: Sansom's biggest bottleneck
143
+ # Partially chainable: No guarantee the returned value responds to :child or :[]
144
+ def child comp
145
+ raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
146
+ case
147
+ when @children.empty? && @wildcard_children.empty? then nil
148
+ when @children.member?(comp) then @children[comp]
149
+ else @wildcard_children.values.detect { |c| c.matches? comp } end
150
+ end
151
+
152
+ alias_method :[], :child
153
+
154
+ # chainable
155
+ def add_child! comp
156
+ raise ArgumentError, "Invalid path component." if comp.nil? || comp.empty?
157
+ c = self[comp] || self.class.new(comp)
158
+ c._set_parent self
159
+ c
160
+ end
161
+
162
+ alias_method :<<, :add_child!
163
+
164
+ def _hchildren; @children; end
165
+ def _hwcchildren; @wildcard_children; end
166
+
167
+ # returns new parent so its chainable
168
+ def _set_parent p
169
+ return if @parent == p
170
+
171
+ # remove from old parent's children structure
172
+ unless @parent.nil?
173
+ @parent._hchildren.delete name unless dynamic?
174
+ @parent._hwcchildren.delete name if dynamic?
175
+ end
176
+
177
+ # add to new parent's children structure
178
+ if wildcard?
179
+ p._hchildren.reject! { |_,c| c.leaf? }
180
+ p._hwcchildren.reject! { |_,c| c.leaf? }
181
+ end
182
+ p._hwcchildren[name] = self
183
+
184
+ @parent = p # set new parent
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rack"
4
+ require_relative "./pine"
5
+ require_relative "../rack/fastlint"
6
+
7
+ module Sansomable
8
+ RouteError = Class.new StandardError
9
+ ResponseError = Class.new StandardError
10
+ HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options, :link, :unlink, :trace].freeze
11
+ ACTION_VERBS = [:map].freeze
12
+ VALID_VERBS = (HTTP_VERBS+ACTION_VERBS).freeze
13
+ RACK_HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
14
+ NOTFOUND_TEXT = "Not found.".freeze
15
+
16
+ def _pine
17
+ if @_pine.nil?
18
+ @_pine = Pine.new
19
+ template if respond_to? :template
20
+ routes if respond_to? :routes
21
+ end
22
+ @_pine
23
+ end
24
+
25
+ def _call_handler handler, *args
26
+ raise ArgumentError, "Handler must not be nil." if handler.nil?
27
+ raise ArgumentError, "Handler must be a valid rack app." unless handler.respond_to? :call
28
+ raise ArgumentError, "Handler cannot take all passed args." if handler.respond_to?(:arity) && args.count != handler.arity
29
+ res = handler.call *args
30
+ res = res.finish if res.is_a? Rack::Response
31
+ raise ResponseError, "Response must either be a rack response, string, or object" unless Rack::Lint.fastlint res # custom method
32
+ res = [200, {}, [res.to_str]] if res.respond_to? :to_str
33
+ res
34
+ end
35
+
36
+ def _not_found
37
+ return _call_route @_not_found, r unless @_not_found.nil?
38
+ [404, {}, [NOTFOUND_TEXT]]
39
+ end
40
+
41
+ def call env
42
+ return _not_found if _pine.empty? # no routes
43
+ m = _pine.match env["PATH_INFO"], env["REQUEST_METHOD"]
44
+ return _not_found if m.nil?
45
+
46
+ r = Rack::Request.new env
47
+
48
+ begin
49
+ r.path_info = m.remaining_path unless Proc === m.handler
50
+
51
+ unless m.params.empty?
52
+ r.env["rack.request.query_string"] = r.query_string # now Rack::Request#GET will return r.env["rack.request.query_hash"]
53
+ (r.env["rack.request.query_hash"] ||= {}).merge! m.params # update the necessary field in the hash
54
+ r.instance_variable_set "@params", nil # tell Rack::Request to recalc Rack::Request#params
55
+ end
56
+
57
+ res = _call_handler @_before, r if @_before # call before block
58
+ res ||= _call_handler m.handler, (Proc === m.handler ? r : r.env) # call route handler block
59
+ res ||= _call_handler @_after, r, res if @_after # call after block
60
+ res ||= _not_found
61
+ res
62
+ rescue => e
63
+ _call_handler @_error_blocks[e.class], e, r rescue raise e
64
+ end
65
+ end
66
+
67
+ def start port=3001, handler=""
68
+ raise RouteError, "No routes." if _pine.empty?
69
+ begin
70
+ h = Rack::Handler.get handler.to_s
71
+ rescue LoadError, NameError
72
+ h = Rack::Handler.pick(RACK_HANDLERS)
73
+ ensure
74
+ h.run self, :Port => port
75
+ end
76
+ end
77
+
78
+ def error error_key=nil, &block
79
+ (@_error_blocks ||= Hash.new { |h| h[:default] })[error_key || :default] = block
80
+ end
81
+
82
+ def before &block
83
+ raise ArgumentError, "Before filter blocks must take one argument." if block && block.arity != 1
84
+ @_before = block
85
+ end
86
+
87
+ def after &block
88
+ raise ArgumentError, "After filter blocks must take two arguments." if block && block.arity != 2
89
+ @_after = block
90
+ end
91
+
92
+ def not_found &block
93
+ raise ArgumentError, "Not found blocks must take one argument." if block && block.arity != 1
94
+ @_not_found = block
95
+ end
96
+
97
+ def method_missing meth, *args, &block
98
+ path, item = *args.dup.push(block)
99
+ return super unless path && item && item != self
100
+ return super unless VALID_VERBS.include? meth
101
+ return super unless item.respond_to? :call
102
+ _pine.map_path path, item, meth
103
+ end
104
+ end
data/sansom.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "sansom"
7
- s.version = "0.1.2"
7
+ s.version = "0.2.0"
8
8
  s.authors = ["Nathaniel Symer"]
9
9
  s.email = ["nate@natesymer.com"]
10
10
  s.summary = "Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, near where it was made."
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sansom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathaniel Symer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-28 00:00:00.000000000 Z
11
+ date: 2014-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -55,6 +55,8 @@ files:
55
55
  - lib/rack/fastlint.rb
56
56
  - lib/sansom.rb
57
57
  - lib/sansom/pine.rb
58
+ - lib/sansom/pine/node.rb
59
+ - lib/sansom/sansomable.rb
58
60
  - sansom.gemspec
59
61
  homepage: http://github.com/fhsjaagshs/sansom
60
62
  licenses: