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.
Files changed (40) hide show
  1. data/.rspec +1 -0
  2. data/.rvmrc +23 -0
  3. data/Gemfile +13 -0
  4. data/Gemfile.lock +37 -0
  5. data/README +2 -0
  6. data/Rakefile +15 -0
  7. data/config.ru +6 -0
  8. data/knot.gemspec +23 -0
  9. data/lib/knot/application/configuration.rb +25 -0
  10. data/lib/knot/application.rb +59 -0
  11. data/lib/knot/dispatch/filter.rb +27 -0
  12. data/lib/knot/dispatch/path.rb +38 -0
  13. data/lib/knot/dispatch/response.rb +10 -0
  14. data/lib/knot/dispatch/route.rb +22 -0
  15. data/lib/knot/dispatch/router/api.rb +39 -0
  16. data/lib/knot/dispatch/router/config.rb +53 -0
  17. data/lib/knot/dispatch/router/context.rb +39 -0
  18. data/lib/knot/dispatch/router.rb +53 -0
  19. data/lib/knot/dispatch.rb +5 -0
  20. data/lib/knot/render/binding.rb +16 -0
  21. data/lib/knot/render/template.rb +30 -0
  22. data/lib/knot/render.rb +16 -0
  23. data/lib/knot/util.rb +11 -0
  24. data/lib/knot.rb +10 -0
  25. data/spec/acceptance/acceptance_spec.rb +73 -0
  26. data/spec/acceptance/templates/override.html.erb +1 -0
  27. data/spec/acceptance/templates/test_router/override.html.erb +1 -0
  28. data/spec/acceptance/templates/test_template.html.erb +2 -0
  29. data/spec/acceptance/test_app.rb +50 -0
  30. data/spec/knot/dispatch/filter_spec.rb +31 -0
  31. data/spec/knot/dispatch/path_spec.rb +32 -0
  32. data/spec/knot/dispatch/response_spec.rb +12 -0
  33. data/spec/knot/dispatch/route_spec.rb +30 -0
  34. data/spec/knot/dispatch/router/config_spec.rb +12 -0
  35. data/spec/knot/dispatch/router_spec.rb +111 -0
  36. data/spec/knot/render/binding_spec.rb +13 -0
  37. data/spec/knot/render_spec.rb +34 -0
  38. data/spec/knot/util_spec.rb +9 -0
  39. data/unicorn.conf.rb +5 -0
  40. 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
@@ -0,0 +1,13 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem "rack"
4
+ gem "rack-protection"
5
+ gem "yajl-ruby"
6
+
7
+
8
+ group :test, :development do
9
+ gem "rack-test"
10
+ gem "rspec"
11
+ gem "rake"
12
+ gem "unicorn"
13
+ end
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
@@ -0,0 +1,2 @@
1
+ # Knot
2
+ It's pretty much top secret.
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
@@ -0,0 +1,6 @@
1
+ $:<< File.dirname(__FILE__)
2
+ $:<< File.join(File.dirname(__FILE__), "lib")
3
+
4
+ require 'spec/acceptance/test_app'
5
+
6
+ run TestApp.new
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,10 @@
1
+ require 'knot/dispatch'
2
+ require 'rack/response'
3
+
4
+ module Knot::Dispatch
5
+ class Response < ::Rack::Response
6
+ def to_rack
7
+ [status, headers, body]
8
+ end
9
+ end
10
+ 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,5 @@
1
+ module Knot
2
+ module Dispatch
3
+ NoRouteError = Class.new(RuntimeError)
4
+ end
5
+ 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
@@ -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
@@ -0,0 +1,11 @@
1
+ module Knot
2
+ class Util
3
+ def self.to_snake(str)
4
+ str.to_s.gsub(/::/, '/').
5
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
6
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
7
+ tr("-", "_").
8
+ downcase
9
+ end
10
+ end
11
+ end
data/lib/knot.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'knot/dispatch/router'
2
+ require 'knot/application'
3
+
4
+ module Knot
5
+ Router = Dispatch::Router
6
+
7
+ def self.env
8
+ ENV['RACK_ENV']
9
+ end
10
+ end
@@ -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,2 @@
1
+ <h1>Hello, <%= name %>,</h1>
2
+ <p>from ERB!</p>
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'knot/util'
2
+
3
+ describe Knot::Util do
4
+ context ".to_snake" do
5
+ it "converts a constant or camelcase to snake case" do
6
+ Knot::Util.to_snake("HelloWorld").should == "hello_world"
7
+ end
8
+ end
9
+ end
data/unicorn.conf.rb ADDED
@@ -0,0 +1,5 @@
1
+ listen 4567 # by default Unicorn listens on port 8080
2
+ worker_processes 1 # this should be >= nr_cpus
3
+ # pid "/path/to/app/shared/pids/unicorn.pid"
4
+ # stderr_path "/path/to/app/shared/log/unicorn.log"
5
+ # stdout_path "/path/to/app/shared/log/unicorn.log"
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