hanami-api 0.1.2 → 0.3.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
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