knot 0.0.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.
- data/.rspec +1 -0
- data/.rvmrc +23 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +37 -0
- data/README +2 -0
- data/Rakefile +15 -0
- data/config.ru +6 -0
- data/knot.gemspec +23 -0
- data/lib/knot/application/configuration.rb +25 -0
- data/lib/knot/application.rb +59 -0
- data/lib/knot/dispatch/filter.rb +27 -0
- data/lib/knot/dispatch/path.rb +38 -0
- data/lib/knot/dispatch/response.rb +10 -0
- data/lib/knot/dispatch/route.rb +22 -0
- data/lib/knot/dispatch/router/api.rb +39 -0
- data/lib/knot/dispatch/router/config.rb +53 -0
- data/lib/knot/dispatch/router/context.rb +39 -0
- data/lib/knot/dispatch/router.rb +53 -0
- data/lib/knot/dispatch.rb +5 -0
- data/lib/knot/render/binding.rb +16 -0
- data/lib/knot/render/template.rb +30 -0
- data/lib/knot/render.rb +16 -0
- data/lib/knot/util.rb +11 -0
- data/lib/knot.rb +10 -0
- data/spec/acceptance/acceptance_spec.rb +73 -0
- data/spec/acceptance/templates/override.html.erb +1 -0
- data/spec/acceptance/templates/test_router/override.html.erb +1 -0
- data/spec/acceptance/templates/test_template.html.erb +2 -0
- data/spec/acceptance/test_app.rb +50 -0
- data/spec/knot/dispatch/filter_spec.rb +31 -0
- data/spec/knot/dispatch/path_spec.rb +32 -0
- data/spec/knot/dispatch/response_spec.rb +12 -0
- data/spec/knot/dispatch/route_spec.rb +30 -0
- data/spec/knot/dispatch/router/config_spec.rb +12 -0
- data/spec/knot/dispatch/router_spec.rb +111 -0
- data/spec/knot/render/binding_spec.rb +13 -0
- data/spec/knot/render_spec.rb +34 -0
- data/spec/knot/util_spec.rb +9 -0
- data/unicorn.conf.rb +5 -0
- metadata +146 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
environment_id="ruby-1.9.3-p125@knot"
|
7
|
+
|
8
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
9
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
10
|
+
then
|
11
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
12
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
13
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
14
|
+
if [[ $- == *i* ]] # check for interactive shells
|
15
|
+
then echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
|
16
|
+
else echo "Using: $GEM_HOME" # don't use colors in non-interactive shells
|
17
|
+
fi
|
18
|
+
else
|
19
|
+
rvm --create use "$environment_id" || {
|
20
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
21
|
+
return 1
|
22
|
+
}
|
23
|
+
fi
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.3)
|
5
|
+
kgio (2.7.4)
|
6
|
+
rack (1.4.1)
|
7
|
+
rack-protection (1.2.0)
|
8
|
+
rack
|
9
|
+
rack-test (0.6.1)
|
10
|
+
rack (>= 1.0)
|
11
|
+
raindrops (0.10.0)
|
12
|
+
rake (0.9.2.2)
|
13
|
+
rspec (2.8.0)
|
14
|
+
rspec-core (~> 2.8.0)
|
15
|
+
rspec-expectations (~> 2.8.0)
|
16
|
+
rspec-mocks (~> 2.8.0)
|
17
|
+
rspec-core (2.8.0)
|
18
|
+
rspec-expectations (2.8.0)
|
19
|
+
diff-lcs (~> 1.1.2)
|
20
|
+
rspec-mocks (2.8.0)
|
21
|
+
unicorn (4.3.1)
|
22
|
+
kgio (~> 2.6)
|
23
|
+
rack
|
24
|
+
raindrops (~> 0.7)
|
25
|
+
yajl-ruby (1.1.0)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
rack
|
32
|
+
rack-protection
|
33
|
+
rack-test
|
34
|
+
rake
|
35
|
+
rspec
|
36
|
+
unicorn
|
37
|
+
yajl-ruby
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
task :default => ["knot:test"]
|
2
|
+
|
3
|
+
namespace :knot do
|
4
|
+
|
5
|
+
desc "Start the server with unicorn"
|
6
|
+
task :start do
|
7
|
+
Kernel.system("unicorn -c unicorn.conf.rb")
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run Tests"
|
11
|
+
task :test do
|
12
|
+
Kernel.system("bundle exec rspec")
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/config.ru
ADDED
data/knot.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'knot/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "knot"
|
8
|
+
gem.version = Knot::VERSION
|
9
|
+
gem.authors = ["Brian Pratt"]
|
10
|
+
gem.email = ["brian@8thlight.com"]
|
11
|
+
gem.description = "Knot"
|
12
|
+
gem.summary = "Build ruby apps"
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency('rack')
|
21
|
+
gem.add_dependency('rack-protection')
|
22
|
+
gem.add_dependency('yajl-ruby')
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Knot
|
2
|
+
class Application
|
3
|
+
class Configuration
|
4
|
+
attr_writer :template_path
|
5
|
+
|
6
|
+
def add_router(router)
|
7
|
+
routers << router
|
8
|
+
end
|
9
|
+
|
10
|
+
def routers
|
11
|
+
@routers ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def template_path
|
15
|
+
@template_path ||= "templates/"
|
16
|
+
end
|
17
|
+
|
18
|
+
def finalize!
|
19
|
+
routers.each do |router|
|
20
|
+
router.config.knot_template_home = template_path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'knot/dispatch/router'
|
2
|
+
require 'knot/application/configuration'
|
3
|
+
require 'rack/protection'
|
4
|
+
|
5
|
+
module Knot
|
6
|
+
class Application
|
7
|
+
class << self;
|
8
|
+
alias :new_without_middleware :new
|
9
|
+
|
10
|
+
def new
|
11
|
+
app = new_without_middleware
|
12
|
+
return build_rack(app).to_app
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_rack(app)
|
16
|
+
builder = Rack::Builder.new
|
17
|
+
builder.use Rack::Session::Cookie
|
18
|
+
builder.use Rack::Protection
|
19
|
+
builder.run app
|
20
|
+
return builder
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure(&block)
|
24
|
+
block.call(config)
|
25
|
+
config.finalize!
|
26
|
+
end
|
27
|
+
|
28
|
+
def config
|
29
|
+
@config ||= Knot::Application::Configuration.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(env)
|
34
|
+
begin
|
35
|
+
route_request(env)
|
36
|
+
rescue => e
|
37
|
+
return [500, {'Content-Type' => 'text/html'}, ["An error occured: ", e.message + "\n", e.backtrace.join("\n")]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def config
|
42
|
+
self.class.config
|
43
|
+
end
|
44
|
+
|
45
|
+
def route_request(env)
|
46
|
+
config.routers.each do |router|
|
47
|
+
begin
|
48
|
+
request = Rack::Request.new(env)
|
49
|
+
response = router._call!(request)
|
50
|
+
return response.to_rack
|
51
|
+
rescue Knot::Dispatch::NoRouteError
|
52
|
+
next
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return [404, {"Content-Type" => "text/html"}, ["Aw snap"]]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'knot/dispatch'
|
2
|
+
|
3
|
+
module Knot::Dispatch
|
4
|
+
class Filter
|
5
|
+
attr_reader :block
|
6
|
+
|
7
|
+
def initialize(name, options = {}, &block)
|
8
|
+
@name = name
|
9
|
+
@options = {:only => [], :except => []}.merge(options)
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply_to?(route)
|
14
|
+
(only.empty? && except.empty?) ||
|
15
|
+
(!only.empty? && only.include?(route.name)) ||
|
16
|
+
(!except.empty? && !except.include?(route.name))
|
17
|
+
end
|
18
|
+
|
19
|
+
def only
|
20
|
+
@options[:only]
|
21
|
+
end
|
22
|
+
|
23
|
+
def except
|
24
|
+
@options[:except]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'knot/dispatch'
|
2
|
+
|
3
|
+
module Knot::Dispatch
|
4
|
+
class Path
|
5
|
+
def self.match?(subject, other)
|
6
|
+
return false if chunks(subject).length != chunks(other).length
|
7
|
+
pairs(subject, other).all? do |subject_chunk, other_chunk|
|
8
|
+
variable?(subject_chunk) || subject_chunk == other_chunk
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.path_params(subject, request)
|
13
|
+
url_params = pairs(subject, request.path).inject({}) do |hash, values|
|
14
|
+
key, value = values
|
15
|
+
hash[symbolize(key)] = value if variable?(key)
|
16
|
+
hash
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.pairs(subject, other)
|
23
|
+
chunks(subject).zip(chunks(other))
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.symbolize(value)
|
27
|
+
value.sub(":", "").to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.variable?(chunk)
|
31
|
+
chunk[0, 1] == ":"
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.chunks(path)
|
35
|
+
path.split("/")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'knot/dispatch'
|
2
|
+
require 'knot/dispatch/path'
|
3
|
+
|
4
|
+
module Knot::Dispatch
|
5
|
+
class Route
|
6
|
+
attr_reader :path, :http_method, :name, :block
|
7
|
+
|
8
|
+
def initialize(http_method, name, path, &block)
|
9
|
+
@http_method, @name, @path, @block = http_method.to_s.upcase, name, path, block
|
10
|
+
end
|
11
|
+
|
12
|
+
def match?(request)
|
13
|
+
http_method_match?(request) && Path.match?(@path, request.path)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def http_method_match?(request)
|
19
|
+
request.request_method == @http_method
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'knot/dispatch'
|
2
|
+
|
3
|
+
module Knot::Dispatch
|
4
|
+
class Router
|
5
|
+
module API
|
6
|
+
def get(name, path, &block)
|
7
|
+
config.register_route(:get, name, path, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(name, path, &block)
|
11
|
+
config.register_route(:post, name, path, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def put(name, path, &block)
|
15
|
+
config.register_route(:put, name, path, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(name, path, &block)
|
19
|
+
config.register_route(:delete, name, path, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def patch(name, path, &block)
|
23
|
+
config.register_route(:patch, name, path, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def head(name, path, &block)
|
27
|
+
config.register_route(:head, name, path, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def options(name, path, &block)
|
31
|
+
config.register_route(:options, name, path, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def filter(name, options, &block)
|
35
|
+
config.add_filter(name, options, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'knot/dispatch/route'
|
2
|
+
require 'knot/dispatch/filter'
|
3
|
+
require 'knot/util'
|
4
|
+
|
5
|
+
module Knot
|
6
|
+
module Dispatch
|
7
|
+
class Router
|
8
|
+
class Config
|
9
|
+
attr_reader :router_name
|
10
|
+
|
11
|
+
def initialize(router)
|
12
|
+
@router_name = router.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def routes
|
16
|
+
@routes ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def register_route(http_method, name, path, &block)
|
20
|
+
routes << Knot::Dispatch::Route.new(http_method, name, path, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def filters
|
24
|
+
@filters ||= []
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_filter(name, options, &block)
|
28
|
+
filters << Knot::Dispatch::Filter.new(name, options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def knot_template_home=(base_path)
|
32
|
+
@template_paths = template_paths.
|
33
|
+
reject { |path| path == base_path }.
|
34
|
+
map { |path| path.prepend(base_path + "/") }
|
35
|
+
template_paths << base_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def template_paths
|
39
|
+
@template_paths ||= [Knot::Util.to_snake(router_name)]
|
40
|
+
end
|
41
|
+
|
42
|
+
def applicable_filters(route)
|
43
|
+
filters.select {|f| f.apply_to?(route)}
|
44
|
+
end
|
45
|
+
|
46
|
+
def reset!
|
47
|
+
@routes = []
|
48
|
+
@filters = []
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'knot/render'
|
2
|
+
require 'knot/dispatch'
|
3
|
+
require 'yajl/json_gem'
|
4
|
+
|
5
|
+
module Knot::Dispatch
|
6
|
+
class Router
|
7
|
+
module Context
|
8
|
+
def params
|
9
|
+
request.params
|
10
|
+
end
|
11
|
+
|
12
|
+
def status(code)
|
13
|
+
response.status = code
|
14
|
+
end
|
15
|
+
|
16
|
+
def redirect(uri)
|
17
|
+
status(302)
|
18
|
+
response.headers['Location'] = uri
|
19
|
+
respond!
|
20
|
+
end
|
21
|
+
|
22
|
+
def json(obj)
|
23
|
+
response.body << obj.to_json
|
24
|
+
response.headers["Content-Type"] = "application/json"
|
25
|
+
respond!
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(filename, options = {})
|
29
|
+
response.body << Knot::Render.render(filename, _config.template_paths, options)
|
30
|
+
respond!
|
31
|
+
end
|
32
|
+
|
33
|
+
# terminate processing and return to router to handle the given response
|
34
|
+
def respond!
|
35
|
+
throw(:halt, response)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'knot/dispatch'
|
2
|
+
require 'knot/dispatch/router/config'
|
3
|
+
require 'knot/dispatch/router/context'
|
4
|
+
require 'knot/dispatch/router/api'
|
5
|
+
require 'knot/dispatch/response'
|
6
|
+
|
7
|
+
module Knot::Dispatch
|
8
|
+
class Router
|
9
|
+
include Knot::Dispatch::Router::Context
|
10
|
+
extend Knot::Dispatch::Router::API
|
11
|
+
|
12
|
+
def self._call!(request)
|
13
|
+
new(request)._call!
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.config
|
17
|
+
@config ||= Knot::Dispatch::Router::Config.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :request, :response
|
21
|
+
|
22
|
+
def initialize(request)
|
23
|
+
@request = request
|
24
|
+
@response = Knot::Dispatch::Response.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def _call!
|
28
|
+
route = _config.routes.find { |route| route.match?(request) }
|
29
|
+
|
30
|
+
raise Knot::Dispatch::NoRouteError, "no route matches #{request.path}" if route.nil?
|
31
|
+
|
32
|
+
request.params.merge! Path.path_params(route.path, request)
|
33
|
+
|
34
|
+
blocks = _config.applicable_filters(route).map(&:block)
|
35
|
+
blocks << route.block
|
36
|
+
|
37
|
+
catch(:halt) {
|
38
|
+
blocks.map do |block|
|
39
|
+
result = instance_eval &block
|
40
|
+
response.body << result if result.is_a?(String)
|
41
|
+
end.last
|
42
|
+
}
|
43
|
+
|
44
|
+
return response
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _config
|
50
|
+
self.class.config
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Knot
|
2
|
+
module Render
|
3
|
+
class Binding
|
4
|
+
def initialize(options)
|
5
|
+
options.each do |key, value|
|
6
|
+
instance_variable_set("@#{key.to_s}", value)
|
7
|
+
self.class.instance_eval { attr_reader key }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_binding
|
12
|
+
binding
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Knot
|
2
|
+
module Render
|
3
|
+
class Template
|
4
|
+
def self.for(filename, search_paths)
|
5
|
+
filename += ".html.erb"
|
6
|
+
directory = search_paths.find { |path| File.exist?(File.join(path, filename)) }
|
7
|
+
|
8
|
+
raise NoTemplateError, "couldn't find template #{filename} in #{search_paths.join(", ")}" if directory.nil?
|
9
|
+
|
10
|
+
template_for(File.join(directory, filename))
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.template_for(filename)
|
14
|
+
if cache.keys.include?(filename)
|
15
|
+
cache[filename]
|
16
|
+
else
|
17
|
+
cache[filename] = ERB.new(File.read(filename))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.cache
|
22
|
+
@cache ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.clear_cache!
|
26
|
+
@cache = {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/knot/render.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'knot/render/binding'
|
2
|
+
require 'knot/render/template'
|
3
|
+
|
4
|
+
module Knot
|
5
|
+
module Render
|
6
|
+
|
7
|
+
NoTemplateError = Class.new(RuntimeError)
|
8
|
+
|
9
|
+
def self.render(filename, search_paths, options = {})
|
10
|
+
binding = Knot::Render::Binding.new(options)
|
11
|
+
template = Knot::Render::Template.for(filename, search_paths)
|
12
|
+
|
13
|
+
return template.result(binding.get_binding)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/knot/util.rb
ADDED
data/lib/knot.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
require './spec/acceptance/test_app'
|
3
|
+
|
4
|
+
describe "basic test for API" do
|
5
|
+
let(:app) { TestApp.new }
|
6
|
+
let!(:request) { Rack::MockRequest.new(app) }
|
7
|
+
|
8
|
+
it "routes to root" do
|
9
|
+
response = request.get "/"
|
10
|
+
response.body.should == "Huzzah"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "gets URL params" do
|
14
|
+
response = request.get "/greet/world"
|
15
|
+
response.body.should == "Hello, world"
|
16
|
+
|
17
|
+
other_response = request.get "/greet/nurse"
|
18
|
+
other_response.body.should == "Hello, nurse"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns 404" do
|
22
|
+
response = request.get "/not_to_be_found"
|
23
|
+
response.status.should == 404
|
24
|
+
response.body.should == "Aw snap"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "redirects" do
|
28
|
+
response = request.get "/onward"
|
29
|
+
response.should be_redirect
|
30
|
+
response.headers['Location'].should == "/greet/redirect"
|
31
|
+
end
|
32
|
+
|
33
|
+
context "json" do
|
34
|
+
it "renders json" do
|
35
|
+
response = request.get "/as_json"
|
36
|
+
response.status.should == 200
|
37
|
+
response.headers["Content-Type"].should == "application/json"
|
38
|
+
response.body.should == "{\"key\":\"value\"}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when an exception is raised" do
|
43
|
+
it "returns 500" do
|
44
|
+
response = request.get "/raise_exception"
|
45
|
+
response.status.should == 500
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "filters" do
|
50
|
+
it "when filter is satisfied" do
|
51
|
+
response = request.get "/filter/pass"
|
52
|
+
response.body.should == "Filtered"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "when filter is not satisfied" do
|
56
|
+
response = request.get "/filter/fail"
|
57
|
+
response.should be_redirect
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "templates" do
|
62
|
+
it "renders the template in the app's template path" do
|
63
|
+
response = request.get "/template/someone"
|
64
|
+
response.body.should == "<h1>Hello, someone,</h1>\n<p>from ERB!</p>\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "renders the template in the router's template path if it exists" do
|
68
|
+
response = request.get "/template_override"
|
69
|
+
response.body.should == "Should render me.\n"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
Shouldn't render me.
|
@@ -0,0 +1 @@
|
|
1
|
+
Should render me.
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "knot"
|
2
|
+
|
3
|
+
class TestRouter < Knot::Router
|
4
|
+
|
5
|
+
filter :redirect_unless_pass, :only => [:filter_test] do
|
6
|
+
redirect "/greet/nurse" unless params[:filter] == "pass"
|
7
|
+
end
|
8
|
+
|
9
|
+
def yay
|
10
|
+
"Huzzah"
|
11
|
+
end
|
12
|
+
|
13
|
+
get(:index, "/") { yay }
|
14
|
+
|
15
|
+
get :url_param_test, "/greet/:slug" do
|
16
|
+
"Hello, " + params[:slug]
|
17
|
+
end
|
18
|
+
|
19
|
+
get :redirect_test, "/onward" do
|
20
|
+
redirect "/greet/redirect"
|
21
|
+
end
|
22
|
+
|
23
|
+
get :json_test, "/as_json" do
|
24
|
+
json key: "value"
|
25
|
+
end
|
26
|
+
|
27
|
+
get :exception_test, "/raise_exception" do
|
28
|
+
raise "whoa nelly!!"
|
29
|
+
end
|
30
|
+
|
31
|
+
get :filter_test, "/filter/:filter" do
|
32
|
+
"Filtered"
|
33
|
+
end
|
34
|
+
|
35
|
+
get :template_test, "/template/:name" do
|
36
|
+
render 'test_template', :name => params[:name]
|
37
|
+
end
|
38
|
+
|
39
|
+
get :template_override_test, "/template_override" do
|
40
|
+
render 'override'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class TestApp < Knot::Application
|
45
|
+
configure do |config|
|
46
|
+
config.add_router TestRouter
|
47
|
+
config.template_path = "spec/acceptance/templates"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'knot/dispatch/filter'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Filter do
|
4
|
+
context "apply_to?" do
|
5
|
+
context "apply to all" do
|
6
|
+
it "applies to any route" do
|
7
|
+
filter = Knot::Dispatch::Filter.new(:example) { "called" }
|
8
|
+
filter.apply_to?(stub(:name => :route)).should be_true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context "only" do
|
13
|
+
it "applies if the route is in the only list" do
|
14
|
+
filter = Knot::Dispatch::Filter.new(:example, :only => [:route]) { "called" }
|
15
|
+
filter.apply_to?(stub(:name => :route)).should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "does not apply if the route is not in the only list" do
|
19
|
+
filter = Knot::Dispatch::Filter.new(:example, :only => [:route]) { "called" }
|
20
|
+
filter.apply_to?(stub(:name => :another)).should_not be_true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "except" do
|
25
|
+
it "applies if the route is not in the except list" do
|
26
|
+
filter = Knot::Dispatch::Filter.new(:example, :except => [:route]) { "called" }
|
27
|
+
filter.apply_to?(stub(:name => :another)).should be_true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'knot/dispatch/path'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Path do
|
4
|
+
context ".match?" do
|
5
|
+
subject { Knot::Dispatch::Path }
|
6
|
+
|
7
|
+
it "fails for wrong route" do
|
8
|
+
subject.match?("/test", "/asdf").should_not be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it "matches a simple path" do
|
12
|
+
subject.match?("/test", "/test").should be_true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "matches a path with param injection" do
|
16
|
+
subject.match?("/test/:id", "/test/1").should be_true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "matches a nested path with multiple params injected" do
|
20
|
+
subject.match?("/test/:foo/and/:bar/edit","/test/1/and/2/edit").should be_true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context ".path_params" do
|
25
|
+
it "gets the variables from path and inserts them into a hash" do
|
26
|
+
request = stub(:path => "/test/1/and/2/edit")
|
27
|
+
|
28
|
+
vars = Knot::Dispatch::Path.path_params("/test/:foo/and/:bar/edit", request)
|
29
|
+
vars.should == {:foo => "1", :bar => "2"}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'knot/dispatch/response'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Response do
|
4
|
+
context "#to_rack" do
|
5
|
+
it "returns the array for rack interface" do
|
6
|
+
response = Knot::Dispatch::Response.new
|
7
|
+
response.body << 'Test'
|
8
|
+
response.status = 200
|
9
|
+
response.to_rack.should == [200, {"Content-Type"=>"text/html"}, ['Test']]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'knot/dispatch/route'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Route do
|
4
|
+
it "target path" do
|
5
|
+
route = Knot::Dispatch::Route.new("GET", :test, "/test")
|
6
|
+
route.path.should == "/test"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "http method" do
|
10
|
+
route = Knot::Dispatch::Route.new("GET", :test, "/test")
|
11
|
+
route.http_method.should == "GET"
|
12
|
+
end
|
13
|
+
|
14
|
+
context "#match?" do
|
15
|
+
let(:route) { Knot::Dispatch::Route.new("GET", :test, "/test") }
|
16
|
+
|
17
|
+
it "delegates to Path" do
|
18
|
+
request = stub(:request_method => "GET", :path => "/test", :params => {})
|
19
|
+
Knot::Dispatch::Path.should_receive(:match?).with(route.path, request.path).and_return(true)
|
20
|
+
route.match?(request).should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "filters paths out by http_method" do
|
24
|
+
request = stub(:request_method => "POST", :path => "/test", :params => {})
|
25
|
+
Knot::Dispatch::Path.should_receive(:match?).never
|
26
|
+
route.match?(request).should_not be_true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'knot/dispatch/router/config'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Router::Config do
|
4
|
+
context "template_paths" do
|
5
|
+
it "defaults to an array with router's directory name in it" do
|
6
|
+
AnExampleRouter = Class.new unless defined?(AnExampleRouter)
|
7
|
+
|
8
|
+
config = Knot::Dispatch::Router::Config.new(AnExampleRouter)
|
9
|
+
config.template_paths.should == ["an_example_router"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'knot/dispatch/router'
|
2
|
+
|
3
|
+
describe Knot::Dispatch::Router do
|
4
|
+
before(:each) { router.config.reset! }
|
5
|
+
let(:router) { Knot::Dispatch::Router }
|
6
|
+
|
7
|
+
context "definition/configuration" do
|
8
|
+
context ".routes" do
|
9
|
+
it "has collection of routes" do
|
10
|
+
router.config.routes.should == []
|
11
|
+
end
|
12
|
+
|
13
|
+
context "registration" do
|
14
|
+
[:get, :post, :put, :delete, :patch, :head, :options].each do |http_method|
|
15
|
+
it "registers a #{http_method} route" do
|
16
|
+
expect {
|
17
|
+
router.send(http_method, :hey, "/") { "hey" }
|
18
|
+
}.to change { router.config.routes.size }.by(1)
|
19
|
+
|
20
|
+
router.config.routes.first.http_method.should == http_method.to_s.upcase
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context ".call!" do
|
27
|
+
before(:each) do
|
28
|
+
router.get(:index, "/")
|
29
|
+
router.post(:test_action, "/test_action") do
|
30
|
+
"test_sentinel"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "finds the route if it exists" do
|
35
|
+
request = stub(:request_method => "POST", :params => {}, :path => "/test_action")
|
36
|
+
router._call!(request).should_not raise_error Knot::Dispatch::NoRouteError
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns a response" do
|
40
|
+
request = stub(:request_method => "POST", :params => {}, :path => "/test_action")
|
41
|
+
router._call!(request).should be_a(Knot::Dispatch::Response)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises NoRouteError if the route isn't found" do
|
45
|
+
request = stub(:request_method => "POST", :params => {}, :path => "/nonsense_path")
|
46
|
+
|
47
|
+
expect {
|
48
|
+
router._call!(request)
|
49
|
+
}.to raise_error Knot::Dispatch::NoRouteError
|
50
|
+
end
|
51
|
+
|
52
|
+
it "triggers the action for the route" do
|
53
|
+
request = stub(:request_method => "POST", :params => {}, :path => "/test_action")
|
54
|
+
router._call!(request).body.should == ["test_sentinel"]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context ".filter" do
|
59
|
+
it "defines a filter" do
|
60
|
+
request = stub(:request_method => "GET", :params => {}, :path => "/path")
|
61
|
+
|
62
|
+
router.filter :example_filter, :only => [:example] do
|
63
|
+
response.body << "Filtered!"
|
64
|
+
respond! # kill processing and respond immediately
|
65
|
+
end
|
66
|
+
|
67
|
+
router.get(:example, "/path") do |*_|
|
68
|
+
"Shouldn't get here"
|
69
|
+
end
|
70
|
+
|
71
|
+
router._call!(request).body.should == ["Filtered!"]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "execution" do
|
77
|
+
let(:request) { stub(:request_method => "POST", :params => {:example_param => true}, :path => "/") }
|
78
|
+
|
79
|
+
context "#params" do
|
80
|
+
it "gives access to the request's params" do
|
81
|
+
router = Knot::Dispatch::Router.new(request)
|
82
|
+
router.params.should == request.params
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "#redirect" do
|
87
|
+
it "sets status and location header" do
|
88
|
+
router = Knot::Dispatch::Router.new(request)
|
89
|
+
response = catch(:halt) { router.redirect('http://www.google.com') }
|
90
|
+
response.status.should == 302
|
91
|
+
response.headers['Location'].should == "http://www.google.com"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "#json" do
|
96
|
+
it "returns the object in json" do
|
97
|
+
router = Knot::Dispatch::Router.new(request)
|
98
|
+
response = catch(:halt) { router.json(:foo => "bar") }
|
99
|
+
JSON.parse(response.body.first).should == {'foo' => 'bar'}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "#status" do
|
104
|
+
it "sets the status code on the response" do
|
105
|
+
router = Knot::Dispatch::Router.new(request)
|
106
|
+
router.status(422)
|
107
|
+
router.response.status.should == 422
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'knot/render/binding'
|
2
|
+
|
3
|
+
describe Knot::Render::Binding do
|
4
|
+
it "sets attributes as instance variables" do
|
5
|
+
binding = Knot::Render::Binding.new(:foo => "bar")
|
6
|
+
binding.foo.should == "bar"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "gets the binding for the template to run against" do
|
10
|
+
binding = Knot::Render::Binding.new(:foo => "bar")
|
11
|
+
binding.get_binding.should be_a ::Binding
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'knot/render'
|
2
|
+
|
3
|
+
describe Knot::Render do
|
4
|
+
context ".render" do
|
5
|
+
it "renders the template if found in given search path" do
|
6
|
+
filename = "dir/filename.html.erb"
|
7
|
+
File.stub(:exist?).with(filename).and_return(true)
|
8
|
+
File.stub(:read).with(filename).and_return("template")
|
9
|
+
|
10
|
+
Knot::Render.render("filename", ["dir"]).should == "template"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises NoTemplateError if it can't find the template" do
|
14
|
+
filename = "dir/filename.html.erb"
|
15
|
+
File.stub(:exist?).with(filename).and_return(false)
|
16
|
+
|
17
|
+
expect {
|
18
|
+
Knot::Render.render("filename", ["dir"])
|
19
|
+
}.to raise_error Knot::Render::NoTemplateError
|
20
|
+
end
|
21
|
+
|
22
|
+
it "caches the results of the render" do
|
23
|
+
Knot::Render::Template.clear_cache!
|
24
|
+
filename = "dir/filename.html.erb"
|
25
|
+
|
26
|
+
File.stub(:exist?).with(filename).and_return(true)
|
27
|
+
File.should_receive(:read).once.with(filename).and_return("template")
|
28
|
+
|
29
|
+
Knot::Render.render("filename", ["dir"])
|
30
|
+
cached_result = Knot::Render.render("filename", ["dir"])
|
31
|
+
cached_result.should == "template"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/unicorn.conf.rb
ADDED
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: knot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brian Pratt
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rack-protection
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yajl-ruby
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Knot
|
63
|
+
email:
|
64
|
+
- brian@8thlight.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .rspec
|
70
|
+
- .rvmrc
|
71
|
+
- Gemfile
|
72
|
+
- Gemfile.lock
|
73
|
+
- README
|
74
|
+
- Rakefile
|
75
|
+
- config.ru
|
76
|
+
- knot.gemspec
|
77
|
+
- lib/knot.rb
|
78
|
+
- lib/knot/application.rb
|
79
|
+
- lib/knot/application/configuration.rb
|
80
|
+
- lib/knot/dispatch.rb
|
81
|
+
- lib/knot/dispatch/filter.rb
|
82
|
+
- lib/knot/dispatch/path.rb
|
83
|
+
- lib/knot/dispatch/response.rb
|
84
|
+
- lib/knot/dispatch/route.rb
|
85
|
+
- lib/knot/dispatch/router.rb
|
86
|
+
- lib/knot/dispatch/router/api.rb
|
87
|
+
- lib/knot/dispatch/router/config.rb
|
88
|
+
- lib/knot/dispatch/router/context.rb
|
89
|
+
- lib/knot/render.rb
|
90
|
+
- lib/knot/render/binding.rb
|
91
|
+
- lib/knot/render/template.rb
|
92
|
+
- lib/knot/util.rb
|
93
|
+
- spec/acceptance/acceptance_spec.rb
|
94
|
+
- spec/acceptance/templates/override.html.erb
|
95
|
+
- spec/acceptance/templates/test_router/override.html.erb
|
96
|
+
- spec/acceptance/templates/test_template.html.erb
|
97
|
+
- spec/acceptance/test_app.rb
|
98
|
+
- spec/knot/dispatch/filter_spec.rb
|
99
|
+
- spec/knot/dispatch/path_spec.rb
|
100
|
+
- spec/knot/dispatch/response_spec.rb
|
101
|
+
- spec/knot/dispatch/route_spec.rb
|
102
|
+
- spec/knot/dispatch/router/config_spec.rb
|
103
|
+
- spec/knot/dispatch/router_spec.rb
|
104
|
+
- spec/knot/render/binding_spec.rb
|
105
|
+
- spec/knot/render_spec.rb
|
106
|
+
- spec/knot/util_spec.rb
|
107
|
+
- unicorn.conf.rb
|
108
|
+
homepage: ''
|
109
|
+
licenses: []
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.24
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: Build ruby apps
|
132
|
+
test_files:
|
133
|
+
- spec/acceptance/acceptance_spec.rb
|
134
|
+
- spec/acceptance/templates/override.html.erb
|
135
|
+
- spec/acceptance/templates/test_router/override.html.erb
|
136
|
+
- spec/acceptance/templates/test_template.html.erb
|
137
|
+
- spec/acceptance/test_app.rb
|
138
|
+
- spec/knot/dispatch/filter_spec.rb
|
139
|
+
- spec/knot/dispatch/path_spec.rb
|
140
|
+
- spec/knot/dispatch/response_spec.rb
|
141
|
+
- spec/knot/dispatch/route_spec.rb
|
142
|
+
- spec/knot/dispatch/router/config_spec.rb
|
143
|
+
- spec/knot/dispatch/router_spec.rb
|
144
|
+
- spec/knot/render/binding_spec.rb
|
145
|
+
- spec/knot/render_spec.rb
|
146
|
+
- spec/knot/util_spec.rb
|