crepe 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+ module Crepe
2
+
3
+ autoload :API, 'crepe/api'
4
+ autoload :Endpoint, 'crepe/endpoint'
5
+ autoload :Middleware, 'crepe/middleware'
6
+ autoload :Params, 'crepe/params'
7
+ autoload :Util, 'crepe/util'
8
+ autoload :VERSION, 'crepe/version'
9
+
10
+ end
@@ -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,10 @@
1
+ module Crepe
2
+ class Endpoint
3
+ module Filter
4
+
5
+ autoload :Acceptance, 'crepe/endpoint/filter/acceptance'
6
+ autoload :Parser, 'crepe/endpoint/filter/parser'
7
+
8
+ end
9
+ end
10
+ 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