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