webmachine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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