Skip to content
Browse files

Initial commit - very incomplete.

  • Loading branch information...
0 parents commit 12987f347a064a70b9094580a5782d07e51ecd23 @seancribbs seancribbs committed
28 .gitignore
@@ -0,0 +1,28 @@
+## MAC OS
+.DS_Store
+
+## TEXTMATE
+*.tmproj
+tmtags
+
+## EMACS
+*~
+\#*
+.\#*
+
+## VIM
+*.swp
+
+## PROJECT::GENERAL
+coverage
+rdoc
+pkg
+
+## PROJECT::SPECIFIC
+doc
+.yardoc
+.bundle
+Gemfile.lock
+**/bin
+*.rbc
+.rvmrc
3 Gemfile
@@ -0,0 +1,3 @@
+source :rubygems
+
+gemspec
66 Rakefile
@@ -0,0 +1,66 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+
+gemspec = Gem::Specification.new do |gem|
+ gem.name = "webmachine"
+ gem.summary = %Q{webmachine is a toolkit for building HTTP applications,}
+ gem.description = <<-DESC.gsub(/\s+/, ' ')
+ webmachine is a toolkit for building HTTP applications in a declarative fashion, that avoids
+ the confusion of going through a CGI-style interface like Rack. It is strongly influenced
+ by the original Erlang project of the same name and shares its opinionated nature about HTTP.
+ It uses the mongrel2 server underneath, since all other Ruby webservers are tied to the broken
+ CGI/Rack model.
+ DESC
+ gem.version = "0.1.0"
+ gem.email = "sean@basho.com"
+ gem.homepage = "http://seancribbs.github.com/webmachine-rb"
+ gem.authors = ["Sean Cribbs"]
+ # Just copying the mongrel2 adapter bits from this gem for now. We
+ # need to extend things anyway.
+ # gem.add_dependency "rack-mongrel2", "~> 0.2.3"
+ gem.add_dependency 'ffi-rzmq', '~> 0.8.0'
+ gem.add_dependency 'multi_json', '~> 1.0.0'
+ gem.add_development_dependency "rspec", "~> 2.6.0"
+ gem.add_development_dependency "yard", "~> 0.6.7"
+
+ files = FileList["**/*"]
+ # Editor and O/S files
+ files.exclude ".DS_Store", "*~", "\#*", ".\#*", "*.swp", "*.tmproj", "tmtags"
+ # Generated artifacts
+ files.exclude "coverage", "rdoc", "pkg", "doc", ".bundle", "*.rbc", ".rvmrc", ".watchr", ".rspec"
+ # Project-specific
+ files.exclude "Gemfile.lock"
+ # Remove directories
+ files.exclude {|d| File.directory?(d) }
+
+ gem.files = files.to_a
+ gem.test_files = gem.files.grep(/_spec\.rb$/)
+end
+
+Rake::GemPackageTask.new(gemspec) do |pkg|
+ pkg.need_zip = false
+ pkg.need_tar = false
+end
+
+task :gem => :gemspec
+
+desc %{Build the gemspec file.}
+task :gemspec do
+ gemspec.validate
+ File.open("#{gemspec.name}.gemspec", 'w'){|f| f.write gemspec.to_ruby }
+end
+
+desc %{Release the gem to RubyGems.org}
+task :release => :gem do
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
+end
+
+require 'rspec/core'
+require 'rspec/core/rake_task'
+
+desc "Run specs"
+RSpec::Core::RakeTask.new(:spec) do |spec|
+ spec.pattern = "spec/**/*_spec.rb"
+end
+
+task :default => :spec
10 lib/webmachine.rb
@@ -0,0 +1,10 @@
+require 'webmachine/mongrel2/connection'
+require 'webmachine/decision'
+require 'webmachine/dispatcher'
+require 'webmachine/resource'
+
+# Webmachine is a toolkit for making well-behaved HTTP applications,
+# and uses Mongrel2 under the hood. It is based on the Erlang library
+# of the same name.
+module Webmachine
+end
2 lib/webmachine/decision.rb
@@ -0,0 +1,2 @@
+require 'webmachine/decision/flow'
+require 'webmachine/decision/fsm'
401 lib/webmachine/decision/flow.rb
@@ -0,0 +1,401 @@
+module Webmachine
+ module Decision
+ # This module encapsulates all of the decisions in Webmachine's
+ # flow-chart. These invoke {Resource} {Callbacks} to determine the
+ # appropriate response code, headers, and body for the response.
+ #
+ # This module is included into {FSM}, which drives the processing
+ # of the chart.
+ # @see http://webmachine.basho.com/images/http-headers-status-v3.png
+ module Flow
+ def b13
+ resource.service_available? ? :b12 : 503
+ end
+
+ # Known method?
+ def b12
+ resource.known_methods.include?(request.method) ? :b11 : 501
+ end
+
+ # URI too long?
+ def b11
+ resource.uri_too_long? ? 414 : :b10
+ end
+
+ # Method allowed?
+ def b10
+ if allowed_methods.include?(request.method)
+ :b9
+ else
+ response.headers.add("Allow", allowed_methods.join(", "))
+ 405
+ end
+ end
+
+ # Malformed?
+ def b9
+ resource.malformed_request? ? 400 : :b8
+ end
+
+ # Authorized?
+ def b8
+ result = resource.is_authorized?(request.authorization)
+ case result
+ when true
+ :b7
+ when String
+ headers.add('WWW-Authenticate', result)
+ 401
+ else
+ 401
+ end
+ end
+
+ # Forbidden?
+ def b7
+ resource.forbidden? ? 403 : :b6
+ end
+
+ # Okay Content-* Headers?
+ def b6
+ resource.valid_content_headers?(headers) ? :b5 : 501
+ end
+
+ # Known Content-Type?
+ def b5
+ resource.known_content_type?(request.content_type) ? :b4 : 415
+ end
+
+ # Req Entity Too Large?
+ def b4
+ resource.valid_entity_length?(request.headers['Content-Length']) ? :b3 : 413
+ end
+
+ # OPTIONS?
+ def b3
+ if request.method == "OPTIONS"
+ response.headers.merge!(resource.options)
+ 200
+ else
+ :c3
+ end
+ end
+
+ # Accept exists?
+ def c3
+ if !request.headers['Accept']
+ types = resource.content_types_provided.map {|pair| pair.first }
+ metadata.add("Content-Type", types.first)
+ :d4
+ else
+ :c4
+ end
+ end
+
+ # Acceptable media type available?
+ def c4
+ types = resource.content_types_provided.map {|pair| pair.first }
+ chosen_type = Util.choose_media_type(types, request.headers['Accept'])
+ if !chosen_type
+ 406
+ else
+ metadata['Content-Type'] = chosen_type
+ :d4
+ end
+ end
+
+ # Accept-Language exists?
+ def d4
+ request.headers['Accept-Language'] ? :e5 : :d5
+ end
+
+ # Acceptable language available
+ def d5
+ resource.language_available?(request.headers['Accept-Language']) ? :e5 : 406
+ end
+
+ # Accept-Charset exists?
+ def e5
+ if !request.headers['Accept-Charset']
+ choose_charset("*") ? :f6 : 406
+ else
+ :e6
+ end
+ end
+
+ # Acceptable Charset available?
+ def e6
+ choose_charset(request.headers['Accept-Charset']) ? :f6 : 406
+ end
+
+ # Accept-Encoding exists?
+ # (also, set content-type header here, now that charset is chosen)
+ def f6
+ chosen_type = metadata['Content-Type']
+ chosen_charset = metadata['Charset']
+ chosen_type << "; charset=#{chosen_charset}" if chosen_charset
+ response.headers['Content-Type'] = chosen_type
+ if !accept_encoding
+ choose_encoding("identity;q=1.0,*;q=0.5") ? :g7 : 406
+ else
+ :f7
+ end
+ end
+
+ # Acceptable encoding available?
+ def f7
+ choose_encoding(request.headers['Accept-Encoding']) ? :g7 : 406
+ end
+
+ # Resource exists?
+ def g7
+ # This is the first place after all conneg, so set Vary here
+ response.headers['Vary'] = variances.join(", ") if variances.any?
+ resource.resource_exists? ? :g8 : :h7
+ end
+
+ # If-Match exists?
+ def g8
+ request.if_match ? :g9 : :h10
+ end
+
+ # If-Match: * exists?
+ def g9
+ request.if_match == "*" ? :h10 : :g11
+ end
+
+ # ETag in If-Match
+ def g11
+ request_etag = Util.unquote_header(if_match)
+ generate_etag == request_etag ? :h10 : 412
+ end
+
+ # If-Match exists?
+ def h7
+ if_match == "*" ? 412 : :i7
+ end
+
+ # If-Unmodified-Since exists?
+ def h10
+ if_unmodified_since ? :i12 : :h11
+ end
+
+ # If-Unmodified-Since is valid date?
+ def h10
+ begin
+ set(:if_unmodified_since, Time.httpdate(if_unmodified_since))
+ rescue ArgumentError
+ :i12
+ else
+ :h12
+ end
+ end
+
+ # Last-Modified > I-UM-S?
+ def h12
+ last_modified > if_unmodified_since ? 412 : :i12
+ end
+
+ # Moved permanently? (apply PUT to different URI)
+ def i4
+ if uri = moved_permanently?
+ headers.add("Location", uri)
+ 301
+ else
+ :p3
+ end
+ end
+
+ # PUT?
+ def i7
+ request_method == "PUT" ? :i4 : :k7
+ end
+
+ # If-none-match exists?
+ def i12
+ if_none_match ? :i13 : :l13
+ end
+
+ # If-none-match: * exists?
+ def i13
+ if_none_match == "*" ? :j18 : :k13
+ end
+
+ # GET or HEAD?
+ def v3j18
+ %w{GET HEAD}.include?(request_method) ? 304 : 412
+ end
+
+ # Moved permanently?
+ def k5
+ if uri = moved_permanently?
+ headers.add("Location", uri)
+ 301
+ else
+ :l5
+ end
+ end
+
+ # Previously existed?
+ def k7
+ previously_existed? ? :k5 : :l7
+ end
+
+ # Etag in if-none-match?
+ def k13
+ request_etag = Util.unquote_header(if_none_match)
+ generate_etag == request_etag ? :j18 : :l13
+ end
+
+ # Moved temporarily?
+ def l5
+ if uri = moved_temporarily
+ headers.add("Location", uri)
+ 307
+ else
+ :m5
+ end
+ end
+
+ # POST?
+ def l7
+ request_method == "POST" ? :m7 : 404
+ end
+
+ # If-Modified-Since exists?
+ def l13
+ if_modified_since ? :l14 : :m16
+ end
+
+ # IMS is valid date?
+ def l14
+ begin
+ set(:if_modified_since, Time.httpdate(if_modified_since))
+ :l15
+ rescue ArgumentError
+ :m16
+ end
+ end
+
+ # IMS > Now?
+ def l15
+ if_modified_since > Time.now.utc ? :m16 : :l17
+ end
+
+ # Last-Modified > IMS?
+ def l17
+ last_modified.nil? || last_modified > if_modified_since ? :m16 : 304
+ end
+
+ # POST?
+ def m5
+ request_method == "POST" ? :n5 : 410
+ end
+
+ # Server allows POST to missing resource?
+ def m7
+ allow_missing_post? ? :n11 : 404
+ end
+
+ # DELETE?
+ def m16
+ request_method == "DELETE" ? :m20 : :n16
+ end
+
+ # DELETE enacted immediately? (Also where DELETE is forced.)
+ def m20
+ delete_resource ? :m20b : 500
+ end
+
+ def m20b
+ delete_completed? ? :o20 : 202
+ end
+
+ # Server allows POST to missing resource?
+ def n5
+ allow_missing_post? ? :n11 : 410
+ end
+
+ # Redirect?
+ def n11
+ # stage1 = if post_is_create?
+ # if uri = create_path
+ # raise WebmachineError, "create_path is not a String" unless String === uri
+
+ # else
+ # raise WebmachineError, "post_is_create? is true by create_path does not return a String"
+ # end
+ # else
+ # _process_post = process_post
+ # case _process_post
+ # when true
+ # encode_body_if_set
+ # :stage1_ok
+ # when Fixnum
+ # _process_post
+ # else
+ # raise WebmachineError, "process_post failed"
+ # end
+ # end
+
+ end
+
+ # POST?
+ def n16
+ request_method == "POST" ? :n11 : :o16
+ end
+
+ # Conflict?
+ def o14
+ if is_conflict?
+ 409
+ else
+ # accept_helper junk
+ end
+ end
+
+ # PUT?
+ def o16
+ request_method == "PUT" ? :o14 : :o18
+ end
+
+ # Multiple representations?
+ def o18
+ if _build_body?
+ headers.add("ETag", generate_etag) if generate_etag
+ headers.add("Last-Modified", Time.httpdate(last_modified)) if last_modified
+ headers.add("Expires", Time.httpdate(expires)) if expires
+ _, meth = content_types_provided.find {|type,m| type == content_type }
+ # THIS SHIT DOESNT TRANSLATE EXACTLY
+ send(meth)
+ else
+ :o18b
+ end
+ end
+
+ # Multiple choices?
+ def o18b
+ multiple_choices? ? 300 : 200
+ end
+
+ # Response includes an entity?
+ def o20
+ has_response_body? ? :o18 : 204
+ end
+
+ # Conflict?
+ def p3
+ if is_conflict?
+ 409
+ else
+ # accept_helper junk
+ end
+ end
+
+ # New resource?
+ def p11
+ headers["Location"].blank? ? :o20 : 201
+ end
+ end
+ end
+end
12 lib/webmachine/headers.rb
@@ -0,0 +1,12 @@
+module Webmachine
+ # Case-insensitive Hash of request headers
+ class Headers < ::Hash
+ def [](key)
+ super key.to_s.downcase
+ end
+
+ def []=(key,value)
+ super key.to_s.downcase, value
+ end
+ end
+end
8 lib/webmachine/mongrel2.rb
@@ -0,0 +1,8 @@
+require 'multi_json'
+
+module Webmachine
+ # This code is adapted from rack-mongrel2, which has an MIT license.
+ module Mongrel2
+ JSON = MultiJson
+ end
+end
40 lib/webmachine/mongrel2/connection.rb
@@ -0,0 +1,40 @@
+require 'ffi-zmq'
+require 'webmachine/mongrel2/request'
+require 'webmachine/mongrel2/response'
+
+module Webmachine
+ module Mongrel2
+ class Connection
+ CTX = ZMQ::Context.new(1)
+
+ def initialize(uuid, sub, pub)
+ @uuid, @sub, @pub = uuid, sub, pub
+
+ # Connect to receive requests
+ @reqs = CTX.socket(ZMQ::PULL)
+ @reqs.connect(sub)
+
+ # Connect to send responses
+ @resp = CTX.socket(ZMQ::PUB)
+ @resp.connect(pub)
+ @resp.setsockopt(ZMQ::IDENTITY, uuid)
+ end
+
+ def recv
+ msg = @reqs.recv_string(0)
+ msg.nil? ? nil : Request.parse(msg)
+ end
+
+ def reply(req, body, status = 200, headers = {})
+ resp = Response.new(@resp)
+ resp.send_http(req, body, status, headers)
+ resp.close(req) if req.close?
+ end
+
+ def close
+ # I think I should be able to just close the context
+ CTX.close rescue nil
+ end
+ end
+ end
+end
42 lib/webmachine/mongrel2/request.rb
@@ -0,0 +1,42 @@
+require 'webmachine/mongrel2'
+
+module Webmachine
+ module Mongrel2
+ class Request
+ attr_reader :headers, :body, :uuid, :conn_id, :path
+
+ class << self
+ def parse(msg)
+ # UUID CONN_ID PATH SIZE:HEADERS,SIZE:BODY,
+ uuid, conn_id, path, rest = msg.split(' ', 4)
+ headers, rest = parse_netstring(rest)
+ body, _ = parse_netstring(rest)
+ headers = Webmachine::Mongrel2::JSON.decode(headers)
+ new(uuid, conn_id, path, headers, body)
+ end
+
+ def parse_netstring(ns)
+ # SIZE:HEADERS,
+
+ len, rest = ns.split(':', 2)
+ len = len.to_i
+ raise "Netstring did not end in ','" unless rest[len].chr == ','
+ [rest[0, len], rest[(len + 1)..-1]]
+ end
+ end
+
+ def initialize(uuid, conn_id, path, headers, body)
+ @uuid, @conn_id, @path, @headers, @body = uuid, conn_id, path, headers, body
+ @data = headers['METHOD'] == 'JSON' ? Webmachine::Mongrel2::JSON.decode(body) : {}
+ end
+
+ def disconnect?
+ headers['METHOD'] == 'JSON' && @data['type'] == 'disconnect'
+ end
+
+ def close?
+ headers['connection'] == 'close' || headers['VERSION'] == 'HTTP/1.0'
+ end
+ end
+ end
+end
72 lib/webmachine/mongrel2/response.rb
@@ -0,0 +1,72 @@
+module Webmachine
+ module Mongrel2
+ class Response
+ StatusMessage = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Request Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported'
+ }
+
+ def initialize(resp)
+ @resp = resp
+ end
+
+ def send_http(req, body, status, headers)
+ send_resp(req.uuid, req.conn_id, build_http_response(body, status, headers))
+ end
+
+ def close(req)
+ send_resp(req.uuid, req.conn_id, '')
+ end
+
+ private
+
+ def send_resp(uuid, conn_id, data)
+ @resp.send_string('%s %d:%s, %s' % [uuid, conn_id.size, conn_id, data])
+ end
+
+ def build_http_response(body, status, headers)
+ headers['Content-Length'] = body.size.to_s
+ headers = headers.map{ |k, v| '%s: %s' % [k,v] }.join("\r\n")
+ "HTTP/1.1 #{status} #{StatusMessage[status.to_i]}\r\n#{headers}\r\n\r\n#{body}"
+ end
+ end
+ end
+end
24 lib/webmachine/request.rb
@@ -0,0 +1,24 @@
+require 'forwardable'
+module Webmachine
+ # This represents a single HTTP request sent from a client.
+ class Request
+ extend Forwardable
+ attr_reader :method, :uri, :headers, :body
+
+ def initialize(meth, uri, headers, body)
+ @method, @uri, @headers, @body = meth, uri, headers, body
+ end
+
+ delegate :[] => :headers
+
+ # @private
+ def method_missing(m, *args)
+ if m.to_s =~ /^(?:[a-z]_)+[a-z]+$/i
+ # Access headers more easily as underscored methods.
+ headers[m.to_s.tr('_', '-')]
+ else
+ super
+ end
+ end
+ end
+end
34 lib/webmachine/resource.rb
@@ -0,0 +1,34 @@
+require 'webmachine/resource/callbacks'
+require 'webmachine/resource/encodings'
+
+module Webmachine
+ # Resource is the primary building block of Webmachine
+ # applications. It includes all of the methods you might want to
+ # override to customize the behavior of the resource. The simplest
+ # resource you can implement looks like this:
+ #
+ # class HelloWorldResource < Webmachine::Resource
+ # def to_html
+ # "<html><body>Hello, world!</body></html>"
+ # end
+ # end
+ #
+ # For more information about how response decisions are made in
+ # Webmachine based on your resource class, refer to the diagram at
+ # {http://webmachine.basho.com/images/http-headers-status-v3.png}.
+ class Resource
+ include Callbacks
+ include Encodings
+
+ attr_reader :request, :response
+
+ # Creates a new Resource to process the request. This is called
+ # internally by Webmachine when dispatching, but can also be used
+ # to test resources in isolation.
+ # @param [Request] request the request object
+ # @param [Response] response the response object
+ def initialize(request, response)
+ @request, @response = request, response
+ end
+ end
+end
328 lib/webmachine/resource/callbacks.rb
@@ -0,0 +1,328 @@
+module Webmachine
+ class Resource
+ # These
+ module Callbacks
+ # Does the resource exist? Returning a falsey value (false or nil)
+ # will result in a '404 Not Found' response. Defaults to true.
+ # @return [true,false] Whether the resource exists
+ # @api callback
+ def resource_exists?
+ true
+ end
+
+ # Is the resource available? Returning a falsey value (false or
+ # nil) will result in a '503 Service Not Available'
+ # response. Defaults to true. If the resource is only temporarily
+ # not available, add a 'Retry-After' response header in the body
+ # of the method.
+ # @return [true,false]
+ # @api callback
+ def service_available?
+ true
+ end
+
+ # Is the client or request authorized? Returning anything other than true
+ # will result in a '401 Unauthorized' response. Defaults to
+ # true. If a String is returned, it will be used as the value in
+ # the WWW-Authenticate header, which can also be set manually.
+ # @param [String] authorization_header The contents of the
+ # 'Authorization' header sent by the client, if present.
+ # @return [true,false,String] Whether the client is authorized,
+ # and if not, the WWW-Authenticate header when a String.
+ # @api callback
+ def is_authorized?(authorization_header = nil)
+ true
+ end
+
+ # Is the request or client forbidden? Returning a truthy value
+ # (true or non-nil) will result in a '403 Forbidden' response.
+ # Defaults to false.
+ # @return [true,false] Whether the client or request is forbidden.
+ # @api callback
+ def forbidden?
+ false
+ end
+
+ # If the resource accepts POST requests to nonexistent resources,
+ # then this should return true. Defaults to false.
+ # @return [true,false] Whether to accept POST requests to missing
+ # resources.
+ # @api callback
+ def allow_missing_post?
+ false
+ end
+
+ # If the request is malformed, this should return true, which will
+ # result in a '400 Malformed Request' response. Defaults to false.
+ # @return [true,false] Whether the request is malformed.
+ # @api callback
+ def malformed_request?
+ false
+ end
+
+ # If the URI is too long to be processed, this should return true,
+ # which will result in a '414 Request URI Too Long'
+ # response. Defaults to false.
+ # @param [URI] uri the request URI
+ # @return [true,false] Whether the request URI is too long.
+ # @api callback
+ def uri_too_long?(uri = nil)
+ false
+ end
+
+ # If the Content-Type on PUT or POST is unknown, this should
+ # return false, which will result in a '415 Unsupported Media
+ # Type' response. Defaults to true.
+ # @param [String] content_type the 'Content-Type' header sent by
+ # the client
+ # @return [true,false] Whether the passed media type is known or
+ # accepted
+ # @api callback
+ def known_content_type?(content_type = nil)
+ true
+ end
+
+ # If the request includes any invalid Content-* headers, this
+ # should return false, which will result in a '501 Not
+ # Implemented' response. Defaults to false.
+ # @param [Hash] content_headers Request headers that begin with
+ # 'Content-'
+ # @return [true,false] Whether the Content-* headers are invalid
+ # or unsupported
+ # @api callback
+ def valid_content_headers?(content_headers = nil)
+ true
+ end
+
+ # If the entity length on PUT or POST is invalid, this should
+ # return false, which will result in a '413 Request Entity Too
+ # Large' response. Defaults to true.
+ # @param [Fixnum] length the size of the request body (entity)
+ # @return [true,false] Whether the body is a valid length (not too
+ # large)
+ # @api callback
+ def valid_entity_length?(length = nil)
+ true
+ end
+
+ # If the OPTIONS method is supported and is used, this method
+ # should return a Hash of headers that should appear in the
+ # response. Defaults to {}.
+ # @return [Hash] headers to appear in the response
+ # @api callback
+ def options
+ {}
+ end
+
+ # HTTP methods that are allowed on this resource. This must return
+ # an Array of Strings in all capitals. Defaults to ['GET','HEAD'].
+ # @return [Array<String>] allowed methods on this resource
+ # @api callback
+ def allowed_methods
+ ['GET', 'HEAD']
+ end
+
+ # HTTP methods that are known to the resource. Like
+ # {#allowed_methods}, this must return an Array of Strings in
+ # all capitals. Default includes all standard HTTP methods. One
+ # could override this callback to allow additional methods,
+ # e.g. WebDAV.
+ # @return [Array<String>] known methods
+ # @api callback
+ def known_methods
+ ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS']
+ end
+
+ # This method is called when a DELETE request should be enacted,
+ # and should return true if the deletion succeeded. Defaults to false.
+ # @return [true,false] Whether the deletion succeeded.
+ # @api callback
+ def delete_resource
+ false
+ end
+
+ # This method is called after a successful call to
+ # {#delete_resource} and should return false if the deletion was
+ # accepted but cannot yet be guaranteed to have finished. Defaults
+ # to true.
+ # @return [true,false] Whether the deletion completed
+ # @api callback
+ def delete_completed?
+ true
+ end
+
+ # If POST requests should be treated as a request to put content
+ # into a (potentially new) resource as opposed to a generic
+ # submission for processing, then this method should return
+ # true. If it does return true, then {#create_path} will be called
+ # and the rest of the request will be treated much like a PUT to
+ # the path returned by that call. Default is false.
+ # @return [true,false] Whether POST creates a new resource
+ # @api callback
+ def post_is_create?
+ false
+ end
+
+ # This will be called on a POST request if post_is_create? returns
+ # true. The path returned should be a valid URI part following the
+ # dispatcher prefix. That path will replace the previous one in
+ # the return value of {Request#disp_path} for all subsequent
+ # resource function calls in the course of this request.
+ # @return [String, URI] the path to the new resource
+ # @api callback
+ def create_path
+ nil
+ end
+
+ # If post_is_create? returns false, then this will be called to
+ # process any POST requess. If it succeeds, it should return true.
+ # @return [true,false] Whether the POST was successfully processed
+ # @api callback
+ def process_post
+ false
+ end
+
+ # This should return an array of pairs where each pair is of the
+ # form [mediatype, :handler] where mediatype is a String of
+ # Content-Type format and :handler is a Symbol naming the method
+ # which can provide a resource representation in that media
+ # type. For example, if a client request includes an 'Accept'
+ # header with a value that does not appear as a first element in
+ # any of the return pairs, then a '406 Not Acceptable' will be
+ # sent. Default is [['text/html', :to_html]].
+ # @return an array of mediatype/handler pairs
+ # @api callback
+ def content_types_provided
+ [['text/html', :to_html]]
+ end
+
+ # Similarly to content_types_provided, this should return an array
+ # of mediatype/handler pairs, except that it is for incoming
+ # resource representations -- for example, PUT requests. Handler
+ # functions usually want to use {Request#body} to access the
+ # incoming entity.
+ # @return an array of mediatype/handler pairs
+ # @api callback
+ def content_types_accepted
+ []
+ end
+
+ # If this is anything other than nil, it must be an array of pairs
+ # where each pair is of the form Charset, Converter where Charset
+ # is a string naming a charset and Converter is an arity-1 method
+ # in the resource which will be called on the produced body in a
+ # GET and ensure that it is in Charset.
+ # @api callback
+ def charsets_provided
+ nil
+ end
+
+ # This should return a hash of encodings mapped to encoding
+ # methods for Content-Encodings your resource wants to
+ # provide. The encoding will be applied to the response body
+ # automatically by Webmachine. A number of built-in encodings
+ # are provided in the {Encodings} module. Default includes only
+ # the 'identity' encoding.
+ # @return [Hash] a hash of encodings and encoder methods/procs
+ # @api callback
+ # @see Encodings
+ def encodings_provided
+ {"identity" => :encode_identity }
+ end
+
+ # If this method is implemented, it should return a list of
+ # strings with header names that should be included in a given
+ # response's Vary header. The standard conneg headers (Accept,
+ # Accept-Encoding, Accept-Charset, Accept-Language) do not need to
+ # be specified here as Webmachine will add the correct elements of
+ # those automatically depending on resource behavior. Default is
+ # [].
+ # @api callback
+ # @return [Array<String>] a list of variance headers
+ def variances
+ []
+ end
+
+ # If this returns true, the client will receive a '409 Conflict'
+ # response. This is only called for PUT requests. Default is false.
+ # @api callback
+ # @return [true,false] whether the submitted entity is in conflict
+ # with the current state of the resource
+ def is_conflict?
+ false
+ end
+
+ # If this returns true, then it is assumed that multiple
+ # representations of the response are possible and a single one
+ # cannot be automatically chosen, so a 300 Multiple Choices will
+ # be sent instead of a 200. Default is false.
+ # @api callback
+ # @return [true,false] whether the multiple representations are
+ # possible
+ def multiple_choices
+ false
+ end
+
+ # If this resource is known to have existed previously, this
+ # method should return true. Default is false.
+ # @api callback
+ # @return [true,false] whether the resource existed previously
+ def previously_existed?
+ false
+ end
+
+ # If this resource has moved to a new location permanently, this
+ # method should return the new location as a String or
+ # URI. Default is to return false.
+ # @api callback
+ # @return [String,URI,false] the new location of the resource, or
+ # false
+ def moved_permanently?
+ false
+ end
+
+ # If this resource has moved to a new location temporarily, this
+ # method should return the new location as a String or
+ # URI. Default is to return false.
+ # @api callback
+ # @return [String,URI,false] the new location of the resource, or
+ # false
+ def moved_temporarily?
+ false
+ end
+
+ # This method should return the last modified date/time of the
+ # resource which will be added as the Last-Modified header in the
+ # response and used in negotiating conditional requests. Default
+ # is nil.
+ # @api callback
+ # @return [Time,DateTime,Date,nil] the last modified time
+ def last_modified
+ nil
+ end
+
+ # If the resource expires, this method should return the date/time
+ # it expires. Default is nil.
+ # @api callback
+ # @return [Time,DateTime,Date,nil] the expiration time
+ def expires
+ nil
+ end
+
+ # If this returns a value, it will be used as the value of the
+ # ETag header and for comparison in conditional requests. Default
+ # is nil.
+ # @api callback
+ # @return [String,nil] the entity tag for this resource
+ def generate_etag
+ nil
+ end
+
+ # This method is called just before the final response is
+ # constructed and sent. The return value is ignored, so any effect
+ # of this method must be by modifying the response.
+ # @api callback
+ def finish_request; end
+ end
+ end
+end
36 lib/webmachine/resource/encodings.rb
@@ -0,0 +1,36 @@
+require 'zlib'
+require 'stringio'
+
+module Webmachine
+ class Resource
+ # This module implements standard Content-Encodings that you might
+ # want to use in your {Resource}. To use one, simply return it in
+ # the hash from {Callbacks#encodings_provided}.
+ module Encodings
+ # The 'identity' encoding, which does no compression.
+ def encode_identity(data)
+ data
+ end
+
+ # The 'deflate' encoding, which uses libz's DEFLATE compression.
+ def encode_deflate(data)
+ # The deflate options were borrowed from Rack and Mongrel1.
+ Zlib::Deflate.deflate(data, *[Zlib::DEFAULT_COMPRESSION,
+ # drop the zlib header which causes both Safari and IE to choke
+ -Zlib::MAX_WBITS,
+ Zlib::DEF_MEM_LEVEL,
+ Zlib::DEFAULT_STRATEGY
+ ])
+ end
+
+ # The 'gzip' encoding, which uses GNU Zip (via libz).
+ # @note Because of the header/checksum requirements, gzip cannot
+ # be used on streamed responses.
+ def encode_gzip(data)
+ "".tap do |out|
+ Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
+ end
+ end
+ end
+ end
+end

0 comments on commit 12987f3

Please sign in to comment.
Something went wrong with that request. Please try again.