jellyfish 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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.6.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 = "2012-11-02"
10
- s.description = "Pico web framework for building API-centric web applications.\nFor Rack applications or Rack middlewares. Under 200 lines of code."
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 = "1.8.24"
48
+ s.rubygems_version = "2.0.3"
41
49
  s.summary = "Pico web framework for building API-centric web applications."
42
- s.test_files = ["test/sinatra/test_base.rb"]
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 = 3
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 request ; @request ||= Rack::Request.new(env); end
56
- def forward ; raise(Jellyfish::NotFound.new) ; end
57
- def found url; raise(Jellyfish:: Found.new(url)); end
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=nil
66
- if value.nil?
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=nil
76
- if value.nil?
79
+ def body value=GetValue
80
+ if value == GetValue
77
81
  @body
78
- elsif value.respond_to?(:each) # per rack SPEC
82
+ elsif value.nil?
79
83
  @body = value
80
84
  else
81
- @body = [value]
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 handle_exceptions value=nil
118
- if value.nil?
123
+ def handlers; @handlers ||= {}; end
124
+ def handle exception, &block; 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 handle exception, &block; handlers[exception] = block; end
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, &block
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 = self.class.handlers.find{ |klass, block|
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
@@ -1,7 +1,6 @@
1
1
 
2
2
  require 'jellyfish'
3
3
  require 'rack/request'
4
-
5
4
  require 'new_relic/agent/instrumentation/controller_instrumentation'
6
5
 
7
6
  module Jellyfish
@@ -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
@@ -0,0 +1,13 @@
1
+
2
+ require 'jellyfish'
3
+ require 'uri'
4
+
5
+
6
+ module Jellyfish
7
+ module NormalizedPath
8
+ def path_info
9
+ path = URI.decode_www_form_component(super, Encoding.default_external)
10
+ if path.start_with?('/') then path else "/#{path}" end
11
+ end
12
+ end
13
+ end
@@ -1,54 +1,13 @@
1
1
 
2
2
  require 'jellyfish'
3
- require 'rack/request'
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
- attr_reader :params
8
- def block_call argument, block
9
- initialize_params(argument)
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