hanami-api 0.0.0 → 0.1.0

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