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