rack-putty 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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-putty.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above
10
+ copyright notice, this list of conditions and the following disclaimer
11
+ in the documentation and/or other materials provided with the
12
+ distribution.
13
+ * Neither the name of Apollic Software, LLC nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Rack::Putty
2
+
3
+ Simple web framework built on rack for mapping sinatra-like routes to middleware stacks.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rack-putty'
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require 'rack-putty'
15
+
16
+ module YourApp
17
+ class Router
18
+ include Rack::Putty::Router
19
+
20
+ class SerializeResponse
21
+ def self.call(env)
22
+ headers = { 'Content-Type' => 'text/plain' }.merge(env.response_headers || {})
23
+ if env.response
24
+ [200, headers, [env.response]]
25
+ else
26
+ [404, headers, ['Not Found']]
27
+ end
28
+ end
29
+ end
30
+
31
+ class CorsHeaders
32
+ def initialize(app)
33
+ @app = app
34
+ end
35
+
36
+ def call(env)
37
+ status, headers, body = @app.call(env)
38
+ headers.merge!(
39
+ 'Access-Control-Allow-Origin' => '*',
40
+ ) if env['HTTP_ORIGIN']
41
+ [status, headers, body]
42
+ end
43
+ end
44
+
45
+ stack_base SerializeResponse
46
+ middleware CorsHeaders
47
+
48
+ class HelloWorld < Rack::Putty::Middleware
49
+ def action(env)
50
+ env.response = 'Hello World'
51
+ env
52
+ end
53
+ end
54
+
55
+ get '/' do |builder|
56
+ builder.use HelloWorld
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Contributing
63
+
64
+ 1. Fork it
65
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
66
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
67
+ 4. Push to the branch (`git push origin my-new-feature`)
68
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/rack-putty.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "rack-putty/version"
2
+
3
+ module Rack
4
+ module Putty
5
+ autoload :Router, 'rack-putty/router'
6
+ autoload :Middleware, 'rack-putty/middleware'
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ module Rack
2
+ module Putty
3
+
4
+ class Middleware
5
+ def initialize(app, options = {})
6
+ @app, @options = app, options
7
+ end
8
+
9
+ def call(env)
10
+ response = action(env)
11
+
12
+ # Allow middleware to cause stack to end early
13
+ response.kind_of?(Hash) ? @app.call(env) : response
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,146 @@
1
+ require 'rack/mount'
2
+
3
+ class Rack::Mount::RouteSet
4
+ def merge_routes(routes)
5
+ routes.each { |r| merge_route(r) }
6
+ rehash
7
+ end
8
+
9
+ def merge_route(route)
10
+ @routes << route
11
+
12
+ @recognition_key_analyzer << route.conditions
13
+
14
+ @named_routes[route.name] = route if route.name
15
+ @generation_route_keys << route.generation_keys
16
+
17
+ expire!
18
+ route
19
+ end
20
+ end
21
+
22
+ module Rack
23
+ module Putty
24
+
25
+ module Router
26
+ MissingStackBaseError = Class.new(StandardError)
27
+
28
+ autoload :ExtractParams, 'rack-putty/router/extract_params'
29
+
30
+ def self.included(base)
31
+ base.extend(ClassMethods)
32
+ base.routes.rehash
33
+ end
34
+
35
+ def call(env)
36
+ self.class.routes.call(env)
37
+ end
38
+
39
+ module ClassMethods
40
+ def stack_base(klass)
41
+ @stack_base = klass
42
+ end
43
+
44
+ def middleware(klass, *args)
45
+ @middleware ||= []
46
+ @middleware << [klass, args]
47
+ end
48
+
49
+ def mount(klass)
50
+ routes.merge_routes klass.routes.instance_variable_get("@routes")
51
+ end
52
+
53
+ def routes
54
+ @routes ||= Rack::Mount::RouteSet.new
55
+ end
56
+
57
+ #### This section heavily "inspired" by sinatra
58
+
59
+ # Defining a `GET` handler also automatically defines
60
+ # a `HEAD` handler.
61
+ def get(path, opts={}, &block)
62
+ route('GET', path, opts, &block)
63
+ route('HEAD', path, opts, &block)
64
+ end
65
+
66
+ def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
67
+ def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
68
+ def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
69
+ def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
70
+ def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
71
+ def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
72
+
73
+ def match(path, opts={}, &bk)
74
+ get(path, opts, &bk)
75
+ put(path, opts, &bk)
76
+ post(path, opts, &bk)
77
+ patch(path, opts, &bk)
78
+ delete(path, opts, &bk)
79
+ options(path, opts, &bk)
80
+ end
81
+
82
+ private
83
+
84
+ def route(verb, path, options={}, &block)
85
+ path, params = compile_path(path)
86
+
87
+ return if route_exists?(verb, path)
88
+
89
+ unless @stack_base
90
+ raise MissingStackBaseError.new("You need to call `stack_base` with a base app class to be passed to Rack::Builder.new.")
91
+ end
92
+
93
+ builder = Rack::Builder.new(@stack_base)
94
+ builder.use(ExtractParams, path, params)
95
+
96
+ (@middleware || []).each do |i|
97
+ klass, args = i
98
+ builder.use(klass, *args)
99
+ end
100
+
101
+ block.call(builder)
102
+
103
+ routes.add_route(builder.to_app, :request_method => verb, :path_info => path)
104
+ routes.rehash
105
+ end
106
+
107
+ def route_exists?(verb, path)
108
+ @added_routes ||= []
109
+ return true if @added_routes.include?("#{verb}#{path}")
110
+ @added_routes << "#{verb}#{path}"
111
+ false
112
+ end
113
+
114
+ def compile_path(path)
115
+ keys = []
116
+ if path.respond_to? :to_str
117
+ ignore = ""
118
+ pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c|
119
+ ignore << escaped(c).join if c.match(/[\.@]/)
120
+ encoded(c)
121
+ end
122
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
123
+ if match == "*"
124
+ keys << 'splat'
125
+ "(.*?)"
126
+ else
127
+ keys << $2[1..-1]
128
+ "([^#{ignore}/?#]+)"
129
+ end
130
+ end
131
+ [/\A#{pattern}\z/, keys]
132
+ elsif path.respond_to?(:keys) && path.respond_to?(:match)
133
+ [path, path.keys]
134
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
135
+ [path, path.names]
136
+ elsif path.respond_to? :match
137
+ [path, keys]
138
+ else
139
+ raise TypeError, path
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,69 @@
1
+ module Rack
2
+ module Putty
3
+
4
+ module Router
5
+
6
+ class ExtractParams
7
+ attr_accessor :pattern, :keys
8
+
9
+ def initialize(app, pattern, keys)
10
+ @app, @pattern, @keys = app, pattern, keys
11
+ end
12
+
13
+ def call(env)
14
+ add_request(env)
15
+ extract_params(env)
16
+ @app.call(env)
17
+ end
18
+
19
+ private
20
+
21
+ def add_request(env)
22
+ env['request'] = Rack::Request.new(env)
23
+ end
24
+
25
+ def extract_params(env)
26
+ route = env[Rack::Mount::Prefix::KEY]
27
+ route = '/' if route.empty?
28
+ return unless match = pattern.match(route)
29
+ values = match.captures.to_a.map { |v| URI.decode_www_form_component(v) if v }
30
+
31
+ params = env['request'].params.dup
32
+
33
+ if env['CONTENT_TYPE'] =~ /\Amultipart/
34
+ env['data'] = params.select { |k,v| Hash === v && v.has_key?(:filename) }
35
+ params = params.reject { |k,v| Hash === v && v.has_key?(:filename) }
36
+ end
37
+
38
+ if values.any?
39
+ params.merge!('captures' => values)
40
+ keys.zip(values) { |k,v| Array === params[k] ? params[k] << v : params[k] = v if v }
41
+ end
42
+
43
+ env['params'] = indifferent_params(params)
44
+ end
45
+
46
+ # Enable string or symbol key access to the nested params hash.
47
+ def indifferent_params(object)
48
+ case object
49
+ when Hash
50
+ new_hash = indifferent_hash
51
+ object.each { |key, value| new_hash[key] = indifferent_params(value) }
52
+ new_hash
53
+ when Array
54
+ object.map { |item| indifferent_params(item) }
55
+ else
56
+ object
57
+ end
58
+ end
59
+
60
+ # Creates a Hash with indifferent access.
61
+ def indifferent_hash
62
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module Putty
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack-putty/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rack-putty"
8
+ gem.version = Rack::Putty::VERSION
9
+ gem.authors = ["Jonathan Rudenberg", "Jesse Stuart"]
10
+ gem.email = ["jonathan@titanous.com", "jesse@jessestuart.ca"]
11
+ gem.description = %q{Simple web framework built on rack for mapping sinatra-like routes to middleware stacks.}
12
+ gem.summary = %q{Simple web framework built on rack.}
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_runtime_dependency 'rack-mount', '~> 0.8.3'
21
+
22
+ gem.add_development_dependency 'rack-test', '~> 0.6.1'
23
+ gem.add_development_dependency 'rspec', '~> 2.11'
24
+ gem.add_development_dependency 'bundler'
25
+ gem.add_development_dependency 'rake'
26
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe Rack::Putty::Router do
5
+ include Rack::Test::Methods
6
+
7
+ class SerializeResponse
8
+ def self.call(env)
9
+ headers = { 'Content-Type' => 'text/plain' }.merge(env['response_headers'] || {})
10
+ if env['response']
11
+ [200, headers, [serialize_response(headers, env['response'])]]
12
+ else
13
+ [404, headers, ['Not Found']]
14
+ end
15
+ end
16
+
17
+ def self.serialize_response(headers, response)
18
+ if headers['Content-Type'] =~ /json/
19
+ response.to_json
20
+ end
21
+ end
22
+ end
23
+
24
+ class TestMiddleware < Rack::Putty::Middleware
25
+ def action(env)
26
+ env['response'] ||= {}
27
+ env['response']['params'] = env['params']
28
+ env['response_headers'] = { 'Content-Type' => 'application/json' }
29
+ env
30
+ end
31
+ end
32
+
33
+ class OtherTestMiddleware < Rack::Putty::Middleware
34
+ def action(env)
35
+ env['response'] ||= {}
36
+ env['response'][@options[:key]] = @options[:val]
37
+ env
38
+ end
39
+ end
40
+
41
+ class TestMiddlewarePrematureResponse < Rack::Putty::Middleware
42
+ def action(env)
43
+ [200, { 'Content-Type' => 'text/plain' }, ['Premature-Response']]
44
+ end
45
+ end
46
+
47
+ class TestMountedApp
48
+ include Rack::Putty::Router
49
+
50
+ stack_base SerializeResponse
51
+ middleware OtherTestMiddleware, :key => 'foo', :val => 'bar'
52
+ middleware OtherTestMiddleware, :key => 'biz', :val => 'baz'
53
+
54
+ get '/chunky/:bacon' do |b|
55
+ b.use TestMiddleware
56
+ end
57
+ end
58
+
59
+ class PrefixMountedApp
60
+ def initialize(app)
61
+ @app = app
62
+ end
63
+
64
+ def call(env)
65
+ myprefix = '/prefix'
66
+
67
+ if env['PATH_INFO'].start_with?(myprefix)
68
+ env['SCRIPT_NAME'] = env['SCRIPT_NAME'][0..-2] if env['SCRIPT_NAME'].end_with?('/') # strip trailing slash
69
+ env['SCRIPT_NAME'] += myprefix
70
+
71
+ env['PATH_INFO'].sub! myprefix, ''
72
+ @app.call(env)
73
+ end
74
+ end
75
+ end
76
+
77
+ class TestApp
78
+ include Rack::Putty::Router
79
+
80
+ stack_base SerializeResponse
81
+
82
+ get '/foo/:bar' do |b|
83
+ b.use TestMiddleware
84
+ end
85
+
86
+ get '/premature/response' do |b|
87
+ b.use TestMiddlewarePrematureResponse
88
+ b.use TestMiddleware
89
+ end
90
+
91
+ post %r{^/foo/([^/]+)/bar} do |b|
92
+ b.use TestMiddleware
93
+ end
94
+
95
+ mount TestMountedApp
96
+ end
97
+
98
+ def app
99
+ TestApp.new
100
+ end
101
+
102
+ let(:env) { {} }
103
+
104
+ context "as a mounted app with a prefix" do
105
+ let(:app) { PrefixMountedApp.new(TestApp.new) }
106
+
107
+ it "still matches the path name" do
108
+ get '/prefix/foo/baz', {}, env
109
+ expect(last_response.status).to eq(200)
110
+ expect(JSON.parse(last_response.body)['params']['bar']).to eq('baz')
111
+ end
112
+ end
113
+
114
+ it "should extract params" do
115
+ get '/foo/baz', nil, env
116
+ expect(last_response.status).to eq(200)
117
+ expect(JSON.parse(last_response.body)['params']['bar']).to eq('baz')
118
+ end
119
+
120
+ it "should merge both sets of params" do
121
+ post '/foo/baz/bar?chunky=bacon', {}, env
122
+ expect(last_response.status).to eq(200)
123
+ actual_body = JSON.parse(last_response.body)
124
+ expect(actual_body['params']['chunky']).to eq('bacon')
125
+ expect(actual_body['params']['captures']).to include('baz')
126
+ end
127
+
128
+ it "should work with mount" do
129
+ get '/chunky/crunch', {}, env
130
+ expect(last_response.status).to eq(200)
131
+ body = JSON.parse(last_response.body)
132
+ expect(body['params']['bacon']).to eq('crunch')
133
+ expect(body['foo']).to eq('bar')
134
+ expect(body['biz']).to eq('baz')
135
+ end
136
+
137
+ it "should allow middleware to prematurely respond" do
138
+ get '/premature/response', {}, env
139
+ expect(last_response.body).to eq('Premature-Response')
140
+ end
141
+ end
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'bundler/setup'
4
+ require 'rack/test'
5
+
6
+ require 'rack-putty'
7
+
8
+ ENV['RACK_ENV'] ||= 'test'
9
+
10
+ RSpec.configure do |config|
11
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-putty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jonathan Rudenberg
9
+ - Jesse Stuart
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-04-07 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack-mount
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.3
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: 0.8.3
31
+ - !ruby/object:Gem::Dependency
32
+ name: rack-test
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 0.6.1
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 0.6.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.11'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '2.11'
63
+ - !ruby/object:Gem::Dependency
64
+ name: bundler
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: rake
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ description: Simple web framework built on rack for mapping sinatra-like routes to
96
+ middleware stacks.
97
+ email:
98
+ - jonathan@titanous.com
99
+ - jesse@jessestuart.ca
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - LICENSE
107
+ - README.md
108
+ - Rakefile
109
+ - lib/rack-putty.rb
110
+ - lib/rack-putty/middleware.rb
111
+ - lib/rack-putty/router.rb
112
+ - lib/rack-putty/router/extract_params.rb
113
+ - lib/rack-putty/version.rb
114
+ - rack-putty.gemspec
115
+ - spec/integration/router_spec.rb
116
+ - spec/spec_helper.rb
117
+ homepage: ''
118
+ licenses: []
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 1.8.23
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: Simple web framework built on rack.
141
+ test_files:
142
+ - spec/integration/router_spec.rb
143
+ - spec/spec_helper.rb