sansom 0.0.4 → 0.0.5

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: c81abee4ed6b93558cbc51b117135cf674fde459
4
- data.tar.gz: 1bbfd96b4165afe38158c02ac0c0f8cbef6fa0da
3
+ metadata.gz: 6ea8ea9f12513294f151757423264d44870d210f
4
+ data.tar.gz: 405f6c890367eaa2c961cc4520f58b1bdc7ddc6d
5
5
  SHA512:
6
- metadata.gz: 52adf8b2ff6fcfa0ebc4e84647a5f3a912c02c47b50363dfd434eecf60f439ce1e32eec5f773b43b4333b7ece92c26824997283d8b62c113af99620cdd735ba0
7
- data.tar.gz: cc53ff5b59f6da3fb832133ec986131621e524ced4fc5e2971f7644c8036ccbac809db0f7557c5cb411cb2238e31309f123ff113804fa0e0a362a1cbecb56ab9
6
+ metadata.gz: f773bca14fef8bdfb91ba0147d97d952fc72189bd4c8d368e76c5920cf7d9c57c4073fdbd216e97c560f58c1e86481ca9737898d6090fb1246096a01e3e87532
7
+ data.tar.gz: da9038bc8791abbbf2aed558613d3b922e6dd95eb8feea4cc7e2d09e668536c565e0bd6b5d8fecfcc00907adfd0c0bb1c7b2bc7302152fdccd837831d0c33097
data/README.md CHANGED
@@ -1,22 +1,23 @@
1
1
  Sansom
2
2
  ===
3
3
 
4
- Flexible, versatile, light web framework named after Sansom street in Philly.
4
+ Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, where it was made.
5
5
 
6
- Installation
6
+ Philosophy
7
7
  -
8
8
 
9
- Add this line to your application's Gemfile:
9
+ *A piece of software should not limit you to one way of thinking.*
10
10
 
11
- gem '.'
11
+ You can write a `Sansomable` for each logical unit of your API, but you also don't have to.
12
12
 
13
- And then execute:
13
+ You can also mount existing Rails/Sinatra apps in your `Sansomable`. But you also don't have to.
14
14
 
15
- $ bundle
15
+ You can write one `Sansomable` for your entire API.
16
16
 
17
- Or install it yourself as:
17
+ Installation
18
+ -
18
19
 
19
- $ gem install .
20
+ `gem install sansom`
20
21
 
21
22
  Usage
22
23
  -
@@ -148,18 +149,20 @@ Matching
148
149
 
149
150
  `Sansom` uses trees to match routes. It follows a certain set of rules:
150
151
 
152
+ - Wildcard routes can't have any siblings
151
153
  - A matching order is enforced:
152
154
  1. The route matching the path and verb
153
155
  2. The first Subsansom that matches the route & verb
154
156
  3. The first mounted non-`Sansom` rack app matching the route
157
+
155
158
 
156
159
  Notes
157
160
  -
158
161
 
159
162
  - `Sansom` does not pollute _any_ `Object` methods, including `initialize`
160
- - `Sansom` is under **140** lines of code at the time of writing. This includes
161
- * Everything above
162
- * Custom routing
163
+ - `Sansom` is under **190** lines of code at the time of writing. This includes
164
+ * Rack conformity & the DSL (`sansom.rb`)
165
+ * Custom tree-based routing (`pine.rb`)
163
166
 
164
167
  Contributing
165
168
  -
data/changelog.md CHANGED
@@ -31,4 +31,10 @@ Here's an example
31
31
 
32
32
  0.0.4
33
33
 
34
- - Fixed bug with with requiring pine
34
+ - Fixed bug with with requiring pine
35
+
36
+ 0.0.5
37
+
38
+ - Parameterized URLs!!! (Stuff like `/user/:id/profile`)
39
+ * Parameterized URLs work with mounted Rack/Sansom apps
40
+ - Improved matching efficiency
data/lib/sansom/pine.rb CHANGED
@@ -2,115 +2,116 @@
2
2
 
3
3
  # Path routing tree
4
4
 
5
- # Custom tree implementation for path routing
6
-
7
- # Custom features:
8
- # 1. Trimming: Limit a node to a single element
9
-
10
5
  module Pine
11
6
  class Node
12
- attr_reader :name
7
+ attr_reader :name, :parent
13
8
  attr_accessor :content
14
- attr_accessor :parent
15
9
 
16
10
  def initialize name, content=Content.new
17
11
  @name = name
18
12
  @content = content
19
13
  @children = {}
20
14
  @parent = nil
21
- @trimmed = false
22
15
  end
23
16
 
24
- # returns a node for chaining
25
- def <<(node)
26
- if trimmed?
27
- # Add to first child
28
- children.first << node
29
- else
30
- node.parent = self
31
- @children[node.name] = node
32
- node
33
- end
34
- end
35
-
36
- def create_if_necessary name
37
- unless @children.keys.include? name
38
- child = self.class.new name
39
- child.parent = self
40
- @children[name] = child
41
- end
42
- @children[name]
17
+ def root?
18
+ @parent.nil?
43
19
  end
44
20
 
45
- def root
46
- n = self
47
- n = n.parent while !n.root?
48
- n
21
+ def leaf?
22
+ @children.count == 0
49
23
  end
50
24
 
51
- def children
52
- @children.values
25
+ def wildcard?
26
+ @wildcard
53
27
  end
54
28
 
55
- def trim(node)
56
- @trimmed = true
57
- @children.clear
58
- @children[node.name] = node
59
- self
29
+ def [] k
30
+ return @children[k] || @children.values.first
31
+ # child = @children[k] || @child.values.first
32
+ # return child unless child.nil?
33
+
34
+ # @children[k] || @children.values.first.wildcard? ? @children.values.first : nil
60
35
  end
61
36
 
62
- def []=(k,v)
63
- self << Node.new(k, v)
37
+ def create_and_save comp
38
+ child = self.class.new comp
39
+ child.instance_variable_set "@parent", self
40
+ @children[comp] = child
41
+ child
64
42
  end
65
-
66
- def [](k)
67
- @children[k]
68
- end
69
-
70
- def root?
71
- @parent.nil?
43
+
44
+ def << comp
45
+ if comp.start_with? ":"
46
+ @wildcard = true
47
+ @children.clear
48
+ create_and_save comp
49
+ else
50
+ child = @children[comp]
51
+ child = create_and_save comp if !child || (!child && child.leaf? && !child.wildcard?)
52
+ child
53
+ end
72
54
  end
73
-
74
- def leaf?
75
- @children.count == 0
55
+
56
+ def parse_path path
57
+ c = path.split "/"
58
+ c[0] = '/'
59
+ c.delete_at(-1) if c[-1].empty?
60
+ c
76
61
  end
77
62
 
78
- def trimmed?
79
- @trimmed
63
+ def map_path path, item, key
64
+ parse_path(path).inject(self) { |node, comp| node << comp }.content[key] = item
65
+ path
80
66
  end
81
67
 
82
- def inspect(level=0)
83
- if root?
84
- print "*"
85
- else
86
- print "|" unless parent.parent.children.last == parent rescue false
87
- print(' ' * level * 4)
88
- print(parent.children.last == self ? "+" : "|")
89
- print "---"
90
- print(leaf? ? ">" : "+")
68
+ def match path, verb
69
+ matched_comps = []
70
+ matched_params = {}
71
+
72
+ walk = parse_path(path).inject self do |node, comp|
73
+ break node if node.leaf?
74
+ matched_comps << comp unless comp == "/"
75
+ child = node[comp]
76
+ matched_params[child.name[1..-1]] = comp if node.wildcard?
77
+ child
91
78
  end
92
79
 
93
- puts " #{name} #{content.map rescue "fuck"}"
80
+ return nil if walk.root? rescue true
81
+
82
+ c = walk.content
83
+ subpath = path.sub "/#{matched_comps.join("/")}", ""
84
+
85
+ match = c.map[verb.downcase.to_sym]
86
+ match ||= c.items.detect { |i| sansom?(i) && i.tree.match(subpath, verb) }
87
+ match ||= c.items.detect { |i| !sansom?(i) }
94
88
 
95
- children.each { |child| child.inspect(level + 1) if child } # Child might be 'nil'
89
+ return nil if match.nil?
90
+
91
+ Result.new match, subpath, matched_params
92
+ end
93
+
94
+ def sansom? obj
95
+ obj.singleton_class.include? Sansomable
96
96
  end
97
97
  end
98
98
 
99
+ Result = Struct.new :item, :remaining_path, :url_params
100
+
99
101
  class Content
100
- attr_accessor :items
101
- attr_accessor :map
102
+ attr_accessor :items, :map
102
103
 
103
104
  def initialize
104
105
  @items = []
105
106
  @map = {}
106
107
  end
107
108
 
108
- def []=(k,v)
109
+ def []= k,v
109
110
  @items << v if k == :map
110
111
  @map[k] = v unless k == :map
111
112
  end
112
113
 
113
- def [](k)
114
+ def [] k
114
115
  @items[k] if Numeric === k
115
116
  @map[k] unless Numeric === k
116
117
  end
data/lib/sansom.rb CHANGED
@@ -1,109 +1,70 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "rack"
4
- require "sansom/pine"
4
+ require_relative "./sansom/pine"
5
5
 
6
6
  module Sansomable
7
7
  InvalidRouteError = Class.new StandardError
8
-
9
8
  HTTP_VERBS = [:get,:head, :post, :put, :delete, :patch, :options].freeze
10
9
  HANDLERS = ["puma", "unicorn", "thin", "webrick"].freeze
11
- NOT_FOUND = [404, {"Content-Type" => "text/plain"}, ["Not found."]].freeze
12
-
10
+ NOT_FOUND = [404, {}, ["Not found."]].freeze
11
+
13
12
  def tree
14
- @tree ||= nil
15
13
  if @tree.nil?
16
- @tree = Pine::Node.new("ROOT", nil)
14
+ @tree = Pine::Node.new "ROOT"
17
15
  template if respond_to? :template
18
16
  end
19
17
  @tree
20
18
  end
21
19
 
22
- def before_block
23
- @before_block ||= nil
24
- end
25
-
26
- def match verb, path
27
- components = s_parse_path(path)
28
- matched_components = []
29
- matched_parameters = {}
30
-
31
- walk = components.inject(tree) do |node, component|
32
- if node.leaf?
33
- node
34
- else
35
- matched_components << component unless component == "/"
36
- node[component]
37
- end
38
- end
39
-
40
- return nil if walk.root?
41
-
42
- c = walk.content
43
- matched_path = "/" + matched_components.join("/")
44
-
45
- match = c[verb.downcase.to_sym] # Check for route
46
- match ||= c.items.select(&method(:sansom?)).reject { |item| item.match(verb, path.sub(matched_path, "")).nil? }.first rescue nil # Check subsansoms
47
- match ||= c.items.reject(&method(:sansom?)).first rescue nil # Check for mounted rack apps
48
- [match, matched_path]
49
- end
50
-
51
20
  def call env
52
21
  return NOT_FOUND if tree.leaf?
53
22
 
54
- r = Rack::Request.new env
23
+ r = Rack::Request.new env.dup
24
+
25
+ if @before_block
26
+ res = @before_block.call r
27
+ return res if [Fixnum, Hash, Array]-res.map(&:class) == 0
28
+ end
55
29
 
56
- m = match r.request_method, r.path_info
57
- item = m.first
30
+ m = tree.match r.path_info, r.request_method
58
31
 
59
- if item.nil?
32
+ if m.url_params.count > 0
33
+ q = r.params.merge(m.url_params)
34
+ s = q.map { |p| p.join '=' }.join("&")
35
+ r.env["rack.request.query_hash"] = q
36
+ r.env["rack.request.query_string"] = s
37
+ r.env["QUERY_STRING"] = s
38
+ r.instance_variable_set "@params", r.POST.merge(q)
39
+ end
40
+
41
+ if !m
60
42
  NOT_FOUND
43
+ elsif m.item.is_a? Proc
44
+ m.item.call r
45
+ elsif m.item.respond_to? :call
46
+ r.env["PATH_INFO"] = m.remaining_path
47
+ m.item.call r.env
61
48
  else
62
- if before_block
63
- res = before_block.call r
64
- return res if res[0].is_a?(Numeric) && res[1].is_a?(Hash) && res[2].respond_to?(:each) rescue false
65
- end
66
-
67
- if item.is_a? Proc
68
- item.call r
69
- elsif sansom? item
70
- r.path_info.sub! m[1], ""
71
- item.call(r.env)
72
- else
73
- raise InvalidRouteError, "Invalid route handler, it must be a block (proc/lambda) or a subclass of Sansom."
74
- end
49
+ raise InvalidRouteError, "Route handlers must be blocks or valid rack apps."
75
50
  end
76
51
  end
77
52
 
78
53
  def start port=3001
79
- raise NoRoutesError if tree.children.empty?
54
+ raise NoRoutesError if tree.leaf?
80
55
  Rack::Handler.pick(HANDLERS).run self, :Port => port
81
56
  end
82
57
 
83
- def before(&block)
58
+ def before &block
84
59
  @before_block = block
85
60
  end
86
61
 
87
- def method_missing(meth, *args, &block)
62
+ def method_missing meth, *args, &block
88
63
  path, item = *args.dup.push(block)
89
64
  return super unless path && item
90
- return super if item == self
91
- return super unless HTTP_VERBS.include?(meth) || meth == :map
92
-
93
- n = s_parse_path(path).inject(tree) { |node, comp| node.create_if_necessary comp }
94
- n.content[meth] = item
95
- end
96
-
97
- private
98
-
99
- def sansom? obj
100
- return true if obj.is_a? Sansom
101
- return true if obj.class.included_modules.include? Sansomable
102
- false
103
- end
104
-
105
- def s_parse_path path
106
- path.split("/").reject(&:empty?).unshift("/")
65
+ return super unless item != self
66
+ return super unless (HTTP_VERBS+[:map]).include?(meth)
67
+ tree.map_path path, item, meth
107
68
  end
108
69
  end
109
70
 
data/sansom.gemspec CHANGED
@@ -4,17 +4,18 @@ $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.0.4"
7
+ s.version = "0.0.5"
8
8
  s.authors = ["Nathaniel Symer"]
9
9
  s.email = ["nate@natesymer.com"]
10
- s.summary = "Flexible, versatile, light web framework named after Sansom street in Philly."
11
- s.description = s.summary + "It's under 140 lines of code & and it's lightning fast. It uses tree-based route resolution."
10
+ s.summary = "Scientific, philosophical, abstract web 'picowork' named after Sansom street in Philly, near where it was made."
11
+ s.description = s.summary + " " + "It's under 200 lines of code & it's lightning fast. It uses tree-based route resolution."
12
12
  s.homepage = "http://github.com/fhsjaagshs/sansom"
13
13
  s.license = "MIT"
14
14
 
15
- s.files = `git ls-files -z`.split("\x0")
16
- s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
+ allfiles = `git ls-files -z`.split("\x0")
16
+ s.files = allfiles.grep(%r{(^[^\/]*$|^lib\/)}) # Match all lib files AND files in the root
17
+ s.executables = allfiles.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = allfiles.grep(%r{^(test|spec|features)/})
18
19
  s.require_paths = ["lib"]
19
20
 
20
21
  s.add_development_dependency "bundler", "~> 1.6"
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.0.4
4
+ version: 0.0.5
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-07-30 00:00:00.000000000 Z
11
+ date: 2014-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,9 +38,9 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1'
41
- description: Flexible, versatile, light web framework named after Sansom street in
42
- Philly.It's under 140 lines of code & and it's lightning fast. It uses tree-based
43
- route resolution.
41
+ description: Scientific, philosophical, abstract web 'picowork' named after Sansom
42
+ street in Philly, near where it was made. It's under 200 lines of code & it's lightning
43
+ fast. It uses tree-based route resolution.
44
44
  email:
45
45
  - nate@natesymer.com
46
46
  executables: []
@@ -52,9 +52,6 @@ files:
52
52
  - LICENSE.txt
53
53
  - README.md
54
54
  - changelog.md
55
- - examples/before.rb
56
- - examples/generic.rb
57
- - examples/mixin.rb
58
55
  - lib/sansom.rb
59
56
  - lib/sansom/pine.rb
60
57
  - sansom.gemspec
@@ -81,5 +78,6 @@ rubyforge_project:
81
78
  rubygems_version: 2.2.2
82
79
  signing_key:
83
80
  specification_version: 4
84
- summary: Flexible, versatile, light web framework named after Sansom street in Philly.
81
+ summary: Scientific, philosophical, abstract web 'picowork' named after Sansom street
82
+ in Philly, near where it was made.
85
83
  test_files: []
data/examples/before.rb DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require_relative "../lib/sansom" rescue require "sansom"
4
-
5
- s = Sansom.new
6
-
7
- s.before do |r|
8
- puts "(#{s.class.to_s}) #{r.request_method.upcase} #{r.path_info}"
9
- [200, {}, ["Hijacked by before!"]] if Random.new.rand(2) == 1
10
- end
11
-
12
- s.get "/" do |r|
13
- [200, { "Content-Type" => "text/plain"}, ["root"]]
14
- end
15
-
16
- s.get "/something" do |r|
17
- [200, { "Content-Type" => "text/plain" }, ["something"]]
18
- end
19
-
20
- s.start 2000
data/examples/generic.rb DELETED
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "json"
4
- require_relative "../lib/sansom" rescue require "sansom"
5
-
6
- class Sansom
7
- def food_response r
8
- [200, { "Content-Type" => "text/plain"}, [{ :type => self.class.to_s, :name => r.path_info.split("/").reject(&:empty?).last}.to_json]]
9
- end
10
- end
11
-
12
- class Meat < Sansom
13
- def template
14
- get "/pork" do |r|
15
- food_response r
16
- end
17
- end
18
- end
19
-
20
- class NonAnimal < Sansom
21
- def template
22
- get "/quinoa" do |r|
23
- food_response r
24
- end
25
-
26
- get "/tahini" do |r|
27
- food_response r
28
- end
29
-
30
- get "/squash" do |r|
31
- food_response r
32
- end
33
- end
34
- end
35
-
36
- class AnimalProducts < Sansom
37
- def template
38
- get "/eggs" do |r|
39
- food_response r
40
- end
41
-
42
- get "/milk" do |r|
43
- food_response r
44
- end
45
- end
46
- end
47
-
48
- class FoodTypes < Sansom
49
- def template
50
- map "/carnivorous", Meat.new
51
- map "/vegetarian", NonAnimal.new
52
- map "/vegetarian", AnimalProducts.new
53
- map "/vegan", NonAnimal.new
54
- end
55
- end
56
-
57
- class Food < Sansom
58
- def template
59
- get "/sushi" do |r|
60
- [200, { "Content-Type" => "text/plain"}, ["Quite delicious, especially cucumber"]]
61
- end
62
-
63
- map "/types", FoodTypes.new
64
- end
65
- end
66
-
67
- s = Sansom.new
68
-
69
- s.get "/" do |r|
70
- [200, { "Content-Type" => "text/plain"}, ["root"]]
71
- end
72
-
73
- s.get "/something" do |r|
74
- [200, { "Content-Type" => "text/plain"}, ["something"]]
75
- end
76
-
77
- s.map "/food", Food.new
78
-
79
- s.start 2000
data/examples/mixin.rb DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require_relative "../lib/sansom" rescue require "sansom"
4
-
5
- class Mixin < Hash
6
- include Sansomable
7
-
8
- def template
9
- get "/sansomable" do |r|
10
- [200, { "Content-Type" => "text/plain"}, ["Sansomable Hash"]]
11
- end
12
- end
13
- end
14
-
15
- s = Sansom.new
16
-
17
- s.get "/" do |r|
18
- [200, { "Content-Type" => "text/plain"}, ["root"]]
19
- end
20
-
21
- s.map "/mixins", Mixin.new
22
- s.start 3002