webmachine 0.1.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.
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ doc
23
+ .yardoc
24
+ .bundle
25
+ Gemfile.lock
26
+ **/bin
27
+ *.rbc
28
+ .rvmrc
data/Gemfile ADDED
@@ -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
data/Guardfile ADDED
@@ -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
data/README.md ADDED
@@ -0,0 +1,89 @@
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
+
81
+ ## Problems/TODOs
82
+
83
+ * 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.
87
+ * Tracing is exposed as an Array of decisions visited on the response
88
+ object. You should be able to turn this off and on, and visualize
89
+ the decisions on the sequence diagram.
data/Rakefile ADDED
@@ -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
@@ -0,0 +1,19 @@
1
+ require 'webmachine'
2
+
3
+ class HelloResource < Webmachine::Resource
4
+ def last_modified
5
+ File.mtime(__FILE__)
6
+ end
7
+
8
+ def encodings_provided
9
+ { "gzip" => :encode_gzip, "identity" => :encode_identity }
10
+ end
11
+
12
+ def to_html
13
+ "<html><head><title>Hello from Webmachine</title></head><body>Hello, world!</body></html>"
14
+ end
15
+ end
16
+
17
+ Webmachine::Dispatcher.add_route([], HelloResource)
18
+
19
+ Webmachine.run
@@ -0,0 +1,74 @@
1
+ require 'webrick'
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 WEBrick.
11
+ module WEBrick
12
+ # Starts the WEBrick adapter
13
+ def self.run
14
+ server = Webmachine::Adapters::WEBrick::Server.new :Port => 3000
15
+ trap("INT"){ server.shutdown }
16
+ Thread.new { server.start }.join
17
+ end
18
+
19
+ class Server < ::WEBrick::HTTPServer
20
+ def service(wreq, wres)
21
+ header = Webmachine::Headers.new
22
+ wreq.each {|k,v| header[k] = v }
23
+ request = Webmachine::Request.new(wreq.request_method,
24
+ wreq.request_uri,
25
+ header,
26
+ RequestBody.new(wreq))
27
+ response = Webmachine::Response.new
28
+ Webmachine::Dispatcher.dispatch(request, response)
29
+ wres.status = response.code.to_i
30
+ response.headers.each do |k,v|
31
+ wres[k] = v
32
+ end
33
+ wres['Server'] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(" ")
34
+ case response.body
35
+ when String
36
+ wres.body << response.body
37
+ when Enumerable
38
+ response.body.each {|part| wres.body << part }
39
+ when response.body.respond_to?(:call)
40
+ wres.body << response.body.call
41
+ end
42
+ end
43
+ end
44
+
45
+ # Wraps the WEBrick request body so that it can be passed to
46
+ # {Request} while still lazily evaluating the body.
47
+ class RequestBody
48
+ def initialize(request)
49
+ @request = request
50
+ end
51
+
52
+ # Converts the body to a String so you can work with the entire
53
+ # thing.
54
+ def to_s
55
+ @value ? @value.join : @request.body
56
+ end
57
+
58
+ # Iterates over the body in chunks. If the body has previously
59
+ # been read, this method can be called again and get the same
60
+ # sequence of chunks.
61
+ # @yield [chunk]
62
+ # @yieldparam [String] chunk a chunk of the request body
63
+ def each
64
+ if @value
65
+ @value.each {|chunk| yield chunk }
66
+ else
67
+ @value = []
68
+ @request.body {|chunk| @value << chunk; yield chunk }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ require 'webmachine/adapters/webrick'
2
+
3
+ module Webmachine
4
+ # Contains classes and modules that connect Webmachine to Ruby
5
+ # application servers.
6
+ module Adapters
7
+ end
8
+
9
+ class << self
10
+ # @return [Symbol] the current webserver adapter
11
+ attr_accessor :adapter
12
+ end
13
+
14
+ self.adapter = :WEBrick
15
+ end
@@ -0,0 +1,304 @@
1
+ require 'webmachine/translation'
2
+
3
+ module Webmachine
4
+ module Decision
5
+ # Contains methods concerned with Content Negotiation,
6
+ # specifically, choosing media types, encodings, character sets
7
+ # and languages.
8
+ module Conneg
9
+ HAS_ENCODING = defined?(::Encoding) # Ruby 1.9 compat
10
+
11
+ # Given the 'Accept' header and provided types, chooses an
12
+ # appropriate media type.
13
+ # @api private
14
+ def choose_media_type(provided, header)
15
+ requested = MediaTypeList.build(header.split(/\s*,\s*/))
16
+ provided = provided.map do |p| # normalize_provided
17
+ MediaType.new(*Array(p))
18
+ end
19
+ # choose_media_type1
20
+ chosen = nil
21
+ requested.each do |_, requested_type|
22
+ break if chosen = media_match(requested_type, provided)
23
+ end
24
+ chosen
25
+ end
26
+
27
+ # Given the 'Accept-Encoding' header and provided encodings, chooses an appropriate
28
+ # encoding.
29
+ # @api private
30
+ def choose_encoding(provided, header)
31
+ encodings = provided.keys
32
+ if encoding = do_choose(encodings, header, "identity")
33
+ response.headers['Content-Encoding'] = encoding unless encoding == 'identity'
34
+ metadata['Content-Encoding'] = encoding
35
+ end
36
+ end
37
+
38
+ # Given the 'Accept-Charset' header and provided charsets,
39
+ # chooses an appropriate charset.
40
+ # @api private
41
+ def choose_charset(provided, header)
42
+ if provided && !provided.empty?
43
+ charsets = provided.map {|c| c.first }
44
+ if charset = do_choose(charsets, header, HAS_ENCODING ? Encoding.default_external.name : kcode_charset)
45
+ metadata['Charset'] = charset
46
+ end
47
+ else
48
+ true
49
+ end
50
+ end
51
+
52
+ # Given the 'Accept-Language' header and provided languages,
53
+ # chooses an appropriate language.
54
+ # @api private
55
+ def choose_language(provided, header)
56
+ if provided && !provided.empty?
57
+ requested = PriorityList.build(header.split(/\s*,\s*/))
58
+ star_priority = requested.priority_of("*")
59
+ any_ok = star_priority && star_priority > 0.0
60
+ accepted = requested.find do |priority, range|
61
+ if priority == 0.0
62
+ provided.delete_if {|tag| language_match(range, tag) }
63
+ false
64
+ else
65
+ provided.any? {|tag| language_match(range, tag) }
66
+ end
67
+ end
68
+ chosen = if accepted
69
+ provided.find {|tag| language_match(accepted.last, tag) }
70
+ elsif any_ok
71
+ provided.first
72
+ end
73
+ if chosen
74
+ metadata['Language'] = chosen
75
+ response.headers['Content-Language'] = chosen
76
+ end
77
+ else
78
+ true
79
+ end
80
+ end
81
+
82
+ # RFC2616, section 14.14:
83
+ #
84
+ # A language-range matches a language-tag if it exactly
85
+ # equals the tag, or if it exactly equals a prefix of the
86
+ # tag such that the first tag character following the prefix
87
+ # is "-".
88
+ def language_match(range, tag)
89
+ range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}\-/i
90
+ end
91
+
92
+ # Makes an conneg choice based what is accepted and what is
93
+ # provided.
94
+ # @api private
95
+ def do_choose(choices, header, default)
96
+ choices = choices.dup.map {|s| s.downcase }
97
+ accepted = PriorityList.build(header.split(/\s*,\s/))
98
+ default_priority = accepted.priority_of(default)
99
+ star_priority = accepted.priority_of("*")
100
+ default_ok = (default_priority.nil? && star_priority != 0.0) || default_priority
101
+ any_ok = star_priority && star_priority > 0.0
102
+ chosen = accepted.find do |priority, acceptable|
103
+ if priority == 0.0
104
+ choices.delete(acceptable.downcase)
105
+ false
106
+ else
107
+ choices.include?(acceptable.downcase)
108
+ end
109
+ end
110
+ (chosen && chosen.last) || # Use the matching one
111
+ (any_ok && choices.first) || # Or first if "*"
112
+ (default_ok && choices.include?(default) && default) # Or default
113
+ end
114
+
115
+ private
116
+ # Matches acceptable items that include 'q' values
117
+ CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/
118
+
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
+ # Matches the requested media type (with potential modifiers)
188
+ # against the provided types (with potential modifiers).
189
+ # @param [MediaType] requested the requested media type
190
+ # @param [Array<MediaType>] provided the provided media
191
+ # types
192
+ # @return [MediaType] the first media type that matches
193
+ def media_match(requested, provided)
194
+ return provided.first if requested.matches_all?
195
+ provided.find do |p|
196
+ p.match?(requested)
197
+ end
198
+ end
199
+
200
+ # Translate a KCODE value to a charset name
201
+ def kcode_charset
202
+ case $KCODE
203
+ when /^U/i
204
+ "UTF-8"
205
+ when /^S/i
206
+ "Shift-JIS"
207
+ when /^B/i
208
+ "Big5"
209
+ else #when /^A/i, nil
210
+ "ASCII"
211
+ end
212
+ end
213
+
214
+ # @private
215
+ # Content-negotiation priority list that takes into account both
216
+ # assigned priority ("q" value) as well as order, since items
217
+ # that come earlier in an acceptance list have higher priority
218
+ # by fiat.
219
+ class PriorityList
220
+ # Given an acceptance list, create a PriorityList from them.
221
+ def self.build(list)
222
+ new.tap do |plist|
223
+ list.each {|item| plist.add_header_val(item) }
224
+ end
225
+ end
226
+
227
+ include Enumerable
228
+
229
+ # Creates a {PriorityList}.
230
+ # @see PriorityList::build
231
+ def initialize
232
+ @hash = Hash.new {|h,k| h[k] = [] }
233
+ @index = {}
234
+ end
235
+
236
+ # Adds an acceptable item with the given priority to the list.
237
+ # @param [Float] q the priority
238
+ # @param [String] choice the acceptable item
239
+ def add(q, choice)
240
+ @index[choice] = q
241
+ @hash[q] << choice
242
+ end
243
+
244
+ # Given a raw acceptable value from an acceptance header,
245
+ # parse and add it to the list.
246
+ # @param [String] c the raw acceptable item
247
+ # @see #add
248
+ def add_header_val(c)
249
+ if c =~ CONNEG_REGEX
250
+ choice, q = $1, $2
251
+ q = "0" << q if q =~ /^\./ # handle strange FeedBurner Accept
252
+ add(q.to_f,choice)
253
+ else
254
+ add(1.0, c)
255
+ end
256
+ end
257
+
258
+ # @param [Float] q the priority to lookup
259
+ # @return [Array<String>] the list of acceptable items at
260
+ # the given priority
261
+ def [](q)
262
+ @hash[q]
263
+ end
264
+
265
+ # @param [String] choice the acceptable item
266
+ # @return [Float] the priority of that value
267
+ def priority_of(choice)
268
+ @index[choice]
269
+ end
270
+
271
+ # Iterates over the list in priority order, that is, taking
272
+ # into account the order in which items were added as well as
273
+ # their priorities.
274
+ # @yield [q,v]
275
+ # @yieldparam [Float] q the acceptable item's priority
276
+ # @yieldparam [String] v the acceptable item
277
+ def each
278
+ @hash.to_a.sort.reverse_each do |q,l|
279
+ l.each {|v| yield q, v }
280
+ end
281
+ end
282
+ end
283
+
284
+ # Like a {PriorityList}, but for {MediaTypes}, since they have
285
+ # parameters in addition to q.
286
+ # @private
287
+ class MediaTypeList < PriorityList
288
+ include Translation
289
+
290
+ # Overrides {PriorityList#add_header_val} to insert
291
+ # {MediaType} items instead of Strings.
292
+ # @see PriorityList#add_header_val
293
+ def add_header_val(c)
294
+ if mt = MediaType.parse(c)
295
+ q = mt.params.delete('q') || 1.0
296
+ add(q.to_f, mt)
297
+ else
298
+ raise MalformedRequest, t('invalid_media_type', :type => c)
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end