gnarly 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,105 @@
1
+ require 'gnarly/url.rb'
2
+ require 'gnarly/request.rb'
3
+ require 'gnarly/mimeparse.rb'
4
+
5
+ module Gnarly
6
+
7
+ class Application
8
+
9
+ def initialize()
10
+ @urls = []
11
+ end
12
+
13
+ def url(url_mask, &block)
14
+ url = Url.new(url_mask)
15
+ url.instance_eval &block
16
+ @urls << url
17
+ end
18
+
19
+ def environment()
20
+ @environment
21
+ end
22
+
23
+ def request()
24
+ @request
25
+ end
26
+
27
+ def ok(body, mime=nil)
28
+ headers = mime ? {"Content-type" => mime} : {}
29
+ [200, headers, [body]]
30
+ end
31
+
32
+ def not_found(body="Not Found", mime="text/plain")
33
+ [404, {"Content-type" => mime}, [body]]
34
+ end
35
+
36
+ def method_not_allowed(allow, body="Method Not Allowed", mime="text/plain")
37
+ [405,
38
+ {"Content-type" => mime, "Allow" => allow},
39
+ [body]]
40
+ end
41
+
42
+ def not_acceptable(acceptable=nil, body=nil, mime="text/plain")
43
+ body = "" unless body
44
+ [406,
45
+ {"Content-type" => mime},
46
+ [body]]
47
+ end
48
+
49
+ def internal_server_error(body="Internal Server Error", mime="text/plain")
50
+ [500, {"Content-type" => mime}, [body]]
51
+ end
52
+
53
+ def provides(*mimes, &block)
54
+ mime = MIMEParse.best_match mimes, request.accept
55
+ if mime
56
+ response = block.call mime if block
57
+ if response.is_a? Array and response.size == 3
58
+ status, headers, body = response
59
+ unless headers.key? "Content-type"
60
+ charset = body_charset(body)
61
+ headers["Content-type"] =
62
+ charset ? "#{mime}; charset=#{charset}" : mime
63
+ end
64
+ response
65
+ else
66
+ name, line = @responder.source_location
67
+ msg = "Bad return type for block in #{filename} line #{line}"
68
+ internal_server_error msg
69
+ end
70
+ else
71
+ not_acceptable
72
+ end
73
+ end
74
+
75
+ def call(env)
76
+ @request = Request.new env
77
+ @environment = env
78
+ path = env["PATH_INFO"]
79
+ if url = @urls.detect { |u| u.match? path }
80
+ method = env["REQUEST_METHOD"]
81
+ @responder = url.responder method
82
+ if @responder
83
+ instance_exec *url.parameters, &@responder
84
+ else
85
+ method_not_allowed url.allow
86
+ end
87
+ else
88
+ not_found
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def body_charset(body)
95
+ charset = nil
96
+ body.each do |s|
97
+ charset = s.encoding.to_s
98
+ break
99
+ end
100
+ charset
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,216 @@
1
+ # mimeparse.rb
2
+ #
3
+ # This module provides basic functions for handling mime-types. It can
4
+ # handle matching mime-types against a list of media-ranges. See section
5
+ # 14.1 of the HTTP specification [RFC 2616] for a complete explanation.
6
+ #
7
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
8
+ #
9
+ # ---------
10
+ #
11
+ # This is a port of Joe Gregario's mimeparse.py, which can be found at
12
+ # <http://code.google.com/p/mimeparse/>.
13
+ #
14
+ # ported from version 0.1.2
15
+ #
16
+ # Comments are mostly excerpted from the original.
17
+
18
+ module MIMEParse
19
+ module_function
20
+
21
+ # Carves up a mime-type and returns an Array of the
22
+ # [type, subtype, params] where "params" is a Hash of all
23
+ # the parameters for the media range.
24
+ #
25
+ # For example, the media range "application/xhtml;q=0.5" would
26
+ # get parsed into:
27
+ #
28
+ # ["application", "xhtml", { "q" => "0.5" }]
29
+ def parse_mime_type(mime_type)
30
+ parts = mime_type.split(";")
31
+
32
+ params = {}
33
+
34
+ parts[1..-1].map do |param|
35
+ k,v = param.split("=").map { |s| s.strip }
36
+ params[k] = v
37
+ end
38
+
39
+ full_type = parts[0].strip
40
+ # Java URLConnection class sends an Accept header that includes a single "*"
41
+ # Turn it into a legal wildcard.
42
+ full_type = "*/*" if full_type == "*"
43
+ type, subtype = full_type.split("/")
44
+ raise "malformed mime type" unless subtype
45
+
46
+ [type.strip, subtype.strip, params]
47
+ end
48
+
49
+ # Carves up a media range and returns an Array of the
50
+ # [type, subtype, params] where "params" is a Hash of all
51
+ # the parameters for the media range.
52
+ #
53
+ # For example, the media range "application/*;q=0.5" would
54
+ # get parsed into:
55
+ #
56
+ # ["application", "*", { "q", "0.5" }]
57
+ #
58
+ # In addition this function also guarantees that there
59
+ # is a value for "q" in the params dictionary, filling it
60
+ # in with a proper default if necessary.
61
+ def parse_media_range(range)
62
+ type, subtype, params = parse_mime_type(range)
63
+ unless params.has_key?("q") and params["q"] and params["q"].to_f and params["q"].to_f <= 1 and params["q"].to_f >= 0
64
+ params["q"] = "1"
65
+ end
66
+
67
+ [type, subtype, params]
68
+ end
69
+
70
+ # Find the best match for a given mime-type against a list of
71
+ # media_ranges that have already been parsed by #parse_media_range
72
+ #
73
+ # Returns the fitness and the "q" quality parameter of the best match,
74
+ # or [-1, 0] if no match was found. Just as for #quality_parsed,
75
+ # "parsed_ranges" must be an Enumerable of parsed media ranges.
76
+ def fitness_and_quality_parsed(mime_type, parsed_ranges)
77
+ best_fitness = -1
78
+ best_fit_q = 0
79
+ target_type, target_subtype, target_params = parse_media_range(mime_type)
80
+
81
+ parsed_ranges.each do |type,subtype,params|
82
+ if (type == target_type or type == "*" or target_type == "*") and
83
+ (subtype == target_subtype or subtype == "*" or target_subtype == "*")
84
+ param_matches = target_params.find_all { |k,v| k != "q" and params.has_key?(k) and v == params[k] }.length
85
+
86
+ fitness = (type == target_type) ? 100 : 0
87
+ fitness += (subtype == target_subtype) ? 10 : 0
88
+ fitness += param_matches
89
+
90
+ if fitness > best_fitness
91
+ best_fitness = fitness
92
+ best_fit_q = params["q"]
93
+ end
94
+ end
95
+ end
96
+
97
+ [best_fitness, best_fit_q.to_f]
98
+ end
99
+
100
+ # Find the best match for a given mime-type against a list of
101
+ # media_ranges that have already been parsed by #parse_media_range
102
+ #
103
+ # Returns the "q" quality parameter of the best match, 0 if no match
104
+ # was found. This function behaves the same as #quality except that
105
+ # "parsed_ranges" must be an Enumerable of parsed media ranges.
106
+ def quality_parsed(mime_type, parsed_ranges)
107
+ fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
108
+ end
109
+
110
+ # Returns the quality "q" of a mime_type when compared against
111
+ # the media-ranges in ranges. For example:
112
+ #
113
+ # irb> quality("text/html", "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")
114
+ # => 0.7
115
+ def quality(mime_type, ranges)
116
+ parsed_ranges = ranges.split(",").map { |r| parse_media_range(r) }
117
+ quality_parsed(mime_type, parsed_ranges)
118
+ end
119
+
120
+ # Takes a list of supported mime-types and finds the best match
121
+ # for all the media-ranges listed in header. The value of header
122
+ # must be a string that conforms to the format of the HTTP Accept:
123
+ # header. The value of supported is an Enumerable of mime-types
124
+ #
125
+ # irb> best_match(["application/xbel+xml", "text/xml"], "text/*;q=0.5,*/*; q=0.1")
126
+ # => "text/xml"
127
+ def best_match(supported, header)
128
+ parsed_header = header.split(",").map { |r| parse_media_range(r) }
129
+
130
+ weighted_matches = supported.map do |mime_type|
131
+ [fitness_and_quality_parsed(mime_type, parsed_header), mime_type]
132
+ end
133
+
134
+ weighted_matches.sort!
135
+
136
+ weighted_matches.last[0][1].zero? ? nil : weighted_matches.last[1]
137
+ end
138
+ end
139
+
140
+ if __FILE__ == $0
141
+ require "test/unit"
142
+
143
+ class TestMimeParsing < Test::Unit::TestCase
144
+ include MIMEParse
145
+
146
+ def test_parse_media_range
147
+ assert_equal [ "application", "xml", { "q" => "1" } ],
148
+ parse_media_range("application/xml;q=1")
149
+
150
+ assert_equal [ "application", "xml", { "q" => "1" } ],
151
+ parse_media_range("application/xml")
152
+
153
+ assert_equal [ "application", "xml", { "q" => "1" } ],
154
+ parse_media_range("application/xml;q=")
155
+
156
+ assert_equal [ "application", "xml", { "q" => "1", "b" => "other" } ],
157
+ parse_media_range("application/xml ; q=1;b=other")
158
+
159
+ assert_equal [ "application", "xml", { "q" => "1", "b" => "other" } ],
160
+ parse_media_range("application/xml ; q=2;b=other")
161
+
162
+ # Java URLConnection class sends an Accept header that includes a single "*"
163
+ assert_equal [ "*", "*", { "q" => ".2" } ],
164
+ parse_media_range(" *; q=.2")
165
+ end
166
+
167
+ def test_rfc_2616_example
168
+ accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
169
+
170
+ assert_equal 1, quality("text/html;level=1", accept)
171
+ assert_equal 0.7, quality("text/html", accept)
172
+ assert_equal 0.3, quality("text/plain", accept)
173
+ assert_equal 0.5, quality("image/jpeg", accept)
174
+ assert_equal 0.4, quality("text/html;level=2", accept)
175
+ assert_equal 0.7, quality("text/html;level=3", accept)
176
+ end
177
+
178
+ def test_best_match
179
+ @supported_mime_types = [ "application/xbel+xml", "application/xml" ]
180
+
181
+ # direct match
182
+ assert_best_match "application/xbel+xml", "application/xbel+xml"
183
+ # direct match with a q parameter
184
+ assert_best_match "application/xbel+xml", "application/xbel+xml; q=1"
185
+ # direct match of our second choice with a q parameter
186
+ assert_best_match "application/xml", "application/xml; q=1"
187
+ # match using a subtype wildcard
188
+ assert_best_match "application/xml", "application/*; q=1"
189
+ # match using a type wildcard
190
+ assert_best_match "application/xml", "*/*"
191
+
192
+ @supported_mime_types = [ "application/xbel+xml", "text/xml" ]
193
+ # match using a type versus a lower weighted subtype
194
+ assert_best_match "text/xml", "text/*;q=0.5,*/*;q=0.1"
195
+ # fail to match anything
196
+ assert_best_match nil, "text/html,application/atom+xml; q=0.9"
197
+ # common AJAX scenario
198
+ @supported_mime_types = [ "application/json", "text/html" ]
199
+ assert_best_match "application/json", "application/json, text/javascript, */*"
200
+ # verify fitness sorting
201
+ assert_best_match "application/json", "application/json, text/html;q=0.9"
202
+ end
203
+
204
+ def test_support_wildcards
205
+ @supported_mime_types = ['image/*', 'application/xml']
206
+ # match using a type wildcard
207
+ assert_best_match 'image/*', 'image/png'
208
+ # match using a wildcard for both requested and supported
209
+ assert_best_match 'image/*', 'image/*'
210
+ end
211
+
212
+ def assert_best_match(expected, header)
213
+ assert_equal(expected, best_match(@supported_mime_types, header))
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,45 @@
1
+
2
+ module Gnarly
3
+
4
+ class Request
5
+
6
+ def initialize(env)
7
+ @environment = env
8
+ parse_content_type
9
+ end
10
+
11
+ def accept()
12
+ @accept ||= @environment["HTTP_ACCEPT"]
13
+ end
14
+
15
+ def content_type()
16
+ @content_type
17
+ end
18
+
19
+ def charset()
20
+ @charset
21
+ end
22
+
23
+ def body()
24
+ unless @body
25
+ input = @environment["rack.input"]
26
+ @body = input.read
27
+ input.rewind
28
+ @body.force_encoding charset if charset
29
+ end
30
+ @body
31
+ end
32
+
33
+ private
34
+
35
+ def parse_content_type()
36
+ if header = @environment["CONTENT_TYPE"]
37
+ header =~ /^([^;\s]+)(\s*;\s*charset=(\S+)\s*)$/
38
+ @content_type = $1
39
+ @charset = $3
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
data/lib/gnarly/url.rb ADDED
@@ -0,0 +1,53 @@
1
+
2
+ module Gnarly
3
+
4
+ class Url
5
+
6
+ def initialize(url_mask)
7
+ @url_mask = url_mask
8
+ @responders = {}
9
+ @allow = []
10
+ end
11
+
12
+ def match?(path)
13
+ @match = @url_mask.match path
14
+ @match != nil
15
+ end
16
+
17
+ def parameters()
18
+ a = @match.to_a
19
+ a.shift
20
+ a
21
+ end
22
+
23
+ def responder(method)
24
+ @responders[method]
25
+ end
26
+
27
+ def allow()
28
+ @allow.join " "
29
+ end
30
+
31
+ def get(&block)
32
+ @responders["GET"] = block
33
+ @allow << "GET"
34
+ end
35
+
36
+ def put(&block)
37
+ @responders["PUT"] = block
38
+ @allow << "PUT"
39
+ end
40
+
41
+ def post(&block)
42
+ @responders["POST"] = block
43
+ @allow << "POST"
44
+ end
45
+
46
+ def delete(&block)
47
+ @responders["DELETE"] = block
48
+ @allow << "DELETE"
49
+ end
50
+
51
+ end
52
+
53
+ end
data/lib/gnarly.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'gnarly/application.rb'
2
+
3
+ module Gnarly
4
+
5
+ def self.application(&block)
6
+ app = Application.new
7
+ app.instance_eval &block
8
+ app
9
+ end
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gnarly
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Kim Dalsgaard
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-02-24 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Super simple DSL style web framework on top of Rack. Makes real HTTP'ish REST services easy.
22
+ email: kim@kimdalsgaard.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/gnarly/application.rb
31
+ - lib/gnarly/mimeparse.rb
32
+ - lib/gnarly/request.rb
33
+ - lib/gnarly/url.rb
34
+ - lib/gnarly.rb
35
+ has_rdoc: true
36
+ homepage:
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ segments:
49
+ - 0
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.3.6
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Super simple web framework on top of Rack
65
+ test_files: []
66
+