toc_doc 1.4.0 → 1.6.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: 97e03e2d5bbc3fffa25a74e1dc6367d27befc2c25009889ff55b25c2f0abad46
4
- data.tar.gz: 847c1832c1a927024404004c11c3adb966142d6e93df3de2692d9734f400b2d2
3
+ metadata.gz: 50818c97b021143ebf2d502a867fb5f20a90299a822ec800b7d02ae4d20999eb
4
+ data.tar.gz: d4ef919a143957c2685fba1f28675786743922387b0703c521a6d5c0b9ab76c3
5
5
  SHA512:
6
- metadata.gz: fe6ff537f03f02a3ecc17013558bcbe165667cc2bb1275d9cea12f28e84457672b278bdfc7f6e00a7f894be7ce5f3587bf02f70d2f6e5f7f70c0c2109372a145
7
- data.tar.gz: c710d06fb3b1137c1ff2bc4a5f8c9a79c527c7c8e777d99bffd4f11f6799ae4c8dad188a558ac6f29a4696bde45eeb70ffb62b396a2e17cc6805005cf381cb41
6
+ metadata.gz: 9aa3dd68984a7cec38789a3a718db4f7c9db9a0335ae8e47744f24990dccb73de12e41903aee8806e06f579288769c13a5b9f82327a031dd3c2052d096ae33e5
7
+ data.tar.gz: 10725265d2117c4d93720c89f5c06360ea567377c6d21f9a848e26151de7039876ac9a7b1da936801f0eaf9a7b8141689e8389b5a2917f3d550a01847aa52767
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.6.0] - 2026-03-30
4
+
5
+ ### Added
6
+
7
+ - **Timeout configuration** — two new config keys: `connect_timeout` (TCP connect, default: `5`, env: `TOCDOC_CONNECT_TIMEOUT`) and `read_timeout` (response read, default: `10`, env: `TOCDOC_READ_TIMEOUT`); passed to Faraday as `open_timeout` and `timeout` respectively
8
+ - **`per_page` guard** — `TocDoc::Configurable` now emits a warning when `per_page` exceeds the maximum value the Doctolib API can handle
9
+ - **`TocDoc::Error` hierarchy** — structured error subclasses; `TocDoc::ResponseError` carries `status`, `body`, and `headers`; specific subclasses: `BadRequest` (400), `NotFound` (404), `UnprocessableEntity` (422), `TooManyRequests` (429), `ClientError` (other 4xx), `ServerError` (5xx); `TocDoc::ConnectionError` raised on network/transport failures
10
+ - **`TocDoc::Middleware::RaiseError`** — reworked to map HTTP error codes to structured `TocDoc::Error` subclasses; wraps Faraday transport errors (`TimeoutError`, `ConnectionFailed`, `SSLError`) into `TocDoc::ConnectionError`
11
+ - **`Default.reset!`** — class method to reset memoized middleware defaults, preventing middleware stack leak between configurations
12
+
13
+ ### Changed
14
+
15
+ - **Max retry** — retry limit logic moved from `BaseMiddleware` into `TocDoc::Default`, eliminating the `BaseMiddleware` class; retry count now derived solely from `Default::MAX_RETRY`
16
+
17
+ ## [1.5.0] - 2026-03-28
18
+
19
+ ### Added
20
+
21
+ - **`TocDoc::BookingInfo`** — new envelope class for the slot-selection funnel info endpoint (`/online_booking/api/slot_selection_funnel/v1/info.json`); `BookingInfo.find(identifier)` fetches booking context by profile slug or numeric ID and returns a typed `BookingInfo` instance
22
+ - **`TocDoc::BookingInfo#profile`** — returns the typed profile (`Practitioner` or `Organization`) via `Profile.build`
23
+ - **`TocDoc::BookingInfo#specialities`** — returns an array of `TocDoc::Speciality` objects
24
+ - **`TocDoc::BookingInfo#visit_motives`** — returns an array of `TocDoc::VisitMotive` objects
25
+ - **`TocDoc::BookingInfo#agendas`** — returns an array of `TocDoc::Agenda` objects, each pre-resolved with its matching `VisitMotive` objects via `visit_motive_ids`
26
+ - **`TocDoc::BookingInfo#places`** — returns an array of `TocDoc::Place` objects
27
+ - **`TocDoc::BookingInfo#practitioners`** — returns an array of `TocDoc::Profile::Practitioner` objects (marked `partial: true`)
28
+ - **`TocDoc::BookingInfo#organization?`** — delegates to the inner profile
29
+ - **`TocDoc::VisitMotive`** — new `Resource`-based model representing a visit motive (reason for consultation); exposes `#id` and `#name` via dot-notation
30
+ - **`TocDoc::Agenda`** — new `Resource`-based model representing an agenda (calendar); exposes `#id` and `#practice_id` via dot-notation; supports pre-resolved `#visit_motives` when built through `BookingInfo`
31
+ - **`TocDoc.booking_info`** — top-level shortcut delegating to `TocDoc::BookingInfo.find`
32
+
3
33
  ## [1.4.0] - 2026-03-19
4
34
 
5
35
  ### Added
@@ -3,16 +3,16 @@
3
3
  - Interesting JSONs [GET]
4
4
  - [x] Availabilities
5
5
  - https://www.doctolib.fr/availabilities.json?visit_motive_ids=7767829&agenda_ids=1101600&practice_ids=377272&telehealth=false&start_date=2026-03-06&limit=5
6
- - Practitioner ?
6
+ - [x] Practitioner
7
7
  - https://www.doctolib.fr/pharmacie/paris/pharmacie-faidherbe.json
8
8
  - https://www.doctolib.fr/dentiste/bordeaux/mathilde-devun-lesparre-medoc.json
9
9
  - https://www.doctolib.fr/a/b/mathilde-devun-lesparre-medoc.json
10
10
  - https://www.doctolib.fr/profiles/mathilde-devun-lesparre-medoc.json
11
- - Rassemblement practiciens / Place Practitioners collection
11
+ - [x] Rassemblement practiciens / Place Practitioners collection
12
12
  - https://www.doctolib.fr/profiles/pavillon-de-la-mutualite-bordeaux-rue-vital-carles.json
13
- - Places
13
+ - [ ] Places
14
14
  - https://www.doctolib.fr/patient_app/place_autocomplete.json?query=47300
15
- - Booking context
15
+ - [x] Booking context
16
16
  - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=brigitte-devun-pujols&locale=fr
17
17
  - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388&locale=fr
18
18
  - Interesting NON JSONs
data/README.md CHANGED
@@ -29,6 +29,7 @@ A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-b
29
29
  - [Availabilities](#availabilities)
30
30
  - [Search](#search)
31
31
  - [Profile](#profile)
32
+ - [BookingInfo](#bookinginfo)
32
33
  5. [Response objects](#response-objects)
33
34
  6. [Pagination](#pagination)
34
35
  7. [Error handling](#error-handling)
@@ -60,6 +61,16 @@ or install it directly:
60
61
  gem install toc_doc
61
62
  ```
62
63
 
64
+ ### Alternative: gem.coop
65
+
66
+ If you prefer to install from [gem.coop](https://gem.coop), toc_doc is also [available there](https://beta.gem.coop/@maxime/toc_doc) :
67
+
68
+ ```ruby
69
+ source 'https://gem.coop' do
70
+ gem 'toc_doc'
71
+ end
72
+ ```
73
+
63
74
  ---
64
75
 
65
76
  ## Quick start
@@ -129,9 +140,11 @@ client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids:
129
140
  | Option | Default | Description |
130
141
  |---|---|---|
131
142
  | `api_endpoint` | `https://www.doctolib.fr` | Base URL. Change to `.de` / `.it` for other countries. |
132
- | `user_agent` | `TocDoc Ruby Gem 1.4.0` | `User-Agent` header sent with every request. |
143
+ | `user_agent` | `TocDoc Ruby Gem 1.6.0` | `User-Agent` header sent with every request. |
133
144
  | `default_media_type` | `application/json` | `Accept` and `Content-Type` headers. |
134
- | `per_page` | `15` | Default number of availability dates per request (capped at `15`). |
145
+ | `per_page` | `15` | Default number of availability dates per request (capped at `15`). Emits a warning if the value exceeds the cap. |
146
+ | `connect_timeout` | `5` | TCP connect timeout in seconds, passed to Faraday as `open_timeout`. Override via `TOCDOC_CONNECT_TIMEOUT`. |
147
+ | `read_timeout` | `10` | Response read timeout in seconds, passed to Faraday as `timeout`. Override via `TOCDOC_READ_TIMEOUT`. |
135
148
  | `middleware` | Retry + RaiseError + JSON + adapter | Full Faraday middleware stack. Override to customise completely. |
136
149
  | `connection_options` | `{}` | Options passed directly to `Faraday.new`. |
137
150
 
@@ -145,6 +158,8 @@ All primary options can be set via environment variables before the gem is loade
145
158
  | `TOCDOC_USER_AGENT` | `user_agent` |
146
159
  | `TOCDOC_MEDIA_TYPE` | `default_media_type` |
147
160
  | `TOCDOC_PER_PAGE` | `per_page` |
161
+ | `TOCDOC_CONNECT_TIMEOUT` | `connect_timeout` (default `5`) |
162
+ | `TOCDOC_READ_TIMEOUT` | `read_timeout` (default `10`) |
148
163
  | `TOCDOC_RETRY_MAX` | Maximum Faraday retry attempts (default `3`) |
149
164
 
150
165
  ---
@@ -241,6 +256,35 @@ profile.places.first.coordinates # => [44.8386722, -0.5780466]
241
256
 
242
257
  **Return value:** a `TocDoc::Profile::Practitioner` or `TocDoc::Profile::Organization` (see [Response objects](#response-objects)).
243
258
 
259
+ ### BookingInfo
260
+
261
+ Fetch the slot-selection funnel context for a practitioner or organization by slug or numeric ID.
262
+
263
+ ```ruby
264
+ # by slug
265
+ info = TocDoc::BookingInfo.find('jane-doe-bordeaux')
266
+
267
+ # by numeric ID
268
+ info = TocDoc::BookingInfo.find(1_542_899)
269
+
270
+ # module-level shortcut
271
+ info = TocDoc.booking_info('jane-doe-bordeaux')
272
+ ```
273
+
274
+ `BookingInfo.find` hits `/online_booking/api/slot_selection_funnel/v1/info.json` and returns a `TocDoc::BookingInfo` instance containing the full booking context needed to drive the appointment-booking funnel.
275
+
276
+ ```ruby
277
+ info.profile # => #<TocDoc::Profile::Practitioner ...>
278
+ info.specialities # => [#<TocDoc::Speciality ...>, ...]
279
+ info.visit_motives # => [#<TocDoc::VisitMotive id=..., name="...">, ...]
280
+ info.agendas # => [#<TocDoc::Agenda id=..., practice_id=...>, ...]
281
+ info.places # => [#<TocDoc::Place ...>, ...]
282
+ info.practitioners # => [#<TocDoc::Profile::Practitioner partial=true>, ...]
283
+ info.organization? # => false
284
+ ```
285
+
286
+ **Return value:** a `TocDoc::BookingInfo` (see [Response objects](#response-objects)).
287
+
244
288
  ---
245
289
 
246
290
  ## Response objects
@@ -337,6 +381,39 @@ Represents a practice location returned inside a full profile response. Inherits
337
381
  | `#formal_name` | `String \| nil` | Formal practice name, if available. |
338
382
  | `#coordinates` | `Array<Float>` | Convenience method returning `[latitude, longitude]`. |
339
383
 
384
+ ### `TocDoc::BookingInfo`
385
+
386
+ Returned by `TocDoc::BookingInfo.find`; also accessible via the `TocDoc.booking_info` module-level shortcut.
387
+
388
+ | Method | Type | Description |
389
+ |---|---|---|
390
+ | `#profile` | `Profile::Practitioner \| Profile::Organization` | Typed profile built via `Profile.build`. |
391
+ | `#specialities` | `Array<TocDoc::Speciality>` | Specialities associated with the booking context. |
392
+ | `#visit_motives` | `Array<TocDoc::VisitMotive>` | Available visit motives (reasons for consultation). |
393
+ | `#agendas` | `Array<TocDoc::Agenda>` | Agendas, each pre-resolved with their matching `VisitMotive` objects. |
394
+ | `#places` | `Array<TocDoc::Place>` | Practice locations. |
395
+ | `#practitioners` | `Array<TocDoc::Profile::Practitioner>` | Practitioners associated with this booking context (`partial: true`). |
396
+ | `#organization?` | `Boolean` | Delegates to the inner profile. |
397
+
398
+ ### `TocDoc::VisitMotive`
399
+
400
+ Represents a visit motive (reason for consultation) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
401
+
402
+ | Method | Type | Description |
403
+ |---|---|---|
404
+ | `#id` | `Integer` | Visit motive identifier. |
405
+ | `#name` | `String` | Human-readable name of the visit motive. |
406
+
407
+ ### `TocDoc::Agenda`
408
+
409
+ Represents an agenda (calendar) returned inside a `BookingInfo` response. Inherits dot-notation attribute access from `TocDoc::Resource`.
410
+
411
+ | Method | Type | Description |
412
+ |---|---|---|
413
+ | `#id` | `Integer` | Agenda identifier. |
414
+ | `#practice_id` | `Integer` | ID of the associated practice. |
415
+ | `#visit_motives` | `Array<TocDoc::VisitMotive>` | Visit motives pre-resolved via `visit_motive_ids` when built through `BookingInfo`. |
416
+
340
417
  ### `TocDoc::Speciality`
341
418
 
342
419
  Represents a speciality returned by the autocomplete endpoint. Inherits dot-notation attribute access from `TocDoc::Resource`.
@@ -411,8 +488,34 @@ rescue TocDoc::Error => e
411
488
  end
412
489
  ```
413
490
 
414
- The default middleware stack also includes `Faraday::Response::RaiseError` for
415
- HTTP-level failures, and a `Faraday::Retry::Middleware` that automatically
491
+ ### Error classes
492
+
493
+ | Class | Raised when |
494
+ |---|---|
495
+ | `TocDoc::Error` | Base class for all TocDoc errors. |
496
+ | `TocDoc::ConnectionError` | Network/transport failure before a response is received (DNS, timeout, SSL). The original cause is available via `#cause`. |
497
+ | `TocDoc::ResponseError` | HTTP response received but indicates an error. Carries `#status`, `#body`, and `#headers`. |
498
+ | `TocDoc::ClientError` | 4xx response not matched by a more specific class. |
499
+ | `TocDoc::BadRequest` | HTTP 400. |
500
+ | `TocDoc::NotFound` | HTTP 404. |
501
+ | `TocDoc::UnprocessableEntity` | HTTP 422. |
502
+ | `TocDoc::TooManyRequests` | HTTP 429. |
503
+ | `TocDoc::ServerError` | 5xx response. |
504
+
505
+ ```ruby
506
+ begin
507
+ TocDoc::Profile.find('unknown-slug')
508
+ rescue TocDoc::NotFound => e
509
+ puts e.status # => 404
510
+ puts e.body # => '{"error":"not found"}'
511
+ rescue TocDoc::ConnectionError => e
512
+ puts e.cause # original Faraday exception
513
+ rescue TocDoc::ServerError => e
514
+ puts "Server error: #{e.status}"
515
+ end
516
+ ```
517
+
518
+ The default middleware stack includes a `Faraday::Retry::Middleware` that automatically
416
519
  retries (up to 3 times, with exponential back-off) on:
417
520
 
418
521
  - `429 Too Many Requests`
data/TODO.md CHANGED
@@ -2,31 +2,68 @@
2
2
 
3
3
  [POTENTIAL_ENDPOINTS][POTENTIAL_ENDPOINTS.md]
4
4
 
5
- ## 1.5
6
5
 
7
- - [ ] Booking context
8
- - https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
6
+ ## 1.7 DevX
9
7
 
10
- ## 1.6
8
+ - [ ] Logging middleware (`:logger` config key)
9
+ - [ ] Resource: `define_singleton_method` on first access + `#attribute_names`
10
+ - [ ] Deep `to_h` and `to_json` on `Resource` and `BookingInfo`
11
+ - [ ] `Collection#filtered_entries` memoization
12
+ - [ ] `BookingInfo#agendas` O(n*m) → hash lookup
13
+
14
+ ## 1.8 — HTTP Layer Robustness
15
+
16
+ - [ ] Configurable availability pagination depth + `Collection#more?` / `#fetch_next_page`
17
+ - [ ] Client-side rate limiter (token-bucket middleware)
18
+ - [ ] Optional response caching (memory or ActiveSupport-compatible store)
11
19
 
12
- ### Better API usage
13
- - [ ] Rate limiting
14
- - [ ] Caching
15
- - [ ] Logging
20
+ ## 1.9 Service Layer rework
16
21
 
17
- ## 2.0
22
+ - [ ] `TocDoc::Services::Availabilities` — extract from `Availability.where`
23
+ - [ ] `TocDoc::Services::Profiles` — extract from `Profile.find`
24
+ - [ ] `TocDoc::Services::Search` — extract from `Search.where`
25
+ - [ ] `TocDoc::Services::BookingInfos` — extract from `BookingInfo.find`
26
+ - [ ] Update top-level shortcuts to delegate to services
27
+ - [ ] Deprecation on old model-level finders
18
28
 
19
- ### Auth / User-based actions
20
- - [ ] Research auth scheme
21
- - [ ] Authentication module + headers
22
- - [ ] Auth specs
29
+ ## 2.0 Breaking Changes
30
+
31
+ - [ ] Remove deprecated model-level finders
32
+ - [ ] `Place#coordinates` as `Data.define(:latitude, :longitude)`
33
+ - [ ] Integration smoke test suite (gated by ENV, weekly CI cron)
34
+ - [ ] Remove unused HTTP verbs from `Connection` (if auth doesn't ship)
23
35
 
24
36
  # ???
25
37
 
26
38
  - [ ] Figure what is `organization_statuses` in the autocomplete endpoint and what to do with it.
39
+ - [ ] Authentication module
40
+ - [ ] Place autocomplete endpoint ? (`/patient_app/place_autocomplete.json`)
27
41
 
28
42
  # DONE & RELEASED
29
43
 
44
+ ## 1.6
45
+
46
+ ### Default connection timeouts
47
+ - [x] Add `CONNECT_TIMEOUT` and `READ_TIMEOUT` constants to `Default`
48
+ - [x] Add config keys and ENV overrides (`TOCDOC_CONNECT_TIMEOUT`, `TOCDOC_READ_TIMEOUT`)
49
+ - [x] Wire into `Connection#faraday_options`
50
+
51
+ ### Error hierarchy with HTTP context
52
+ - [x] Build error subclass tree (`ConnectionError`, `ResponseError`, `ClientError`, `NotFound`, `TooManyRequests`, `ServerError`)
53
+ - [x] Rewrite `RaiseError` middleware (`on_complete` pattern, status → subclass mapping)
54
+ - [x] Remove `Faraday::Response::RaiseError` from middleware stack
55
+
56
+ ### Fixes
57
+ - [x] Fix middleware memoization leak (`Default.reset!`)
58
+ - [x] Warn on silent `per_page` clamping
59
+ - [x] Remove dead code (`base_middleware.rb`, `retry_options_fallback` duplication)
60
+
61
+ ## 1.5
62
+
63
+ - [x] Booking context
64
+ - practitioner : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=926388
65
+ - organization : https://www.doctolib.fr/online_booking/api/slot_selection_funnel/v1/info.json?profile_slug=325629
66
+
30
67
  ## 1.4
31
68
 
32
69
  - [x] Profile
@@ -101,4 +138,4 @@
101
138
  - [x] on rubygem
102
139
  - [x] release on GH
103
140
  - [x] gem.coop/@maxime
104
- - [x] Add test coverage tool
141
+ - [x] Add test coverage tool
@@ -29,6 +29,8 @@ module TocDoc
29
29
  connection_options
30
30
  default_media_type
31
31
  per_page
32
+ connect_timeout
33
+ read_timeout
32
34
  ].freeze
33
35
 
34
36
  # @!attribute [rw] api_endpoint
@@ -41,15 +43,26 @@ module TocDoc
41
43
  # @return [Hash] additional Faraday connection options
42
44
  # @!attribute [rw] default_media_type
43
45
  # @return [String] the Accept / Content-Type header value
46
+ # @!attribute [rw] connect_timeout
47
+ # @return [Integer] TCP connect timeout in seconds
48
+ # @!attribute [rw] read_timeout
49
+ # @return [Integer] read (response) timeout in seconds
44
50
  attr_accessor(*VALID_CONFIG_KEYS)
45
51
 
46
52
  # Set the number of results per page, clamped to
47
53
  # {TocDoc::Default::MAX_PER_PAGE}.
48
54
  #
55
+ # Emits a warning on +$stderr+ when +value+ exceeds the hard cap so callers
56
+ # are not silently surprised by the lower effective value.
57
+ #
49
58
  # @param value [Integer, #to_i] desired page size
50
59
  # @return [Integer] the effective page size after clamping
51
60
  def per_page=(value)
52
- @per_page = [value.to_i, TocDoc::Default::MAX_PER_PAGE].min
61
+ int = value.to_i
62
+ if int > TocDoc::Default::MAX_PER_PAGE
63
+ warn "[TocDoc] per_page #{int} exceeds MAX_PER_PAGE (#{TocDoc::Default::MAX_PER_PAGE}); clamped."
64
+ end
65
+ @per_page = [int, TocDoc::Default::MAX_PER_PAGE].min
53
66
  end
54
67
 
55
68
  # Returns the list of recognised configurable attribute names.
@@ -76,8 +89,13 @@ module TocDoc
76
89
 
77
90
  # Reset all configuration options to their {TocDoc::Default} values.
78
91
  #
92
+ # Calls {TocDoc::Default.reset!} first so that memoized values such as
93
+ # {TocDoc::Default.middleware} are cleared and rebuilt fresh on the next
94
+ # access, preventing stale middleware stacks from being reused.
95
+ #
79
96
  # @return [self]
80
97
  def reset!
98
+ TocDoc::Default.reset!
81
99
  TocDoc::Default.options.each do |key, value|
82
100
  public_send("#{key}=", value)
83
101
  end
@@ -24,7 +24,7 @@ module TocDoc
24
24
 
25
25
  # Perform a GET request.
26
26
  #
27
- # @param path [String] API path (relative to {Configurable#api_endpoint})
27
+ # @param path [String] API path (relative to {TocDoc::Configurable#api_endpoint})
28
28
  # @param options [Hash] query / header options forwarded to {#request}
29
29
  # @return [Object] parsed response body
30
30
  def get(path, options = {})
@@ -93,6 +93,7 @@ module TocDoc
93
93
  def faraday_options
94
94
  opts = connection_options.dup
95
95
  opts[:builder] = middleware if middleware
96
+ opts[:request] = { timeout: read_timeout, open_timeout: connect_timeout }
96
97
  opts
97
98
  end
98
99
 
@@ -31,6 +31,12 @@ module TocDoc
31
31
  # @return [Integer] the default maximum number of retries
32
32
  MAX_RETRY = 3
33
33
 
34
+ # @return [Integer] the default TCP connect timeout in seconds
35
+ CONNECT_TIMEOUT = 5
36
+
37
+ # @return [Integer] the default read (response) timeout in seconds
38
+ READ_TIMEOUT = 10
39
+
34
40
  class << self
35
41
  # Returns a hash of all default configuration values, suitable for
36
42
  # passing to {TocDoc::Configurable#reset!}.
@@ -43,7 +49,9 @@ module TocDoc
43
49
  default_media_type: default_media_type,
44
50
  per_page: per_page,
45
51
  middleware: middleware,
46
- connection_options: connection_options
52
+ connection_options: connection_options,
53
+ connect_timeout: connect_timeout,
54
+ read_timeout: read_timeout
47
55
  }
48
56
  end
49
57
 
@@ -91,8 +99,9 @@ module TocDoc
91
99
 
92
100
  # The default Faraday middleware stack.
93
101
  #
94
- # Includes retry logic, error raising, JSON parsing, and the default
95
- # adapter.
102
+ # Stack order (outermost first): RaiseError, retry, JSON parsing, adapter.
103
+ # RaiseError is outermost so it wraps retry and maps the final response or
104
+ # re-raised transport exception into a typed {TocDoc::Error}.
96
105
  #
97
106
  # @return [Faraday::RackBuilder]
98
107
  def middleware
@@ -106,13 +115,48 @@ module TocDoc
106
115
  @connection_options ||= {}
107
116
  end
108
117
 
118
+ # The TCP connect timeout in seconds.
119
+ #
120
+ # Falls back to the `TOCDOC_CONNECT_TIMEOUT` environment variable, then
121
+ # {CONNECT_TIMEOUT}. Invalid ENV values fall back to {CONNECT_TIMEOUT}.
122
+ #
123
+ # @return [Integer]
124
+ def connect_timeout
125
+ Integer(ENV.fetch('TOCDOC_CONNECT_TIMEOUT', CONNECT_TIMEOUT), 10)
126
+ rescue ArgumentError
127
+ CONNECT_TIMEOUT
128
+ end
129
+
130
+ # The read (response) timeout in seconds.
131
+ #
132
+ # Falls back to the `TOCDOC_READ_TIMEOUT` environment variable, then
133
+ # {READ_TIMEOUT}. Invalid ENV values fall back to {READ_TIMEOUT}.
134
+ #
135
+ # @return [Integer]
136
+ def read_timeout
137
+ Integer(ENV.fetch('TOCDOC_READ_TIMEOUT', READ_TIMEOUT), 10)
138
+ rescue ArgumentError
139
+ READ_TIMEOUT
140
+ end
141
+
142
+ # Clears all memoized values so the next call to {.middleware} and
143
+ # {.connection_options} rebuilds them from scratch.
144
+ #
145
+ # Called by {TocDoc::Configurable#reset!} to ensure each reset produces a
146
+ # fresh middleware stack rather than reusing a stale memoized instance.
147
+ #
148
+ # @return [void]
149
+ def reset!
150
+ @middleware = nil
151
+ @connection_options = nil
152
+ end
153
+
109
154
  private
110
155
 
111
156
  def build_middleware
112
157
  Faraday::RackBuilder.new do |builder|
113
- builder.request :retry, retry_options
114
158
  builder.use TocDoc::Middleware::RaiseError
115
- builder.response :raise_error
159
+ builder.request :retry, retry_options
116
160
  builder.response :json, content_type: /\bjson$/
117
161
  builder.adapter Faraday.default_adapter
118
162
  end
@@ -120,26 +164,19 @@ module TocDoc
120
164
 
121
165
  def retry_options
122
166
  {
123
- max: Integer(ENV.fetch('TOCDOC_RETRY_MAX', MAX_RETRY), 10),
167
+ max: retry_max,
124
168
  interval: 0.5,
125
169
  interval_randomness: 0.5,
126
170
  backoff_factor: 2,
127
171
  retry_statuses: [429, 500, 502, 503, 504],
128
172
  methods: %i[get head options]
129
173
  }
130
- rescue ArgumentError
131
- retry_options_fallback
132
174
  end
133
175
 
134
- def retry_options_fallback
135
- {
136
- max: MAX_RETRY,
137
- interval: 0.5,
138
- interval_randomness: 0.5,
139
- backoff_factor: 2,
140
- retry_statuses: [429, 500, 502, 503, 504],
141
- methods: %i[get head options]
142
- }
176
+ def retry_max
177
+ Integer(ENV.fetch('TOCDOC_RETRY_MAX', MAX_RETRY), 10)
178
+ rescue ArgumentError
179
+ MAX_RETRY
143
180
  end
144
181
  end
145
182
  end
@@ -3,7 +3,7 @@
3
3
  module TocDoc
4
4
  # Base error class for all TocDoc errors.
5
5
  #
6
- # Inherits from +StandardError+ so consumers can rescue `TocDoc::Error`
6
+ # Inherits from +StandardError+ so consumers can rescue +TocDoc::Error+
7
7
  # without any knowledge of Faraday or other internal HTTP details.
8
8
  #
9
9
  # @example Rescuing TocDoc errors
@@ -13,4 +13,78 @@ module TocDoc
13
13
  # puts e.message
14
14
  # end
15
15
  class Error < StandardError; end
16
+
17
+ # Raised when a network-level failure occurs before an HTTP response is
18
+ # received (e.g. DNS resolution failure, connection refused, timeout).
19
+ #
20
+ # The original low-level exception is available via Ruby's built-in
21
+ # +#cause+ mechanism — no extra attribute needed.
22
+ #
23
+ # @example
24
+ # rescue TocDoc::ConnectionError => e
25
+ # puts e.cause # original Faraday::ConnectionFailed, etc.
26
+ class ConnectionError < Error; end
27
+
28
+ # Raised when an HTTP response is received but indicates an error.
29
+ #
30
+ # Carries the raw response details so callers can act on them without
31
+ # needing to reach into Faraday internals.
32
+ #
33
+ # @example
34
+ # rescue TocDoc::ResponseError => e
35
+ # puts e.status # => 404
36
+ # puts e.body # => '{"error":"not found"}'
37
+ # puts e.headers # => {"content-type"=>"application/json"}
38
+ class ResponseError < Error
39
+ # @return [Integer] the HTTP status code
40
+ attr_reader :status
41
+
42
+ # @return [String, nil] the raw response body
43
+ attr_reader :body
44
+
45
+ # @return [Hash, nil] the response headers
46
+ attr_reader :headers
47
+
48
+ # @param status [Integer] the HTTP status code
49
+ # @param body [String, nil] the raw response body
50
+ # @param headers [Hash, nil] the response headers
51
+ # @param message [String, nil] optional override for the error message;
52
+ # defaults to +"HTTP #{status}"+
53
+ def initialize(status:, body: nil, headers: nil, message: nil)
54
+ @status = status
55
+ @body = body
56
+ @headers = headers
57
+ super(message || "HTTP #{status}")
58
+ end
59
+ end
60
+
61
+ # Raised for 4xx client error responses.
62
+ #
63
+ # @see ResponseError
64
+ class ClientError < ResponseError; end
65
+
66
+ # Raised for HTTP 400 Bad Request responses.
67
+ #
68
+ # @see ClientError
69
+ class BadRequest < ClientError; end
70
+
71
+ # Raised for HTTP 404 Not Found responses.
72
+ #
73
+ # @see ClientError
74
+ class NotFound < ClientError; end
75
+
76
+ # Raised for HTTP 422 Unprocessable Entity responses.
77
+ #
78
+ # @see ClientError
79
+ class UnprocessableEntity < ClientError; end
80
+
81
+ # Raised for HTTP 429 Too Many Requests responses.
82
+ #
83
+ # @see ClientError
84
+ class TooManyRequests < ClientError; end
85
+
86
+ # Raised for 5xx server error responses.
87
+ #
88
+ # @see ResponseError
89
+ class ServerError < ResponseError; end
16
90
  end
@@ -4,5 +4,5 @@ module TocDoc
4
4
  # The current version of the TocDoc gem.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '1.4.0'
7
+ VERSION = '1.6.0'
8
8
  end
@@ -4,24 +4,81 @@ require 'faraday'
4
4
 
5
5
  module TocDoc
6
6
  # @api private
7
+ # Namespace for Faraday middleware used internally by {TocDoc}.
7
8
  module Middleware
8
- # Faraday middleware that translates Faraday HTTP errors into
9
- # {TocDoc::Error}, keeping Faraday as an internal implementation detail.
9
+ # Faraday middleware that translates HTTP error responses and transport-level
10
+ # failures into typed {TocDoc::Error} subclasses, keeping Faraday as an
11
+ # internal implementation detail.
10
12
  #
11
- # Registered in the default middleware stack built by {TocDoc::Default}.
13
+ # Placed as the outermost middleware so the retry middleware operates on raw
14
+ # Faraday exceptions and returns the final response after exhaustion.
15
+ #
16
+ # Two-pronged error mapping:
17
+ # - +on_complete+: inspects the HTTP status and raises the appropriate
18
+ # {TocDoc::ResponseError} subclass for 4xx/5xx responses.
19
+ # - +rescue+ in +call+: catches Faraday transport errors and raises
20
+ # {TocDoc::ConnectionError}.
12
21
  #
13
22
  # @see TocDoc::Error
23
+ # @see TocDoc::ConnectionError
24
+ # @see TocDoc::ResponseError
14
25
  class RaiseError < Faraday::Middleware
15
- # Executes the request and re-raises any +Faraday::Error+ as a
16
- # {TocDoc::Error}.
26
+ # Maps specific HTTP status codes to their typed error classes.
27
+ # Statuses not in this map fall back to {TocDoc::ClientError} (4xx) or
28
+ # {TocDoc::ServerError} (5xx).
29
+ #
30
+ # @return [Hash{Integer => Class}]
31
+ STATUS_MAP = {
32
+ 400 => TocDoc::BadRequest,
33
+ 404 => TocDoc::NotFound,
34
+ 422 => TocDoc::UnprocessableEntity,
35
+ 429 => TocDoc::TooManyRequests
36
+ }.freeze
37
+
38
+ # Executes the request, raises {TocDoc::ConnectionError} on transport
39
+ # failures, and delegates HTTP error mapping to +on_complete+.
17
40
  #
18
41
  # @param env [Faraday::Env] the Faraday request environment
19
- # @return [Faraday::Env] the response environment on success
20
- # @raise [TocDoc::Error] when Faraday raises an HTTP error
42
+ # @return [Faraday::Response] the response on success
43
+ # @raise [TocDoc::ConnectionError] on network/transport-level failures
44
+ # @raise [TocDoc::ResponseError] on 4xx or 5xx HTTP responses
21
45
  def call(env)
22
- @app.call(env)
23
- rescue Faraday::Error => e
24
- raise TocDoc::Error, e.message
46
+ @app.call(env).on_complete do |response_env|
47
+ on_complete(response_env)
48
+ end
49
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError => e
50
+ raise TocDoc::ConnectionError, e.message
51
+ end
52
+
53
+ private
54
+
55
+ # Maps the HTTP status in +env+ to a typed {TocDoc::ResponseError} and
56
+ # raises it. No-ops for non-error statuses.
57
+ #
58
+ # @param env [Faraday::Env] the completed response environment
59
+ # @return [void]
60
+ # @raise [TocDoc::ResponseError] for 4xx or 5xx status codes
61
+ def on_complete(env)
62
+ status = env[:status]
63
+ body = env[:body]
64
+ headers = env[:response_headers]
65
+
66
+ error_class = error_class_for(status)
67
+ raise error_class.new(status: status, body: body, headers: headers) if error_class
68
+ end
69
+
70
+ # Resolves the error class for a given HTTP status code.
71
+ #
72
+ # @param status [Integer] the HTTP status code
73
+ # @return [Class, nil] the error class, or +nil+ if the status is not an error
74
+ def error_class_for(status)
75
+ if STATUS_MAP.key?(status)
76
+ STATUS_MAP[status]
77
+ elsif status >= 400 && status < 500
78
+ TocDoc::ClientError
79
+ elsif status >= 500
80
+ TocDoc::ServerError
81
+ end
25
82
  end
26
83
  end
27
84
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ # Represents an agenda (calendar) returned by the booking info endpoint.
5
+ #
6
+ # The +id+ and +practice_id+ fields are the primary attributes. Additional
7
+ # fields such as +visit_motive_ids+ and +visit_motive_ids_by_practice_id+ are
8
+ # accessible via dot-notation inherited from {TocDoc::Resource}.
9
+ #
10
+ # @example
11
+ # agenda = TocDoc::Agenda.new(
12
+ # 'id' => 42,
13
+ # 'practice_id' => 'practice-125055',
14
+ # 'visit_motive_ids' => [1, 2]
15
+ # )
16
+ # agenda.id #=> 42
17
+ # agenda.practice_id #=> "practice-125055"
18
+ class Agenda < Resource
19
+ main_attrs :id, :practice_id
20
+ end
21
+ end
@@ -17,6 +17,8 @@ module TocDoc
17
17
 
18
18
  attr_reader :date, :slots
19
19
 
20
+ # API path for the availabilities endpoint.
21
+ # @return [String]
20
22
  PATH = '/availabilities.json'
21
23
 
22
24
  class << self
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'toc_doc/models/profile'
4
+ require 'toc_doc/models/speciality'
5
+ require 'toc_doc/models/place'
6
+ require 'toc_doc/models/visit_motive'
7
+ require 'toc_doc/models/agenda'
8
+
9
+ module TocDoc
10
+ # Envelope returned by the slot-selection funnel info endpoint.
11
+ #
12
+ # Wraps the raw API response and exposes typed collections for the booking
13
+ # context: profile, specialities, visit motives, agendas, places, and
14
+ # practitioners.
15
+ #
16
+ # Unlike {TocDoc::Profile}, this class is NOT a {Resource} subclass — it is
17
+ # a plain envelope class, similar in role to {TocDoc::Search::Result}.
18
+ #
19
+ # @example
20
+ # info = TocDoc::BookingInfo.find('jane-doe-bordeaux')
21
+ # info.profile #=> #<TocDoc::Profile::Practitioner>
22
+ # info.visit_motives #=> [#<TocDoc::VisitMotive>, ...]
23
+ # info.organization? #=> false
24
+ class BookingInfo
25
+ # API path for the slot-selection funnel info endpoint.
26
+ # @return [String]
27
+ PATH = '/online_booking/api/slot_selection_funnel/v1/info.json'
28
+
29
+ class << self
30
+ # Fetches booking info for a profile slug or numeric ID.
31
+ #
32
+ # @param identifier [String, Integer] profile slug or numeric ID,
33
+ # forwarded as the +profile_slug+ query parameter
34
+ # @return [BookingInfo]
35
+ # @raise [ArgumentError] if +identifier+ is +nil+
36
+ #
37
+ # @example
38
+ # TocDoc::BookingInfo.find('jane-doe-bordeaux')
39
+ # TocDoc::BookingInfo.find(325629)
40
+ def find(identifier)
41
+ raise ArgumentError, 'identifier cannot be nil' if identifier.nil?
42
+
43
+ data = TocDoc.client.get(PATH, query: { profile_slug: identifier })['data']
44
+ new(data)
45
+ end
46
+ end
47
+
48
+ # @param data [Hash] the +data+ value from the API response body
49
+ def initialize(data)
50
+ @data = data
51
+ end
52
+
53
+ # The profile associated with this booking context, typed via
54
+ # {TocDoc::Profile.build}.
55
+ #
56
+ # @return [TocDoc::Profile::Practitioner, TocDoc::Profile::Organization]
57
+ def profile
58
+ @profile ||= Profile.build(@data['profile'])
59
+ end
60
+
61
+ # All specialities for this booking context.
62
+ #
63
+ # @return [Array<TocDoc::Speciality>]
64
+ def specialities
65
+ @specialities ||= Array(@data['specialities']).map do |speciality_attrs|
66
+ Speciality.new(speciality_attrs)
67
+ end
68
+ end
69
+
70
+ # All visit motives for this booking context.
71
+ #
72
+ # @return [Array<TocDoc::VisitMotive>]
73
+ def visit_motives
74
+ @visit_motives ||= Array(@data['visit_motives']).map do |visit_motive_attrs|
75
+ VisitMotive.new(visit_motive_attrs)
76
+ end
77
+ end
78
+
79
+ # All agendas for this booking context.
80
+ #
81
+ # @return [Array<TocDoc::Agenda>]
82
+ def agendas
83
+ @agendas ||= Array(@data['agendas']).map do |agenda_attrs|
84
+ agenda_visit_motives = visit_motives.select do |vm|
85
+ agenda_attrs['visit_motive_ids'].include?(vm.id)
86
+ end
87
+
88
+ Agenda.new(agenda_attrs.merge('visit_motives' => agenda_visit_motives))
89
+ end
90
+ end
91
+
92
+ # All practice locations for this booking context.
93
+ #
94
+ # @return [Array<TocDoc::Place>]
95
+ def places
96
+ @places ||= Array(@data['places']).map do |place_attrs|
97
+ Place.new(place_attrs)
98
+ end
99
+ end
100
+
101
+ # All practitioners associated with this booking context.
102
+ #
103
+ # Always constructed as {TocDoc::Profile::Practitioner} since the
104
+ # +practitioners+ array in this endpoint exclusively contains practitioners.
105
+ # Marked as partial since the data is a summary, not a full profile page.
106
+ #
107
+ # @return [Array<TocDoc::Profile::Practitioner>]
108
+ def practitioners
109
+ @practitioners ||= Array(@data['practitioners']).map do |practitioner_attrs|
110
+ Profile::Practitioner.new(practitioner_attrs.merge('partial' => true))
111
+ end
112
+ end
113
+
114
+ # Returns +true+ when the top-level profile is an organization.
115
+ #
116
+ # Delegates to {TocDoc::Profile#organization?}.
117
+ #
118
+ # @return [Boolean]
119
+ def organization?
120
+ profile.organization?
121
+ end
122
+
123
+ # Returns the raw data hash.
124
+ #
125
+ # @return [Hash]
126
+ def to_h
127
+ @data
128
+ end
129
+ end
130
+ end
@@ -27,6 +27,12 @@ module TocDoc
27
27
  # place.latitude #=> 44.8386722
28
28
  # place.elevator #=> true
29
29
  class Place < Resource
30
+ # Returns the geographic coordinates of the place.
31
+ #
32
+ # @return [Array(Float, Float)] +[latitude, longitude]+
33
+ #
34
+ # @example
35
+ # place.coordinates #=> [44.8386722, -0.5780466]
30
36
  def coordinates
31
37
  [latitude, longitude]
32
38
  end
@@ -6,8 +6,14 @@ module TocDoc
6
6
  class Practitioner < Profile
7
7
  main_attrs :name_with_title
8
8
 
9
+ # Returns the practitioner's display name.
10
+ #
11
+ # Prefers +name_with_title+ (e.g. "Dr Jane Doe") when present,
12
+ # falling back to plain +name+.
13
+ #
14
+ # @return [String]
9
15
  def to_s
10
- name_with_title || name
16
+ (respond_to?(:name_with_title) && name_with_title) || name
11
17
  end
12
18
  end
13
19
  end
@@ -12,6 +12,8 @@ module TocDoc
12
12
  # profile.practitioner? #=> true
13
13
  # profile.name #=> "Dr Smith"
14
14
  class Profile < Resource
15
+ # API path template for a full profile page (+sprintf+-style, requires +identifier+).
16
+ # @return [String]
15
17
  PATH = '/profiles/%<identifier>s.json'
16
18
 
17
19
  main_attrs :id, :partial
@@ -19,20 +21,26 @@ module TocDoc
19
21
  class << self
20
22
  # Factory — returns a +Profile::Practitioner+ or +Profile::Organization+.
21
23
  #
22
- # Resolves type via +owner_type+ first (search context), then falls back
24
+ # Resolves type via +owner_type+ first (autocomplete context), then falls back
23
25
  # to the boolean flags present on profile-page responses.
24
26
  #
25
27
  # @param attrs [Hash] raw attribute hash from the API response
26
28
  # @return [Profile::Practitioner, Profile::Organization]
29
+ # @raise [ArgumentError] if +attrs+ contains an unknown +owner_type+ or the
30
+ # profile type cannot be determined from the available flags
31
+ #
32
+ # @example Build from an autocomplete result
33
+ # TocDoc::Profile.build('owner_type' => 'Account', 'name' => 'Dr Smith')
34
+ # @example Build from a full profile response
35
+ # TocDoc::Profile.build('is_practitioner' => true, 'name' => 'Dr Smith')
27
36
  def build(attrs = {})
28
37
  attrs = normalize_attrs(attrs)
29
- return find(attrs['value']) if attrs['force_full_profile'] && attrs['owner_type']
30
38
 
31
- case attrs['owner_type']
32
- when 'Account' then Practitioner.new(attrs.merge('partial' => true))
33
- when 'Organization' then Organization.new(attrs.merge('partial' => true))
34
- else build_from_flags(attrs)
35
- end
39
+ return find(attrs['value']) if attrs['force_full_profile']
40
+
41
+ build_from_autocomplete(attrs) ||
42
+ build_from_booking_info(attrs) ||
43
+ build_from_full_profile(attrs)
36
44
  end
37
45
 
38
46
  # Fetches a full profile page by slug or numeric ID.
@@ -53,7 +61,31 @@ module TocDoc
53
61
 
54
62
  private
55
63
 
56
- def build_from_flags(attrs)
64
+ # Autocomplete results carry an +owner_type+ key that tells us the
65
+ # profile type directly. When +force_full_profile+ is set, fetches
66
+ # the full profile page instead of building a partial.
67
+ #
68
+ # @param attrs [Hash] normalized attribute hash
69
+ # @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
70
+ # +owner_type+ is absent
71
+ # @raise [ArgumentError] if +owner_type+ is present but unrecognised
72
+ def build_from_autocomplete(attrs)
73
+ return unless attrs['owner_type']
74
+
75
+ case attrs['owner_type']
76
+ when 'Account' then Practitioner.new(attrs.merge('partial' => true))
77
+ when 'Organization' then Organization.new(attrs.merge('partial' => true))
78
+ else raise ArgumentError, "Unknown owner_type: #{attrs['owner_type']}"
79
+ end
80
+ end
81
+
82
+ # Builds a profile from a full profile-page response.
83
+ #
84
+ # @param attrs [Hash] normalized attribute hash from a full profile page
85
+ # @return [Profile::Practitioner, Profile::Organization]
86
+ # @raise [ArgumentError] if neither +is_practitioner+ nor +organization+
87
+ # flag is present in +attrs+
88
+ def build_from_full_profile(attrs)
57
89
  if attrs['is_practitioner']
58
90
  Practitioner.new(attrs.merge('partial' => false))
59
91
  elsif attrs['organization']
@@ -63,6 +95,26 @@ module TocDoc
63
95
  end
64
96
  end
65
97
 
98
+ # Booking-info profiles carry `organization` (true/false) but lack
99
+ # `is_practitioner`. Returns nil when the shape doesn't match so the
100
+ # caller can fall through to build_from_full_profile.
101
+ #
102
+ # @param attrs [Hash] normalized attribute hash
103
+ # @return [Profile::Practitioner, Profile::Organization, nil] +nil+ when
104
+ # the booking-info shape is not matched
105
+ def build_from_booking_info(attrs)
106
+ return unless attrs.key?('organization') && !attrs.key?('is_practitioner')
107
+
108
+ return Organization.new(attrs.merge('partial' => true)) if attrs['organization']
109
+
110
+ Practitioner.new(attrs.merge('partial' => true))
111
+ end
112
+
113
+ # Extracts and coerces profile attributes from the raw API response.
114
+ #
115
+ # @param data [Hash] the +data+ key from the API response body
116
+ # @return [Hash] merged attribute hash with +speciality+ and +places+
117
+ # coerced into model objects
66
118
  def profile_attrs(data)
67
119
  data['profile'].merge(
68
120
  'speciality' => TocDoc::Speciality.new(data['profile']['speciality'] || {}),
@@ -78,6 +130,9 @@ module TocDoc
78
130
  # Returns all skills across all practices as an array of {TocDoc::Resource}.
79
131
  #
80
132
  # @return [Array<TocDoc::Resource>]
133
+ #
134
+ # @example
135
+ # profile.skills #=> [#<TocDoc::Resource ...>, ...]
81
136
  def skills
82
137
  hash = self['skills_by_practice'] || {}
83
138
  hash.values.flatten.map { |s| TocDoc::Resource.new(s) }
@@ -87,17 +142,26 @@ module TocDoc
87
142
  #
88
143
  # @param practice_id [Integer, String] the practice ID
89
144
  # @return [Array<TocDoc::Resource>]
145
+ #
146
+ # @example
147
+ # profile.skills_for(123) #=> [#<TocDoc::Resource ...>, ...]
90
148
  def skills_for(practice_id)
91
149
  hash = self['skills_by_practice'] || {}
92
150
  Array(hash[practice_id.to_s]).map { |s| TocDoc::Resource.new(s) }
93
151
  end
94
152
 
95
153
  # @return [Boolean] true when this profile is a practitioner
154
+ #
155
+ # @example
156
+ # profile.practitioner? #=> true
96
157
  def practitioner?
97
158
  is_a?(Practitioner)
98
159
  end
99
160
 
100
161
  # @return [Boolean] true when this profile is an organization
162
+ #
163
+ # @example
164
+ # profile.organization? #=> false
101
165
  def organization?
102
166
  is_a?(Organization)
103
167
  end
@@ -17,7 +17,12 @@ module TocDoc
17
17
  # TocDoc::Search.where(query: 'dentiste', type: 'practitioner')
18
18
  # #=> [#<TocDoc::Profile::Practitioner>, ...]
19
19
  class Search
20
+ # API path for the autocomplete / searchbar endpoint.
21
+ # @return [String]
20
22
  PATH = '/api/searchbar/autocomplete.json'
23
+
24
+ # Accepted values for the +type:+ keyword in {.where}.
25
+ # @return [Array<String>]
21
26
  VALID_TYPES = %w[profile practitioner organization speciality].freeze
22
27
 
23
28
  class << self
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ # Represents a visit motive (reason for consultation) returned by the booking
5
+ # info endpoint.
6
+ #
7
+ # The +id+ and +name+ fields are the primary attributes. Additional fields
8
+ # such as +restrictions+ are accessible via dot-notation inherited from
9
+ # {TocDoc::Resource}.
10
+ #
11
+ # @example
12
+ # motive = TocDoc::VisitMotive.new('id' => 1, 'name' => 'Consultation', 'restrictions' => [])
13
+ # motive.id #=> 1
14
+ # motive.name #=> "Consultation"
15
+ class VisitMotive < Resource
16
+ main_attrs :id, :name
17
+ end
18
+ end
@@ -3,6 +3,9 @@
3
3
  require 'toc_doc/models/resource'
4
4
  require 'toc_doc/models/speciality'
5
5
  require 'toc_doc/models/place'
6
+ require 'toc_doc/models/visit_motive'
7
+ require 'toc_doc/models/agenda'
8
+ require 'toc_doc/models/booking_info'
6
9
  require 'toc_doc/models/search'
7
10
 
8
11
  require 'toc_doc/models/availability'
data/lib/toc_doc.rb CHANGED
@@ -100,6 +100,17 @@ module TocDoc
100
100
  TocDoc::Profile.find(identifier)
101
101
  end
102
102
 
103
+ # Fetches booking context data for a profile by slug or numeric ID.
104
+ #
105
+ # Delegates to {TocDoc::BookingInfo.find} — see that method for full
106
+ # parameter documentation.
107
+ #
108
+ # @param identifier [String, Integer] profile slug or numeric ID
109
+ # @return [TocDoc::BookingInfo]
110
+ def booking_info(identifier)
111
+ TocDoc::BookingInfo.find(identifier)
112
+ end
113
+
103
114
  # @!visibility private
104
115
  def method_missing(method_name, ...)
105
116
  if client.respond_to?(method_name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toc_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 01max
@@ -71,11 +71,12 @@ files:
71
71
  - lib/toc_doc/core/uri_utils.rb
72
72
  - lib/toc_doc/core/version.rb
73
73
  - lib/toc_doc/http/middleware/raise_error.rb
74
- - lib/toc_doc/http/response/base_middleware.rb
75
74
  - lib/toc_doc/middleware/.keep
76
75
  - lib/toc_doc/models.rb
76
+ - lib/toc_doc/models/agenda.rb
77
77
  - lib/toc_doc/models/availability.rb
78
78
  - lib/toc_doc/models/availability/collection.rb
79
+ - lib/toc_doc/models/booking_info.rb
79
80
  - lib/toc_doc/models/place.rb
80
81
  - lib/toc_doc/models/profile.rb
81
82
  - lib/toc_doc/models/profile/organization.rb
@@ -84,6 +85,7 @@ files:
84
85
  - lib/toc_doc/models/search.rb
85
86
  - lib/toc_doc/models/search/result.rb
86
87
  - lib/toc_doc/models/speciality.rb
88
+ - lib/toc_doc/models/visit_motive.rb
87
89
  - sig/toc_doc.rbs
88
90
  homepage: https://github.com/01max/toc_doc
89
91
  licenses:
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
-
5
- module TocDoc
6
- # @api private
7
- module Response
8
- # Abstract base class for TocDoc Faraday response middleware.
9
- #
10
- # Subclasses **must** override {#on_complete} to inspect or transform
11
- # the response environment.
12
- #
13
- # @abstract
14
- class BaseMiddleware < Faraday::Middleware
15
- # Called by Faraday after the response has been received.
16
- #
17
- # @param env [Faraday::Env] the Faraday response environment
18
- # @return [void]
19
- # @raise [NotImplementedError] always — subclasses must override
20
- def on_complete(env)
21
- raise NotImplementedError, "#{self.class} must implement #on_complete"
22
- end
23
- end
24
- end
25
- end