jellyfish 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +0 -1
- data/.travis.yml +1 -0
- data/CHANGES.md +71 -0
- data/README.md +488 -20
- data/jellyfish.gemspec +25 -10
- data/lib/jellyfish.rb +84 -40
- data/lib/jellyfish/chunked_body.rb +20 -0
- data/lib/jellyfish/multi_actions.rb +35 -0
- data/lib/jellyfish/newrelic.rb +0 -1
- data/lib/jellyfish/normalized_params.rb +55 -0
- data/lib/jellyfish/normalized_path.rb +13 -0
- data/lib/jellyfish/sinatra.rb +6 -47
- data/lib/jellyfish/test.rb +7 -2
- data/lib/jellyfish/version.rb +1 -1
- data/task/gemgem.rb +7 -6
- data/test/sinatra/test_base.rb +110 -0
- data/test/sinatra/test_chunked_body.rb +43 -0
- data/test/sinatra/test_error.rb +145 -0
- data/test/sinatra/test_multi_actions.rb +217 -0
- data/test/sinatra/test_routing.rb +425 -0
- data/test/test_from_readme.rb +39 -0
- data/test/test_inheritance.rb +88 -0
- metadata +32 -25
- data/example/config.ru +0 -118
- data/example/rainbows.rb +0 -4
- data/example/server.sh +0 -3
data/jellyfish.gemspec
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "jellyfish"
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "0.8.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Lin Jen-Shin (godfat)"]
|
9
|
-
s.date = "
|
10
|
-
s.description = "Pico web framework for building API-centric web applications.\nFor Rack applications or Rack middlewares.
|
9
|
+
s.date = "2013-06-15"
|
10
|
+
s.description = "Pico web framework for building API-centric web applications.\nFor Rack applications or Rack middlewares. Around 200 lines of code."
|
11
11
|
s.email = ["godfat (XD) godfat.org"]
|
12
12
|
s.files = [
|
13
13
|
".gitignore",
|
@@ -19,13 +19,14 @@ Gem::Specification.new do |s|
|
|
19
19
|
"README.md",
|
20
20
|
"Rakefile",
|
21
21
|
"TODO.md",
|
22
|
-
"example/config.ru",
|
23
|
-
"example/rainbows.rb",
|
24
|
-
"example/server.sh",
|
25
22
|
"jellyfish.gemspec",
|
26
23
|
"jellyfish.png",
|
27
24
|
"lib/jellyfish.rb",
|
25
|
+
"lib/jellyfish/chunked_body.rb",
|
26
|
+
"lib/jellyfish/multi_actions.rb",
|
28
27
|
"lib/jellyfish/newrelic.rb",
|
28
|
+
"lib/jellyfish/normalized_params.rb",
|
29
|
+
"lib/jellyfish/normalized_path.rb",
|
29
30
|
"lib/jellyfish/public/302.html",
|
30
31
|
"lib/jellyfish/public/404.html",
|
31
32
|
"lib/jellyfish/public/500.html",
|
@@ -34,15 +35,29 @@ Gem::Specification.new do |s|
|
|
34
35
|
"lib/jellyfish/version.rb",
|
35
36
|
"task/.gitignore",
|
36
37
|
"task/gemgem.rb",
|
37
|
-
"test/sinatra/test_base.rb"
|
38
|
+
"test/sinatra/test_base.rb",
|
39
|
+
"test/sinatra/test_chunked_body.rb",
|
40
|
+
"test/sinatra/test_error.rb",
|
41
|
+
"test/sinatra/test_multi_actions.rb",
|
42
|
+
"test/sinatra/test_routing.rb",
|
43
|
+
"test/test_from_readme.rb",
|
44
|
+
"test/test_inheritance.rb"]
|
38
45
|
s.homepage = "https://github.com/godfat/jellyfish"
|
46
|
+
s.licenses = ["Apache License 2.0"]
|
39
47
|
s.require_paths = ["lib"]
|
40
|
-
s.rubygems_version = "
|
48
|
+
s.rubygems_version = "2.0.3"
|
41
49
|
s.summary = "Pico web framework for building API-centric web applications."
|
42
|
-
s.test_files = [
|
50
|
+
s.test_files = [
|
51
|
+
"test/sinatra/test_base.rb",
|
52
|
+
"test/sinatra/test_chunked_body.rb",
|
53
|
+
"test/sinatra/test_error.rb",
|
54
|
+
"test/sinatra/test_multi_actions.rb",
|
55
|
+
"test/sinatra/test_routing.rb",
|
56
|
+
"test/test_from_readme.rb",
|
57
|
+
"test/test_inheritance.rb"]
|
43
58
|
|
44
59
|
if s.respond_to? :specification_version then
|
45
|
-
s.specification_version =
|
60
|
+
s.specification_version = 4
|
46
61
|
|
47
62
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
48
63
|
s.add_development_dependency(%q<rack>, [">= 0"])
|
data/lib/jellyfish.rb
CHANGED
@@ -4,6 +4,14 @@ module Jellyfish
|
|
4
4
|
autoload :Sinatra , 'jellyfish/sinatra'
|
5
5
|
autoload :NewRelic, 'jellyfish/newrelic'
|
6
6
|
|
7
|
+
autoload :MultiActions , 'jellyfish/multi_actions'
|
8
|
+
autoload :NormalizedParams, 'jellyfish/normalized_params'
|
9
|
+
autoload :NormalizedPath , 'jellyfish/normalized_path'
|
10
|
+
|
11
|
+
autoload :ChunkedBody, 'jellyfish/chunked_body'
|
12
|
+
|
13
|
+
GetValue = Object.new
|
14
|
+
|
7
15
|
class Response < RuntimeError
|
8
16
|
def headers
|
9
17
|
@headers ||= {'Content-Type' => 'text/html'}
|
@@ -27,34 +35,30 @@ module Jellyfish
|
|
27
35
|
# -----------------------------------------------------------------
|
28
36
|
|
29
37
|
class Controller
|
30
|
-
module Call
|
31
|
-
def call env
|
32
|
-
@env = env
|
33
|
-
block_call(*dispatch)
|
34
|
-
end
|
35
|
-
|
36
|
-
def block_call argument, block
|
37
|
-
ret = instance_exec(argument, &block)
|
38
|
-
body ret if body.nil? # prefer explicitly set values
|
39
|
-
body '' if body.nil? # at least give an empty string
|
40
|
-
[status || 200, headers || {}, body]
|
41
|
-
rescue LocalJumpError
|
42
|
-
jellyfish.log("Use `next' if you're trying to `return' or" \
|
43
|
-
" `break' from the block.", env['rack.errors'])
|
44
|
-
raise
|
45
|
-
end
|
46
|
-
end
|
47
|
-
include Call
|
48
|
-
|
49
38
|
attr_reader :routes, :jellyfish, :env
|
50
39
|
def initialize routes, jellyfish
|
51
40
|
@routes, @jellyfish = routes, jellyfish
|
52
41
|
@status, @headers, @body = nil
|
53
42
|
end
|
54
43
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
44
|
+
def call env
|
45
|
+
@env = env
|
46
|
+
block_call(*dispatch)
|
47
|
+
end
|
48
|
+
|
49
|
+
def block_call argument, block
|
50
|
+
val = instance_exec(argument, &block)
|
51
|
+
[status || 200, headers || {}, body || with_each(val || '')]
|
52
|
+
rescue LocalJumpError
|
53
|
+
jellyfish.log("Use `next' if you're trying to `return' or" \
|
54
|
+
" `break' from the block.", env['rack.errors'])
|
55
|
+
raise
|
56
|
+
end
|
57
|
+
|
58
|
+
def request ; @request ||= Rack::Request.new(env); end
|
59
|
+
def halt *args; throw(:halt, *args) ; end
|
60
|
+
def forward ; raise(Jellyfish::NotFound.new) ; end
|
61
|
+
def found url; raise(Jellyfish:: Found.new(url)); end
|
58
62
|
alias_method :redirect, :found
|
59
63
|
|
60
64
|
def path_info ; env['PATH_INFO'] || '/' ; end
|
@@ -62,8 +66,8 @@ module Jellyfish
|
|
62
66
|
|
63
67
|
%w[status headers].each do |field|
|
64
68
|
module_eval <<-RUBY
|
65
|
-
def #{field} value=
|
66
|
-
if value
|
69
|
+
def #{field} value=GetValue
|
70
|
+
if value == GetValue
|
67
71
|
@#{field}
|
68
72
|
else
|
69
73
|
@#{field} = value
|
@@ -72,13 +76,13 @@ module Jellyfish
|
|
72
76
|
RUBY
|
73
77
|
end
|
74
78
|
|
75
|
-
def body value=
|
76
|
-
if value
|
79
|
+
def body value=GetValue
|
80
|
+
if value == GetValue
|
77
81
|
@body
|
78
|
-
elsif value.
|
82
|
+
elsif value.nil?
|
79
83
|
@body = value
|
80
84
|
else
|
81
|
-
@body =
|
85
|
+
@body = with_each(value)
|
82
86
|
end
|
83
87
|
end
|
84
88
|
|
@@ -106,40 +110,70 @@ module Jellyfish
|
|
106
110
|
end
|
107
111
|
} || raise(Jellyfish::NotFound.new)
|
108
112
|
end
|
113
|
+
|
114
|
+
def with_each value
|
115
|
+
if value.respond_to?(:each) then value else [value] end
|
116
|
+
end
|
109
117
|
end
|
110
118
|
|
111
119
|
# -----------------------------------------------------------------
|
112
120
|
|
113
121
|
module DSL
|
114
|
-
def handlers; @handlers ||= {}; end
|
115
122
|
def routes ; @routes ||= {}; end
|
116
|
-
|
117
|
-
def
|
118
|
-
|
123
|
+
def handlers; @handlers ||= {}; end
|
124
|
+
def handle exception, █ handlers[exception] = block; end
|
125
|
+
def handle_exceptions value=GetValue
|
126
|
+
if value == GetValue
|
119
127
|
@handle_exceptions
|
120
128
|
else
|
121
129
|
@handle_exceptions = value
|
122
130
|
end
|
123
131
|
end
|
124
132
|
|
125
|
-
def
|
133
|
+
def controller_include *value
|
134
|
+
(@controller_include ||= []).push(*value)
|
135
|
+
end
|
136
|
+
|
137
|
+
def controller value=GetValue
|
138
|
+
if value == GetValue
|
139
|
+
@controller ||= controller_inject(
|
140
|
+
const_set(:Controller, Class.new(Controller)))
|
141
|
+
else
|
142
|
+
@controller = controller_inject(value)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def controller_inject value
|
147
|
+
controller_include.
|
148
|
+
inject(value){ |ctrl, mod| ctrl.__send__(:include, mod) }
|
149
|
+
end
|
126
150
|
|
127
151
|
%w[options get head post put delete patch].each do |method|
|
128
152
|
module_eval <<-RUBY
|
129
|
-
def #{method} route
|
153
|
+
def #{method} route=//, &block
|
154
|
+
raise TypeError.new("Route \#{route} should respond to :match") \
|
155
|
+
unless route.respond_to?(:match)
|
130
156
|
(routes['#{method}'] ||= []) << [route, block]
|
131
157
|
end
|
132
158
|
RUBY
|
133
159
|
end
|
160
|
+
|
161
|
+
def inherited sub
|
162
|
+
sub.handle_exceptions(handle_exceptions)
|
163
|
+
sub.controller_include(*controller_include)
|
164
|
+
[:handlers, :routes].each{ |m|
|
165
|
+
val = __send__(m).inject({}){ |r, (k, v)| r[k] = v.dup; r }
|
166
|
+
sub.__send__(m).replace(val) # dup the routing arrays
|
167
|
+
}
|
168
|
+
end
|
134
169
|
end
|
135
170
|
|
136
171
|
# -----------------------------------------------------------------
|
137
172
|
|
138
173
|
def initialize app=nil; @app = app; end
|
139
|
-
def controller ; Controller; end
|
140
174
|
|
141
175
|
def call env
|
142
|
-
ctrl = controller.new(self.class.routes, self)
|
176
|
+
ctrl = self.class.controller.new(self.class.routes, self)
|
143
177
|
ctrl.call(env)
|
144
178
|
rescue NotFound => e # forward
|
145
179
|
if app
|
@@ -167,10 +201,7 @@ module Jellyfish
|
|
167
201
|
|
168
202
|
private
|
169
203
|
def handle ctrl, e, stderr=nil
|
170
|
-
handler =
|
171
|
-
break block if e.kind_of?(klass)
|
172
|
-
}
|
173
|
-
if handler
|
204
|
+
if handler = best_handler(e)
|
174
205
|
ctrl.block_call(e, handler)
|
175
206
|
elsif !self.class.handle_exceptions
|
176
207
|
raise e
|
@@ -182,6 +213,19 @@ module Jellyfish
|
|
182
213
|
end
|
183
214
|
end
|
184
215
|
|
216
|
+
def best_handler e
|
217
|
+
handlers = self.class.handlers
|
218
|
+
if handlers.key?(e.class)
|
219
|
+
handlers[e.class]
|
220
|
+
else # or find the nearest match and cache it
|
221
|
+
ancestors = e.class.ancestors
|
222
|
+
handlers[e.class] = handlers.
|
223
|
+
inject([nil, Float::INFINITY]){ |(handler, val), (klass, block)|
|
224
|
+
idx = ancestors.index(klass) || Float::INFINITY # lower is better
|
225
|
+
if idx < val then [block, idx] else [handler, val] end }.first
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
185
229
|
# -----------------------------------------------------------------
|
186
230
|
|
187
231
|
def self.included mod
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
require 'jellyfish'
|
3
|
+
|
4
|
+
module Jellyfish
|
5
|
+
class ChunkedBody
|
6
|
+
include Enumerable
|
7
|
+
attr_reader :body
|
8
|
+
def initialize &body
|
9
|
+
@body = body
|
10
|
+
end
|
11
|
+
|
12
|
+
def each &block
|
13
|
+
if block
|
14
|
+
body.call(block)
|
15
|
+
else
|
16
|
+
to_enum(:each)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'jellyfish'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
module Jellyfish
|
7
|
+
module MultiActions
|
8
|
+
Identity = lambda{|_|_}
|
9
|
+
|
10
|
+
def call env
|
11
|
+
@env = env
|
12
|
+
catch(:halt){
|
13
|
+
dispatch.inject(nil){ |_, route_block| block_call(*route_block) }
|
14
|
+
} || block_call(nil, Identity) # respond the default if halted
|
15
|
+
end
|
16
|
+
|
17
|
+
def dispatch
|
18
|
+
acts = actions.map{ |(route, block)|
|
19
|
+
case route
|
20
|
+
when String
|
21
|
+
[route, block] if route == path_info
|
22
|
+
else#Regexp, using else allows you to use custom matcher
|
23
|
+
match = route.match(path_info)
|
24
|
+
[match, block] if match
|
25
|
+
end
|
26
|
+
}.compact
|
27
|
+
|
28
|
+
if acts.empty?
|
29
|
+
raise(Jellyfish::NotFound.new)
|
30
|
+
else
|
31
|
+
acts
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/jellyfish/newrelic.rb
CHANGED
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
require 'jellyfish'
|
3
|
+
require 'rack/request'
|
4
|
+
|
5
|
+
|
6
|
+
module Jellyfish
|
7
|
+
module NormalizedParams
|
8
|
+
attr_reader :params
|
9
|
+
def block_call argument, block
|
10
|
+
initialize_params(argument)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def initialize_params argument
|
16
|
+
@params = force_encoding(indifferent_params(
|
17
|
+
if argument.kind_of?(MatchData)
|
18
|
+
# merge captured data from matcher into params as sinatra
|
19
|
+
request.params.merge(Hash[argument.names.zip(argument.captures)])
|
20
|
+
else
|
21
|
+
request.params
|
22
|
+
end))
|
23
|
+
end
|
24
|
+
|
25
|
+
# stolen from sinatra
|
26
|
+
# Enable string or symbol key access to the nested params hash.
|
27
|
+
def indifferent_params(params)
|
28
|
+
params = indifferent_hash.merge(params)
|
29
|
+
params.each do |key, value|
|
30
|
+
next unless value.is_a?(Hash)
|
31
|
+
params[key] = indifferent_params(value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# stolen from sinatra
|
36
|
+
# Creates a Hash with indifferent access.
|
37
|
+
def indifferent_hash
|
38
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
39
|
+
end
|
40
|
+
|
41
|
+
# stolen from sinatra
|
42
|
+
# Fixes encoding issues by casting params to Encoding.default_external
|
43
|
+
def force_encoding(data, encoding=Encoding.default_external)
|
44
|
+
return data if data.respond_to?(:rewind) # e.g. Tempfile, File, etc
|
45
|
+
if data.respond_to?(:force_encoding)
|
46
|
+
data.force_encoding(encoding).encode!
|
47
|
+
elsif data.respond_to?(:each_value)
|
48
|
+
data.each_value{ |v| force_encoding(v, encoding) }
|
49
|
+
elsif data.respond_to?(:each)
|
50
|
+
data.each{ |v| force_encoding(v, encoding) }
|
51
|
+
end
|
52
|
+
data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/jellyfish/sinatra.rb
CHANGED
@@ -1,54 +1,13 @@
|
|
1
1
|
|
2
2
|
require 'jellyfish'
|
3
|
-
require '
|
3
|
+
require 'jellyfish/multi_actions'
|
4
|
+
require 'jellyfish/normalized_params'
|
5
|
+
require 'jellyfish/normalized_path'
|
4
6
|
|
5
7
|
module Jellyfish
|
6
8
|
module Sinatra
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
def initialize_params argument
|
15
|
-
@params ||= force_encoding(indifferent_params(
|
16
|
-
if argument.kind_of?(MatchData)
|
17
|
-
# merge captured data from matcher into params as sinatra
|
18
|
-
request.params.merge(Hash[argument.names.zip(argument.captures)])
|
19
|
-
else
|
20
|
-
request.params
|
21
|
-
end))
|
22
|
-
end
|
23
|
-
|
24
|
-
# stolen from sinatra
|
25
|
-
# Enable string or symbol key access to the nested params hash.
|
26
|
-
def indifferent_params(params)
|
27
|
-
params = indifferent_hash.merge(params)
|
28
|
-
params.each do |key, value|
|
29
|
-
next unless value.is_a?(Hash)
|
30
|
-
params[key] = indifferent_params(value)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# stolen from sinatra
|
35
|
-
# Creates a Hash with indifferent access.
|
36
|
-
def indifferent_hash
|
37
|
-
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
38
|
-
end
|
39
|
-
|
40
|
-
# stolen from sinatra
|
41
|
-
# Fixes encoding issues by casting params to Encoding.default_external
|
42
|
-
def force_encoding(data, encoding=Encoding.default_external)
|
43
|
-
return data if data.respond_to?(:rewind) # e.g. Tempfile, File, etc
|
44
|
-
if data.respond_to?(:force_encoding)
|
45
|
-
data.force_encoding(encoding).encode!
|
46
|
-
elsif data.respond_to?(:each_value)
|
47
|
-
data.each_value{ |v| force_encoding(v, encoding) }
|
48
|
-
elsif data.respond_to?(:each)
|
49
|
-
data.each{ |v| force_encoding(v, encoding) }
|
50
|
-
end
|
51
|
-
data
|
52
|
-
end
|
9
|
+
include MultiActions
|
10
|
+
include NormalizedParams
|
11
|
+
include NormalizedPath
|
53
12
|
end
|
54
13
|
end
|