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