hanami-api 0.1.2 → 0.3.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
2
  SHA256:
3
- metadata.gz: 671e8eab5f3b562214858055a272e049368bfce7524d639ffa63cd7003bab804
4
- data.tar.gz: 461f4066a630f1a1bd66dbd52f4a977a79a4e09737f5861aeabfcc89d87aee04
3
+ metadata.gz: 11eac74d32637ac1f12ee6d7a774a80501167f46b5a51f6a66f04f4518d82476
4
+ data.tar.gz: b7b1801a85993adcd839e5ce9d1b460e137ebbbf2a8913af2fb583bf765c7196
5
5
  SHA512:
6
- metadata.gz: e5398ddac6f97af3898faefd795bbb37f6be9d334163e90e0b68b004eab11e4923b366cc6b44504d06e16b7b67c2b3a3e46ca8a106ee694e65f2f83ab2ab1f41
7
- data.tar.gz: 34ad6218b1001a4b66a4ec9512f320649843928afc98bcd968a347dae0d23df2989ebc46bbd407f93734f934cbd725ded04e8528e9f04e365c180d8748a73097
6
+ metadata.gz: 011cbdb50d4372d08d14d7c037599e3f0c3a6ff3838518702272969bfabf0f1692ee9a8bbbfe1f128b4bcc63d2ef6935ec01e330d78b731e264e3f9be62d9030
7
+ data.tar.gz: 49bec19e9eaf598b31935e02bd45dfbb2cb949f54beee482210f8a33f50b4eb5ab333915fd9d600c34773faaa4f6a8d480966192077f3428bfc7bb4a65f1a37c
@@ -12,7 +12,7 @@ name: ci
12
12
  - ".rubocop.yml"
13
13
  pull_request:
14
14
  branches:
15
- - master
15
+ - main
16
16
  create:
17
17
 
18
18
  jobs:
@@ -22,7 +22,9 @@ jobs:
22
22
  fail-fast: false
23
23
  matrix:
24
24
  ruby:
25
- - "2.7"
25
+ - "3.2"
26
+ - "3.1"
27
+ - "3.0"
26
28
  env:
27
29
  CODACY_RUN_LOCAL: true
28
30
  CODACY_PROJECT_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}}
data/.rubocop.yml CHANGED
@@ -5,6 +5,10 @@
5
5
  # * https://github.com/bbatsov/ruby-style-guide
6
6
  # * https://rubocop.readthedocs.io/
7
7
  inherit_from:
8
- - https://raw.githubusercontent.com/hanami/devtools/master/.rubocop-unstable.yml
8
+ - https://raw.githubusercontent.com/hanami/devtools/main/.rubocop.yml
9
9
  AllCops:
10
- TargetRubyVersion: 2.7
10
+ TargetRubyVersion: 3.0
11
+
12
+ Lint/EmptyBlock:
13
+ Exclude:
14
+ - "./spec/integration/hanami/api/block_spec.rb"
data/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Hanami::API
2
2
  Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
3
3
 
4
+ ## v0.3.0 - 2022-12-25
5
+
6
+ ### Added
7
+ - [Luca Guidi] Official support for Ruby 3.1 & 3.2
8
+ - [Thomas Jachmann] Streamed responses
9
+ - [Luca Guidi] Introduce `Hanami::API.helpers` to define helper methods to be used in route blocks
10
+ - [Luca Guidi] Introduce `Hanami::API#to_inspect` to inspect app routes
11
+
12
+ ### Changed
13
+ - [Luca Guidi] Drop support for Ruby 2.7
14
+
15
+ ## v0.2.0 - 2021-01-05
16
+ ### Added
17
+ - [Luca Guidi] Official support for Ruby: MRI 3.0
18
+ - [Luca Guidi] Introduce `Hanami::API::DSL` which gives the ability to other Ruby web frameworks to use the `Hanami::API` DSL
19
+
4
20
  ## v0.1.2 - 2020-10-21
5
21
  ### Fixed
6
22
  - [Luca Guidi] Ensure to be able to instantiate an `Hanami::API` app multiple times
data/Gemfile CHANGED
@@ -4,4 +4,4 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  gem "byebug", require: false
7
- gem "hanami-router", git: "https://github.com/hanami/router.git", branch: "unstable"
7
+ gem "hanami-router", git: "https://github.com/hanami/router.git", branch: "main"
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright © 2014-2022 - Hanami Team
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,7 +1,53 @@
1
1
  # Hanami::API
2
2
 
3
3
  Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
4
- * [Installation](#installation)
4
+
5
+ ## Version
6
+
7
+ **This branch contains the code for `hanami-api` 0.3.x.**
8
+
9
+ ## Status
10
+
11
+ [![Gem Version](https://badge.fury.io/rb/hanami-api.svg)](https://badge.fury.io/rb/hanami-api)
12
+ [![CI](https://github.com/hanami/api/workflows/ci/badge.svg?branch=main)](https://github.com/hanami/api/actions?query=workflow%3Aci+branch%3Amain)
13
+ [![Test Coverage](https://codecov.io/gh/hanami/api/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/api)
14
+ [![Depfu](https://badges.depfu.com/badges/a8545fb67cf32a2c75b6227bc0821027/overview.svg)](https://depfu.com/github/hanami/api?project=Bundler)
15
+ [![Inline Docs](http://inch-ci.org/github/hanami/api.svg)](http://inch-ci.org/github/hanami/api)
16
+
17
+ ## Contact
18
+
19
+ * Home page: http://hanamirb.org
20
+ * Mailing List: http://hanamirb.org/mailing-list
21
+ * API Doc: http://rdoc.info/gems/hanami-api
22
+ * Bugs/Issues: https://github.com/hanami/api/issues
23
+ * Support: http://stackoverflow.com/questions/tagged/hanami
24
+ * Chat: http://chat.hanamirb.org
25
+
26
+ ## Rubies
27
+
28
+ __Hanami::API__ supports Ruby (MRI) 3.0+
29
+
30
+ ## Installation
31
+
32
+ Add these lines to your application's `Gemfile`:
33
+
34
+ ```ruby
35
+ gem "hanami-api"
36
+ gem "puma" # or "webrick", or "thin", "falcon"
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ ```shell
42
+ $ bundle install
43
+ ```
44
+
45
+ Or install it yourself as:
46
+
47
+ ```shell
48
+ $ gem install hanami-api
49
+ ```
50
+
5
51
  * [Performance](#performance)
6
52
  + [Runtime](#runtime)
7
53
  + [Memory](#memory)
@@ -13,9 +59,12 @@ Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
13
59
  - [Rack endpoint](#rack-endpoint)
14
60
  - [Block endpoint](#block-endpoint)
15
61
  * [String (body)](#string-body)
62
+ * [Enumerator (body)](#enumerator-body)
16
63
  * [Integer (status code)](#integer-status-code)
17
64
  * [Integer, String (status code, body)](#integer-string-status-code-body)
65
+ * [Integer, Enumerator (status code, body)](#integer-enumerator-status-code-body)
18
66
  * [Integer, Hash, String (status code, headers, body)](#integer-hash-string-status-code-headers-body)
67
+ * [Integer, Hash, Enumerator (status code, headers, body)](#integer-hash-enumerator-status-code-headers-body)
19
68
  + [Block context](#block-context)
20
69
  - [env](#env)
21
70
  - [status](#status)
@@ -27,31 +76,14 @@ Minimal, extremely fast, lightweight Ruby framework for HTTP APIs.
27
76
  - [back](#back)
28
77
  - [json](#json)
29
78
  + [Scope](#scope)
79
+ + [Helpers](#helpers)
30
80
  + [Rack Middleware](#rack-middleware)
81
+ + [Streamed Responses](#streamed-responses)
31
82
  + [Body Parsers](#body-parsers)
83
+ * [Testing](#testing)
32
84
  * [Development](#development)
33
85
  * [Contributing](#contributing)
34
86
 
35
- ## Installation
36
-
37
- Add this line to your application's `Gemfile`:
38
-
39
- ```ruby
40
- gem "hanami-api"
41
- ```
42
-
43
- And then execute:
44
-
45
- ```shell
46
- $ bundle install
47
- ```
48
-
49
- Or install it yourself as:
50
-
51
- ```shell
52
- $ gem install hanami-api
53
- ```
54
-
55
87
  ## Performance
56
88
 
57
89
  Benchmark against an app with 10,000 routes, hitting the 10,000th to measure the worst case scenario.
@@ -145,6 +177,7 @@ get "/", to: MyEndpoint.new
145
177
  * `post`
146
178
  * `patch`
147
179
  * `put`
180
+ * `delete`
148
181
  * `options`
149
182
  * `trace`
150
183
  * `link`
@@ -177,6 +210,16 @@ end
177
210
 
178
211
  It will return `[200, {}, ["Hello, world"]]`
179
212
 
213
+ ##### Enumerator (body)
214
+
215
+ ```ruby
216
+ get "/" do
217
+ Enumerator.new { ... }
218
+ end
219
+ ```
220
+
221
+ It will return `[200, {}, Enumerator]`, see [Streamed Responses](#streamed-responses)
222
+
180
223
  ##### Integer (status code)
181
224
 
182
225
  ```ruby
@@ -197,6 +240,16 @@ end
197
240
 
198
241
  It will return `[401, {}, ["You shall not pass"]]`
199
242
 
243
+ ##### Integer, Enumerator (status code, body)
244
+
245
+ ```ruby
246
+ get "/" do
247
+ [401, Enumerator.new { ... }]
248
+ end
249
+ ```
250
+
251
+ It will return `[401, {}, Enumerator]`, see [Streamed Responses](#streamed-responses)
252
+
200
253
  ##### Integer, Hash, String (status code, headers, body)
201
254
 
202
255
  ```ruby
@@ -207,6 +260,16 @@ end
207
260
 
208
261
  It will return `[401, {"X-Custom-Header" => "foo"}, ["You shall not pass"]]`
209
262
 
263
+ ##### Integer, Hash, Enumerator (status code, headers, body)
264
+
265
+ ```ruby
266
+ get "/" do
267
+ [401, {"X-Custom-Header" => "foo"}, Enumerator.new { ... }]
268
+ end
269
+ ```
270
+
271
+ It will return `[401, {"X-Custom-Header" => "foo"}, Enumerator]`, see [Streamed Responses](#streamed-responses)
272
+
210
273
  ### Block context
211
274
 
212
275
  When using the block syntax there is a rich API to use.
@@ -272,6 +335,14 @@ get "/" do
272
335
  end
273
336
  ```
274
337
 
338
+ Set HTTP response body using a [Streamed Response](#streamed-responses)
339
+
340
+ ```ruby
341
+ get "/" do
342
+ body Enumerator.new { ... }
343
+ end
344
+ ```
345
+
275
346
  #### params
276
347
 
277
348
  Access params for current request
@@ -307,6 +378,14 @@ end
307
378
 
308
379
  It sets a Rack response: `[401, {}, ["You shall not pass"]]`
309
380
 
381
+ You can also use a [Streamed Response](#streamed-responses) here
382
+
383
+ ```ruby
384
+ get "/authenticate" do
385
+ halt(401, Enumerator.new { ... })
386
+ end
387
+ ```
388
+
310
389
  #### redirect
311
390
 
312
391
  Redirects request and immediately halts it
@@ -363,6 +442,15 @@ get "/user/:id" do
363
442
  end
364
443
  ```
365
444
 
445
+ If you want a [Streamed Response](#streamed-responses)
446
+
447
+ ```ruby
448
+ get "/users" do
449
+ users = Enumerator.new { ... }
450
+ json(users)
451
+ end
452
+ ```
453
+
366
454
  ### Scope
367
455
 
368
456
  Prefixing routes is possible with routing scopes:
@@ -377,6 +465,58 @@ end
377
465
 
378
466
  It will generate a route with `"/api/v1/users"` as path.
379
467
 
468
+ ### Helpers
469
+
470
+ Define helper methods available within the block context.
471
+ Helper methods have access to default utilities available in block context (e.g. `#halt`).
472
+
473
+ Helpers can be defined inline by passing a block to the `.helpers` method:
474
+
475
+ ```ruby
476
+ require "hanami/api"
477
+
478
+ class MyAPI < Hanami::API
479
+ helpers do
480
+ def redirect_to_root
481
+ # redirect method is provided by Hanami::API block context
482
+ redirect "/"
483
+ end
484
+ end
485
+
486
+ root { "Hello, World" }
487
+
488
+ get "/legacy" do
489
+ redirect_to_root
490
+ end
491
+ end
492
+ ```
493
+
494
+ Alternatively, `.helpers` accepts a module.
495
+
496
+ ```ruby
497
+ require "hanami/api"
498
+
499
+ class MyAPI < Hanami::API
500
+ module Authentication
501
+ private
502
+
503
+ def unauthorized
504
+ halt(401)
505
+ end
506
+ end
507
+
508
+ helpers(Authentication)
509
+
510
+ root { "Hello, World" }
511
+
512
+ get "/secrets" do
513
+ unauthorized
514
+ end
515
+ end
516
+ ```
517
+
518
+ You can use `.helpers` multiple times in the same app.
519
+
380
520
  ### Rack Middleware
381
521
 
382
522
  To mount a Rack middleware it's possible with `.use`
@@ -411,6 +551,56 @@ it's part of the top level scope. `ApiAuthentication` it's used for all the API
411
551
  versions, because it's defined in the `"api"` scope. `ApiV1Deprecation` is used
412
552
  only by the routes in `"v1"` scope, but not by `"v2"`.
413
553
 
554
+ ### Streamed Responses
555
+
556
+ When the work to be done by the server takes time, it may be a good idea to
557
+ stream your response. For this, you just use an `Enumerator` anywhere you would
558
+ normally use a `String` as body or another `Object` as JSON response. Here's an
559
+ example of streaming JSON data:
560
+
561
+ ```ruby
562
+ scope "stream" do
563
+ use ::Rack::Chunked
564
+
565
+ get "/data" do
566
+ Enumerator.new do |yielder|
567
+ data = %w[a b c]
568
+ data.each do |item|
569
+ yielder << item
570
+ end
571
+ end
572
+ end
573
+
574
+ get "/to_enum" do
575
+ %w[a b c].to_enum
576
+ end
577
+
578
+ get "/json" do
579
+ result = Enumerator.new do |yielder|
580
+ data = %w[a b c]
581
+ data.each do |item|
582
+ yielder << item
583
+ end
584
+ end
585
+
586
+ json(result)
587
+ end
588
+ end
589
+ ```
590
+
591
+ Note:
592
+
593
+ * Returning an `Enumerator` will also work without `Rack::Chunked`, it just
594
+ won't stream but return the whole body at the end instead.
595
+ * Data pushed to `yielder` MUST be a `String`.
596
+ * Streaming does not work with WEBrick as it buffers its response. We recommend
597
+ using `puma`, though you may find success with other servers.
598
+ * To manual test this feature use a web browser or cURL:
599
+
600
+ ```shell
601
+ $ curl --raw -i http://localhost:2300/stream/data
602
+ ```
603
+
414
604
  ### Body Parsers
415
605
 
416
606
  Rack ignores request bodies unless they come from a form submission.
@@ -425,6 +615,59 @@ require "hanami/middleware/body_parser"
425
615
  use Hanami::Middleware::BodyParser, :json
426
616
  ```
427
617
 
618
+ ## Testing
619
+
620
+ ## Unit testing
621
+ You can unit test your `Hanami::API` app by passing a `env` hash to your app's `#call` method.
622
+
623
+ The keys that (based on the Rack standard) `Hanami::API` uses for routing are:
624
+ * `PATH_INFO`
625
+ * `REQUEST_METHOD`
626
+
627
+
628
+ For example, a spec for the basic app in the [Usage section](https://github.com/hanami/api#usage) could be:
629
+
630
+ ```ruby
631
+ require "my_project/app"
632
+
633
+ RSpec.describe App do
634
+ describe "#call" do
635
+ it "returns successfully" do
636
+ response = subject.call({"PATH_INFO" => "/", "REQUEST_METHOD" => "GET"})
637
+ expect(response).to eq([200, {}, ["Hello, world"]])
638
+ end
639
+ end
640
+ end
641
+ ```
642
+
643
+ ## Integration testing
644
+ Add this line to your application's `Gemfile`:
645
+
646
+ ```ruby
647
+ gem "rack-test", group: :test
648
+ ```
649
+
650
+ In a test, load `Rack::Test`:
651
+
652
+ ```ruby
653
+ require "rack/test"
654
+ ```
655
+
656
+ and then, inside your spec/test, include its helper methods:
657
+
658
+ ```ruby
659
+ include Rack::Test::Methods
660
+ ```
661
+
662
+ Then you can use its methods like `get` and `last_response`, e.g.:
663
+
664
+ ```ruby
665
+ it "returns the status 200" do
666
+ get "/"
667
+ expect(last_response.status).to eq 200
668
+ end
669
+ ```
670
+
428
671
  ## Development
429
672
 
430
673
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -435,3 +678,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
435
678
 
436
679
  Bug reports and pull requests are welcome on GitHub at https://github.com/hanami/api.
437
680
 
681
+ ## Copyright
682
+
683
+ Copyright © 2014-2022 Hanami Team – Released under MIT License.
data/hanami-api.gemspec CHANGED
@@ -11,13 +11,14 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Hanami API"
12
12
  spec.description = "Extremely fast and lightweight HTTP API"
13
13
  spec.homepage = "http://rubygems.org"
14
- spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
14
+ spec.licenses = ["MIT"]
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
15
16
 
16
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
18
 
18
19
  spec.metadata["homepage_uri"] = spec.homepage
19
20
  spec.metadata["source_code_uri"] = "https://github.com/hanami/api"
20
- spec.metadata["changelog_uri"] = "https://github.com/hanami/api/blob/master/CHANGELOG.md"
21
+ spec.metadata["changelog_uri"] = "https://github.com/hanami/api/blob/main/CHANGELOG.md"
21
22
 
22
23
  # Specify which files should be added to the gem when it is released.
23
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -28,11 +29,12 @@ Gem::Specification.new do |spec|
28
29
  spec.bindir = "exe"
29
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
31
  spec.require_paths = ["lib"]
32
+ spec.metadata["rubygems_mfa_required"] = "true"
31
33
 
32
- spec.add_dependency "hanami-router", "~> 2.0.alpha"
34
+ spec.add_dependency "hanami-router", "~> 2.0"
33
35
 
34
36
  spec.add_development_dependency "rake", "~> 13.0"
35
37
  spec.add_development_dependency "rspec", "~> 3.8"
36
- spec.add_development_dependency "rubocop", "~> 0.86"
38
+ spec.add_development_dependency "rubocop", "~> 1.0"
37
39
  spec.add_development_dependency "yard", "~> 0.9"
38
40
  end
@@ -124,7 +124,12 @@ module Hanami
124
124
  # end
125
125
  def json(object, mime = "application/json")
126
126
  headers["Content-Type"] = mime
127
- JSON.generate(object)
127
+ case object
128
+ in Enumerator => collection
129
+ json_enum(collection)
130
+ else
131
+ JSON.generate(object)
132
+ end
128
133
  end
129
134
 
130
135
  # @since 0.1.0
@@ -132,10 +137,11 @@ module Hanami
132
137
  #
133
138
  def call
134
139
  case caught
135
- in String => body
140
+ in String => body
136
141
  [status, headers, [body]]
137
- in Integer => status
138
- # rubocop:disable Style/RedundantSelf
142
+ in Enumerator => body
143
+ [status, headers, body]
144
+ in Integer => status
139
145
  #
140
146
  # NOTE: It must use `self.body` so it will pick the method defined above.
141
147
  #
@@ -144,12 +150,16 @@ module Hanami
144
150
  # When that happens, the body that was manually set is ignored,
145
151
  # which results in a bug.
146
152
  [status, headers, [self.body || http_status(status)]]
147
- # rubocop:enable Style/RedundantSelf
148
- in [Integer => status, String => body]
153
+ in [Integer => status, String => body]
149
154
  [status, headers, [body]]
150
- in [Integer => status, Hash => caught_headers, String => body]
155
+ in [Integer => status, Enumerator => body]
156
+ [status, headers, body]
157
+ in [Integer => status, Hash => caught_headers, String => body]
151
158
  headers.merge!(caught_headers)
152
159
  [status, headers, [body]]
160
+ in [Integer => status, Hash => caught_headers, Enumerator => body]
161
+ headers.merge!(caught_headers)
162
+ [status, headers, body]
153
163
  end
154
164
  end
155
165
 
@@ -168,6 +178,20 @@ module Hanami
168
178
  def http_status(code)
169
179
  Rack::Utils::HTTP_STATUS_CODES.fetch(code)
170
180
  end
181
+
182
+ # @since x.x.x
183
+ # @api private
184
+ def json_enum(collection)
185
+ Enumerator.new do |yielder|
186
+ yielder << "["
187
+ collection.each_with_index do |item, i|
188
+ yielder << "," if i.positive?
189
+ yielder << JSON.generate(item)
190
+ end
191
+ ensure
192
+ yielder << "]"
193
+ end
194
+ end
171
195
  end
172
196
  end
173
197
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class API
5
+ # Expose Hanami::API features to third party frameworks that need to expose
6
+ # a routing DSL.
7
+ #
8
+ # @since 0.2.0
9
+ #
10
+ # @example
11
+ # # framework.rb
12
+ # require "hanami/api"
13
+ #
14
+ # module Framework
15
+ # class App
16
+ # def self.inherited(base)
17
+ # super
18
+ # base.extend(Hanami::API::DSL)
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # # app.rb
24
+ # require "framework/app"
25
+ #
26
+ # class MyApp < Framework::App
27
+ # routes do
28
+ # root { "Hello, World!" }
29
+ # end
30
+ # end
31
+ #
32
+ # # config.ru
33
+ # require_relative "./app"
34
+ #
35
+ # run MyApp.new
36
+ module DSL
37
+ # @since 0.2.0
38
+ # @api private
39
+ def self.extended(app)
40
+ super
41
+
42
+ app.extend(ClassMethods)
43
+ app.extend(ClassMethods::Routes)
44
+ app.include(InstanceMethods)
45
+ end
46
+
47
+ # @since 0.2.0
48
+ # @api private
49
+ module ClassMethods
50
+ # @since 0.2.0
51
+ # @api private
52
+ attr_reader :router
53
+
54
+ # @since 0.2.0
55
+ # @api private
56
+ def self.extended(app)
57
+ super
58
+
59
+ app.class_eval do
60
+ klass = Block::Context.dup
61
+ app.const_set(:BlockContext, klass)
62
+
63
+ @router = Router.new(block_context: klass)
64
+ end
65
+ end
66
+
67
+ # @since 0.2.0
68
+ # @api private
69
+ module Routes
70
+ # A block to define application routes
71
+ #
72
+ # This is ONLY available for third-party frameworks that use
73
+ # Hanami::API DSL.
74
+ #
75
+ # If you use Hanami::API directly, this method isn't available.
76
+ #
77
+ # @param blk [Proc] the block to define the routes
78
+ #
79
+ # @see Hanami::API::Router
80
+ #
81
+ # @since 0.2.0
82
+ # @api public
83
+ def routes(&blk)
84
+ router.instance_eval(&blk)
85
+ end
86
+ end
87
+ end
88
+
89
+ module InstanceMethods
90
+ # Initialize the app
91
+ #
92
+ # @param router [Hanami::API::Router] the application router
93
+ #
94
+ # @since 0.2.0
95
+ # @api public
96
+ def initialize(router: self.class.router.dup)
97
+ @router = router
98
+
99
+ freeze
100
+ end
101
+
102
+ # @since 0.2.0
103
+ # @api private
104
+ def freeze
105
+ @app = @router.to_rack_app
106
+ @url_helpers = @router.url_helpers
107
+ @router.remove_instance_variable(:@url_helpers)
108
+ remove_instance_variable(:@router)
109
+ @url_helpers.freeze
110
+ @app.freeze
111
+ super
112
+ end
113
+
114
+ # Compatibility with Rack protocol
115
+ #
116
+ # @param env [Hash] a Rack env for the current request
117
+ #
118
+ # @since 0.2.0
119
+ # @api public
120
+ def call(env)
121
+ @app.call(env)
122
+ end
123
+
124
+ # Printable routes
125
+ #
126
+ # @return [String] printable routes
127
+ #
128
+ # @since x.x.x
129
+ # @api public
130
+ def to_inspect
131
+ @app.to_inspect
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hanami/middleware/app"
4
+
3
5
  module Hanami
4
6
  class API
5
7
  # Hanami::API middleware stack
@@ -7,9 +9,6 @@ module Hanami
7
9
  # @since 0.1.0
8
10
  # @api private
9
11
  module Middleware
10
- require "hanami/api/middleware/app"
11
- require "hanami/api/middleware/trie"
12
-
13
12
  # Middleware stack
14
13
  #
15
14
  # @since 0.1.0
@@ -44,7 +43,7 @@ module Hanami
44
43
  mapping = to_hash
45
44
  return app if mapping.empty?
46
45
 
47
- App.new(app, mapping)
46
+ Hanami::Middleware::App.new(app, mapping)
48
47
  end
49
48
 
50
49
  private
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "hanami/router"
4
+ require "hanami/router/inspector"
4
5
  require "hanami/api/block/context"
5
6
 
6
7
  module Hanami
7
8
  class API
8
9
  # @since 0.1.0
9
10
  class Router < ::Hanami::Router
11
+ # @since x.x.x
12
+ # @api private
13
+ attr_reader :inspector
14
+
10
15
  # @since 0.1.0
11
16
  # @api private
12
- def initialize(block_context: Block::Context, **kwargs)
13
- super(block_context: block_context, **kwargs)
17
+ def initialize(block_context: Block::Context, inspector: Inspector.new, **kwargs)
18
+ super(block_context: block_context, inspector: inspector, **kwargs)
14
19
  @stack = Middleware::Stack.new(@path_prefix.to_s)
15
20
  end
16
21
 
@@ -34,6 +39,16 @@ module Hanami
34
39
  def to_rack_app
35
40
  @stack.finalize(self)
36
41
  end
42
+
43
+ # Returns formatted routes
44
+ #
45
+ # @return [String] formatted routes
46
+ #
47
+ # @since x.x.x
48
+ # @api private
49
+ def to_inspect
50
+ @inspector.call
51
+ end
37
52
  end
38
53
  end
39
54
  end
@@ -3,6 +3,6 @@
3
3
  module Hanami
4
4
  class API
5
5
  # @since 0.1.0
6
- VERSION = "0.1.2"
6
+ VERSION = "0.3.0"
7
7
  end
8
8
  end
data/lib/hanami/api.rb CHANGED
@@ -9,21 +9,66 @@ module Hanami
9
9
  require "hanami/api/error"
10
10
  require "hanami/api/router"
11
11
  require "hanami/api/middleware"
12
+ require "hanami/api/dsl"
12
13
 
13
14
  # @since 0.1.0
14
15
  # @api private
15
16
  def self.inherited(app)
16
17
  super
17
18
 
18
- app.class_eval do
19
- @router = Router.new
20
- end
19
+ app.extend(DSL::ClassMethods)
20
+ app.include(DSL::InstanceMethods)
21
21
  end
22
22
 
23
- class << self
24
- # @since 0.1.1
25
- # @api private
26
- attr_reader :router
23
+ # Defines helper methods available within the block context.
24
+ # Helper methods have access to default utilities available in block
25
+ # context (e.g. `#halt`).
26
+ #
27
+ # @param mod [Module] optional module to include in block context
28
+ # @param blk [Proc] inline helper definitions
29
+ #
30
+ # @since x.x.x
31
+ #
32
+ # @example Inline helpers definition
33
+ # require "hanami/api"
34
+ #
35
+ # class MyAPI < Hanami::API
36
+ # helpers do
37
+ # def redirect_to_root
38
+ # # redirect method is provided by Hanami::API block context
39
+ # redirect "/"
40
+ # end
41
+ # end
42
+ #
43
+ # root { "Hello, World" }
44
+ #
45
+ # get "/legacy" do
46
+ # redirect_to_root
47
+ # end
48
+ # end
49
+ #
50
+ # @example Module helpers definition
51
+ # require "hanami/api"
52
+ #
53
+ # class MyAPI < Hanami::API
54
+ # module Authentication
55
+ # private
56
+ #
57
+ # def unauthorized
58
+ # halt(401)
59
+ # end
60
+ # end
61
+ #
62
+ # helpers(Authentication)
63
+ #
64
+ # root { "Hello, World" }
65
+ #
66
+ # get "/secrets" do
67
+ # unauthorized
68
+ # end
69
+ # end
70
+ def self.helpers(mod = nil, &blk)
71
+ const_get(:BlockContext).include(mod || Module.new(&blk))
27
72
  end
28
73
 
29
74
  # Defines a named root route (a GET route for "/")
@@ -50,8 +95,8 @@ module Hanami
50
95
  # "Hello from Hanami!"
51
96
  # end
52
97
  # end
53
- def self.root(*args, **kwargs, &blk)
54
- @router.root(*args, **kwargs, &blk)
98
+ def self.root(...)
99
+ @router.root(...)
55
100
  end
56
101
 
57
102
  # Defines a route that accepts GET requests for the given path.
@@ -87,8 +132,8 @@ module Hanami
87
132
  # class MyAPI < Hanami::API
88
133
  # get "/users/:id", to: ->(*) { [200, {}, ["OK"]] }, id: /\d+/
89
134
  # end
90
- def self.get(*args, **kwargs, &blk)
91
- @router.get(*args, **kwargs, &blk)
135
+ def self.get(...)
136
+ @router.get(...)
92
137
  end
93
138
 
94
139
  # Defines a route that accepts POST requests for the given path.
@@ -102,8 +147,8 @@ module Hanami
102
147
  # @since 0.1.0
103
148
  #
104
149
  # @see .get
105
- def self.post(*args, **kwargs, &blk)
106
- @router.post(*args, **kwargs, &blk)
150
+ def self.post(...)
151
+ @router.post(...)
107
152
  end
108
153
 
109
154
  # Defines a route that accepts PATCH requests for the given path.
@@ -117,8 +162,8 @@ module Hanami
117
162
  # @since 0.1.0
118
163
  #
119
164
  # @see .get
120
- def self.patch(*args, **kwargs, &blk)
121
- @router.patch(*args, **kwargs, &blk)
165
+ def self.patch(...)
166
+ @router.patch(...)
122
167
  end
123
168
 
124
169
  # Defines a route that accepts PUT requests for the given path.
@@ -132,8 +177,8 @@ module Hanami
132
177
  # @since 0.1.0
133
178
  #
134
179
  # @see .get
135
- def self.put(*args, **kwargs, &blk)
136
- @router.put(*args, **kwargs, &blk)
180
+ def self.put(...)
181
+ @router.put(...)
137
182
  end
138
183
 
139
184
  # Defines a route that accepts DELETE requests for the given path.
@@ -147,8 +192,8 @@ module Hanami
147
192
  # @since 0.1.0
148
193
  #
149
194
  # @see .get
150
- def self.delete(*args, **kwargs, &blk)
151
- @router.delete(*args, **kwargs, &blk)
195
+ def self.delete(...)
196
+ @router.delete(...)
152
197
  end
153
198
 
154
199
  # Defines a route that accepts TRACE requests for the given path.
@@ -162,8 +207,8 @@ module Hanami
162
207
  # @since 0.1.0
163
208
  #
164
209
  # @see .get
165
- def self.trace(*args, **kwargs, &blk)
166
- @router.trace(*args, **kwargs, &blk)
210
+ def self.trace(...)
211
+ @router.trace(...)
167
212
  end
168
213
 
169
214
  # Defines a route that accepts OPTIONS requests for the given path.
@@ -177,8 +222,8 @@ module Hanami
177
222
  # @since 0.1.0
178
223
  #
179
224
  # @see .get
180
- def self.options(*args, **kwargs, &blk)
181
- @router.options(*args, **kwargs, &blk)
225
+ def self.options(...)
226
+ @router.options(...)
182
227
  end
183
228
 
184
229
  # Defines a route that accepts LINK requests for the given path.
@@ -192,8 +237,8 @@ module Hanami
192
237
  # @since 0.1.0
193
238
  #
194
239
  # @see .get
195
- def self.link(*args, **kwargs, &blk)
196
- @router.link(*args, **kwargs, &blk)
240
+ def self.link(...)
241
+ @router.link(...)
197
242
  end
198
243
 
199
244
  # Defines a route that accepts UNLINK requests for the given path.
@@ -207,8 +252,8 @@ module Hanami
207
252
  # @since 0.1.0
208
253
  #
209
254
  # @see .get
210
- def self.unlink(*args, **kwargs, &blk)
211
- @router.unlink(*args, **kwargs, &blk)
255
+ def self.unlink(...)
256
+ @router.unlink(...)
212
257
  end
213
258
 
214
259
  # Defines a route that redirects the incoming request to another path.
@@ -221,8 +266,8 @@ module Hanami
221
266
  # @since 0.1.0
222
267
  #
223
268
  # @see .get
224
- def self.redirect(*args, **kwargs, &blk)
225
- @router.redirect(*args, **kwargs, &blk)
269
+ def self.redirect(...)
270
+ @router.redirect(...)
226
271
  end
227
272
 
228
273
  # Defines a routing scope. Routes defined in the context of a scope,
@@ -245,8 +290,8 @@ module Hanami
245
290
  # end
246
291
  #
247
292
  # # It generates a route with a path `/v1/users`
248
- def self.scope(*args, **kwargs, &blk)
249
- @router.scope(*args, **kwargs, &blk)
293
+ def self.scope(...)
294
+ @router.scope(...)
250
295
  end
251
296
 
252
297
  # Mount a Rack application at the specified path.
@@ -270,8 +315,8 @@ module Hanami
270
315
  # class MyAPI < Hanami::API
271
316
  # mount MyRackApp.new, at: "/foo"
272
317
  # end
273
- def self.mount(*args, **kwargs, &blk)
274
- @router.mount(*args, **kwargs, &blk)
318
+ def self.mount(...)
319
+ @router.mount(...)
275
320
  end
276
321
 
277
322
  # Use a Rack middleware
@@ -292,29 +337,6 @@ module Hanami
292
337
  @router.use(middleware, *args, &blk)
293
338
  end
294
339
 
295
- # @since 0.1.0
296
- def initialize(router: self.class.router.dup)
297
- @router = router
298
-
299
- freeze
300
- end
301
-
302
- # @since 0.1.0
303
- def freeze
304
- @app = @router.to_rack_app
305
- @url_helpers = @router.url_helpers
306
- @router.remove_instance_variable(:@url_helpers)
307
- remove_instance_variable(:@router)
308
- @url_helpers.freeze
309
- @app.freeze
310
- super
311
- end
312
-
313
- # @since 0.1.0
314
- def call(env)
315
- @app.call(env)
316
- end
317
-
318
340
  # TODO: verify if needed here on in block context
319
341
  #
320
342
  # @since 0.1.0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.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: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2022-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hanami-router
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.0.alpha
19
+ version: '2.0'
20
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: 2.0.alpha
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.86'
61
+ version: '1.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.86'
68
+ version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +93,7 @@ files:
93
93
  - ".rubocop.yml"
94
94
  - CHANGELOG.md
95
95
  - Gemfile
96
+ - LICENSE.md
96
97
  - README.md
97
98
  - Rakefile
98
99
  - bin/console
@@ -100,20 +101,20 @@ files:
100
101
  - hanami-api.gemspec
101
102
  - lib/hanami/api.rb
102
103
  - lib/hanami/api/block/context.rb
104
+ - lib/hanami/api/dsl.rb
103
105
  - lib/hanami/api/error.rb
104
106
  - lib/hanami/api/middleware.rb
105
- - lib/hanami/api/middleware/app.rb
106
- - lib/hanami/api/middleware/node.rb
107
- - lib/hanami/api/middleware/trie.rb
108
107
  - lib/hanami/api/router.rb
109
108
  - lib/hanami/api/version.rb
110
109
  homepage: http://rubygems.org
111
- licenses: []
110
+ licenses:
111
+ - MIT
112
112
  metadata:
113
113
  allowed_push_host: https://rubygems.org
114
114
  homepage_uri: http://rubygems.org
115
115
  source_code_uri: https://github.com/hanami/api
116
- changelog_uri: https://github.com/hanami/api/blob/master/CHANGELOG.md
116
+ changelog_uri: https://github.com/hanami/api/blob/main/CHANGELOG.md
117
+ rubygems_mfa_required: 'true'
117
118
  post_install_message:
118
119
  rdoc_options: []
119
120
  require_paths:
@@ -122,14 +123,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
123
  requirements:
123
124
  - - ">="
124
125
  - !ruby/object:Gem::Version
125
- version: 2.7.0
126
+ version: 3.0.0
126
127
  required_rubygems_version: !ruby/object:Gem::Requirement
127
128
  requirements:
128
129
  - - ">="
129
130
  - !ruby/object:Gem::Version
130
131
  version: '0'
131
132
  requirements: []
132
- rubygems_version: 3.1.3
133
+ rubygems_version: 3.4.1
133
134
  signing_key:
134
135
  specification_version: 4
135
136
  summary: Hanami API
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rack/builder"
4
-
5
- module Hanami
6
- class API
7
- module Middleware
8
- # Hanami::API middleware stack
9
- #
10
- # @since 0.1.1
11
- # @api private
12
- class App
13
- # @since 0.1.1
14
- # @api private
15
- def initialize(app, mapping)
16
- @trie = Hanami::API::Middleware::Trie.new(app)
17
-
18
- mapping.each do |path, stack|
19
- builder = Rack::Builder.new
20
-
21
- stack.each do |middleware, args, blk|
22
- builder.use(middleware, *args, &blk)
23
- end
24
-
25
- builder.run(app)
26
-
27
- @trie.add(path, builder.to_app.freeze)
28
- end
29
-
30
- @trie.freeze
31
- end
32
-
33
- # @since 0.1.1
34
- # @api private
35
- def call(env)
36
- @trie.find(env["PATH_INFO"]).call(env)
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hanami
4
- class API
5
- module Middleware
6
- # Trie node to register scopes with custom Rack middleware
7
- #
8
- # @api private
9
- # @since 0.1.1
10
- class Node
11
- # @api private
12
- # @since 0.1.1
13
- attr_reader :app
14
-
15
- # @api private
16
- # @since 0.1.1
17
- def initialize
18
- @app = nil
19
- @children = {}
20
- end
21
-
22
- # @api private
23
- # @since 0.1.1
24
- def freeze
25
- @children.each(&:freeze)
26
- super
27
- end
28
-
29
- # @api private
30
- # @since 0.1.1
31
- def put(segment)
32
- @children[segment] ||= self.class.new
33
- end
34
-
35
- # @api private
36
- # @since 0.1.1
37
- def get(segment)
38
- @children.fetch(segment) { self if leaf? }
39
- end
40
-
41
- # @api private
42
- # @since 0.1.1
43
- def app!(app)
44
- @app = app
45
- end
46
-
47
- # @api private
48
- # @since 0.1.1
49
- def app?
50
- @app
51
- end
52
-
53
- # @api private
54
- # @since 0.1.1
55
- def leaf?
56
- @children.empty?
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "hanami/api/middleware/node"
4
-
5
- module Hanami
6
- class API
7
- module Middleware
8
- # Trie to register scopes with custom Rack middleware
9
- #
10
- # @api private
11
- # @since 0.1.1
12
- class Trie
13
- # @api private
14
- # @since 0.1.1
15
- def initialize(app)
16
- @app = app
17
- @root = Node.new
18
- end
19
-
20
- # @api private
21
- # @since 0.1.1
22
- def freeze
23
- @root.freeze
24
- super
25
- end
26
-
27
- # @api private
28
- # @since 0.1.1
29
- def add(path, app)
30
- node = @root
31
- for_each_segment(path) do |segment|
32
- node = node.put(segment)
33
- end
34
-
35
- node.app!(app)
36
- end
37
-
38
- # @api private
39
- # @since 0.1.1
40
- def find(path)
41
- node = @root
42
-
43
- for_each_segment(path) do |segment|
44
- break unless node
45
-
46
- node = node.get(segment)
47
- end
48
-
49
- return node.app if node&.app?
50
-
51
- @root.app || @app
52
- end
53
-
54
- # @api private
55
- # @since 0.1.1
56
- def empty?
57
- @root.leaf?
58
- end
59
-
60
- private
61
-
62
- # @api private
63
- # @since 0.1.1
64
- def for_each_segment(path, &blk)
65
- _, *segments = path.split(/\//)
66
- segments.each(&blk)
67
- end
68
- end
69
- end
70
- end
71
- end