webmachine 0.1.0 → 0.2.0

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 (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