jellyfish 0.9.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -2
- data/.travis.yml +3 -5
- data/CHANGES.md +18 -0
- data/Gemfile +10 -1
- data/README.md +121 -22
- data/Rakefile +6 -15
- data/config.ru +105 -0
- data/jellyfish.gemspec +21 -24
- data/lib/jellyfish.rb +24 -17
- data/lib/jellyfish/json.rb +64 -0
- data/lib/jellyfish/multi_actions.rb +1 -1
- data/lib/jellyfish/swagger.rb +166 -0
- data/lib/jellyfish/test.rb +4 -1
- data/lib/jellyfish/version.rb +1 -1
- data/public/css/screen.css +1070 -0
- data/public/index.html +45 -0
- data/public/js/shred.bundle.js +2765 -0
- data/public/js/shred/content.js +193 -0
- data/public/js/swagger-ui.js +2116 -0
- data/public/js/swagger.js +1400 -0
- data/task/README.md +54 -0
- data/task/gemgem.rb +151 -157
- data/test/sinatra/test_error.rb +31 -4
- data/test/test_from_readme.rb +2 -1
- data/test/test_log.rb +42 -0
- data/test/test_misc.rb +24 -0
- data/test/test_swagger.rb +131 -0
- data/test/test_threads.rb +1 -1
- metadata +25 -52
- data/task/.gitignore +0 -1
data/jellyfish.gemspec
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: jellyfish 0.
|
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.
|
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 = "
|
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
|
-
"
|
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.
|
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 ||
|
53
|
+
[status || 200, headers || {}, body || rack_body(val || '')]
|
53
54
|
rescue LocalJumpError
|
54
|
-
|
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
|
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 =
|
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] ||
|
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
|
-
} ||
|
115
|
+
} || action_missing
|
113
116
|
end
|
114
117
|
|
115
|
-
def
|
116
|
-
if
|
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
|
180
|
-
|
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,
|
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
|
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
|
@@ -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
|
data/lib/jellyfish/test.rb
CHANGED
@@ -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
|
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
|