api_recipes 2.10.3 → 2.10.4

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: 8c8e887543d5b2571190093a25ee60ba47e7454244ec4b2a8f543934368ec34c
4
- data.tar.gz: 5fa9d7866a4d52c064aaec98ef3402aef2ea2a3dcb6728a17d60c76e508e337e
3
+ metadata.gz: 1a4721f0a12f39f1dd0614a8787cbdfdde44efd74efa193d8b914b209492e887
4
+ data.tar.gz: 9d3b3706ac992b5ab5b9bde22f022892746bf0dcb496e7dffe51d39fb9d2028c
5
5
  SHA512:
6
- metadata.gz: 5edaaffcf15dfb67ec212b2efc5f50778169b05d8b011b6a3a20f5dc3fe59120db746274671d4da3ff889a7f0c633ade3f5b60d950a0f0ce30f4e4a4e4ab8648
7
- data.tar.gz: 20294d6d13e34334692f8dba9056deb5d47653d3b80fe8eb7cfbcef753b27286be32f33d1b96d3b9189ff482266b6705230a281c8b28de5bb72d3d7d5e50ddfe
6
+ metadata.gz: a32eec192e29132ecbf39fc92ef5ad1823efb2747f15b53e9d67ad83c0293b4f5fb067b16a9e27001e0693cd26c678330186bed6cc7fbf67ffe372e2ad51fb59
7
+ data.tar.gz: 1b5fbaf21da5d84c5558c4e5c09d5fcfbd024ac206cebc61e4e6c891502f477939ff09a641c26e2d9d81f4f564e98a09cc4f2d7691243a28c9135ba7fee50d90
data/README.md CHANGED
@@ -1,57 +1,755 @@
1
- [![Code Climate](https://codeclimate.com/github/madAle/api_recipes/badges/gpa.svg)](https://codeclimate.com/github/madAle/api_recipes)
1
+ [![Gem Version](https://img.shields.io/gem/v/api_recipes)](https://rubygems.org/gems/api_recipes)
2
+ [![CI](https://github.com/madAle/api_recipes/actions/workflows/ci.yml/badge.svg)](https://github.com/madAle/api_recipes/actions/workflows/ci.yml)
2
3
 
3
4
  # ApiRecipes
4
5
 
5
- <p align="center">
6
+ <p align="center">
6
7
  <img width="300" height="160" src="api_recipes.png"/>
7
8
  </p>
8
9
 
9
- Create your HTTP APIs recipes and tastefully consume it!
10
+ Describe an HTTP API once, in YAML, and call it with plain Ruby methods.
11
+
12
+ ```ruby
13
+ ApiRecipes.github.users.repos('octocat').run.data
14
+ # GET https://api.github.com/users/octocat/repos
15
+ # => [{ "name" => "Hello-World", ... }, ...]
16
+ ```
17
+
18
+ There are no client classes to write. Hosts, paths, verbs, timeouts and expected
19
+ status codes live in a configuration file, and ApiRecipes turns them into a chain
20
+ of methods. Requests are made with [http.rb](https://github.com/httprb/http).
21
+
22
+ ## Contents
23
+
24
+ - [Installation](#installation)
25
+ - [Quick start](#quick-start)
26
+ - [Describing an API](#describing-an-api)
27
+ - [Loading the configuration](#loading-the-configuration)
28
+ - [Where an API is available](#where-an-api-is-available)
29
+ - [Making requests](#making-requests)
30
+ - [Reading the response](#reading-the-response)
31
+ - [Deciding what counts as success](#deciding-what-counts-as-success)
32
+ - [Handling errors](#handling-errors)
33
+ - [Authorization](#authorization)
34
+ - [Logging and debugging](#logging-and-debugging)
35
+ - [Recipes](#recipes)
36
+ - [Options reference](#options-reference)
37
+ - [Good to know](#good-to-know)
38
+ - [Runnable examples](#runnable-examples)
39
+ - [Compatibility](#compatibility)
40
+ - [Development](#development)
10
41
 
11
- Api recipes lets you easily describe and consume an HTTP API.
12
-
13
42
  ## Installation
14
43
 
15
- Add this line to your application's Gemfile:
44
+ Add the gem to your Gemfile and run `bundle install`:
16
45
 
17
46
  ```ruby
18
47
  gem 'api_recipes'
19
48
  ```
20
49
 
21
- And then execute:
50
+ Or install it directly:
22
51
 
23
- $ bundle
52
+ $ gem install api_recipes
24
53
 
25
- Or install it yourself as:
54
+ ## Quick start
26
55
 
27
- $ gem install api_recipes
28
-
29
- ## Compatibility
56
+ **1. Describe the API** in a YAML file, for example `config/apis/github.yml`:
30
57
 
31
- Ruby 2.7.0 or higher.
58
+ ```yaml
59
+ github:
60
+ host: api.github.com
61
+ endpoints:
62
+ users: # GET /users
63
+ endpoints:
64
+ show: # GET /users/:login
65
+ path: /:login
66
+ repos: # GET /users/:login/repos
67
+ path: /:login/repos
68
+ ```
32
69
 
33
- ## Usage
70
+ **2. Load it** once, when your application starts:
34
71
 
35
- Help needed.
72
+ ```ruby
73
+ require 'api_recipes'
36
74
 
37
- In the meantime take a look at the ``` examples ``` folder. Thank you.
75
+ ApiRecipes.configure do |config|
76
+ config.apis_files_paths = ['config/apis/github.yml']
77
+ end
78
+ ```
38
79
 
39
- ## TODO
80
+ **3. Call it:**
40
81
 
41
- * Re-write specs
42
- * Write a good Usage
43
- * Add more examples
44
- * Add documentation
45
- * Delete this TODO section
82
+ ```ruby
83
+ response = ApiRecipes.github.users.show('octocat').run
46
84
 
47
- ## Contributing
85
+ response.code # => 200
86
+ response.data['name'] # => "The Octocat"
87
+ ```
48
88
 
49
- Bug reports and pull requests are welcome on GitHub at https://github.com/madAle/api_recipes.
50
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
51
- the [Contributor Covenant](contributor-covenant.org) code of conduct.
89
+ Each endpoint in the file becomes a method, arguments fill the `:placeholders`
90
+ of the path, and `run` sends the request.
52
91
 
92
+ ## Describing an API
53
93
 
54
- ## License
94
+ The examples in this guide share one configuration. It is longer than the quick
95
+ start because it shows every way of shaping an endpoint:
96
+
97
+ ```yaml
98
+ github:
99
+ host: api.github.com
100
+ default_headers:
101
+ Accept: application/vnd.github+json
102
+ endpoints:
103
+ users: # GET /users
104
+ endpoints:
105
+ show: # GET /users/:login
106
+ path: /:login
107
+ repos: # GET /users/:login/repos
108
+ path: /:login/repos
109
+ repos:
110
+ route: no # groups the endpoints below, is not callable itself
111
+ path: /repos/:owner/:repo
112
+ endpoints:
113
+ show: # GET /repos/:owner/:repo
114
+ path: ''
115
+ contributors: # GET /repos/:owner/:repo/contributors
116
+ issues: # GET /repos/:owner/:repo/issues
117
+ endpoints:
118
+ create: # POST /repos/:owner/:repo/issues
119
+ path: ''
120
+ verb: post
121
+ ok_code: 201
122
+ search:
123
+ route: no
124
+ endpoints:
125
+ repositories: # GET /search/repositories
126
+ ```
127
+
128
+ ### How a path is built
129
+
130
+ The path of an endpoint is the path of its parent followed by its own segment:
131
+
132
+ | In the file | Segment added |
133
+ | --- | --- |
134
+ | no `path` | the endpoint name: `contributors` adds `/contributors` |
135
+ | `path: /something` | that text: `path: /:login/repos` adds `/:login/repos` |
136
+ | `path: ''` | nothing: the endpoint has the same URL as its parent |
137
+
138
+ `path: ''` is how two endpoints share a URL with different verbs, like `issues`
139
+ (GET) and `issues.create` (POST) above.
140
+
141
+ ### Path parameters
142
+
143
+ A `:name` in a path is a placeholder. Values are given, in order, to the method
144
+ of the endpoint whose `path` declares them:
145
+
146
+ ```ruby
147
+ ApiRecipes.github.users.show('octocat').url.to_s
148
+ # => "https://api.github.com/users/octocat"
149
+
150
+ ApiRecipes.github.repos('rails', 'rails').contributors.url.to_s
151
+ # => "https://api.github.com/repos/rails/rails/contributors"
152
+ ```
153
+
154
+ The wrong number of values raises `ApiRecipes::PathParamsMismatch`.
155
+
156
+ ### Endpoints that only group others
157
+
158
+ With `route: no` an endpoint exists only to hold nested endpoints. `search` above
159
+ is not callable, `search.repositories` is. Calling `run` on it raises
160
+ `ApiRecipes::NoRouteExists`.
161
+
162
+ ### Settings for the whole API
163
+
164
+ ```yaml
165
+ billing:
166
+ protocol: http # default: https
167
+ host: billing_service # underscores are accepted
168
+ port: 3000 # default: the port of the protocol
169
+ base_url: api/v1 # prefix of every path
170
+ timeout: 5 # seconds for the whole request, default: 3
171
+ default_headers:
172
+ Accept: application/json
173
+ endpoints:
174
+ invoices:
175
+ endpoints:
176
+ show:
177
+ path: /:id
178
+ ```
179
+
180
+ ```ruby
181
+ ApiRecipes.billing.invoices.show(42).url.to_s
182
+ # => "http://billing_service:3000/api/v1/invoices/42"
183
+ ```
184
+
185
+ All options are listed in the [options reference](#options-reference).
186
+
187
+ ## Loading the configuration
188
+
189
+ Configure ApiRecipes once, before the first request.
190
+
191
+ ### From YAML files
192
+
193
+ ```ruby
194
+ ApiRecipes.configure do |config|
195
+ config.apis_files_paths = ['config/apis/github.yml', 'config/apis/billing.yml']
196
+ end
197
+ ```
198
+
199
+ A file can describe one API or several. If two files describe the same API, the
200
+ later one replaces the earlier one entirely.
201
+
202
+ Files are evaluated with ERB before being parsed, so values can come from the
203
+ environment:
204
+
205
+ ```yaml
206
+ github:
207
+ host: <%= ENV.fetch('GITHUB_HOST', 'api.github.com') %>
208
+ timeout: <%= ENV.fetch('GITHUB_TIMEOUT', 3) %>
209
+ endpoints:
210
+ users:
211
+ ```
212
+
213
+ ### From a Hash
214
+
215
+ ```ruby
216
+ ApiRecipes.configure do |config|
217
+ config.apis_configs = {
218
+ github: {
219
+ host: 'api.github.com',
220
+ endpoints: {
221
+ users: { endpoints: { show: { path: '/:login' } } }
222
+ }
223
+ }
224
+ }
225
+ end
226
+ ```
227
+
228
+ Keys can be strings or symbols.
229
+
230
+ ## Where an API is available
231
+
232
+ ### On the `ApiRecipes` module
233
+
234
+ Every configured API is a method of the module:
235
+
236
+ ```ruby
237
+ ApiRecipes.github.users.run
238
+ ```
239
+
240
+ ### In your classes
55
241
 
56
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
242
+ Include the module and declare the APIs the class uses. They become available to
243
+ the class and to its instances:
244
+
245
+ ```ruby
246
+ class GithubProfile
247
+ include ApiRecipes
248
+ api :github
249
+
250
+ def self.all
251
+ github.users.run.data
252
+ end
253
+
254
+ def initialize(login)
255
+ @login = login
256
+ end
257
+
258
+ def repo_names
259
+ github.users.repos(@login).run.data.map { |repo| repo['name'] }
260
+ end
261
+ end
262
+ ```
263
+
264
+ ### With settings of its own
265
+
266
+ A class can change the configuration it uses. The Hash is merged over the global
267
+ one, so only the differences are needed:
268
+
269
+ ```ruby
270
+ class EnterpriseProfile
271
+ include ApiRecipes
272
+ api :github, host: 'github.example.com', timeout: 10
273
+ end
274
+
275
+ EnterpriseProfile.github.users.url.to_s
276
+ # => "https://github.example.com/users"
277
+ ```
278
+
279
+ A class can also declare an API that is not in the global configuration at all:
280
+
281
+ ```ruby
282
+ class Forecast
283
+ include ApiRecipes
284
+ api :weather, host: 'api.weather.test',
285
+ endpoints: { forecast: { path: '/forecast/:city' } }
286
+ end
287
+
288
+ Forecast.weather.forecast('rome').url.to_s
289
+ # => "https://api.weather.test/forecast/rome"
290
+ ```
291
+
292
+ ## Making requests
293
+
294
+ ### `run` or a block
295
+
296
+ `run` sends the request and returns the response:
297
+
298
+ ```ruby
299
+ response = ApiRecipes.github.users.show('octocat').run
300
+ ```
301
+
302
+ A block does the same and receives the response:
303
+
304
+ ```ruby
305
+ ApiRecipes.github.users.show('octocat') do |response|
306
+ puts response.data['name']
307
+ end
308
+ ```
309
+
310
+ Use `run` when you need the returned value: the block form returns the endpoint,
311
+ not the response.
312
+
313
+ ### Query string, body and headers
314
+
315
+ After the path parameters, an endpoint method takes one Hash of request options.
316
+ They are the options of
317
+ [http.rb](https://github.com/httprb/http/wiki/Passing-Parameters):
318
+
319
+ | Option | Sends |
320
+ | --- | --- |
321
+ | `params:` | a query string |
322
+ | `json:` | a JSON body, with `Content-Type: application/json` |
323
+ | `form:` | a form-encoded body |
324
+ | `body:` | a raw body |
325
+ | `headers:` | extra headers for this request only |
326
+
327
+ ```ruby
328
+ # GET /search/repositories?q=api%20language:ruby&per_page=5
329
+ ApiRecipes.github.search.repositories(params: { q: 'api language:ruby', per_page: 5 }).run
330
+
331
+ # GET /users/octocat/repos?type=owner
332
+ ApiRecipes.github.users.repos('octocat', params: { type: 'owner' }).run
333
+
334
+ # POST /repos/rails/rails/issues with a JSON body
335
+ ApiRecipes.github.repos('rails', 'rails').issues.create(json: { title: 'Found a bug' }).run
336
+
337
+ # One more header, only for this request
338
+ ApiRecipes.github.users.show('octocat', headers: { 'X-Request-Id' => 'abc123' }).run
339
+ ```
340
+
341
+ ### Verbs
342
+
343
+ The verb is part of the endpoint description (`verb: post`, `put`, `patch`,
344
+ `delete`, `head`...). The default is `get`.
345
+
346
+ ### Looking at the URL without calling it
347
+
348
+ ```ruby
349
+ ApiRecipes.github.users.repos('octocat').url.to_s
350
+ # => "https://api.github.com/users/octocat/repos"
351
+ ```
352
+
353
+ ## Reading the response
354
+
355
+ `run` returns an `ApiRecipes::Response`. It adds `data` and forwards everything
356
+ else to the [http.rb response](https://github.com/httprb/http/wiki/Response-Handling):
357
+
358
+ ```ruby
359
+ response = ApiRecipes.github.users.show('octocat').run
360
+
361
+ response.data # => { "login" => "octocat", ... } parsed JSON
362
+ response.code # => 200
363
+ response.status.success? # => true
364
+ response.headers['Content-Type'] # => "application/json; charset=utf-8"
365
+ response.body.to_s # => "{\"login\":\"octocat\", ...}"
366
+ ```
367
+
368
+ `data` parses the body according to the `Content-Type` of the response. For
369
+ anything that is not JSON, or for an empty body, it raises `HTTP::Error`: read
370
+ `response.body.to_s` instead. If a server sends JSON with the wrong
371
+ `Content-Type`, set `mime_type: application/json` on the endpoint or on the API.
372
+
373
+ ## Deciding what counts as success
374
+
375
+ By default any 2xx status is a success and anything else raises
376
+ `ApiRecipes::ResponseCodeNotAsExpected`. Three options change that, for one
377
+ endpoint or for the whole API.
378
+
379
+ **Expect one exact status** with `ok_code`:
380
+
381
+ ```yaml
382
+ create:
383
+ path: ''
384
+ verb: post
385
+ ok_code: 201 # a 200 is now an error
386
+ ```
387
+
388
+ **Never raise** with `on_bad_code: ignore`, and inspect the response yourself:
389
+
390
+ ```yaml
391
+ health:
392
+ on_bad_code: ignore
393
+ ```
394
+
395
+ ```ruby
396
+ response = ApiRecipes.status.health.run
397
+ response.status.success? # => false, and no exception
398
+ ```
399
+
400
+ **Decide in Ruby** with `verify_with`, the name of a method of the object that
401
+ makes the call. It receives the response; a false or nil result makes the call
402
+ fail:
403
+
404
+ ```yaml
405
+ me:
406
+ path: /user
407
+ verify_with: valid_user?
408
+ ```
409
+
410
+ ```ruby
411
+ class Account
412
+ include ApiRecipes
413
+ api :github
414
+
415
+ def current
416
+ github.me.run.data
417
+ end
418
+
419
+ private
420
+
421
+ def valid_user?(response)
422
+ response.code == 200 && response.data.key?('login')
423
+ end
424
+ end
425
+ ```
426
+
427
+ The method can be private. It is looked up on the caller, so `verify_with` is
428
+ meant for APIs used from a class or an instance.
429
+
430
+ ## Handling errors
431
+
432
+ ```ruby
433
+ begin
434
+ ApiRecipes.github.users.show('nobody-here').run
435
+ rescue ApiRecipes::ResponseCodeNotAsExpected => e
436
+ e.response.code # => 404
437
+ e.response.data # => { "message" => "Not Found", ... }
438
+ e.path # => "/users/nobody-here"
439
+ e.expected_code # => "200 <= CODE < 300"
440
+ rescue HTTP::TimeoutError
441
+ # the request took longer than `timeout`
442
+ rescue HTTP::ConnectionError
443
+ # the server could not be reached
444
+ end
445
+ ```
446
+
447
+ > **Rescue ApiRecipes errors by name.** They inherit from `Exception`, not from
448
+ > `StandardError`, so a bare `rescue` or `rescue => e` does not catch them.
449
+
450
+ | Error | Raised when |
451
+ | --- | --- |
452
+ | `ApiRecipes::ResponseCodeNotAsExpected` | the response is not a success; has `response`, `path` and `expected_code` |
453
+ | `ApiRecipes::PathParamsMismatch` | an endpoint gets the wrong number of path parameters |
454
+ | `ApiRecipes::NoRouteExists` | `run` is called on an endpoint with `route: no` |
455
+ | `ApiRecipes::ApiNameClashError` | `api :name` is declared in a class that already has a method `name` |
456
+ | `ApiRecipes::ApiConfigIsNotAnHash` | the second argument of `api` is not a Hash |
457
+ | `ApiRecipes::RouteNameClashWithExistentMethod` | a nested endpoint has the name of an existing method, such as `url`, `run` or `name` |
458
+ | `HTTP::TimeoutError`, `HTTP::ConnectionError` | http.rb network errors; these are `StandardError`s |
459
+
460
+ ## Authorization
461
+
462
+ Credentials are set on the API of the `ApiRecipes` module. They apply to every
463
+ request to that API, including those made from classes that declare it.
464
+
465
+ **A token**, sent as the `Authorization` header:
466
+
467
+ ```ruby
468
+ ApiRecipes.github.authorization = "Bearer #{ENV.fetch('GITHUB_TOKEN')}"
469
+ ```
470
+
471
+ **Basic auth:**
472
+
473
+ ```ruby
474
+ ApiRecipes.github.basic_auth = { user: 'me', pass: 'secret' }
475
+ ```
476
+
477
+ **A different token for one class**, through its own default headers:
478
+
479
+ ```ruby
480
+ class AdminClient
481
+ include ApiRecipes
482
+ api :github, default_headers: { 'Authorization' => "Bearer #{ENV.fetch('ADMIN_TOKEN')}" }
483
+ end
484
+ ```
485
+
486
+ **A different token for one request**, for example the token of the current user:
487
+
488
+ ```ruby
489
+ ApiRecipes.github.users.repos('octocat', headers: { 'Authorization' => "Bearer #{user.token}" }).run
490
+ ```
491
+
492
+ > Set credentials on `ApiRecipes.github`, not on `MyClass.github`. The second form
493
+ > returns a new object at every call, so the value is lost.
494
+
495
+ ## Logging and debugging
496
+
497
+ ```ruby
498
+ ApiRecipes.configure do |config|
499
+ config.logger = Rails.logger # any Logger; set it before the files
500
+ config.print_urls = true # print every URL to STDOUT as it is built
501
+ config.apis_files_paths = ['config/apis/github.yml']
502
+ end
503
+ ```
504
+
505
+ Without `logger`, ApiRecipes logs to STDOUT at debug level. `config.log_to`
506
+ (a path or an IO) and `config.log_level` (`:debug`, `:info`, `:warn`, `:error`,
507
+ `:fatal`) change that without building a Logger.
508
+
509
+ ## Recipes
510
+
511
+ ### Rails setup
512
+
513
+ ```ruby
514
+ # config/initializers/api_recipes.rb
515
+ ApiRecipes.configure do |config|
516
+ config.logger = Rails.logger
517
+ config.apis_files_paths = Dir[Rails.root.join('config', 'apis', '*.yml').to_s]
518
+ end
519
+
520
+ ApiRecipes.github.authorization = "Bearer #{Rails.application.credentials.github_token}"
521
+ ```
522
+
523
+ ### One host per environment
524
+
525
+ Keep one file and let ERB pick the values:
526
+
527
+ ```yaml
528
+ billing:
529
+ protocol: <%= ENV.fetch('BILLING_PROTOCOL', 'https') %>
530
+ host: <%= ENV.fetch('BILLING_HOST', 'billing.example.com') %>
531
+ endpoints:
532
+ invoices:
533
+ ```
534
+
535
+ ### Every page of a list
536
+
537
+ ```ruby
538
+ def all_repos(login)
539
+ repos = []
540
+ page = 1
541
+ loop do
542
+ batch = ApiRecipes.github.users.repos(login, params: { per_page: 100, page: page }).run.data
543
+ repos.concat(batch)
544
+ break if batch.size < 100
545
+ page += 1
546
+ end
547
+ repos
548
+ end
549
+ ```
550
+
551
+ ### Creating a resource
552
+
553
+ ```ruby
554
+ response = ApiRecipes.github.repos('rails', 'rails').issues.create(
555
+ json: { title: 'Found a bug', labels: ['bug'] }
556
+ ).run
557
+
558
+ response.code # => 201
559
+ response.data['number'] # => 1347
560
+ ```
561
+
562
+ ### A token per user in a multi-threaded application
563
+
564
+ Credentials set with `authorization=` are shared by the whole process. When each
565
+ user has a token, pass it with the request instead:
566
+
567
+ ```ruby
568
+ class UserRepos
569
+ def initialize(user)
570
+ @headers = { 'Authorization' => "Bearer #{user.github_token}" }
571
+ end
572
+
573
+ def names
574
+ ApiRecipes.github.users.repos('octocat', headers: @headers).run.data.map { |repo| repo['name'] }
575
+ end
576
+ end
577
+ ```
578
+
579
+ ### A health check that never raises
580
+
581
+ ```yaml
582
+ status:
583
+ host: status.example.com
584
+ timeout: 1
585
+ on_bad_code: ignore
586
+ endpoints:
587
+ health:
588
+ ```
589
+
590
+ ```ruby
591
+ def service_up?
592
+ ApiRecipes.status.health.run.status.success?
593
+ rescue HTTP::Error # timeout, refused connection, DNS failure
594
+ false
595
+ end
596
+ ```
597
+
598
+ ### Returning nil when a record does not exist
599
+
600
+ ```ruby
601
+ def find_user(login)
602
+ ApiRecipes.github.users.show(login).run.data
603
+ rescue ApiRecipes::ResponseCodeNotAsExpected => e
604
+ raise unless e.response.code == 404
605
+ nil
606
+ end
607
+ ```
608
+
609
+ ### Testing code that uses ApiRecipes
610
+
611
+ Stub the HTTP layer with [WebMock](https://github.com/bblimke/webmock); nothing in
612
+ ApiRecipes needs to be mocked. Send a `Content-Type`, or `data` cannot parse the
613
+ body:
614
+
615
+ ```ruby
616
+ require 'webmock/rspec'
617
+
618
+ RSpec.describe GithubProfile do
619
+ it 'lists the repository names' do
620
+ stub_request(:get, 'https://api.github.com/users/octocat/repos')
621
+ .to_return(status: 200,
622
+ body: [{ name: 'hello-world' }].to_json,
623
+ headers: { 'Content-Type' => 'application/json' })
624
+
625
+ expect(GithubProfile.new('octocat').repo_names).to eq(['hello-world'])
626
+ end
627
+ end
628
+ ```
629
+
630
+ If your tests configure ApiRecipes themselves, `ApiRecipes.reset!` removes the
631
+ global configuration and the API methods defined on the module, so each example
632
+ can start clean:
633
+
634
+ ```ruby
635
+ RSpec.configure do |config|
636
+ config.before(:each) { ApiRecipes.reset! }
637
+ end
638
+ ```
639
+
640
+ ## Options reference
641
+
642
+ ### `ApiRecipes.configure`
643
+
644
+ | Option | Meaning |
645
+ | --- | --- |
646
+ | `apis_files_paths` | Array of YAML files, evaluated with ERB |
647
+ | `apis_configs` | Hash of API descriptions |
648
+ | `logger` | a Logger to use |
649
+ | `log_to` | path or IO for the default logger (default: STDOUT) |
650
+ | `log_level` | level of the default logger (default: the `LOG_LEVEL` variable, or debug) |
651
+ | `print_urls` | print each URL to STDOUT when it is built (default: false) |
652
+
653
+ ### API
654
+
655
+ | Option | Default | Meaning |
656
+ | --- | --- | --- |
657
+ | `protocol` | `https` | `http` or `https` |
658
+ | `host` | `localhost` | host name |
659
+ | `port` | port of the protocol | port number |
660
+ | `base_url` | none | prefix of every path, for example `api/v1` |
661
+ | `timeout` | `3` | seconds allowed for the whole request |
662
+ | `default_headers` | none | headers sent with every request |
663
+ | `on_bad_code` | `raise` | `raise` or `ignore` when the response is not a success |
664
+ | `mime_type` | from the response | content type used by `response.data` |
665
+ | `verify_with` | none | method that decides whether a response is a success |
666
+ | `endpoints` | none | the endpoints of the API |
667
+
668
+ ### Endpoint
669
+
670
+ | Option | Default | Meaning |
671
+ | --- | --- | --- |
672
+ | `path` | the endpoint name | segment added to the parent path; `''` adds nothing |
673
+ | `verb` | `get` | HTTP verb |
674
+ | `route` | `yes` | `no` makes the endpoint a group that cannot be called |
675
+ | `ok_code` | any 2xx | the only status accepted as a success |
676
+ | `timeout` | the API value | seconds allowed for the whole request |
677
+ | `on_bad_code` | the API value | `raise` or `ignore` |
678
+ | `mime_type` | the API value | content type used by `response.data` |
679
+ | `verify_with` | the API or parent value | method that decides whether a response is a success |
680
+ | `endpoints` | none | nested endpoints |
681
+
682
+ An endpoint takes `timeout`, `on_bad_code` and `mime_type` from the API, not from
683
+ the endpoint that contains it.
684
+
685
+ ## Good to know
686
+
687
+ - **Configure before the first request.** A class keeps the configuration it saw
688
+ the first time it used its API.
689
+ - **Unknown options are ignored without a message.** If an option seems to have
690
+ no effect, check its spelling: the verb is `verb`, not `method`.
691
+ - **Path parameters are not encoded.** A value containing `/` spans two path
692
+ segments, and `nil` or an empty string produce the URL of the parent
693
+ (`/users/`). Check values that come from user input, and encode them with
694
+ `ERB::Util.url_encode` when they can contain reserved characters.
695
+ - **Credentials are shared by the whole process.** For a token per user, see
696
+ [the recipe above](#a-token-per-user-in-a-multi-threaded-application).
697
+ - **Some names are taken.** An endpoint cannot be called like a method its parent
698
+ already has. Directly under the API, such an endpoint (`name`, `configs`,
699
+ `object`) is skipped without a message; a nested one (`url`, `run`, `name`,
700
+ `params`, `route`, `children`) raises `RouteNameClashWithExistentMethod`. An
701
+ API must not be called `configure`, `configuration`, `logger` or `print_urls`:
702
+ it would replace that method of the module.
703
+ - **Core extensions.** The gem adds a few methods to `Hash` and `Array`
704
+ (`deep_symbolize_keys`, `deep_merge`, `extract_options!` and similar, with the
705
+ same behaviour as in ActiveSupport). With the uri gem older than 1.0 it also
706
+ makes `URI` accept underscores in host names.
707
+
708
+ ## Runnable examples
709
+
710
+ The [`examples`](examples) folder contains scripts that call public APIs:
711
+
712
+ | Script | Shows |
713
+ | --- | --- |
714
+ | `simple_usage.rb` | configuration from a YAML file; class, instance and module access |
715
+ | `direct_configuration.rb` | an API described directly in a class |
716
+ | `manual_configs.rb` | configuration from a Hash; nested endpoints with path parameters |
717
+ | `multiple_endpoints.rb` | two APIs from two files |
718
+ | `authorization.rb` | token authorization |
719
+ | `basic_auth.rb` | basic auth |
720
+ | `authorization_with_default_headers.rb` | a token for one class, through default headers |
721
+
722
+ Run one from the root of the repository:
723
+
724
+ $ bundle exec ruby examples/simple_usage.rb
725
+
726
+ [`examples/config/github.yml.erb`](examples/config/github.yml.erb) is a
727
+ configuration file with every option commented.
728
+
729
+ ## Compatibility
730
+
731
+ - Ruby 2.7 or higher. The test suite runs on the maintained versions: 3.3, 3.4 and 4.0
732
+ - http.rb from 4.4 to 5.x
733
+ - uri 0.x and 1.x
734
+
735
+ ## Development
736
+
737
+ $ bundle install
738
+ $ bundle exec rspec
739
+
740
+ The specs never use the network. To run them against a specific version of the
741
+ uri gem:
742
+
743
+ $ URI_VERSION='~> 1.0' bundle exec rspec
744
+
745
+ What is planned for the next releases is in the [roadmap](docs/ROADMAP.md); the
746
+ reasons behind the main design choices are in [docs/decisions](docs/decisions).
747
+
748
+ Bug reports and pull requests are welcome on
749
+ [GitHub](https://github.com/madAle/api_recipes). Contributors are expected to
750
+ follow the [Contributor Covenant](CODE_OF_CONDUCT.md).
751
+
752
+ ## License
57
753
 
754
+ The gem is available as open source under the terms of the
755
+ [MIT License](http://opensource.org/licenses/MIT).
@@ -1,3 +1,3 @@
1
1
  module ApiRecipes
2
- VERSION = '2.10.3'.freeze
2
+ VERSION = '2.10.4'.freeze
3
3
  end
data/lib/api_recipes.rb CHANGED
@@ -114,6 +114,17 @@ module ApiRecipes
114
114
  end
115
115
  end
116
116
 
117
+ # Removes the global api methods, the storage and the configuration.
118
+ def self.reset!
119
+ _aprcps_global_storage.each_key do |api_name|
120
+ singleton_class.send(:remove_method, api_name) if singleton_class.method_defined?(api_name, false)
121
+ end
122
+ @storage = nil
123
+ @configuration = nil
124
+ @print_urls = nil
125
+ nil
126
+ end
127
+
117
128
  def self._aprcps_global_storage
118
129
  unless @storage
119
130
  @storage = {}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_recipes
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.3
4
+ version: 2.10.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Verlato
@@ -29,6 +29,20 @@ dependencies:
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '6'
32
+ - !ruby/object:Gem::Dependency
33
+ name: logger
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
32
46
  email:
33
47
  - averlato@gmail.com
34
48
  executables: []
@@ -50,7 +64,9 @@ files:
50
64
  homepage: https://github.com/madAle/api_recipes
51
65
  licenses:
52
66
  - MIT
53
- metadata: {}
67
+ metadata:
68
+ changelog_uri: https://github.com/madAle/api_recipes/blob/master/CHANGELOG.md
69
+ source_code_uri: https://github.com/madAle/api_recipes
54
70
  rdoc_options: []
55
71
  require_paths:
56
72
  - lib