jellyfish 0.9.2 → 1.0.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
@@ -1,13 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: jellyfish 0.9.2 ruby lib
2
+ # stub: jellyfish 1.0.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "jellyfish"
6
- s.version = "0.9.2"
6
+ s.version = "1.0.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.require_paths = ["lib"]
9
10
  s.authors = ["Lin Jen-Shin (godfat)"]
10
- s.date = "2013-09-26"
11
+ s.date = "2014-03-17"
11
12
  s.description = "Pico web framework for building API-centric web applications.\nFor Rack applications or Rack middlewares. Around 250 lines of code."
12
13
  s.email = ["godfat (XD) godfat.org"]
13
14
  s.files = [
@@ -20,10 +21,12 @@ Gem::Specification.new do |s|
20
21
  "README.md",
21
22
  "Rakefile",
22
23
  "TODO.md",
24
+ "config.ru",
23
25
  "jellyfish.gemspec",
24
26
  "jellyfish.png",
25
27
  "lib/jellyfish.rb",
26
28
  "lib/jellyfish/chunked_body.rb",
29
+ "lib/jellyfish/json.rb",
27
30
  "lib/jellyfish/multi_actions.rb",
28
31
  "lib/jellyfish/newrelic.rb",
29
32
  "lib/jellyfish/normalized_params.rb",
@@ -32,9 +35,16 @@ Gem::Specification.new do |s|
32
35
  "lib/jellyfish/public/404.html",
33
36
  "lib/jellyfish/public/500.html",
34
37
  "lib/jellyfish/sinatra.rb",
38
+ "lib/jellyfish/swagger.rb",
35
39
  "lib/jellyfish/test.rb",
36
40
  "lib/jellyfish/version.rb",
37
- "task/.gitignore",
41
+ "public/css/screen.css",
42
+ "public/index.html",
43
+ "public/js/shred.bundle.js",
44
+ "public/js/shred/content.js",
45
+ "public/js/swagger-ui.js",
46
+ "public/js/swagger.js",
47
+ "task/README.md",
38
48
  "task/gemgem.rb",
39
49
  "test/sinatra/test_base.rb",
40
50
  "test/sinatra/test_chunked_body.rb",
@@ -43,11 +53,13 @@ Gem::Specification.new do |s|
43
53
  "test/sinatra/test_routing.rb",
44
54
  "test/test_from_readme.rb",
45
55
  "test/test_inheritance.rb",
56
+ "test/test_log.rb",
57
+ "test/test_misc.rb",
58
+ "test/test_swagger.rb",
46
59
  "test/test_threads.rb"]
47
60
  s.homepage = "https://github.com/godfat/jellyfish"
48
61
  s.licenses = ["Apache License 2.0"]
49
- s.require_paths = ["lib"]
50
- s.rubygems_version = "2.1.5"
62
+ s.rubygems_version = "2.2.2"
51
63
  s.summary = "Pico web framework for building API-centric web applications."
52
64
  s.test_files = [
53
65
  "test/sinatra/test_base.rb",
@@ -57,23 +69,8 @@ Gem::Specification.new do |s|
57
69
  "test/sinatra/test_routing.rb",
58
70
  "test/test_from_readme.rb",
59
71
  "test/test_inheritance.rb",
72
+ "test/test_log.rb",
73
+ "test/test_misc.rb",
74
+ "test/test_swagger.rb",
60
75
  "test/test_threads.rb"]
61
-
62
- if s.respond_to? :specification_version then
63
- s.specification_version = 4
64
-
65
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
66
- s.add_development_dependency(%q<rack>, [">= 0"])
67
- s.add_development_dependency(%q<bacon>, [">= 0"])
68
- s.add_development_dependency(%q<muack>, [">= 0"])
69
- else
70
- s.add_dependency(%q<rack>, [">= 0"])
71
- s.add_dependency(%q<bacon>, [">= 0"])
72
- s.add_dependency(%q<muack>, [">= 0"])
73
- end
74
- else
75
- s.add_dependency(%q<rack>, [">= 0"])
76
- s.add_dependency(%q<bacon>, [">= 0"])
77
- s.add_dependency(%q<muack>, [">= 0"])
78
- end
79
76
  end
data/lib/jellyfish.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  module Jellyfish
3
3
  autoload :VERSION , 'jellyfish/version'
4
4
  autoload :Sinatra , 'jellyfish/sinatra'
5
+ autoload :Swagger , 'jellyfish/swagger'
5
6
  autoload :NewRelic, 'jellyfish/newrelic'
6
7
 
7
8
  autoload :MultiActions , 'jellyfish/multi_actions'
@@ -10,8 +11,8 @@ module Jellyfish
10
11
 
11
12
  autoload :ChunkedBody, 'jellyfish/chunked_body'
12
13
 
14
+ Cascade = Object.new
13
15
  GetValue = Object.new
14
- Identity = lambda{|_|_}
15
16
 
16
17
  class Response < RuntimeError
17
18
  def headers
@@ -49,16 +50,18 @@ module Jellyfish
49
50
 
50
51
  def block_call argument, block
51
52
  val = instance_exec(argument, &block)
52
- [status || 200, headers || {}, body || with_each(val || '')]
53
+ [status || 200, headers || {}, body || rack_body(val || '')]
53
54
  rescue LocalJumpError
54
- jellyfish.log("Use `next' if you're trying to `return' or" \
55
- " `break' from the block.", env['rack.errors'])
55
+ log("Use `next' if you're trying to `return' or `break' from a block.")
56
56
  raise
57
57
  end
58
58
 
59
+ def log message; jellyfish.log( message, env['rack.errors']); end
60
+ def log_error error; jellyfish.log_error(error, env['rack.errors']); end
59
61
  def request ; @request ||= Rack::Request.new(env); end
60
62
  def halt *args; throw(:halt, *args) ; end
61
- def forward ; halt(Jellyfish::NotFound.new) ; end
63
+ def cascade ; halt(Jellyfish::Cascade) ; end
64
+ def not_found ; halt(Jellyfish::NotFound.new) ; end
62
65
  def found url; halt(Jellyfish:: Found.new(url)); end
63
66
  alias_method :redirect, :found
64
67
 
@@ -83,7 +86,7 @@ module Jellyfish
83
86
  elsif value.nil?
84
87
  @body = value
85
88
  else
86
- @body = with_each(value)
89
+ @body = rack_body(value)
87
90
  end
88
91
  end
89
92
 
@@ -97,7 +100,7 @@ module Jellyfish
97
100
 
98
101
  private
99
102
  def actions
100
- routes[request_method.downcase] || halt(Jellyfish::NotFound.new)
103
+ routes[request_method.downcase] || action_missing
101
104
  end
102
105
 
103
106
  def dispatch
@@ -109,11 +112,15 @@ module Jellyfish
109
112
  match = route.match(path_info)
110
113
  break match, block if match
111
114
  end
112
- } || halt(Jellyfish::NotFound.new)
115
+ } || action_missing
113
116
  end
114
117
 
115
- def with_each value
116
- if value.respond_to?(:each) then value else [value] end
118
+ def action_missing
119
+ if jellyfish.app then cascade else not_found end
120
+ end
121
+
122
+ def rack_body v
123
+ if v.respond_to?(:each) || v.respond_to?(:to_path) then v else [v] end
117
124
  end
118
125
  end
119
126
 
@@ -151,10 +158,10 @@ module Jellyfish
151
158
 
152
159
  %w[options get head post put delete patch].each do |method|
153
160
  module_eval <<-RUBY
154
- def #{method} route=//, &block
161
+ def #{method} route=//, meta={}, &block
155
162
  raise TypeError.new("Route \#{route} should respond to :match") \
156
163
  unless route.respond_to?(:match)
157
- (routes['#{method}'] ||= []) << [route, block]
164
+ (routes['#{method}'] ||= []) << [route, block || lambda{|_|}, meta]
158
165
  end
159
166
  RUBY
160
167
  end
@@ -176,12 +183,12 @@ module Jellyfish
176
183
  def call env
177
184
  ctrl = self.class.controller.new(self.class.routes, self)
178
185
  case res = catch(:halt){ ctrl.call(env) }
179
- when NotFound
180
- app && forward(ctrl, env) || handle(ctrl, res)
186
+ when Cascade
187
+ cascade(ctrl, env)
181
188
  when Response
182
189
  handle(ctrl, res, env['rack.errors'])
183
- else
184
- res || ctrl.block_call(nil, Identity)
190
+ else # make sure we return rack triple
191
+ res || ctrl.block_call(nil, lambda{|_|_})
185
192
  end
186
193
  rescue => e
187
194
  handle(ctrl, e, env['rack.errors'])
@@ -198,7 +205,7 @@ module Jellyfish
198
205
  end
199
206
 
200
207
  private
201
- def forward ctrl, env
208
+ def cascade ctrl, env
202
209
  app.call(env)
203
210
  rescue => e
204
211
  handle(ctrl, e, env['rack.errors'])
@@ -0,0 +1,64 @@
1
+
2
+ module Jellyfish; end
3
+ module Jellyfish::Json
4
+ module MultiJson
5
+ def self.extended mod
6
+ mod.const_set(:ParseError, ::MultiJson::DecodeError)
7
+ end
8
+ def encode hash
9
+ ::MultiJson.dump(hash)
10
+ end
11
+ def decode json
12
+ ::MultiJson.load(json)
13
+ end
14
+ end
15
+
16
+ module YajlRuby
17
+ def self.extended mod
18
+ mod.const_set(:ParseError, Yajl::ParseError)
19
+ end
20
+ def encode hash
21
+ Yajl::Encoder.encode(hash)
22
+ end
23
+ def decode json
24
+ Yajl::Parser.parse(json)
25
+ end
26
+ end
27
+
28
+ module Json
29
+ def self.extended mod
30
+ mod.const_set(:ParseError, JSON::ParserError)
31
+ end
32
+ def encode hash
33
+ JSON.dump(hash)
34
+ end
35
+ def decode json
36
+ JSON.parse(json)
37
+ end
38
+ end
39
+
40
+ def self.select_json! mod, picked=false
41
+ if Object.const_defined?(:MultiJson)
42
+ mod.send(:extend, MultiJson)
43
+ elsif Object.const_defined?(:Yajl)
44
+ mod.send(:extend, YajlRuby)
45
+ elsif Object.const_defined?(:JSON)
46
+ mod.send(:extend, Json)
47
+ elsif picked
48
+ raise LoadError.new(
49
+ 'No JSON library found. Tried: multi_json, yajl-ruby, json.')
50
+ else
51
+ # pick a json gem if available
52
+ %w[multi_json yajl json].each{ |json|
53
+ begin
54
+ require json
55
+ break
56
+ rescue LoadError
57
+ end
58
+ }
59
+ select_json!(mod, true)
60
+ end
61
+ end
62
+
63
+ select_json!(self)
64
+ end
@@ -22,7 +22,7 @@ module Jellyfish
22
22
  }.compact
23
23
 
24
24
  if acts.empty?
25
- halt(Jellyfish::NotFound.new)
25
+ action_missing
26
26
  else
27
27
  acts
28
28
  end
@@ -0,0 +1,166 @@
1
+
2
+ require 'jellyfish/json'
3
+
4
+ module Jellyfish
5
+ class Swagger
6
+ include Jellyfish
7
+ controller_include Jellyfish::NormalizedPath, Module.new{
8
+ def dispatch
9
+ headers_merge 'Content-Type' => 'application/json; charset=utf-8'
10
+ super
11
+ end
12
+ }
13
+
14
+ get '/' do
15
+ [Jellyfish::Json.encode(
16
+ :swaggerVersion => '1.2' ,
17
+ :info => jellyfish.swagger_info ,
18
+ :apiVersion => jellyfish.swagger_apiVersion,
19
+ :apis => jellyfish.swagger_apis )]
20
+ end
21
+
22
+ get %r{\A/(?<name>.+)\Z} do |match|
23
+ basePath =
24
+ "#{request.scheme}://#{request.host_with_port}#{jellyfish.path_prefix}"
25
+ name = "/#{match[:name]}"
26
+ apis = jellyfish.jellyfish_apis[name].map{ |path, operations|
27
+ {:path => path, :operations => operations}
28
+ }
29
+
30
+ [Jellyfish::Json.encode(
31
+ :swaggerVersion => '1.2' ,
32
+ :basePath => basePath ,
33
+ :resourcePath => name ,
34
+ :apiVersion => jellyfish.swagger_apiVersion,
35
+ :apis => apis )]
36
+ end
37
+
38
+ # The application should define the API description.
39
+ def swagger_info
40
+ if app.respond_to?(:info)
41
+ app.info
42
+ else
43
+ {}
44
+ end
45
+ end
46
+
47
+ # The application should define the API version.
48
+ def swagger_apiVersion
49
+ if app.respond_to?(:swagger_apiVersion)
50
+ app.swagger_apiVersion
51
+ else
52
+ '0.1.0'
53
+ end
54
+ end
55
+
56
+ def swagger_apis
57
+ @swagger_apis ||= jellyfish_apis.keys.map do |name|
58
+ {:path => name}
59
+ end
60
+ end
61
+
62
+ attr_reader :jellys, :path_prefix
63
+ def initialize path_prefix='', *apps
64
+ super(apps.first)
65
+ @jellys = apps
66
+ @path_prefix = path_prefix
67
+ end
68
+
69
+ def jellyfish_apis
70
+ @jellyfish_apis ||= jellys.flat_map{ |j|
71
+ j.routes.flat_map{ |meth, routes|
72
+ routes.map{ |(path, _, meta)|
73
+ meta.merge(operation(meth, path, meta))
74
+ }
75
+ }
76
+ }.group_by{ |api| api[:path] }.
77
+ inject(Hash.new{[]}){ |r, (path, operations)|
78
+ r[path] = operations.group_by{ |op| op[:nickname] }
79
+ r
80
+ }
81
+ end
82
+
83
+ private
84
+ def operation meth, path, meta
85
+ if path.respond_to?(:source)
86
+ nick = nickname(path)
87
+ {:path => swagger_path(nick) ,
88
+ :method => meth.to_s.upcase ,
89
+ :nickname => nick ,
90
+ :summary => meta[:summary] ,
91
+ :notes => notes(meta) ,
92
+ :parameters => path_parameters(path, meta) }
93
+ else
94
+ {:path => swagger_path(path) ,
95
+ :method => meth.to_s.upcase ,
96
+ :nickname => path ,
97
+ :summary => meta[:summary] ,
98
+ :notes => notes(meta) ,
99
+ :parameters => query_parameters(meta) }
100
+ end
101
+ end
102
+
103
+ def swagger_path nickname
104
+ nickname[%r{^/[^/]*}]
105
+ end
106
+
107
+ def nickname path
108
+ if path.respond_to?(:source)
109
+ path.source.gsub(param_pattern, '{\1}').gsub(/\\\w+|[\^\$]/, '')
110
+ else
111
+ path.to_s
112
+ end
113
+ end
114
+
115
+ def notes meta
116
+ if meta[:notes]
117
+ "#{meta[:summary]}<br>#{meta[:notes]}"
118
+ else
119
+ meta[:summary]
120
+ end
121
+ end
122
+
123
+ def path_parameters path, meta
124
+ Hash[path.source.scan(param_pattern)].map{ |name, pattern|
125
+ path_param(name, pattern, meta)
126
+ }
127
+ end
128
+
129
+ def query_parameters meta
130
+ if meta[:parameters]
131
+ meta[:parameters].map{ |(name, param)|
132
+ param.merge(:name => name.to_s,
133
+ :type => param[:type] || 'string',
134
+ :required => !!param[:required],
135
+ :paramType => param[:paramType] || 'query')
136
+ }
137
+ else
138
+ []
139
+ end
140
+ end
141
+
142
+ def path_param name, pattern, meta
143
+ param = (meta[:parameters] || {})[name.to_sym] || {}
144
+ param.merge(:name => name ,
145
+ :type => param[:type] || param_type(pattern),
146
+ :required => true ,
147
+ :paramType => 'path' )
148
+ end
149
+
150
+ def param_type pattern
151
+ if pattern.start_with?('\\d')
152
+ if pattern.include?('.')
153
+ 'number'
154
+ else
155
+ 'integer'
156
+ end
157
+ else
158
+ 'string'
159
+ end
160
+ end
161
+
162
+ def param_pattern
163
+ /\(\?<(\w+)>([^\)]+)\)/
164
+ end
165
+ end
166
+ end
@@ -13,7 +13,10 @@ shared :jellyfish do
13
13
  app.call({'PATH_INFO' => path ,
14
14
  'REQUEST_METHOD' => '#{method}'.upcase,
15
15
  'SCRIPT_NAME' => '' ,
16
- 'rack.input' => input }.merge(env))
16
+ 'rack.input' => input ,
17
+ 'rack.url_scheme'=> 'https' ,
18
+ 'SERVER_NAME' => 'localhost' ,
19
+ 'SERVER_PORT' => '8080'}.merge(env))
17
20
  end
18
21
  end
19
22
  RUBY