crepe 0.0.1.pre
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/lib/crepe.rb +10 -0
- data/lib/crepe/api.rb +242 -0
- data/lib/crepe/endpoint.rb +173 -0
- data/lib/crepe/endpoint/filter.rb +10 -0
- data/lib/crepe/endpoint/filter/acceptance.rb +34 -0
- data/lib/crepe/endpoint/filter/parser.rb +43 -0
- data/lib/crepe/endpoint/renderer.rb +19 -0
- data/lib/crepe/endpoint/renderer/base.rb +95 -0
- data/lib/crepe/endpoint/renderer/simple.rb +21 -0
- data/lib/crepe/endpoint/renderer/tilt.rb +87 -0
- data/lib/crepe/endpoint/request.rb +59 -0
- data/lib/crepe/middleware.rb +9 -0
- data/lib/crepe/middleware/content_negotiation.rb +110 -0
- data/lib/crepe/middleware/head.rb +30 -0
- data/lib/crepe/middleware/restful_status.rb +33 -0
- data/lib/crepe/params.rb +74 -0
- data/lib/crepe/util.rb +73 -0
- data/lib/crepe/util/chained_include.rb +78 -0
- data/lib/crepe/util/hash_stack.rb +57 -0
- data/lib/crepe/version.rb +10 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 607eb8356da44e5aff8eed0dd093b1843d361730
|
4
|
+
data.tar.gz: 7f51f2c1953f9d2872dce4d01af9431a62a870ad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f3d1c4851a4dbc373646bf9851290232cd683285c9013d77d89bab68f5d20da780a6197bd83687ba052c51f82fca64baaf8a35a46eb0008510ddfeb9279cf878
|
7
|
+
data.tar.gz: cc943546f40ce8826880abdd2b63bf2d30b72c6847690ae92ceb230c3109d61142a88bd6e1f7ef4465671eeb07059eae07a5684b622fa4c9f6d3937cf58031f1
|
data/lib/crepe.rb
ADDED
data/lib/crepe/api.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'rack/mount'
|
2
|
+
|
3
|
+
module Crepe
|
4
|
+
# The API class provides a DSL to build a collection of endpoints.
|
5
|
+
class API
|
6
|
+
|
7
|
+
# Module class that is instantiated and stores an API's helper methods.
|
8
|
+
# Supports dynamic extensibility via {Util::ChainedInclude}, ensuring that
|
9
|
+
# helpers defined after endpoints are still accessible to those endpoints.
|
10
|
+
class Helper < Module
|
11
|
+
include Util::ChainedInclude
|
12
|
+
end
|
13
|
+
|
14
|
+
METHODS = %w[GET POST PUT PATCH DELETE]
|
15
|
+
|
16
|
+
@running = false
|
17
|
+
|
18
|
+
@config = Util::HashStack.new(
|
19
|
+
endpoint: Endpoint.default_config,
|
20
|
+
helper: Helper.new,
|
21
|
+
middleware: [
|
22
|
+
Rack::Runtime,
|
23
|
+
Middleware::ContentNegotiation,
|
24
|
+
Middleware::RestfulStatus,
|
25
|
+
Middleware::Head,
|
26
|
+
Rack::ConditionalGet,
|
27
|
+
Rack::ETag
|
28
|
+
],
|
29
|
+
namespace: nil,
|
30
|
+
routes: [],
|
31
|
+
vendor: nil,
|
32
|
+
version: nil
|
33
|
+
)
|
34
|
+
|
35
|
+
class << self
|
36
|
+
|
37
|
+
attr_reader :config
|
38
|
+
|
39
|
+
def running?
|
40
|
+
@running
|
41
|
+
end
|
42
|
+
|
43
|
+
def running!
|
44
|
+
@running = true
|
45
|
+
end
|
46
|
+
|
47
|
+
def inherited subclass
|
48
|
+
subclass.config = config.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
def namespace path, options = {}, &block
|
52
|
+
if block
|
53
|
+
config.with namespaced_config(path, options), &block
|
54
|
+
else
|
55
|
+
config[:namespace] = path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias_method :resource, :namespace
|
59
|
+
|
60
|
+
def param name, &block
|
61
|
+
namespace "/:#{name}", &block
|
62
|
+
end
|
63
|
+
|
64
|
+
def vendor vendor
|
65
|
+
config[:endpoint][:vendor] = vendor
|
66
|
+
end
|
67
|
+
|
68
|
+
def version version, &block
|
69
|
+
config.with version: version do
|
70
|
+
namespace version, &block
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def use middleware, *args, &block
|
75
|
+
config[:middleware] << [middleware, args, block]
|
76
|
+
end
|
77
|
+
|
78
|
+
def respond_to *formats
|
79
|
+
config[:endpoint][:formats] = []
|
80
|
+
|
81
|
+
formats.each do |format|
|
82
|
+
if format.respond_to? :each_pair
|
83
|
+
format.each_pair do |f, renderer|
|
84
|
+
config[:endpoint][:formats] << f.to_sym
|
85
|
+
config[:endpoint][:renderers][f.to_sym] = renderer
|
86
|
+
end
|
87
|
+
else
|
88
|
+
config[:endpoint][:formats] << format.to_sym
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def rescue_from exception, options = {}, &block
|
94
|
+
config[:endpoint][:rescuers] << {
|
95
|
+
exception_class: exception, options: options, block: block
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def before_filter mod = nil, &block
|
100
|
+
warn 'block takes precedence over module' if block && mod
|
101
|
+
filter = block || mod
|
102
|
+
config[:endpoint][:before_filters] << filter if filter
|
103
|
+
end
|
104
|
+
|
105
|
+
def after_filter mod = nil, &block
|
106
|
+
warn 'block takes precedence over module' if block && mod
|
107
|
+
filter = block || mod
|
108
|
+
config[:endpoint][:after_filters] << filter if filter
|
109
|
+
end
|
110
|
+
|
111
|
+
def basic_auth *args, &block
|
112
|
+
before_filter do
|
113
|
+
unless instance_exec request.credentials, &block
|
114
|
+
unauthorized!(*args)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def helper mod = nil, &block
|
120
|
+
if block
|
121
|
+
warn 'block takes precedence over module' if mod
|
122
|
+
mod = Module.new(&block)
|
123
|
+
end
|
124
|
+
config[:helper].send :include, mod
|
125
|
+
end
|
126
|
+
|
127
|
+
def call env
|
128
|
+
app.call env
|
129
|
+
end
|
130
|
+
|
131
|
+
METHODS.each do |method|
|
132
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
133
|
+
def #{method.downcase} *args, &block
|
134
|
+
route '#{method}', *args, &block
|
135
|
+
end
|
136
|
+
RUBY
|
137
|
+
end
|
138
|
+
|
139
|
+
def any *args, &block
|
140
|
+
route nil, *args, &block
|
141
|
+
end
|
142
|
+
|
143
|
+
def route method, path = '/', options = {}, &block
|
144
|
+
options = config[:endpoint].merge(handler: block).merge options
|
145
|
+
endpoint = Endpoint.new(options).extend config[:helper]
|
146
|
+
mount endpoint, (options[:conditions] || {}).merge(
|
147
|
+
at: path, method: method, anchor: true
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def mount app, options = nil
|
152
|
+
if options
|
153
|
+
path = options.delete(:at) { '/' }
|
154
|
+
else
|
155
|
+
options = app
|
156
|
+
app, path = options.find { |k, v| k.respond_to? :call }
|
157
|
+
options.delete app if app
|
158
|
+
end
|
159
|
+
|
160
|
+
method = options.delete :method
|
161
|
+
method = %r{#{method.join '|'}}i if method.respond_to? :join
|
162
|
+
|
163
|
+
path_info = mount_path path, options
|
164
|
+
conditions = { path_info: path_info, request_method: method }
|
165
|
+
|
166
|
+
defaults = { format: config[:endpoint][:formats].first }
|
167
|
+
defaults[:version] = config[:version].to_s if config[:version]
|
168
|
+
|
169
|
+
config[:routes] << [app, conditions, defaults]
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
attr_writer :config
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def namespaced_config namespace, options = {}
|
179
|
+
parent_helper = config[:helper]
|
180
|
+
options.merge(
|
181
|
+
namespace: namespace,
|
182
|
+
endpoint: Util.deep_dup(config[:endpoint]),
|
183
|
+
helper: Helper.new { include parent_helper }
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
def app
|
188
|
+
@app ||= begin
|
189
|
+
generate_options_routes!
|
190
|
+
routes = Rack::Mount::RouteSet.new
|
191
|
+
config[:routes].each { |route| routes.add_route(*route) }
|
192
|
+
routes.freeze
|
193
|
+
|
194
|
+
if Crepe::API.running?
|
195
|
+
app = routes
|
196
|
+
else
|
197
|
+
builder = Rack::Builder.new
|
198
|
+
config[:middleware].each do |middleware, args, block|
|
199
|
+
builder.use middleware, *args, &block
|
200
|
+
end
|
201
|
+
builder.run routes
|
202
|
+
app = builder.to_app
|
203
|
+
Crepe::API.running!
|
204
|
+
end
|
205
|
+
|
206
|
+
app
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def mount_path path, conditions
|
211
|
+
return path if path.is_a? Regexp
|
212
|
+
|
213
|
+
namespaces = config.all(:namespace).compact
|
214
|
+
separator = conditions.delete(:separator) { %w[ / . ? ] }
|
215
|
+
anchor = conditions.delete(:anchor) { false }
|
216
|
+
|
217
|
+
path = Util.normalize_path ['/', namespaces, path].join '/'
|
218
|
+
path << '(.:format)' if anchor
|
219
|
+
|
220
|
+
Rack::Mount::Strexp.compile path, conditions, separator, anchor
|
221
|
+
end
|
222
|
+
|
223
|
+
def generate_options_routes!
|
224
|
+
paths = config[:routes].group_by { |_, cond| cond[:path_info] }
|
225
|
+
paths.each do |path, routes|
|
226
|
+
allowed = routes.map { |_, cond| cond[:request_method] }
|
227
|
+
|
228
|
+
route 'OPTIONS', path do
|
229
|
+
headers['Allow'] = allowed.join ', '
|
230
|
+
{ allow: allowed }
|
231
|
+
end
|
232
|
+
route METHODS - allowed, path do
|
233
|
+
headers['Allow'] = allowed.join ', '
|
234
|
+
error! :method_not_allowed, allow: allowed
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'active_support/core_ext/hash/deep_merge'
|
2
|
+
require 'rack/utils'
|
3
|
+
|
4
|
+
module Crepe
|
5
|
+
# A single API endpoint.
|
6
|
+
class Endpoint
|
7
|
+
|
8
|
+
autoload :Filter, 'crepe/endpoint/filter'
|
9
|
+
autoload :Renderer, 'crepe/endpoint/renderer'
|
10
|
+
autoload :Request, 'crepe/endpoint/request'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def default_config
|
15
|
+
{
|
16
|
+
after_filters: [],
|
17
|
+
before_filters: [
|
18
|
+
Filter::Acceptance,
|
19
|
+
Filter::Parser
|
20
|
+
],
|
21
|
+
formats: [:json],
|
22
|
+
handler: nil,
|
23
|
+
renderers: Hash.new(Renderer::Tilt),
|
24
|
+
rescuers: []
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :config
|
31
|
+
|
32
|
+
attr_reader :env
|
33
|
+
|
34
|
+
attr_accessor :body
|
35
|
+
|
36
|
+
attr_reader :io
|
37
|
+
|
38
|
+
def initialize config = {}, &block
|
39
|
+
@config = self.class.default_config.deep_merge config
|
40
|
+
@status = 200
|
41
|
+
|
42
|
+
if block
|
43
|
+
warn 'block takes precedence over handler option' if @config[:handler]
|
44
|
+
@config[:handler] = block
|
45
|
+
end
|
46
|
+
|
47
|
+
if @config[:formats].empty?
|
48
|
+
raise ArgumentError, 'wrong number of formats (at least 1)'
|
49
|
+
end
|
50
|
+
|
51
|
+
@config.freeze
|
52
|
+
end
|
53
|
+
|
54
|
+
def call env
|
55
|
+
clone.call! env
|
56
|
+
end
|
57
|
+
|
58
|
+
def request
|
59
|
+
@request ||= Request.new env
|
60
|
+
end
|
61
|
+
|
62
|
+
def params
|
63
|
+
@params ||= Params.new request.params
|
64
|
+
end
|
65
|
+
|
66
|
+
def format
|
67
|
+
@format ||= params.fetch(:format, config[:formats].first).to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
def status value = nil
|
71
|
+
@status = Rack::Utils.status_code value if value
|
72
|
+
@status
|
73
|
+
end
|
74
|
+
|
75
|
+
def headers
|
76
|
+
@headers ||= {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def content_type
|
80
|
+
@content_type ||= begin
|
81
|
+
extension = format == :json && params[:callback] ? :js : format
|
82
|
+
content_type = Rack::Mime.mime_type ".#{extension}"
|
83
|
+
vendor = config[:vendor]
|
84
|
+
version = params[:version]
|
85
|
+
|
86
|
+
if vendor || version
|
87
|
+
type, subtype = content_type.split '/'
|
88
|
+
content_type = "#{type}/vnd.#{vendor || 'crepe'}"
|
89
|
+
content_type << ".#{version}" if version
|
90
|
+
content_type << "+#{subtype}"
|
91
|
+
end
|
92
|
+
|
93
|
+
content_type
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def render object, options = {}
|
98
|
+
renderer = config[:renderers][format].new self
|
99
|
+
if io
|
100
|
+
io.puts renderer.render object, options
|
101
|
+
else
|
102
|
+
headers['Content-Type'] ||= content_type
|
103
|
+
self.body ||= catch(:head) { renderer.render object, options }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def stream
|
108
|
+
headers['rack.hijack'] = -> io { @io = io and yield }
|
109
|
+
throw :halt, [status, headers, nil]
|
110
|
+
end
|
111
|
+
|
112
|
+
def error! code = :bad_request, message = nil, data = {}
|
113
|
+
throw :halt, error(code, message, data)
|
114
|
+
end
|
115
|
+
|
116
|
+
def unauthorized! message = nil, data = {}
|
117
|
+
data, message = message, nil if message.respond_to? :each_pair
|
118
|
+
realm = data.delete(:realm) { config[:vendor] }
|
119
|
+
headers['WWW-Authenticate'] = %(Basic realm="#{realm}")
|
120
|
+
error! :unauthorized, message || data.delete(:message), data
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
|
125
|
+
def call! env
|
126
|
+
@env = env
|
127
|
+
|
128
|
+
halt = catch :halt do
|
129
|
+
begin
|
130
|
+
config[:before_filters].each { |filter| run_filter filter }
|
131
|
+
render instance_eval(&config[:handler])
|
132
|
+
break
|
133
|
+
rescue => e
|
134
|
+
handle_exception e
|
135
|
+
end
|
136
|
+
end
|
137
|
+
render halt if halt
|
138
|
+
config[:after_filters].each { |filter| run_filter filter }
|
139
|
+
|
140
|
+
[status, headers, [body]]
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def run_filter filter
|
146
|
+
return filter.filter self if filter.respond_to? :filter
|
147
|
+
filter = filter.to_proc if filter.respond_to? :to_proc
|
148
|
+
instance_eval(&filter)
|
149
|
+
end
|
150
|
+
|
151
|
+
def handle_exception exception
|
152
|
+
rescuer = config[:rescuers].find do |r|
|
153
|
+
exception.is_a? r[:exception_class]
|
154
|
+
end
|
155
|
+
|
156
|
+
if rescuer && rescuer[:block]
|
157
|
+
instance_exec exception, &rescuer[:block]
|
158
|
+
else
|
159
|
+
code = rescuer && rescuer[:options][:status] ||
|
160
|
+
:internal_server_error
|
161
|
+
error! code, exception.message, backtrace: exception.backtrace
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def error code, message = nil, data = {}
|
166
|
+
data, message = message, nil if message.respond_to? :each_pair
|
167
|
+
status code
|
168
|
+
message ||= Rack::Utils::HTTP_STATUS_CODES[status]
|
169
|
+
{ error: data.merge(message: message) }
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Crepe
|
2
|
+
class Endpoint
|
3
|
+
module Filter
|
4
|
+
# A default before filter that makes sure an endpoint is capable of
|
5
|
+
# responding with a format acceptable to the request.
|
6
|
+
class Acceptance
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def filter endpoint
|
11
|
+
endpoint.instance_eval do
|
12
|
+
unless config[:formats].include? format
|
13
|
+
@format = config[:formats].first
|
14
|
+
not_acceptable = true
|
15
|
+
end
|
16
|
+
|
17
|
+
if [config[:vendor], env['crepe.vendor']].compact.uniq.length > 1
|
18
|
+
not_acceptable = true
|
19
|
+
end
|
20
|
+
|
21
|
+
if not_acceptable
|
22
|
+
error! :not_acceptable, accepts: config[:formats].map { |f|
|
23
|
+
content_type.sub(/#{format}$/, f.to_s)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|