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
@@ -52,15 +52,7 @@ module Webmachine
52
52
  Webmachine.render_error(code, request, response)
53
53
  when 304
54
54
  response.headers.delete('Content-Type')
55
- if etag = resource.generate_etag
56
- response.headers['ETag'] = ensure_quoted_header(etag)
57
- end
58
- if expires = resource.expires
59
- response.headers['Expires'] = expires.httpdate
60
- end
61
- if modified = resource.last_modified
62
- response.headers['Last-Modified'] = modified.httpdate
63
- end
55
+ add_caching_headers
64
56
  end
65
57
  response.code = code
66
58
  resource.finish_request
@@ -1,4 +1,6 @@
1
1
  require 'webmachine/streaming'
2
+ require 'webmachine/media_type'
3
+
2
4
  module Webmachine
3
5
  module Decision
4
6
  # Methods that assist the Decision {Flow}.
@@ -28,11 +30,19 @@ module Webmachine
28
30
  resource.send(encoder, resource.send(charsetter, body))
29
31
  when Enumerable
30
32
  EnumerableEncoder.new(resource, encoder, charsetter, body)
31
- when body.respond_to?(:call)
32
- CallableEncoder.new(resource, encoder, charsetter, body)
33
33
  else
34
- resource.send(encoder, resource.send(charsetter, body))
34
+ if body.respond_to?(:call)
35
+ CallableEncoder.new(resource, encoder, charsetter, body)
36
+ else
37
+ resource.send(encoder, resource.send(charsetter, body))
38
+ end
35
39
  end
40
+ if String === response.body
41
+ response.headers['Content-Length'] = response.body.respond_to?(:bytesize) ? response.body.bytesize.to_s : response.body.length.to_s
42
+ else
43
+ response.headers.delete 'Content-Length'
44
+ response.headers['Transfer-Encoding'] = 'chunked'
45
+ end
36
46
  end
37
47
 
38
48
  # Ensures that a header is quoted (like ETag)
@@ -55,10 +65,8 @@ module Webmachine
55
65
 
56
66
  # Assists in receiving request bodies
57
67
  def accept_helper
58
- content_type = request.content_type || 'application/octet-stream'
59
- mt = Conneg::MediaType.parse(content_type)
60
- metadata['mediaparams'] = mt.params
61
- acceptable = resource.content_types_accepted.find {|ct, _| mt.type_matches?(Conneg::MediaType.parse(ct)) }
68
+ content_type = MediaType.parse(request.content_type || 'application/octet-stream')
69
+ acceptable = resource.content_types_accepted.find {|ct, _| content_type.match?(ct) }
62
70
  if acceptable
63
71
  resource.send(acceptable.last)
64
72
  else
@@ -75,6 +83,18 @@ module Webmachine
75
83
  v.unshift "Accept" if resource.content_types_provided.size > 1
76
84
  end
77
85
  end
86
+
87
+ def add_caching_headers
88
+ if etag = resource.generate_etag
89
+ response.headers['ETag'] = ensure_quoted_header(etag)
90
+ end
91
+ if expires = resource.expires
92
+ response.headers['Expires'] = expires.httpdate
93
+ end
94
+ if modified = resource.last_modified
95
+ response.headers['Last-Modified'] = modified.httpdate
96
+ end
97
+ end
78
98
  end
79
99
  end
80
100
  end
@@ -12,6 +12,7 @@ module Webmachine
12
12
  # @param [Hash] options keys to override the defaults when rendering
13
13
  # the response body
14
14
  def self.render_error(code, req, res, options={})
15
+ res.code = code
15
16
  unless res.body
16
17
  title, message = t(["errors.#{code}.title", "errors.#{code}.message"],
17
18
  { :method => req.method,
@@ -1,16 +1,25 @@
1
1
  module Webmachine
2
- # Case-insensitive Hash of request headers
2
+ # Case-insensitive Hash of Request headers
3
3
  class Headers < ::Hash
4
4
  def [](key)
5
- super key.to_s.downcase
5
+ super transform_key(key)
6
6
  end
7
7
 
8
8
  def []=(key,value)
9
- super key.to_s.downcase, value
9
+ super transform_key(key), value
10
10
  end
11
11
 
12
+ def delete(key)
13
+ super transform_key(key)
14
+ end
15
+
12
16
  def grep(pattern)
13
17
  self.class[select { |k,_| pattern === k }]
14
18
  end
19
+
20
+ private
21
+ def transform_key(key)
22
+ key.to_s.downcase
23
+ end
15
24
  end
16
25
  end
@@ -19,10 +19,10 @@ en:
19
19
  message: "The server does not support the %{method} method."
20
20
  "503":
21
21
  title: 503 Service Unavailable
22
- message: The server is currently unable to handl the request due to a temporary overloading or maintenance of the server.
22
+ message: The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
23
23
  create_path_nil: "post_is_create? returned true but create_path is nil! Define the create_path method in %{class}"
24
24
  do_redirect: "Response had do_redirect but no Location header."
25
25
  fsm_broke: "Decision FSM returned an unexpected value %{result} from decision %{state}."
26
- invalid_media_type: "Invalid media type specified in Accept header: %{type}"
26
+ invalid_media_type: "Invalid media type: %{type}"
27
27
  not_resource_class: "%{class} is not a subclass of Webmachine::Resource"
28
28
  process_post_invalid: "process_post returned %{result}"
@@ -0,0 +1,117 @@
1
+ require 'webmachine/translation'
2
+
3
+ module Webmachine
4
+ # Encapsulates a MIME media type, with logic for matching types.
5
+ class MediaType
6
+ extend Translation
7
+ # Matches valid media types
8
+ MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/
9
+
10
+ # Matches sub-type parameters
11
+ PARAMS_REGEX = /;\s*([^=]+)=([^;=\s]+)/
12
+
13
+ # Creates a new MediaType by parsing an alternate representation.
14
+ # @param [MediaType, String, Array<String,Hash>] obj the raw type
15
+ # to be parsed
16
+ # @return [MediaType] the parsed media type
17
+ # @raise [ArgumentError] when the type could not be parsed
18
+ def self.parse(obj)
19
+ case obj
20
+ when MediaType
21
+ obj
22
+ when MEDIA_TYPE_REGEX
23
+ type, raw_params = $1, $2
24
+ params = Hash[raw_params.scan(PARAMS_REGEX)]
25
+ new(type, params)
26
+ else
27
+ unless Array === obj && String === obj[0] && Hash === obj[1]
28
+ raise ArgumentError, t('invalid_media_type', :type => obj.inspect)
29
+ end
30
+ type = parse(obj[0])
31
+ type.params.merge!(obj[1])
32
+ type
33
+ end
34
+ end
35
+
36
+ # @return [String] the MIME media type
37
+ attr_accessor :type
38
+
39
+ # @return [Hash] any type parameters, e.g. charset
40
+ attr_accessor :params
41
+
42
+ # @param [String] type the main media type, e.g. application/json
43
+ # @param [Hash] params the media type parameters
44
+ def initialize(type, params={})
45
+ @type, @params = type, params
46
+ end
47
+
48
+ # Detects whether the {MediaType} represents an open wildcard
49
+ # type, that is, "*/*" without any {#params}.
50
+ def matches_all?
51
+ @type == "*/*" && @params.empty?
52
+ end
53
+
54
+ def ==(other)
55
+ other = self.class.parse(other)
56
+ other.type == type && other.params == params
57
+ end
58
+
59
+ # Detects whether this {MediaType} matches the other {MediaType},
60
+ # taking into account wildcards. Sub-type parameters are treated
61
+ # strictly.
62
+ # @param [MediaType, String, Array<String,Hash>] other the other type
63
+ # @return [true,false] whether it is an acceptable match
64
+ def exact_match?(other)
65
+ other = self.class.parse(other)
66
+ type_matches?(other) && other.params == params
67
+ end
68
+
69
+ # Detects whether the {MediaType} is an acceptable match for the
70
+ # other {MediaType}, taking into account wildcards and satisfying
71
+ # all requested parameters, but allowing this type to have extra
72
+ # specificity.
73
+ # @param [MediaType, String, Array<String,Hash>] other the other type
74
+ # @return [true,false] whether it is an acceptable match
75
+ def match?(other)
76
+ other = self.class.parse(other)
77
+ type_matches?(other) && params_match?(other.params)
78
+ end
79
+
80
+ # Detects whether the passed sub-type parameters are all satisfied
81
+ # by this {MediaType}. The receiver is allowed to have other
82
+ # params than the ones specified, but all specified must be equal.
83
+ # @param [Hash] params the requested params
84
+ # @return [true,false] whether it is an acceptable match
85
+ def params_match?(other)
86
+ other.all? {|k,v| params[k] == v }
87
+ end
88
+
89
+ # Reconstitutes the type into a String
90
+ # @return [String] the type as a String
91
+ def to_s
92
+ [type, *params.map {|k,v| "#{k}=#{v}" }].join(";")
93
+ end
94
+
95
+ # @return [String] The major type, e.g. "application", "text", "image"
96
+ def major
97
+ type.split("/").first
98
+ end
99
+
100
+ # @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
101
+ def minor
102
+ type.split("/").last
103
+ end
104
+
105
+ # @param [MediaType] other the other type
106
+ # @return [true,false] whether the main media type is acceptable,
107
+ # ignoring params and taking into account wildcards
108
+ def type_matches?(other)
109
+ other = self.class.parse(other)
110
+ if ["*", "*/*", type].include?(other.type)
111
+ true
112
+ else
113
+ other.major == major && other.minor == "*"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -239,6 +239,15 @@ module Webmachine
239
239
  []
240
240
  end
241
241
 
242
+ # This should receive the chosen language and do something with
243
+ # it that is resource-specific. The default is to store the
244
+ # value in the @language instance variable.
245
+ # @param [String] lang the negotiated language
246
+ # @api callback
247
+ def language_chosen(lang)
248
+ @language = lang
249
+ end
250
+
242
251
  # This should return a hash of encodings mapped to encoding
243
252
  # methods for Content-Encodings your resource wants to
244
253
  # provide. The encoding will be applied to the response body
@@ -9,15 +9,15 @@ module Webmachine
9
9
  include Enumerable
10
10
 
11
11
  def each
12
- body.each do |block|
13
- yield @resource.send(@encoder, resource.send(@charsetter, block))
12
+ @body.each do |block|
13
+ yield @resource.send(@encoder, @resource.send(@charsetter, block))
14
14
  end
15
15
  end
16
16
  end
17
17
 
18
18
  class CallableEncoder < StreamingEncoder
19
19
  def call
20
- @resource.send(@encoder, @resource.send(@charsetter, body.call))
20
+ @resource.send(@encoder, @resource.send(@charsetter, @body.call))
21
21
  end
22
22
 
23
23
  def to_proc
@@ -1,4 +1,4 @@
1
1
  module Webmachine
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  SERVER_STRING = "Webmachine-Ruby/#{VERSION}"
4
4
  end
data/lib/webmachine.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'webmachine/configuration'
1
2
  require 'webmachine/headers'
2
3
  require 'webmachine/request'
3
4
  require 'webmachine/response'
@@ -14,6 +15,7 @@ require 'webmachine/version'
14
15
  module Webmachine
15
16
  # Starts Webmachine serving requests
16
17
  def self.run
17
- Adapters.const_get(adapter).run
18
+ configure unless configuration
19
+ Adapters.const_get(configuration.adapter).run
18
20
  end
19
21
  end
@@ -0,0 +1,16 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'bundler'
6
+
7
+ unless ENV['TRAVIS']
8
+ gem 'guard-rspec'
9
+ gem 'rb-fsevent'
10
+ gem 'growl'
11
+ gem 'growl_notify'
12
+ end
13
+
14
+ platforms :jruby do
15
+ gem 'jruby-openssl'
16
+ end
@@ -0,0 +1,11 @@
1
+ gemset = ENV['RVM_GEMSET'] || 'webmachine'
2
+ gemset = "@#{gemset}" unless gemset.to_s == ''
3
+
4
+ rvms = %W[ 1.9.2 ].map {|v| "#{v}#{gemset}" }
5
+
6
+ guard 'rspec', :cli => "--color --profile", :growl => true, :rvm => rvms do
7
+ watch(%r{^lib/webmachine/locale/.+$}) { "spec" }
8
+ watch(%r{^spec/.+_spec\.rb$})
9
+ watch(%r{^lib/(.+)\.rb$}){ |m| "spec/#{m[1]}_spec.rb" }
10
+ watch('spec/spec_helper.rb') { "spec" }
11
+ end
@@ -0,0 +1,90 @@
1
+ # webmachine for Ruby [![travis](https://secure.travis-ci.org/seancribbs/webmachine-ruby.png)](http://travis-ci.org/seancribbs/webmachine-ruby)
2
+
3
+ webmachine-ruby is a port of
4
+ [Webmachine](https://github.com/basho/webmachine), which is written in
5
+ Erlang. The goal of both projects is to expose interesting parts of
6
+ the HTTP protocol to your application in a declarative way. This
7
+ means that you are less concerned with handling requests directly and
8
+ more with describing the behavior of the resources that make up your
9
+ application. Webmachine is not a web framework _per se_, but more of a
10
+ toolkit for building HTTP-friendly applications. For example, it does
11
+ not provide a templating engine or a persistence layer; those choices
12
+ are up to you.
13
+
14
+ **NOTE**: _Webmachine is NOT compatible with Rack._ This is
15
+ intentional! Rack obscures HTTP in a way that makes it hard for
16
+ Webmachine to do its job properly, and encourages people to add
17
+ middleware that might break Webmachine's behavior. Rack is also built
18
+ on the tradition of CGI, which is nice for backwards compatibility but
19
+ also an antiquated paradigm and should be scuttled (IMHO). _Rack may
20
+ be supported in the future, but only as a shim to support other web
21
+ application servers._
22
+
23
+ ## Getting Started
24
+
25
+ Webmachine is very young, but it's still easy to construct an
26
+ application for it!
27
+
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
38
+ ```
39
+
40
+ Your resource will look something like this:
41
+
42
+ ```ruby
43
+ class MyResource < Webmachine::Resource
44
+ def to_html
45
+ "<html><body>Hello, world!</body></html>"
46
+ end
47
+ end
48
+ ```
49
+
50
+ Run the first file and your application is up. That's all there is to
51
+ it! If you want to customize your resource more, look at the available
52
+ callbacks in lib/webmachine/resource/callbacks.rb. For example, you
53
+ might want to enable "gzip" compression on your resource, for which
54
+ you can simply add an `encodings_provided` callback method:
55
+
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
66
+ ```
67
+
68
+ There are many other HTTP features exposed to your resource through
69
+ callbacks. Give them a try!
70
+
71
+ ## Features
72
+
73
+ * Handles the hard parts of content negotiation, conditional
74
+ requests, and response codes for you.
75
+ * Most callbacks can interrupt the decision flow by returning an
76
+ integer response code. You generally only want to do this when new
77
+ information comes to light, requiring a modification of the response.
78
+ * Currently supports WEBrick. Other host servers are planned.
79
+ * Streaming/chunked response bodies are permitted as Enumerables or Procs.
80
+ * Unlike the Erlang original, it does real Language negotiation.
81
+
82
+ ## Problems/TODOs
83
+
84
+ * Support streamed responses as Fibers.
85
+ * Configuration, command-line tools, and general polish.
86
+ * An effort has been made to make the code feel as Ruby-ish as
87
+ possible, but there is still work to do.
88
+ * Tracing is exposed as an Array of decisions visited on the response
89
+ object. You should be able to turn this off and on, and visualize
90
+ the decisions on the sequence diagram.
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package_task'
3
+
4
+ def gemspec
5
+ $webmachine_gemspec ||= Gem::Specification.load("webmachine.gemspec")
6
+ end
7
+
8
+ Gem::PackageTask.new(gemspec) do |pkg|
9
+ pkg.need_zip = false
10
+ pkg.need_tar = false
11
+ end
12
+
13
+ task :gem => :gemspec
14
+
15
+ desc %{Validate the gemspec file.}
16
+ task :gemspec do
17
+ gemspec.validate
18
+ end
19
+
20
+ desc %{Release the gem to RubyGems.org}
21
+ task :release => :gem do
22
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
23
+ end
24
+
25
+ require 'rspec/core'
26
+ require 'rspec/core/rake_task'
27
+
28
+ desc "Run specs"
29
+ RSpec::Core::RakeTask.new(:spec)
30
+
31
+ task :default => :spec