gnarly 0.0.1

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