jellyfish 0.6.0 → 0.8.0
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.
- 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
|