gnarly 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/gnarly/application.rb +105 -0
- data/lib/gnarly/mimeparse.rb +216 -0
- data/lib/gnarly/request.rb +45 -0
- data/lib/gnarly/url.rb +53 -0
- data/lib/gnarly.rb +11 -0
- metadata +66 -0
@@ -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
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
|
+
|