webmachine 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +11 -3
  2. data/README.md +55 -27
  3. data/lib/webmachine/adapters/mongrel.rb +84 -0
  4. data/lib/webmachine/adapters/webrick.rb +12 -3
  5. data/lib/webmachine/adapters.rb +1 -7
  6. data/lib/webmachine/configuration.rb +30 -0
  7. data/lib/webmachine/decision/conneg.rb +7 -72
  8. data/lib/webmachine/decision/flow.rb +13 -11
  9. data/lib/webmachine/decision/fsm.rb +1 -9
  10. data/lib/webmachine/decision/helpers.rb +27 -7
  11. data/lib/webmachine/errors.rb +1 -0
  12. data/lib/webmachine/headers.rb +12 -3
  13. data/lib/webmachine/locale/en.yml +2 -2
  14. data/lib/webmachine/media_type.rb +117 -0
  15. data/lib/webmachine/resource/callbacks.rb +9 -0
  16. data/lib/webmachine/streaming.rb +3 -3
  17. data/lib/webmachine/version.rb +1 -1
  18. data/lib/webmachine.rb +3 -1
  19. data/pkg/webmachine-0.1.0/Gemfile +16 -0
  20. data/pkg/webmachine-0.1.0/Guardfile +11 -0
  21. data/pkg/webmachine-0.1.0/README.md +90 -0
  22. data/pkg/webmachine-0.1.0/Rakefile +31 -0
  23. data/pkg/webmachine-0.1.0/examples/webrick.rb +19 -0
  24. data/pkg/webmachine-0.1.0/lib/webmachine/adapters/webrick.rb +74 -0
  25. data/pkg/webmachine-0.1.0/lib/webmachine/adapters.rb +15 -0
  26. data/pkg/webmachine-0.1.0/lib/webmachine/decision/conneg.rb +304 -0
  27. data/pkg/webmachine-0.1.0/lib/webmachine/decision/flow.rb +502 -0
  28. data/pkg/webmachine-0.1.0/lib/webmachine/decision/fsm.rb +79 -0
  29. data/pkg/webmachine-0.1.0/lib/webmachine/decision/helpers.rb +80 -0
  30. data/pkg/webmachine-0.1.0/lib/webmachine/decision.rb +12 -0
  31. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher/route.rb +85 -0
  32. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher.rb +40 -0
  33. data/pkg/webmachine-0.1.0/lib/webmachine/errors.rb +37 -0
  34. data/pkg/webmachine-0.1.0/lib/webmachine/headers.rb +16 -0
  35. data/pkg/webmachine-0.1.0/lib/webmachine/locale/en.yml +28 -0
  36. data/pkg/webmachine-0.1.0/lib/webmachine/request.rb +56 -0
  37. data/pkg/webmachine-0.1.0/lib/webmachine/resource/callbacks.rb +362 -0
  38. data/pkg/webmachine-0.1.0/lib/webmachine/resource/encodings.rb +36 -0
  39. data/pkg/webmachine-0.1.0/lib/webmachine/resource.rb +48 -0
  40. data/pkg/webmachine-0.1.0/lib/webmachine/response.rb +49 -0
  41. data/pkg/webmachine-0.1.0/lib/webmachine/streaming.rb +27 -0
  42. data/pkg/webmachine-0.1.0/lib/webmachine/translation.rb +11 -0
  43. data/pkg/webmachine-0.1.0/lib/webmachine/version.rb +4 -0
  44. data/pkg/webmachine-0.1.0/lib/webmachine.rb +19 -0
  45. data/pkg/webmachine-0.1.0/spec/spec_helper.rb +13 -0
  46. data/pkg/webmachine-0.1.0/spec/tests.org +57 -0
  47. data/pkg/webmachine-0.1.0/spec/webmachine/decision/conneg_spec.rb +152 -0
  48. data/pkg/webmachine-0.1.0/spec/webmachine/decision/flow_spec.rb +1030 -0
  49. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher/route_spec.rb +109 -0
  50. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher_spec.rb +34 -0
  51. data/pkg/webmachine-0.1.0/spec/webmachine/headers_spec.rb +19 -0
  52. data/pkg/webmachine-0.1.0/spec/webmachine/request_spec.rb +24 -0
  53. data/pkg/webmachine-0.1.0/webmachine.gemspec +44 -0
  54. data/pkg/webmachine-0.1.0.gem +0 -0
  55. data/spec/webmachine/configuration_spec.rb +27 -0
  56. data/spec/webmachine/decision/conneg_spec.rb +18 -11
  57. data/spec/webmachine/decision/flow_spec.rb +2 -0
  58. data/spec/webmachine/decision/helpers_spec.rb +105 -0
  59. data/spec/webmachine/errors_spec.rb +13 -0
  60. data/spec/webmachine/headers_spec.rb +2 -1
  61. data/spec/webmachine/media_type_spec.rb +78 -0
  62. data/webmachine.gemspec +4 -1
  63. metadata +69 -11
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Dispatcher::Route do
4
+ let(:request){ Webmachine::Request.new("GET", URI.parse("http://localhost:8098/"), Webmachine::Headers.new, "") }
5
+ let(:resource){ Class.new(Webmachine::Resource) }
6
+
7
+ matcher :match_route do |*expected|
8
+ route = described_class.new(expected[0], resource, expected[1] || {})
9
+ match do |actual|
10
+ request.uri.path = actual if String === actual
11
+ route.match?(request)
12
+ end
13
+
14
+ failure_message_for_should do |_|
15
+ "expected route #{expected[0].inspect} to match path #{request.uri.path}"
16
+ end
17
+ failure_message_for_should_not do |_|
18
+ "expected route #{expected[0].inspect} not to match path #{request.uri.path}"
19
+ end
20
+ end
21
+
22
+ context "matching a request" do
23
+ context "on the root path" do
24
+ subject { "/" }
25
+ it { should match_route([]) }
26
+ it { should match_route ['*'] }
27
+ it { should_not match_route %w{foo} }
28
+ it { should_not match_route [:id] }
29
+ end
30
+
31
+ context "on a deep path" do
32
+ subject { "/foo/bar/baz" }
33
+ it { should match_route %w{foo bar baz} }
34
+ it { should match_route ['foo', :id, "baz"] }
35
+ it { should match_route %w{foo *} }
36
+ it { should match_route [:id, '*'] }
37
+ it { should_not match_route [] }
38
+ it { should_not match_route %w{bar *} }
39
+ end
40
+ end
41
+
42
+ context "applying bindings" do
43
+ context "on the root path" do
44
+ subject { described_class.new([], resource) }
45
+ before { subject.apply(request) }
46
+
47
+ it "should assign the dispatched path to the empty string" do
48
+ request.disp_path.should == ""
49
+ end
50
+
51
+ it "should assign empty bindings" do
52
+ request.path_info.should == {}
53
+ end
54
+
55
+ it "should assign empty path tokens" do
56
+ request.path_tokens.should == []
57
+ end
58
+
59
+ context "with extra user-defined bindings" do
60
+ subject { described_class.new([], resource, "bar" => "baz") }
61
+
62
+ it "should assign the user-defined bindings" do
63
+ request.path_info.should == {"bar" => "baz"}
64
+ end
65
+ end
66
+
67
+ context "with a splat" do
68
+ subject { described_class.new(['*'], resource) }
69
+
70
+ it "should assign empty path tokens" do
71
+ request.path_tokens.should == []
72
+ end
73
+ end
74
+ end
75
+
76
+ context "on a deep path" do
77
+ subject { described_class.new(%w{foo bar baz}, resource) }
78
+ before { request.uri.path = "/foo/bar/baz"; subject.apply(request) }
79
+
80
+ it "should assign the dispatched path as the path past the initial slash" do
81
+ request.disp_path.should == "foo/bar/baz"
82
+ end
83
+
84
+ it "should assign empty bindings" do
85
+ request.path_info.should == {}
86
+ end
87
+
88
+ it "should assign empty path tokens" do
89
+ request.path_tokens.should == []
90
+ end
91
+
92
+ context "with path variables" do
93
+ subject { described_class.new(['foo', :id, 'baz'], resource) }
94
+
95
+ it "should assign the path variables in the bindings" do
96
+ request.path_info.should == {:id => "bar"}
97
+ end
98
+ end
99
+
100
+ context "with a splat" do
101
+ subject { described_class.new(%w{foo *}, resource) }
102
+
103
+ it "should capture the path tokens matched by the splat" do
104
+ request.path_tokens.should == %w{ bar baz }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Dispatcher do
4
+ let(:dispatcher) { described_class }
5
+ let(:request) { Webmachine::Request.new("GET", URI.parse("http://localhost:8080/"), Webmachine::Headers["accept" => "*/*"], "") }
6
+ let(:response) { Webmachine::Response.new }
7
+ let(:resource) do
8
+ Class.new(Webmachine::Resource) do
9
+ def to_html; "hello world!"; end
10
+ end
11
+ end
12
+ let(:resource2) do
13
+ Class.new(Webmachine::Resource) do
14
+ def to_html; "goodbye, cruel world"; end
15
+ end
16
+ end
17
+ let(:fsm){ mock }
18
+
19
+ before { dispatcher.reset }
20
+
21
+ it "should add routes" do
22
+ expect {
23
+ dispatcher.add_route ['*'], resource
24
+ }.should_not raise_error
25
+ end
26
+
27
+ it "should route to the proper resource" do
28
+ dispatcher.add_route ["goodbye"], resource2
29
+ dispatcher.add_route ['*'], resource
30
+ Webmachine::Decision::FSM.should_receive(:new).with(instance_of(resource), request, response).and_return(fsm)
31
+ fsm.should_receive(:run)
32
+ dispatcher.dispatch(request, response)
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Headers do
4
+ it "should set and access values insensitive to case" do
5
+ subject['Content-TYPE'] = "text/plain"
6
+ subject['CONTENT-TYPE'].should == 'text/plain'
7
+ end
8
+
9
+ context "filtering with #grep" do
10
+ subject { described_class["content-type" => "text/plain", "etag" => '"abcdef1234567890"'] }
11
+ it "should filter keys by the given pattern" do
12
+ subject.grep(/content/i).should include("content-type")
13
+ end
14
+
15
+ it "should return a Headers instance" do
16
+ subject.grep(/etag/i).should be_instance_of(described_class)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Request do
4
+ subject { Webmachine::Request.new("GET", URI.parse("http://localhost:8080/some/resource"), Webmachine::Headers.new, "") }
5
+ it "should provide access to the headers via brackets" do
6
+ subject.headers['Accept'] = "*/*"
7
+ subject["accept"].should == "*/*"
8
+ end
9
+
10
+ it "should provide access to the headers via underscored methods" do
11
+ subject.headers["Accept-Encoding"] = "identity"
12
+ subject.accept_encoding.should == "identity"
13
+ subject.content_md5.should be_nil
14
+ end
15
+
16
+ it "should calculate a base URI" do
17
+ subject.base_uri.should == URI.parse("http://localhost:8080/")
18
+ end
19
+
20
+ it "should provide a hash of query parameters" do
21
+ subject.uri.query = "foo=bar&baz=bam"
22
+ subject.query.should == {"foo" => "bar", "baz" => "bam"}
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'webmachine/version'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "webmachine"
6
+ gem.version = Webmachine::VERSION
7
+ gem.date = File.mtime("lib/webmachine/version.rb")
8
+ gem.summary = %Q{webmachine is a toolkit for building HTTP applications,}
9
+ gem.description = <<-DESC.gsub(/\s+/, ' ')
10
+ webmachine is a toolkit for building HTTP applications in a declarative fashion, that avoids
11
+ the confusion of going through a CGI-style interface like Rack. It is strongly influenced
12
+ by the original Erlang project of the same name and shares its opinionated nature about HTTP.
13
+ DESC
14
+ gem.homepage = "http://github.com/seancribbs/webmachine-ruby"
15
+ gem.authors = ["Sean Cribbs"]
16
+ gem.email = ["sean@basho.com"]
17
+
18
+ if gem.respond_to? :specification_version then
19
+ gem.specification_version = 3
20
+
21
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
22
+ gem.add_runtime_dependency(%q<i18n>, [">= 0.4.0"])
23
+ gem.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
24
+ gem.add_development_dependency(%q<yard>, ["~> 0.6.7"])
25
+ gem.add_development_dependency(%q<rake>)
26
+ else
27
+ gem.add_dependency(%q<i18n>, [">= 0.4.0"])
28
+ gem.add_dependency(%q<rspec>, ["~> 2.6.0"])
29
+ gem.add_dependency(%q<yard>, ["~> 0.6.7"])
30
+ gem.add_dependency(%q<rake>)
31
+ end
32
+ else
33
+ gem.add_dependency(%q<i18n>, [">= 0.4.0"])
34
+ gem.add_dependency(%q<rspec>, ["~> 2.6.0"])
35
+ gem.add_dependency(%q<yard>, ["~> 0.6.7"])
36
+ gem.add_dependency(%q<rake>)
37
+ end
38
+
39
+ ignores = File.read(".gitignore").split(/\r?\n/).reject{ |f| f =~ /^(#.+|\s*)$/ }.map {|f| Dir[f] }.flatten
40
+ gem.files = (Dir['**/*','.gitignore'] - ignores).reject {|f| !File.file?(f) }
41
+ gem.test_files = (Dir['spec/**/*','features/**/*','.gitignore'] - ignores).reject {|f| !File.file?(f) }
42
+ gem.executables = Dir['bin/*'].map { |f| File.basename(f) }
43
+ gem.require_paths = ['lib']
44
+ end
Binary file
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Configuration do
4
+ before { Webmachine.configuration = nil }
5
+
6
+ %w{ip port adapter adapter_options}.each do |field|
7
+ it { should respond_to(field) }
8
+ it { should respond_to("#{field}=") }
9
+ end
10
+
11
+ it "should yield configuration to the block" do
12
+ Webmachine.configure do |config|
13
+ config.should be_kind_of(described_class)
14
+ end
15
+ end
16
+
17
+ it "should set the global configuration from the yielded instance" do
18
+ Webmachine.configure do |config|
19
+ @config = config
20
+ end
21
+ @config.should == Webmachine.configuration
22
+ end
23
+
24
+ it "should return the module from the configure call so you can chain it" do
25
+ Webmachine.configure {|c|}.should == Webmachine
26
+ end
27
+ end
@@ -8,36 +8,43 @@ describe Webmachine::Decision::Conneg do
8
8
  def to_html; "hello world!"; end
9
9
  end
10
10
  end
11
+
11
12
  subject do
12
13
  Webmachine::Decision::FSM.new(resource, request, response)
13
14
  end
14
15
 
15
16
  context "choosing a media type" do
16
17
  it "should not choose a type when none are provided" do
17
- subject.choose_media_type([], "*/*").should be_nil
18
+ subject.choose_media_type([], "*/*").should be_nil
18
19
  end
19
-
20
+
20
21
  it "should not choose a type when none are acceptable" do
21
22
  subject.choose_media_type(["text/html"], "application/json").should be_nil
22
23
  end
23
-
24
+
24
25
  it "should choose the first acceptable type" do
25
26
  subject.choose_media_type(["text/html", "application/xml"],
26
27
  "application/xml, text/html, */*").should == "application/xml"
27
28
  end
28
-
29
+
29
30
  it "should choose the type that matches closest when matching subparams" do
30
31
  subject.choose_media_type(["text/html",
31
32
  ["text/html", {"charset" => "iso8859-1"}]],
32
33
  "text/html;charset=iso8859-1, application/xml").
33
34
  should == "text/html;charset=iso8859-1"
34
-
35
35
  end
36
36
 
37
+ it "should choose a type more specific than requested when an exact match is not present" do
38
+ subject.choose_media_type(["application/json;v=3;foo=bar", "application/json;v=2"],
39
+ "text/html, application/json").
40
+ should == "application/json;v=3;foo=bar"
41
+ end
42
+
43
+
37
44
  it "should choose the preferred type over less-preferred types" do
38
45
  subject.choose_media_type(["text/html", "application/xml"],
39
46
  "application/xml;q=0.7, text/html, */*").should == "text/html"
40
-
47
+
41
48
  end
42
49
 
43
50
  it "should raise an exception when a media-type is improperly formatted" do
@@ -112,17 +119,17 @@ describe Webmachine::Decision::Conneg do
112
119
  subject.choose_language([], "en")
113
120
  subject.metadata['Language'].should be_nil
114
121
  end
115
-
122
+
116
123
  it "should choose the first acceptable language" do
117
124
  subject.choose_language(['en', 'en-US', 'es'], "en-US, es")
118
125
  subject.metadata['Language'].should == "en-US"
119
126
  response.headers['Content-Language'].should == "en-US"
120
127
  end
121
-
128
+
122
129
  it "should choose the preferred language over less-preferred languages" do
123
130
  subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es")
124
131
  subject.metadata['Language'].should == "es"
125
- response.headers['Content-Language'].should == "es"
132
+ response.headers['Content-Language'].should == "es"
126
133
  end
127
134
 
128
135
  it "should select the first language if all are acceptable" do
@@ -130,13 +137,13 @@ describe Webmachine::Decision::Conneg do
130
137
  subject.metadata['Language'].should == "en"
131
138
  response.headers['Content-Language'].should == "en"
132
139
  end
133
-
140
+
134
141
  it "should select the closest acceptable language when an exact match is not available" do
135
142
  subject.choose_language(['en-US', 'es'], "en, fr")
136
143
  subject.metadata['Language'].should == 'en-US'
137
144
  response.headers['Content-Language'].should == 'en-US'
138
145
  end
139
-
146
+
140
147
  it "should not set the language if none are acceptable" do
141
148
  subject.choose_language(['en'], 'es')
142
149
  subject.metadata['Language'].should be_nil
@@ -309,6 +309,7 @@ describe Webmachine::Decision::Flow do
309
309
  subject.run
310
310
  response.code.should_not == 406
311
311
  response.headers['Content-Language'].should == "en-US"
312
+ resource.instance_variable_get(:@language).should == 'en-US'
312
313
  end
313
314
  end
314
315
 
@@ -318,6 +319,7 @@ describe Webmachine::Decision::Flow do
318
319
  subject.should_not_receive(:d5)
319
320
  subject.run
320
321
  response.headers['Content-Language'].should == 'en-US'
322
+ resource.instance_variable_get(:@language).should == 'en-US'
321
323
  end
322
324
  end
323
325
  end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Decision::Helpers do
4
+ subject { Webmachine::Decision::FSM.new(resource, request, response) }
5
+ let(:method) { 'GET' }
6
+ let(:uri) { URI.parse('http://localhost/') }
7
+ let(:headers) { Webmachine::Headers.new }
8
+ let(:body) { '' }
9
+ let(:request) { Webmachine::Request.new(method, uri, headers, body) }
10
+ let(:response) { Webmachine::Response.new }
11
+
12
+ def resource_with(&block)
13
+ klass = Class.new(Webmachine::Resource) do
14
+ def to_html; "test resource"; end
15
+ end
16
+ klass.module_eval(&block) if block_given?
17
+ klass.new(request, response)
18
+ end
19
+
20
+ let(:resource) { resource_with }
21
+
22
+ describe "accepting request bodies" do
23
+ let(:resource) do
24
+ resource_with do
25
+ def initialize
26
+ @accepted, @result = [], true
27
+ end
28
+ attr_accessor :accepted, :result
29
+ def content_types_accepted
30
+ (accepted || []).map {|t| Array === t ? t : [t, :accept_doc] }
31
+ end
32
+ def accept_doc; result; end
33
+ end
34
+ end
35
+
36
+ it "should return 415 when no types are accepted" do
37
+ subject.accept_helper.should == 415
38
+ end
39
+
40
+ it "should return 415 when the posted type is not acceptable" do
41
+ resource.accepted = %W{application/json}
42
+ headers['Content-Type'] = "text/xml"
43
+ subject.accept_helper.should == 415
44
+ end
45
+
46
+ it "should call the method for the first acceptable type, taking into account params" do
47
+ resource.accepted = ["application/json;v=3", ["application/json", :other]]
48
+ resource.should_receive(:other).and_return(true)
49
+ headers['Content-Type'] = 'application/json;v=2'
50
+ subject.accept_helper.should be_true
51
+ end
52
+ end
53
+
54
+ describe "#encode_body" do
55
+ before { subject.run }
56
+
57
+ context "with a String body" do
58
+ before { response.body = '<body></body>' }
59
+
60
+ it "does not modify the response body" do
61
+ subject.encode_body
62
+ String.should === response.body
63
+ end
64
+
65
+ it "sets the Content-Length header in the response" do
66
+ subject.encode_body
67
+ response.headers['Content-Length'].should == response.body.bytesize.to_s
68
+ end
69
+ end
70
+
71
+ shared_examples_for "a non-String body" do
72
+ it "does not set the Content-Length header in the response" do
73
+ subject.encode_body
74
+ response.headers.should_not have_key('Content-Length')
75
+ end
76
+
77
+ it "sets the Transfer-Encoding response header to chunked" do
78
+ subject.encode_body
79
+ response.headers['Transfer-Encoding'].should == 'chunked'
80
+ end
81
+ end
82
+
83
+ context "with an Enumerable body" do
84
+ before { response.body = ['one', 'two'] }
85
+
86
+ it "wraps the response body in an EnumerableEncoder" do
87
+ subject.encode_body
88
+ Webmachine::EnumerableEncoder.should === response.body
89
+ end
90
+
91
+ it_should_behave_like "a non-String body"
92
+ end
93
+
94
+ context "with a callable body" do
95
+ before { response.body = Proc.new { 'proc' } }
96
+
97
+ it "wraps the response body in a CallableEncoder" do
98
+ subject.encode_body
99
+ Webmachine::CallableEncoder.should === response.body
100
+ end
101
+
102
+ it_should_behave_like "a non-String body"
103
+ end
104
+ end
105
+ end