hanami-api 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fb567b59fdf0ad2b6bc543c20857caed2015b596
4
- data.tar.gz: c337a9fc89f227ca2f001e518a026d6398875796
2
+ SHA256:
3
+ metadata.gz: b78d8afabe0c9e3a00e3d809866366d1961786fab864788d37bb0b4edc0b67c8
4
+ data.tar.gz: 7df6b562a2721f299b9c2a307f546c4e79d72f500b67032ed434907bc880dc09
5
5
  SHA512:
6
- metadata.gz: 8ad8ccecfbc46dcb2ae9e2349f00acbf1a9f653f3ffd23f9b0ee7101c4aa82f7241140448da6c99c7a5888f763edac56ce54af12ad872e0fdcf1b5b62d2da252
7
- data.tar.gz: 4ca503c7ec8016ef8ad947a84119f133b4490727fe1c6f349ba8e2513eab6c629f29f3ed33e9b3aa9a7da3203598903c2763ae804118c502af1572a6fab9a2a6
6
+ metadata.gz: bb8c28fded8d1518c02d286d57d56afff643caab1da5c11e0f357b6dea72e79a4788be00b11169599b692d74fc867244516220785932e3b30895b72077375a77
7
+ data.tar.gz: 82db2bdc48a9416f884d22fb7d3fe9d3bb7f6dab1632416ca0939e6ed66c87f4a4fc81e2a7a0d86da18a9e8a3e418d189a375a78ca8e63b2a7177e7e9a5bb91b
data/.gitignore CHANGED
@@ -1,9 +1,10 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
4
3
  /_yardoc/
5
4
  /coverage/
6
5
  /doc/
7
6
  /pkg/
8
7
  /spec/reports/
9
8
  /tmp/
9
+ Gemfile.lock
10
+ .rubocop-*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,10 @@
1
+ # Please keep AllCops, Bundler, Layout, Style, Metrics groups and then order cops
2
+ # alphabetically
3
+ #
4
+ # References:
5
+ # * https://github.com/bbatsov/ruby-style-guide
6
+ # * https://rubocop.readthedocs.io/
7
+ inherit_from:
8
+ - https://raw.githubusercontent.com/hanami/devtools/master/.rubocop-unstable.yml
9
+ AllCops:
10
+ TargetRubyVersion: 2.7
@@ -0,0 +1,6 @@
1
+ # Hanami::API
2
+ Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
3
+
4
+ ## v0.1.0 - 2020-02-19
5
+ ### Added
6
+ - [Luca Guidi] Introduced `Hanami::API` superclass
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
2
 
3
- # Specify your gem's dependencies in hanami-api.gemspec
3
+ source "https://rubygems.org"
4
4
  gemspec
5
+
6
+ gem "byebug", require: false
7
+ gem "hanami-router", git: "https://github.com/hanami/router.git", branch: "feature/tree-rewrite"
data/README.md CHANGED
@@ -1,28 +1,371 @@
1
- # Hanami::Api
1
+ # Hanami::API
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hanami/api`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
6
4
 
7
5
  ## Installation
8
6
 
9
- Add this line to your application's Gemfile:
7
+ Add this line to your application's `Gemfile`:
10
8
 
11
9
  ```ruby
12
- gem 'hanami-api'
10
+ gem "hanami-api"
13
11
  ```
14
12
 
15
13
  And then execute:
16
14
 
17
- $ bundle
15
+ ```shell
16
+ $ bundle install
17
+ ```
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install hanami-api
21
+ ```shell
22
+ $ gem install hanami-api
23
+ ```
24
+
25
+ ## Performance
26
+
27
+ Benchmark against an app with 10,000 routes, hitting the 10,000th to measure the worst case scenario.
28
+ Based on [`jeremyevans/r10k`](https://github.com/jeremyevans/r10k), `Hanami::API` scores first for speed, and second for memory footprint.
29
+
30
+ ### Runtime
31
+
32
+ Runtime to complete 20,000 requests (lower is better).
33
+
34
+ | Framework | Seconds to complete |
35
+ |------------|---------------------|
36
+ | hanami-api | 0.11628299998119473 |
37
+ | watts | 0.23525599995628 |
38
+ | roda | 0.348202999914065 |
39
+ | syro | 0.355627000099048 |
40
+ | rack-app | 0.6226229998283088 |
41
+ | cuba | 1.2913489998318255 |
42
+ | rails | 17.04722599987872 |
43
+ | synfeld | 171.83788800006732 |
44
+ | sinatra | 197.47695700009353 |
45
+
46
+ ### Memory
47
+
48
+ Memory footprint for 10,000 routes app (lower is better).
49
+
50
+ | Framework | Bytes |
51
+ |------------|--------|
52
+ | roda | 47252 |
53
+ | hanami-api | 53988 |
54
+ | cuba | 55420 |
55
+ | syro | 60256 |
56
+ | rack-app | 82976 |
57
+ | watts | 84956 |
58
+ | sinatra | 124980 |
59
+ | rails | 143048 |
60
+ | synfeld | 172680 |
22
61
 
23
62
  ## Usage
24
63
 
25
- TODO: Write usage instructions here
64
+ Create `config.ru` at the root of your project:
65
+
66
+ ```ruby
67
+ # frozen_string_literal: true
68
+
69
+ require "bundler/setup"
70
+ require "hanami/api"
71
+
72
+ class App < Hanami::API
73
+ get "/" do
74
+ "Hello, world"
75
+ end
76
+ end
77
+
78
+ run App.new
79
+ ```
80
+
81
+ Start the Rack server with `bundle exec rackup`
82
+
83
+ ### Routes
84
+
85
+ A route is a combination of three elements:
86
+
87
+ * HTTP method (e.g. `get`)
88
+ * Path (e.g. `"/"`)
89
+ * Endpoint (e.g. `MyEndpoint.new`)
90
+
91
+ ```ruby
92
+ get "/", to: MyEndpoint.new
93
+ ```
94
+
95
+ ### HTTP methods
96
+
97
+ `Hanami::API` supports the following HTTP methods:
98
+
99
+ * `get`
100
+ * `head`
101
+ * `post`
102
+ * `patch`
103
+ * `put`
104
+ * `options`
105
+ * `trace`
106
+ * `link`
107
+ * `unlink`
108
+
109
+ ### Endpoints
110
+
111
+ `Hanami::API` supports two kind of endpoints: block and Rack.
112
+
113
+ #### Rack endpoint
114
+
115
+ The framework is compatible with Rack. Any Rack endpoint, can be passed to the route:
116
+
117
+ ```ruby
118
+ get "/", to: MyRackEndpoint.new
119
+ ```
120
+
121
+ #### Block endpoint
122
+
123
+ A block passed to the route definition is named a block endpoint.
124
+ The returning value will compose the Rack response. It can be:
125
+
126
+ ##### String
127
+
128
+ ```ruby
129
+ get "/" do
130
+ "Hello, world"
131
+ end
132
+ ```
133
+
134
+ It will return `[200, {}, ["Hello, world"]]`
135
+
136
+ ##### Integer
137
+
138
+ ```ruby
139
+ get "/" do
140
+ 418
141
+ end
142
+ ```
143
+
144
+ It will return `[418, {}, ["I'm a teapot"]]`
145
+
146
+ ##### Integer, String
147
+
148
+ ```ruby
149
+ get "/" do
150
+ [401, "You shall not pass"]
151
+ end
152
+ ```
153
+
154
+ It will return `[401, {}, ["You shall not pass"]]`
155
+
156
+ ##### Integer, Hash, String
157
+
158
+ ```ruby
159
+ get "/" do
160
+ [401, {"X-Custom-Header" => "foo"}, "You shall not pass"]
161
+ end
162
+ ```
163
+
164
+ It will return `[401, {"X-Custom-Header" => "foo"}, ["You shall not pass"]]`
165
+
166
+ ### Block context
167
+
168
+ When using the block syntax there is a rich API to use.
169
+
170
+ #### env
171
+
172
+ The `#env` method exposes the Rack environment for the current request
173
+
174
+ #### status
175
+
176
+ Get HTTP status
177
+
178
+ ```ruby
179
+ get "/" do
180
+ puts status
181
+ # => 200
182
+ end
183
+ ```
184
+
185
+ Set HTTP status
186
+
187
+ ```ruby
188
+ get "/" do
189
+ status(201)
190
+ end
191
+ ```
192
+
193
+ #### headers
194
+
195
+ Get HTTP response headers
196
+
197
+ ```ruby
198
+ get "/" do
199
+ puts headers
200
+ # => {}
201
+ end
202
+ ```
203
+
204
+ Set HTTP status
205
+
206
+ ```ruby
207
+ get "/" do
208
+ headers["X-My-Header"] = "OK"
209
+ end
210
+ ```
211
+
212
+ #### body
213
+
214
+ Get HTTP response body
215
+
216
+ ```ruby
217
+ get "/" do
218
+ puts body
219
+ # => nil
220
+ end
221
+ ```
222
+
223
+ Get HTTP response body
224
+
225
+ ```ruby
226
+ get "/" do
227
+ body "Hello, world"
228
+ end
229
+ ```
230
+
231
+ #### params
232
+
233
+ Access params for current request
234
+
235
+ ```ruby
236
+ get "/" do
237
+ id = params[:id]
238
+ # ...
239
+ end
240
+ ```
241
+
242
+ #### halt
243
+
244
+ Halts the flow of the block and immediately returns with the current HTTP status
245
+
246
+ ```ruby
247
+ get "/authenticate" do
248
+ halt(401)
249
+
250
+ # this code will never be reached
251
+ end
252
+ ```
253
+
254
+ It sets a Rack response: `[401, {}, ["Unauthorized"]]`
255
+
256
+ ```ruby
257
+ get "/authenticate" do
258
+ halt(401, "You shall not pass")
259
+
260
+ # this code will never be reached
261
+ end
262
+ ```
263
+
264
+ It sets a Rack response: `[401, {}, ["You shall not pass"]]`
265
+
266
+ #### redirect
267
+
268
+ Redirects request and immediately halts it
269
+
270
+ ```ruby
271
+ get "/legacy" do
272
+ redirect "/dashboard"
273
+
274
+ # this code will never be reached
275
+ end
276
+ ```
277
+
278
+ It sets a Rack response: `[301, {"Location" => "/new"}, ["Moved Permanently"]]`
279
+
280
+ ```ruby
281
+ get "/legacy" do
282
+ redirect "/dashboard", 302
283
+
284
+ # this code will never be reached
285
+ end
286
+ ```
287
+
288
+ It sets a Rack response: `[302, {"Location" => "/new"}, ["Moved"]]`
289
+
290
+ #### back
291
+
292
+ Utility for redirect back using HTTP request header `HTTP_REFERER`
293
+
294
+ ```ruby
295
+ get "/authenticate" do
296
+ if authenticate(env)
297
+ redirect back
298
+ else
299
+ # ...
300
+ end
301
+ end
302
+ ```
303
+
304
+ #### json
305
+
306
+ Sets a JSON response for the given object
307
+
308
+ ```ruby
309
+ get "/user/:id" do
310
+ user = UserRepository.new.find(params[:id])
311
+ json(user)
312
+ end
313
+ ```
314
+
315
+ ```ruby
316
+ get "/user/:id" do
317
+ user = UserRepository.new.find(params[:id])
318
+ json(user, "application/vnd.api+json")
319
+ end
320
+ ```
321
+
322
+ ### Scope
323
+
324
+ Prefixing routes is possible with routing scopes:
325
+
326
+ ```ruby
327
+ scope "api" do
328
+ scope "v1" do
329
+ get "/users", to: Actions::V1::Users::Index.new
330
+ end
331
+ end
332
+ ```
333
+
334
+ It will generate a route with `"/api/v1/users"` as path.
335
+
336
+ ### Rack Middleware
337
+
338
+ To mount a Rack middleware it's possible with `.use`
339
+
340
+ ```ruby
341
+ # frozen_string_literal: true
342
+
343
+ require "bundler/setup"
344
+ require "hanami/api"
345
+
346
+ class App < Hanami::API
347
+ use ElapsedTime
348
+
349
+ scope "api" do
350
+ use ApiAuthentication
351
+
352
+ scope "v1" do
353
+ use ApiV1Deprecation
354
+ end
355
+
356
+ scope "v2" do
357
+ # ...
358
+ end
359
+ end
360
+ end
361
+ ```
362
+
363
+ Middleware are inherited from top level scope.
364
+
365
+ In the example above, `ElapsedTime` is used for each incoming request because
366
+ it's part of the top level scope. `ApiAuthentication` it's used for all the API
367
+ versions, because it's defined in the `"api"` scope. `ApiV1Deprecation` is used
368
+ only by the routes in `"v1"` scope, but not by `"v2"`.
26
369
 
27
370
  ## Development
28
371
 
@@ -32,5 +375,5 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
375
 
33
376
  ## Contributing
34
377
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/jodosha/hanami-api.
378
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hanami/api.
36
379
 
data/Rakefile CHANGED
@@ -1,2 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
1
4
  require "bundler/gem_tasks"
2
- task :default => :spec
5
+ require "rspec/core/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ file_list = FileList["spec/**/*_spec.rb"]
9
+
10
+ task.pattern = file_list
11
+ end
12
+
13
+ task default: "spec"
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "hanami/api"
@@ -1,27 +1,37 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'hanami/api/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/hanami/api/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "hanami-api"
8
- spec.version = Hanami::Api::VERSION
7
+ spec.version = Hanami::API::VERSION
9
8
  spec.authors = ["Luca Guidi"]
10
9
  spec.email = ["me@lucaguidi.com"]
11
10
 
12
11
  spec.summary = "Hanami API"
13
- spec.description = "Hanami API for fast, lightweight applications"
14
- spec.homepage = "http://hanamirb.org"
12
+ spec.description = "Extremely fast and lightweight HTTP API"
13
+ spec.homepage = "http://rubygems.org"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
17
 
16
- spec.metadata['allowed_push_host'] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/hanami/api"
20
+ spec.metadata["changelog_uri"] = "https://github.com/hanami/api/blob/master/CHANGELOG.md"
17
21
 
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
26
  end
27
+
21
28
  spec.bindir = "exe"
22
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
30
  spec.require_paths = ["lib"]
24
31
 
25
- spec.add_development_dependency "bundler", "~> 1.14"
26
- spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_dependency "hanami-router", "~> 2.0.alpha"
33
+
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rspec", "~> 3.8"
36
+ spec.add_development_dependency "rubocop", "~> 0.79"
27
37
  end
@@ -1,7 +1,345 @@
1
- require "hanami/api/version"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Hanami
4
- module Api
5
- # Your code goes here...
4
+ # Hanami::API
5
+ #
6
+ # @since 0.1.0
7
+ class API
8
+ require "hanami/api/version"
9
+ require "hanami/api/error"
10
+ require "hanami/api/router"
11
+ require "hanami/api/middleware"
12
+
13
+ # @since 0.1.0
14
+ # @api private
15
+ def self.inherited(app)
16
+ super
17
+
18
+ app.class_eval do
19
+ @routes = []
20
+ @stack = Middleware::Stack.new
21
+ end
22
+ end
23
+
24
+ class << self
25
+ # @since 0.1.0
26
+ # @api private
27
+ attr_reader :routes
28
+
29
+ # @since 0.1.0
30
+ # @api private
31
+ attr_reader :stack
32
+ end
33
+
34
+ # Defines a named root route (a GET route for "/")
35
+ #
36
+ # @param to [#call] the Rack endpoint
37
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
38
+ #
39
+ # @since 0.1.0
40
+ #
41
+ # @see .get
42
+ #
43
+ # @example Proc endpoint
44
+ # require "hanami/router"
45
+ #
46
+ # router = Hanami::Router.new do
47
+ # root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
48
+ # end
49
+ #
50
+ # @example Block endpoint
51
+ # require "hanami/router"
52
+ #
53
+ # router = Hanami::Router.new do
54
+ # root do
55
+ # "Hello from Hanami!"
56
+ # end
57
+ # end
58
+ def self.root(*args, **kwargs, &blk)
59
+ @routes << [:root, args, kwargs, blk]
60
+ end
61
+
62
+ # Defines a route that accepts GET requests for the given path.
63
+ # It also defines a route to accept HEAD requests.
64
+ #
65
+ # @param path [String] the relative URL to be matched
66
+ # @param to [#call] the Rack endpoint
67
+ # @param as [Symbol] a unique name for the route
68
+ # @param constraints [Hash] a set of constraints for path variables
69
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
70
+ #
71
+ # @since 0.1.0
72
+ #
73
+ # @example Proc endpoint
74
+ # require "hanami/api"
75
+ #
76
+ # class MyAPI < Hanami::API
77
+ # get "/", to: ->(*) { [200, {}, ["OK"]] }
78
+ # end
79
+ #
80
+ # @example Block endpoint
81
+ # require "hanami/api"
82
+ #
83
+ # class MyAPI < Hanami::API
84
+ # get "/" do
85
+ # "OK"
86
+ # end
87
+ # end
88
+ #
89
+ # @example Constraints
90
+ # require "hanami/api"
91
+ #
92
+ # class MyAPI < Hanami::API
93
+ # get "/users/:id", to: ->(*) { [200, {}, ["OK"]] }, id: /\d+/
94
+ # end
95
+ def self.get(*args, **kwargs, &blk)
96
+ @routes << [:get, args, kwargs, blk]
97
+ end
98
+
99
+ # Defines a route that accepts POST requests for the given path.
100
+ #
101
+ # @param path [String] the relative URL to be matched
102
+ # @param to [#call] the Rack endpoint
103
+ # @param as [Symbol] a unique name for the route
104
+ # @param constraints [Hash] a set of constraints for path variables
105
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
106
+ #
107
+ # @since 0.1.0
108
+ #
109
+ # @see .get
110
+ def self.post(*args, **kwargs, &blk)
111
+ @routes << [:post, args, kwargs, blk]
112
+ end
113
+
114
+ # Defines a route that accepts PATCH requests for the given path.
115
+ #
116
+ # @param path [String] the relative URL to be matched
117
+ # @param to [#call] the Rack endpoint
118
+ # @param as [Symbol] a unique name for the route
119
+ # @param constraints [Hash] a set of constraints for path variables
120
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
121
+ #
122
+ # @since 0.1.0
123
+ #
124
+ # @see .get
125
+ def self.patch(*args, **kwargs, &blk)
126
+ @routes << [:patch, args, kwargs, blk]
127
+ end
128
+
129
+ # Defines a route that accepts PUT requests for the given path.
130
+ #
131
+ # @param path [String] the relative URL to be matched
132
+ # @param to [#call] the Rack endpoint
133
+ # @param as [Symbol] a unique name for the route
134
+ # @param constraints [Hash] a set of constraints for path variables
135
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
136
+ #
137
+ # @since 0.1.0
138
+ #
139
+ # @see .get
140
+ def self.put(*args, **kwargs, &blk)
141
+ @routes << [:put, args, kwargs, blk]
142
+ end
143
+
144
+ # Defines a route that accepts DELETE requests for the given path.
145
+ #
146
+ # @param path [String] the relative URL to be matched
147
+ # @param to [#call] the Rack endpoint
148
+ # @param as [Symbol] a unique name for the route
149
+ # @param constraints [Hash] a set of constraints for path variables
150
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
151
+ #
152
+ # @since 0.1.0
153
+ #
154
+ # @see .get
155
+ def self.delete(*args, **kwargs, &blk)
156
+ @routes << [:delete, args, kwargs, blk]
157
+ end
158
+
159
+ # Defines a route that accepts TRACE requests for the given path.
160
+ #
161
+ # @param path [String] the relative URL to be matched
162
+ # @param to [#call] the Rack endpoint
163
+ # @param as [Symbol] a unique name for the route
164
+ # @param constraints [Hash] a set of constraints for path variables
165
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
166
+ #
167
+ # @since 0.1.0
168
+ #
169
+ # @see .get
170
+ def self.trace(*args, **kwargs, &blk)
171
+ @routes << [:trace, args, kwargs, blk]
172
+ end
173
+
174
+ # Defines a route that accepts OPTIONS requests for the given path.
175
+ #
176
+ # @param path [String] the relative URL to be matched
177
+ # @param to [#call] the Rack endpoint
178
+ # @param as [Symbol] a unique name for the route
179
+ # @param constraints [Hash] a set of constraints for path variables
180
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
181
+ #
182
+ # @since 0.1.0
183
+ #
184
+ # @see .get
185
+ def self.options(*args, **kwargs, &blk)
186
+ @routes << [:options, args, kwargs, blk]
187
+ end
188
+
189
+ # Defines a route that accepts LINK requests for the given path.
190
+ #
191
+ # @param path [String] the relative URL to be matched
192
+ # @param to [#call] the Rack endpoint
193
+ # @param as [Symbol] a unique name for the route
194
+ # @param constraints [Hash] a set of constraints for path variables
195
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
196
+ #
197
+ # @since 0.1.0
198
+ #
199
+ # @see .get
200
+ def self.link(*args, **kwargs, &blk)
201
+ @routes << [:link, args, kwargs, blk]
202
+ end
203
+
204
+ # Defines a route that accepts UNLINK requests for the given path.
205
+ #
206
+ # @param path [String] the relative URL to be matched
207
+ # @param to [#call] the Rack endpoint
208
+ # @param as [Symbol] a unique name for the route
209
+ # @param constraints [Hash] a set of constraints for path variables
210
+ # @param blk [Proc] the anonymous proc to be used as endpoint for the route
211
+ #
212
+ # @since 0.1.0
213
+ #
214
+ # @see .get
215
+ def self.unlink(*args, **kwargs, &blk)
216
+ @routes << [:unlink, args, kwargs, blk]
217
+ end
218
+
219
+ # Defines a route that redirects the incoming request to another path.
220
+ #
221
+ # @param path [String] the relative URL to be matched
222
+ # @param to [#call] the Rack endpoint
223
+ # @param as [Symbol] a unique name for the route
224
+ # @param code [Integer] a HTTP status code to use for the redirect
225
+ #
226
+ # @since 0.1.0
227
+ #
228
+ # @see .get
229
+ def self.redirect(*args, **kwargs, &blk)
230
+ @routes << [:redirect, args, kwargs, blk]
231
+ end
232
+
233
+ # Defines a routing scope. Routes defined in the context of a scope,
234
+ # inherit the given path as path prefix and as a named routes prefix.
235
+ #
236
+ # @param path [String] the scope path to be used as a path prefix
237
+ # @param blk [Proc] the routes definitions withing the scope
238
+ #
239
+ # @since 0.1.0
240
+ #
241
+ # @see #path
242
+ #
243
+ # @example
244
+ # require "hanami/api"
245
+ #
246
+ # class MyAPI < Hanami::API
247
+ # scope "v1" do
248
+ # get "/users", to: ->(*) { ... }, as: :users
249
+ # end
250
+ # end
251
+ #
252
+ # # It generates a route with a path `/v1/users`
253
+ def self.scope(*args, **kwargs, &blk)
254
+ @routes << [:scope, args, kwargs, blk]
255
+ end
256
+
257
+ # Mount a Rack application at the specified path.
258
+ # All the requests starting with the specified path, will be forwarded to
259
+ # the given application.
260
+ #
261
+ # All the other methods (eg `#get`) support callable objects, but they
262
+ # restrict the range of the acceptable HTTP verb. Mounting an application
263
+ # with #mount doesn't apply this kind of restriction at the router level,
264
+ # but let the application to decide.
265
+ #
266
+ # @param app [#call] a class or an object that responds to #call
267
+ # @param at [String] the relative path where to mount the app
268
+ # @param constraints [Hash] a set of constraints for path variables
269
+ #
270
+ # @since 0.1.0
271
+ #
272
+ # @example
273
+ # require "hanami/api"
274
+ #
275
+ # class MyAPI < Hanami::API
276
+ # mount MyRackApp.new, at: "/foo"
277
+ # end
278
+ def self.mount(*args, **kwargs, &blk)
279
+ @routes << [:mount, args, kwargs, blk]
280
+ end
281
+
282
+ # Use a Rack middleware
283
+ #
284
+ # @param middleware [Class,#call] a Rack middleware
285
+ # @param args [Array<Object>] an optional array of arguments for Rack middleware
286
+ # @param blk [Block] an optional block to pass to the Rack middleware
287
+ #
288
+ # @since 0.1.0
289
+ #
290
+ # @example
291
+ # require "hanami/api"
292
+ #
293
+ # class MyAPI < Hanami::API
294
+ # use MyRackMiddleware
295
+ # end
296
+ def self.use(middleware, *args, &blk)
297
+ @stack.use(middleware, args, &blk)
298
+ end
299
+
300
+ # @since 0.1.0
301
+ def initialize(routes: self.class.routes, stack: self.class.stack)
302
+ @stack = stack
303
+ @router = Router.new(stack: @stack) do
304
+ routes.each do |method_name, args, kwargs, blk|
305
+ send(method_name, *args, **kwargs, &blk)
306
+ end
307
+ end
308
+
309
+ freeze
310
+ end
311
+
312
+ # @since 0.1.0
313
+ def freeze
314
+ @app = @stack.finalize(@router)
315
+ @url_helpers = @router.url_helpers
316
+ @router.remove_instance_variable(:@url_helpers)
317
+ remove_instance_variable(:@stack)
318
+ remove_instance_variable(:@router)
319
+ @url_helpers.freeze
320
+ @app.freeze
321
+ super
322
+ end
323
+
324
+ # @since 0.1.0
325
+ def call(env)
326
+ @app.call(env)
327
+ end
328
+
329
+ # TODO: verify if needed here on in block context
330
+ #
331
+ # @since 0.1.0
332
+ # @api private
333
+ def path(name, variables = {})
334
+ @url_helpers.path(name, variables)
335
+ end
336
+
337
+ # TODO: verify if needed here on in block context
338
+ #
339
+ # @since 0.1.0
340
+ # @api private
341
+ def url(name, variables = {})
342
+ @url_helpers.url(name, variables)
343
+ end
6
344
  end
7
345
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/router/block"
4
+ require "rack/utils"
5
+ require "json"
6
+
7
+ module Hanami
8
+ class API
9
+ module Block
10
+ class Context < Hanami::Router::Block::Context
11
+ # @overload body
12
+ # Gets the current HTTP response body
13
+ # @return [String] the HTTP body
14
+ # @overload body(value)
15
+ # Sets the HTTP body
16
+ # @param value [String] the HTTP response body
17
+ #
18
+ # @since 0.1.0
19
+ def body(value = nil)
20
+ if value
21
+ @body = value
22
+ else
23
+ @body
24
+ end
25
+ end
26
+
27
+ # Halts the flow of the block and immediately returns with the current
28
+ # HTTP status
29
+ #
30
+ # @param status [Integer] a valid HTTP status code
31
+ # @param body [String] an optional HTTP response body
32
+ #
33
+ # @example HTTP Status
34
+ # get "/authenticate" do
35
+ # halt(401)
36
+ #
37
+ # # this code will never be reached
38
+ # end
39
+ #
40
+ # # It sets a Rack response: [401, {}, ["Unauthorized"]]
41
+ #
42
+ # @example HTTP Status and body
43
+ # get "/authenticate" do
44
+ # halt(401, "You shall not pass")
45
+ #
46
+ # # this code will never be reached
47
+ # end
48
+ #
49
+ # # It sets a Rack response: [401, {}, ["You shall not pass"]]
50
+ #
51
+ # @since 0.1.0
52
+ def halt(status, body = nil)
53
+ body ||= http_status(status)
54
+ throw :halt, [status, body]
55
+ end
56
+
57
+ # Redirects request and immediately halts it
58
+ #
59
+ # @param url [String] the destination URL
60
+ # @param status [Integer] an optional HTTP code for the redirect
61
+ #
62
+ # @see #halt
63
+ #
64
+ # @since 0.1.0
65
+ #
66
+ # @example URL
67
+ # get "/legacy" do
68
+ # redirect "/dashboard"
69
+ #
70
+ # # this code will never be reached
71
+ # end
72
+ #
73
+ # # It sets a Rack response: [301, {"Location" => "/new"}, ["Moved Permanently"]]
74
+ #
75
+ # @example URL and HTTP status
76
+ # get "/legacy" do
77
+ # redirect "/dashboard", 302
78
+ #
79
+ # # this code will never be reached
80
+ # end
81
+ #
82
+ # # It sets a Rack response: [302, {"Location" => "/new"}, ["Moved"]]
83
+ def redirect(url, status = 301)
84
+ headers["Location"] = url
85
+ halt(status)
86
+ end
87
+
88
+ # Utility for redirect back using HTTP request header `HTTP_REFERER`
89
+ #
90
+ # @since 0.1.0
91
+ #
92
+ # @example
93
+ # get "/authenticate" do
94
+ # if authenticate(env)
95
+ # redirect back
96
+ # else
97
+ # # ...
98
+ # end
99
+ # end
100
+ def back
101
+ env["HTTP_REFERER"] || "/"
102
+ end
103
+
104
+ # Sets a JSON response for the given object
105
+ #
106
+ # @param object [Object] a JSON serializable object
107
+ # @param mime [String] optional MIME type to set for the response
108
+ #
109
+ # @since 0.1.0
110
+ #
111
+ # @example JSON serializable object
112
+ # get "/user/:id" do
113
+ # user = UserRepository.new.find(params[:id])
114
+ # json(user)
115
+ # end
116
+ #
117
+ # @example JSON serializable object and custom MIME type
118
+ # get "/user/:id" do
119
+ # user = UserRepository.new.find(params[:id])
120
+ # json(user, "application/vnd.api+json")
121
+ # end
122
+ def json(object, mime = "application/json")
123
+ headers["Content-Type"] = mime
124
+ JSON.generate(object)
125
+ end
126
+
127
+ # @since 0.1.0
128
+ # @api private
129
+ def call
130
+ case caught
131
+ in String => body
132
+ [status, headers, [body]]
133
+ in Integer => status
134
+ [status, headers, [self.body || http_status(status)]]
135
+ in [Integer, String] => response
136
+ [response[0], headers, [response[1]]]
137
+ in [Integer, Hash, String] => response
138
+ headers.merge!(response[1])
139
+ [response[0], headers, [response[2]]]
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ # @since 0.1.0
146
+ # @api private
147
+ def caught
148
+ catch :halt do
149
+ instance_exec(&@blk)
150
+ end
151
+ end
152
+
153
+ # @since 0.1.0
154
+ # @api private
155
+ def http_status(code)
156
+ Rack::Utils::HTTP_STATUS_CODES.fetch(code)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class API
5
+ # @since 0.1.0
6
+ class Error < StandardError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/builder"
4
+
5
+ module Hanami
6
+ class API
7
+ # Hanami::API middleware stack
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ module Middleware
12
+ # Middleware stack
13
+ #
14
+ # @since 0.1.0
15
+ # @api private
16
+ class Stack
17
+ # @since 0.1.0
18
+ # @api private
19
+ ROOT_PREFIX = "/"
20
+ private_constant :ROOT_PREFIX
21
+
22
+ # @since 0.1.0
23
+ # @api private
24
+ def initialize
25
+ @prefix = ROOT_PREFIX
26
+ @stack = Hash.new { |hash, key| hash[key] = [] }
27
+ end
28
+
29
+ # @since 0.1.0
30
+ # @api private
31
+ def use(middleware, args, &blk)
32
+ @stack[@prefix].push([middleware, args, blk])
33
+ end
34
+
35
+ # @since 0.1.0
36
+ # @api private
37
+ def with(path)
38
+ prefix = @prefix
39
+ @prefix = path
40
+ yield
41
+ ensure
42
+ @prefix = prefix
43
+ end
44
+
45
+ # @since 0.1.0
46
+ # @api private
47
+ def finalize(app) # rubocop:disable Metrics/MethodLength
48
+ uniq!
49
+ return app if @stack.empty?
50
+
51
+ s = self
52
+
53
+ Rack::Builder.new do
54
+ s.each do |prefix, stack|
55
+ s.mapped(self, prefix) do
56
+ stack.each do |middleware, args, blk|
57
+ use(middleware, *args, &blk)
58
+ end
59
+ end
60
+
61
+ run app
62
+ end
63
+ end
64
+ end
65
+
66
+ # @since 0.1.0
67
+ # @api private
68
+ def each(&blk)
69
+ uniq!
70
+ @stack.each(&blk)
71
+ end
72
+
73
+ # @since 0.1.0
74
+ # @api private
75
+ def mapped(builder, prefix, &blk)
76
+ if prefix == ROOT_PREFIX
77
+ builder.instance_eval(&blk)
78
+ else
79
+ builder.map(prefix, &blk)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # @since 0.1.0
86
+ # @api private
87
+ def uniq!
88
+ @stack.each_value(&:uniq!)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/router"
4
+ require "hanami/api/block/context"
5
+
6
+ module Hanami
7
+ class API
8
+ # @since 0.1.0
9
+ class Router < ::Hanami::Router
10
+ # @since 0.1.0
11
+ # @api private
12
+ def initialize(stack:, **kwargs, &blk)
13
+ @stack = stack
14
+ super(block_context: Block::Context, **kwargs, &blk)
15
+ end
16
+
17
+ # @since 0.1.0
18
+ # @api private
19
+ def freeze
20
+ return self if frozen?
21
+
22
+ remove_instance_variable(:@stack)
23
+ super
24
+ end
25
+
26
+ # @since 0.1.0
27
+ # @api private
28
+ def use(middleware, *args, &blk)
29
+ @stack.use(middleware, args, &blk)
30
+ end
31
+
32
+ # @since 0.1.0
33
+ # @api private
34
+ def scope(*args, **kwargs, &blk)
35
+ @stack.with(args.first) do
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hanami
2
- module Api
3
- VERSION = "0.0.0"
4
+ class API
5
+ # @since 0.1.0
6
+ VERSION = "0.1.0"
4
7
  end
5
8
  end
metadata CHANGED
@@ -1,44 +1,72 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-04 00:00:00.000000000 Z
11
+ date: 2020-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: hanami-router
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.14'
20
- type: :development
19
+ version: 2.0.alpha
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.14'
26
+ version: 2.0.alpha
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.8'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '10.0'
41
- description: Hanami API for fast, lightweight applications
54
+ version: '3.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.79'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.79'
69
+ description: Extremely fast and lightweight HTTP API
42
70
  email:
43
71
  - me@lucaguidi.com
44
72
  executables: []
@@ -46,6 +74,9 @@ extensions: []
46
74
  extra_rdoc_files: []
47
75
  files:
48
76
  - ".gitignore"
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - CHANGELOG.md
49
80
  - Gemfile
50
81
  - README.md
51
82
  - Rakefile
@@ -53,11 +84,18 @@ files:
53
84
  - bin/setup
54
85
  - hanami-api.gemspec
55
86
  - lib/hanami/api.rb
87
+ - lib/hanami/api/block/context.rb
88
+ - lib/hanami/api/error.rb
89
+ - lib/hanami/api/middleware.rb
90
+ - lib/hanami/api/router.rb
56
91
  - lib/hanami/api/version.rb
57
- homepage: http://hanamirb.org
92
+ homepage: http://rubygems.org
58
93
  licenses: []
59
94
  metadata:
60
95
  allowed_push_host: https://rubygems.org
96
+ homepage_uri: http://rubygems.org
97
+ source_code_uri: https://github.com/hanami/api
98
+ changelog_uri: https://github.com/hanami/api/blob/master/CHANGELOG.md
61
99
  post_install_message:
62
100
  rdoc_options: []
63
101
  require_paths:
@@ -66,15 +104,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
104
  requirements:
67
105
  - - ">="
68
106
  - !ruby/object:Gem::Version
69
- version: '0'
107
+ version: 2.7.0
70
108
  required_rubygems_version: !ruby/object:Gem::Requirement
71
109
  requirements:
72
110
  - - ">="
73
111
  - !ruby/object:Gem::Version
74
112
  version: '0'
75
113
  requirements: []
76
- rubyforge_project:
77
- rubygems_version: 2.6.11
114
+ rubygems_version: 3.1.2
78
115
  signing_key:
79
116
  specification_version: 4
80
117
  summary: Hanami API