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
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ require 'rbconfig'
2
+
1
3
  source :rubygems
2
4
 
3
5
  gemspec
@@ -6,9 +8,15 @@ gem 'bundler'
6
8
 
7
9
  unless ENV['TRAVIS']
8
10
  gem 'guard-rspec'
9
- gem 'rb-fsevent'
10
- gem 'growl'
11
- gem 'growl_notify'
11
+
12
+ case RbConfig::CONFIG['host_os']
13
+ when /darwin/
14
+ gem 'rb-fsevent'
15
+ gem 'growl_notify'
16
+ when /linux/
17
+ gem 'rb-inotify'
18
+ gem 'libnotify'
19
+ end
12
20
  end
13
21
 
14
22
  platforms :jruby do
data/README.md CHANGED
@@ -26,25 +26,25 @@ Webmachine is very young, but it's still easy to construct an
26
26
  application for it!
27
27
 
28
28
  ```ruby
29
- require 'webmachine'
30
- # Require any of the files that contain your resources here
31
- require 'my_resource'
32
-
33
- # Point all URIs at the MyResource class
34
- Webmachine::Dispatcher.add_route(['*'], MyResource)
35
-
36
- # Start the server, binds to port 3000 using WEBrick
37
- Webmachine.run
29
+ require 'webmachine'
30
+ # Require any of the files that contain your resources here
31
+ require 'my_resource'
32
+
33
+ # Point all URIs at the MyResource class
34
+ Webmachine::Dispatcher.add_route(['*'], MyResource)
35
+
36
+ # Start the server, binds to port 3000 using WEBrick
37
+ Webmachine.run
38
38
  ```
39
39
 
40
40
  Your resource will look something like this:
41
41
 
42
42
  ```ruby
43
- class MyResource < Webmachine::Resource
44
- def to_html
45
- "<html><body>Hello, world!</body></html>"
46
- end
47
- end
43
+ class MyResource < Webmachine::Resource
44
+ def to_html
45
+ "<html><body>Hello, world!</body></html>"
46
+ end
47
+ end
48
48
  ```
49
49
 
50
50
  Run the first file and your application is up. That's all there is to
@@ -54,15 +54,15 @@ might want to enable "gzip" compression on your resource, for which
54
54
  you can simply add an `encodings_provided` callback method:
55
55
 
56
56
  ```ruby
57
- class MyResource < Webmachine::Resource
58
- def encodings_provided
59
- {"gzip" => :encode_gzip, "identity" => :encode_identity}
60
- end
61
-
62
- def to_html
63
- "<html><body>Hello, world!</body></html>"
64
- end
65
- end
57
+ class MyResource < Webmachine::Resource
58
+ def encodings_provided
59
+ {"gzip" => :encode_gzip, "identity" => :encode_identity}
60
+ end
61
+
62
+ def to_html
63
+ "<html><body>Hello, world!</body></html>"
64
+ end
65
+ end
66
66
  ```
67
67
 
68
68
  There are many other HTTP features exposed to your resource through
@@ -75,15 +75,43 @@ callbacks. Give them a try!
75
75
  * Most callbacks can interrupt the decision flow by returning an
76
76
  integer response code. You generally only want to do this when new
77
77
  information comes to light, requiring a modification of the response.
78
- * Currently supports WEBrick. Other host servers are planned.
78
+ * Supports WEBrick and Mongrel (1.2pre+). Other host servers are being
79
+ investigated.
79
80
  * Streaming/chunked response bodies are permitted as Enumerables or Procs.
81
+ * Unlike the Erlang original, it does real Language negotiation.
80
82
 
81
83
  ## Problems/TODOs
82
84
 
83
85
  * Support streamed responses as Fibers.
84
- * Configuration, command-line tools, and general polish.
85
- * An effort has been made to make the code feel as Ruby-ish as
86
- possible, but there is still work to do.
86
+ * Command-line tools, and general polish.
87
87
  * Tracing is exposed as an Array of decisions visited on the response
88
88
  object. You should be able to turn this off and on, and visualize
89
89
  the decisions on the sequence diagram.
90
+
91
+ ## Changelog
92
+
93
+ ### 0.2.0 September 11, 2011
94
+
95
+ 0.2.0 includes an adapter for Mongrel and a central place for
96
+ configuration as well as numerous bugfixes. Added Ian Plosker and
97
+ Bernd Ahlers as committers. Thank you for your contributions!
98
+
99
+ * Acceptable media types are matched less strictly, which has
100
+ implications on both responses and PUT requests. See the
101
+ [discussion on the commit](https://github.com/seancribbs/webmachine-ruby/commit/3686d0d9ff77fc98aff59f89478e9c6c18844ca1).
102
+ * Resources now receive a callback after the language has been
103
+ negotiated, so they can decide what to do with it.
104
+ * Added `Webmachine::Configuration` so we can more easily support more
105
+ than one host server/adapter.
106
+ * Added Mongrel adapter, supporting 1.2pre+.
107
+ * Media type headers are more lax about whitespace following
108
+ semicolons.
109
+ * Fix some problems with callable response bodies.
110
+ * Make sure String response bodies get a Content-Length header added
111
+ and streaming responses get chunked encoding.
112
+ * Numerous refactorings, including extracting `MediaType` into its own
113
+ top-level class.
114
+
115
+ ### 0.1.0 August 25, 2011
116
+
117
+ This is the initial release. Most things work, but only WEBrick is supported.
@@ -0,0 +1,84 @@
1
+ require 'mongrel'
2
+ require 'webmachine/version'
3
+ require 'webmachine/headers'
4
+ require 'webmachine/request'
5
+ require 'webmachine/response'
6
+ require 'webmachine/dispatcher'
7
+
8
+ module Webmachine
9
+ module Adapters
10
+ # Connects Webmachine to Mongrel.
11
+ module Mongrel
12
+ # Starts the Mongrel adapter
13
+ def self.run
14
+ c = Webmachine.configuration
15
+ options = {
16
+ :port => c.port,
17
+ :host => c.ip
18
+ }.merge(c.adapter_options)
19
+ config = ::Mongrel::Configurator.new(options) do
20
+ listener do
21
+ uri '/', :handler => Webmachine::Adapters::Mongrel::Handler.new
22
+ end
23
+ trap("INT") { stop }
24
+ run
25
+ end
26
+ config.join
27
+ end
28
+
29
+ class Handler < ::Mongrel::HttpHandler
30
+ def process(wreq, wres)
31
+ header = http_headers(wreq.params, Webmachine::Headers.new)
32
+
33
+ request = Webmachine::Request.new(wreq.params["REQUEST_METHOD"],
34
+ URI.parse(wreq.params["REQUEST_URI"]),
35
+ header,
36
+ wreq.body || StringIO.new(''))
37
+
38
+ response = Webmachine::Response.new
39
+ Webmachine::Dispatcher.dispatch(request, response)
40
+
41
+ begin
42
+ wres.status = response.code.to_i
43
+ wres.send_status(nil)
44
+
45
+ response.headers.each { |k, vs|
46
+ vs.split("\n").each { |v|
47
+ wres.header[k] = v
48
+ }
49
+ }
50
+ wres.header['Server'] = [Webmachine::SERVER_STRING, "Mongrel/#{::Mongrel::Const::MONGREL_VERSION}"].join(" ")
51
+ wres.send_header
52
+
53
+ case response.body
54
+ when String
55
+ wres.write response.body
56
+ wres.socket.flush
57
+ when Enumerable
58
+ response.body.each { |part|
59
+ wres.write part
60
+ wres.socket.flush
61
+ }
62
+ else
63
+ if response.body.respond_to?(:call)
64
+ wres.write part
65
+ wres.socket.flush
66
+ end
67
+ end
68
+ ensure
69
+ response.body.close if response.body.respond_to? :close
70
+ end
71
+ end
72
+
73
+ def http_headers(env, headers)
74
+ env.inject(headers) do |h,(k,v)|
75
+ if k =~ /^HTTP_(\w+)$/
76
+ h[$1.tr("_", "-")] = v
77
+ end
78
+ h
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -11,7 +11,12 @@ module Webmachine
11
11
  module WEBrick
12
12
  # Starts the WEBrick adapter
13
13
  def self.run
14
- server = Webmachine::Adapters::WEBrick::Server.new :Port => 3000
14
+ c = Webmachine.configuration
15
+ options = {
16
+ :Port => c.port,
17
+ :BindAddress => c.ip
18
+ }.merge(c.adapter_options)
19
+ server = Webmachine::Adapters::WEBrick::Server.new options
15
20
  trap("INT"){ server.shutdown }
16
21
  Thread.new { server.start }.join
17
22
  end
@@ -35,9 +40,13 @@ module Webmachine
35
40
  when String
36
41
  wres.body << response.body
37
42
  when Enumerable
43
+ wres.chunked = true
38
44
  response.body.each {|part| wres.body << part }
39
- when response.body.respond_to?(:call)
40
- wres.body << response.body.call
45
+ else
46
+ if response.body.respond_to?(:call)
47
+ wres.chunked = true
48
+ wres.body << response.body.call
49
+ end
41
50
  end
42
51
  end
43
52
  end
@@ -4,12 +4,6 @@ module Webmachine
4
4
  # Contains classes and modules that connect Webmachine to Ruby
5
5
  # application servers.
6
6
  module Adapters
7
+ autoload :Mongrel, 'webmachine/adapters/mongrel'
7
8
  end
8
-
9
- class << self
10
- # @return [Symbol] the current webserver adapter
11
- attr_accessor :adapter
12
- end
13
-
14
- self.adapter = :WEBrick
15
9
  end
@@ -0,0 +1,30 @@
1
+ module Webmachine
2
+ # A simple configuration container for items that are used across
3
+ # multiple web server adapters. Typically set using
4
+ # {Webmachine::configure}. If not set by your application, the
5
+ # defaults will be filled in when {Webmachine::run} is called.
6
+ # @attr [String] ip the interface to bind to, defaults to "0.0.0.0"
7
+ # (all interfaces)
8
+ # @attr [Fixnum] port the port to bind to, defaults to 8080
9
+ # @attr [Symbol] adapter the adapter to use, defaults to :WEBrick
10
+ # @attr [Hash] adapter_options adapter-specific options, defaults to {}
11
+ Configuration = Struct.new(:ip, :port, :adapter, :adapter_options)
12
+
13
+ class << self
14
+ # @return [Configuration] the current configuration
15
+ attr_accessor :configuration
16
+ end
17
+
18
+ # Sets configuration for the web server via the passed
19
+ # block. Returns Webmachine so you can chain it with
20
+ # Webmachine.run.
21
+ # @yield [config] a block in which to set configuration values
22
+ # @yieldparam [Configuration] config the Configuration instance
23
+ # @return [Webmachine]
24
+ def self.configure
25
+ @configuration ||= Configuration.new("0.0.0.0", 8080, :WEBrick, {})
26
+ yield @configuration if block_given?
27
+ self
28
+ end
29
+ end
30
+
@@ -1,4 +1,5 @@
1
1
  require 'webmachine/translation'
2
+ require 'webmachine/media_type'
2
3
 
3
4
  module Webmachine
4
5
  module Decision
@@ -14,7 +15,7 @@ module Webmachine
14
15
  def choose_media_type(provided, header)
15
16
  requested = MediaTypeList.build(header.split(/\s*,\s*/))
16
17
  provided = provided.map do |p| # normalize_provided
17
- MediaType.new(*Array(p))
18
+ MediaType.parse(p)
18
19
  end
19
20
  # choose_media_type1
20
21
  chosen = nil
@@ -79,7 +80,8 @@ module Webmachine
79
80
  end
80
81
  end
81
82
 
82
- # RFC2616, section 14.14:
83
+ # Implements language-negotation matching as described in
84
+ # RFC2616, section 14.14.
83
85
  #
84
86
  # A language-range matches a language-tag if it exactly
85
87
  # equals the tag, or if it exactly equals a prefix of the
@@ -116,74 +118,6 @@ module Webmachine
116
118
  # Matches acceptable items that include 'q' values
117
119
  CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/
118
120
 
119
- # Matches sub-type parameters
120
- PARAMS_REGEX = /;([^=]+)=([^;=\s]+)/
121
-
122
- # Matches valid media types
123
- MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\S+\s*)*)\s*$/
124
-
125
- # Encapsulates a MIME media type, with logic for matching types.
126
- class MediaType
127
- # Creates a new MediaType by parsing its string representation.
128
- def self.parse(str)
129
- if str =~ MEDIA_TYPE_REGEX
130
- type, raw_params = $1, $2
131
- params = Hash[raw_params.scan(PARAMS_REGEX)]
132
- new(type, params)
133
- end
134
- end
135
-
136
- # @return [String] the MIME media type
137
- attr_accessor :type
138
-
139
- # @return [Hash] any type parameters, e.g. charset
140
- attr_accessor :params
141
-
142
- def initialize(type, params={})
143
- @type, @params = type, params
144
- end
145
-
146
- # Detects whether the {MediaType} represents an open wildcard
147
- # type, that is, "*/*" without any {#params}.
148
- def matches_all?
149
- @type == "*/*" && @params.empty?
150
- end
151
-
152
- def ==(other)
153
- other = self.class.parse(other) if String === other
154
- other.type == type && other.params == params
155
- end
156
-
157
- # Detects whether this {MediaType} matches the other {MediaType},
158
- # taking into account wildcards.
159
- def match?(other)
160
- type_matches?(other) && other.params == params
161
- end
162
-
163
- # Reconstitutes the type into a String
164
- def to_s
165
- [type, *params.map {|k,v| "#{k}=#{v}" }].join(";")
166
- end
167
-
168
- # @return [String] The major type, e.g. "application", "text", "image"
169
- def major
170
- type.split("/").first
171
- end
172
-
173
- # @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
174
- def minor
175
- type.split("/").last
176
- end
177
-
178
- def type_matches?(other)
179
- if ["*", "*/*", type].include?(other.type)
180
- true
181
- else
182
- other.major == major && other.minor == "*"
183
- end
184
- end
185
- end
186
-
187
121
  # Matches the requested media type (with potential modifiers)
188
122
  # against the provided types (with potential modifiers).
189
123
  # @param [MediaType] requested the requested media type
@@ -291,10 +225,11 @@ module Webmachine
291
225
  # {MediaType} items instead of Strings.
292
226
  # @see PriorityList#add_header_val
293
227
  def add_header_val(c)
294
- if mt = MediaType.parse(c)
228
+ begin
229
+ mt = MediaType.parse(c)
295
230
  q = mt.params.delete('q') || 1.0
296
231
  add(q.to_f, mt)
297
- else
232
+ rescue ArgumentError
298
233
  raise MalformedRequest, t('invalid_media_type', :type => c)
299
234
  end
300
235
  end
@@ -163,7 +163,12 @@ module Webmachine
163
163
  # Accept-Language exists?
164
164
  def d4
165
165
  if !request.accept_language
166
- choose_language(resource.languages_provided, "*") ? :e5 : 406
166
+ if language = choose_language(resource.languages_provided, "*")
167
+ resource.language_chosen(language)
168
+ :e5
169
+ else
170
+ 406
171
+ end
167
172
  else
168
173
  :d5
169
174
  end
@@ -171,7 +176,12 @@ module Webmachine
171
176
 
172
177
  # Acceptable language available?
173
178
  def d5
174
- choose_language(resource.languages_provided, request.accept_language) ? :e5 : 406
179
+ if language = choose_language(resource.languages_provided, request.accept_language)
180
+ resource.language_chosen(language)
181
+ :e5
182
+ else
183
+ 406
184
+ end
175
185
  end
176
186
 
177
187
  # Accept-Charset exists?
@@ -449,15 +459,7 @@ module Webmachine
449
459
  # Also where body generation for GET and HEAD is done.
450
460
  def o18
451
461
  if request.method =~ /^(GET|HEAD)$/
452
- if etag = resource.generate_etag
453
- response.headers['ETag'] = ensure_quoted_header(etag)
454
- end
455
- if last_modified = resource.last_modified
456
- response.headers['Last-Modified'] = last_modified.httpdate
457
- end
458
- if expires = resource.expires
459
- response.headers['Expires'] = expires.httpdate
460
- end
462
+ add_caching_headers
461
463
  content_type = metadata['Content-Type']
462
464
  handler = resource.content_types_provided.find {|ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last
463
465
  result = resource.send(handler)