toc_doc 1.0.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 +7 -0
- data/.env.example +17 -0
- data/.yardopts +12 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.md +595 -0
- data/README.md +370 -0
- data/Rakefile +12 -0
- data/TODO.md +77 -0
- data/lib/toc_doc/client/availabilities.rb +118 -0
- data/lib/toc_doc/client.rb +55 -0
- data/lib/toc_doc/core/authentication.rb +15 -0
- data/lib/toc_doc/core/configurable.rb +121 -0
- data/lib/toc_doc/core/connection.rb +196 -0
- data/lib/toc_doc/core/default.rb +163 -0
- data/lib/toc_doc/core/error.rb +16 -0
- data/lib/toc_doc/core/uri_utils.rb +30 -0
- data/lib/toc_doc/core/version.rb +8 -0
- data/lib/toc_doc/http/middleware/raise_error.rb +28 -0
- data/lib/toc_doc/http/response/base_middleware.rb +25 -0
- data/lib/toc_doc/middleware/.keep +0 -0
- data/lib/toc_doc/models/availability.rb +21 -0
- data/lib/toc_doc/models/resource.rb +82 -0
- data/lib/toc_doc/models/response/availability.rb +79 -0
- data/lib/toc_doc/models.rb +6 -0
- data/lib/toc_doc.rb +90 -0
- data/sig/toc_doc.rbs +4 -0
- metadata +107 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'toc_doc/core/default'
|
|
4
|
+
|
|
5
|
+
module TocDoc
|
|
6
|
+
# Mixin providing shared configuration behaviour for both the top-level
|
|
7
|
+
# {TocDoc} module and individual {TocDoc::Client} instances.
|
|
8
|
+
#
|
|
9
|
+
# Include this module to gain attribute accessors for every configurable key,
|
|
10
|
+
# a block-based {#configure} helper, and a {#reset!} method to restore
|
|
11
|
+
# defaults from {TocDoc::Default}.
|
|
12
|
+
#
|
|
13
|
+
# @example Module-level configuration
|
|
14
|
+
# TocDoc.configure do |config|
|
|
15
|
+
# config.api_endpoint = 'https://www.doctolib.de'
|
|
16
|
+
# config.per_page = 10
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Per-client configuration
|
|
20
|
+
# client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.it')
|
|
21
|
+
#
|
|
22
|
+
# @see TocDoc::Default
|
|
23
|
+
module Configurable
|
|
24
|
+
# @return [Array<Symbol>] all recognised configuration keys
|
|
25
|
+
VALID_CONFIG_KEYS = %i[
|
|
26
|
+
api_endpoint
|
|
27
|
+
user_agent
|
|
28
|
+
middleware
|
|
29
|
+
connection_options
|
|
30
|
+
default_media_type
|
|
31
|
+
per_page
|
|
32
|
+
auto_paginate
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# @!attribute [rw] api_endpoint
|
|
36
|
+
# @return [String] the base URL for API requests
|
|
37
|
+
# @!attribute [rw] user_agent
|
|
38
|
+
# @return [String] the User-Agent header value
|
|
39
|
+
# @!attribute [rw] middleware
|
|
40
|
+
# @return [Faraday::RackBuilder, nil] custom Faraday middleware stack
|
|
41
|
+
# @!attribute [rw] connection_options
|
|
42
|
+
# @return [Hash] additional Faraday connection options
|
|
43
|
+
# @!attribute [rw] default_media_type
|
|
44
|
+
# @return [String] the Accept / Content-Type header value
|
|
45
|
+
# @!attribute [rw] auto_paginate
|
|
46
|
+
# @return [Boolean] whether to follow pagination automatically
|
|
47
|
+
attr_accessor(*VALID_CONFIG_KEYS)
|
|
48
|
+
|
|
49
|
+
# Set the number of results per page, clamped to
|
|
50
|
+
# {TocDoc::Default::MAX_PER_PAGE}.
|
|
51
|
+
#
|
|
52
|
+
# @param value [Integer, #to_i] desired page size
|
|
53
|
+
# @return [Integer] the effective page size after clamping
|
|
54
|
+
def per_page=(value)
|
|
55
|
+
@per_page = [value.to_i, TocDoc::Default::MAX_PER_PAGE].min
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the list of recognised configurable attribute names.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Symbol>]
|
|
61
|
+
def self.keys
|
|
62
|
+
VALID_CONFIG_KEYS
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Yields +self+ so callers can set options in a block.
|
|
66
|
+
#
|
|
67
|
+
# @yield [config] the object being configured
|
|
68
|
+
# @yieldparam config [TocDoc::Configurable] self
|
|
69
|
+
# @return [self]
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# TocDoc.configure do |config|
|
|
73
|
+
# config.api_endpoint = 'https://www.doctolib.de'
|
|
74
|
+
# end
|
|
75
|
+
def configure
|
|
76
|
+
yield self
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Reset all configuration options to their {TocDoc::Default} values.
|
|
81
|
+
#
|
|
82
|
+
# @return [self]
|
|
83
|
+
def reset!
|
|
84
|
+
TocDoc::Default.options.each do |key, value|
|
|
85
|
+
public_send("#{key}=", value)
|
|
86
|
+
end
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns a frozen snapshot of the current configuration as a Hash.
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash{Symbol => Object}]
|
|
93
|
+
def options
|
|
94
|
+
Configurable.keys.to_h { |key| [key, public_send(key)] }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Compares the given options to the current configuration.
|
|
98
|
+
#
|
|
99
|
+
# Used internally for memoising the {TocDoc.client} instance — a new
|
|
100
|
+
# client is created only when the options have actually changed.
|
|
101
|
+
#
|
|
102
|
+
# @param other_options [Hash{Symbol => Object}] options to compare
|
|
103
|
+
# @return [Boolean] +true+ when both option sets are equal
|
|
104
|
+
def same_options?(other_options)
|
|
105
|
+
candidate =
|
|
106
|
+
if other_options.respond_to?(:to_hash)
|
|
107
|
+
other_options.to_hash.transform_keys!(&:to_sym)
|
|
108
|
+
else
|
|
109
|
+
other_options
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
candidate == options
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @!visibility private
|
|
116
|
+
# When extended (e.g., by the TocDoc module), initialize with defaults.
|
|
117
|
+
def self.extended(base)
|
|
118
|
+
base.reset!
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module TocDoc
|
|
6
|
+
# Faraday-based HTTP connection and request helpers.
|
|
7
|
+
#
|
|
8
|
+
# Included into {TocDoc::Client} to provide low-level HTTP verbs
|
|
9
|
+
# (`get`, `post`, `put`, `patch`, `delete`, `head`), a memoised
|
|
10
|
+
# Faraday connection, pagination support, and response tracking.
|
|
11
|
+
#
|
|
12
|
+
# All HTTP methods are **private** — callers interact with them through
|
|
13
|
+
# higher-level endpoint modules (e.g. {TocDoc::Client::Availabilities}).
|
|
14
|
+
#
|
|
15
|
+
# @see TocDoc::Client
|
|
16
|
+
module Connection
|
|
17
|
+
# The most-recent raw Faraday response, set after every request.
|
|
18
|
+
#
|
|
19
|
+
# @return [Faraday::Response, nil]
|
|
20
|
+
attr_accessor :last_response
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Perform a GET request.
|
|
25
|
+
#
|
|
26
|
+
# @param path [String] API path (relative to {Configurable#api_endpoint})
|
|
27
|
+
# @param options [Hash] query / header options forwarded to {#request}
|
|
28
|
+
# @return [Object] parsed response body
|
|
29
|
+
def get(path, options = {})
|
|
30
|
+
request(:get, path, nil, options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Perform a POST request.
|
|
34
|
+
#
|
|
35
|
+
# @param path [String] API path
|
|
36
|
+
# @param data [Object, nil] request body
|
|
37
|
+
# @param options [Hash] query / header options
|
|
38
|
+
# @return [Object] parsed response body
|
|
39
|
+
def post(path, data = nil, options = {})
|
|
40
|
+
request(:post, path, data, options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Perform a PUT request.
|
|
44
|
+
#
|
|
45
|
+
# @param path [String] API path
|
|
46
|
+
# @param data [Object, nil] request body
|
|
47
|
+
# @param options [Hash] query / header options
|
|
48
|
+
# @return [Object] parsed response body
|
|
49
|
+
def put(path, data = nil, options = {})
|
|
50
|
+
request(:put, path, data, options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Perform a PATCH request.
|
|
54
|
+
#
|
|
55
|
+
# @param path [String] API path
|
|
56
|
+
# @param data [Object, nil] request body
|
|
57
|
+
# @param options [Hash] query / header options
|
|
58
|
+
# @return [Object] parsed response body
|
|
59
|
+
def patch(path, data = nil, options = {})
|
|
60
|
+
request(:patch, path, data, options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Perform a DELETE request.
|
|
64
|
+
#
|
|
65
|
+
# @param path [String] API path
|
|
66
|
+
# @param options [Hash] query / header options
|
|
67
|
+
# @return [Object] parsed response body
|
|
68
|
+
def delete(path, options = {})
|
|
69
|
+
request(:delete, path, nil, options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Perform a HEAD request.
|
|
73
|
+
#
|
|
74
|
+
# @param path [String] API path
|
|
75
|
+
# @param options [Hash] query / header options
|
|
76
|
+
# @return [Object] parsed response body
|
|
77
|
+
def head(path, options = {})
|
|
78
|
+
request(:head, path, nil, options)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Memoised Faraday connection configured from the current
|
|
82
|
+
# {Configurable} options.
|
|
83
|
+
#
|
|
84
|
+
# @return [Faraday::Connection]
|
|
85
|
+
def agent
|
|
86
|
+
@agent ||= Faraday.new(api_endpoint, faraday_options) do |conn|
|
|
87
|
+
configure_faraday_headers(conn)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Hash] merged Faraday connection options
|
|
92
|
+
def faraday_options
|
|
93
|
+
opts = connection_options.dup
|
|
94
|
+
opts[:builder] = middleware if middleware
|
|
95
|
+
opts
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Sets default HTTP headers on a Faraday connection.
|
|
99
|
+
#
|
|
100
|
+
# @param conn [Faraday::Connection]
|
|
101
|
+
# @return [void]
|
|
102
|
+
def configure_faraday_headers(conn)
|
|
103
|
+
conn.headers['Accept'] = default_media_type
|
|
104
|
+
conn.headers['Content-Type'] = default_media_type
|
|
105
|
+
conn.headers['User-Agent'] = user_agent
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Performs a paginated GET, accumulating results across pages.
|
|
109
|
+
#
|
|
110
|
+
# When {Configurable#auto_paginate} is disabled or no block is given,
|
|
111
|
+
# behaves exactly like {#get}.
|
|
112
|
+
#
|
|
113
|
+
# When +auto_paginate+ is +true+ **and** a block is provided, the block is
|
|
114
|
+
# yielded after every page fetch — including the first — with
|
|
115
|
+
# +(accumulator, last_response)+. The block must:
|
|
116
|
+
#
|
|
117
|
+
# 1. Detect whether it is a continuation call by comparing object identity:
|
|
118
|
+
# `acc.equal?(last_response.body)` is `true` only on the first yield,
|
|
119
|
+
# when the accumulator *is* the first-page body. On subsequent yields
|
|
120
|
+
# the block should merge `last_response.body` into `acc`.
|
|
121
|
+
# 2. Return a Hash of options to pass to the next {#get} call (pagination
|
|
122
|
+
# continues), or `nil` / `false` to halt.
|
|
123
|
+
#
|
|
124
|
+
# @param path [String] the API path
|
|
125
|
+
# @param options [Hash] query / header options forwarded to every request
|
|
126
|
+
# @yieldparam acc [Object] the growing accumulator (first-page body initially)
|
|
127
|
+
# @yieldparam last_response [Faraday::Response] the most-recent raw response
|
|
128
|
+
# @yieldreturn [Hash, nil] next-page options, or +nil+/+false+ to halt
|
|
129
|
+
# @return [Object] the fully-accumulated response body
|
|
130
|
+
def paginate(path, options = {}, &)
|
|
131
|
+
data = get(path, options)
|
|
132
|
+
return data unless block_given? && auto_paginate
|
|
133
|
+
|
|
134
|
+
loop do
|
|
135
|
+
next_options = yield(data, last_response)
|
|
136
|
+
break unless next_options
|
|
137
|
+
|
|
138
|
+
get(path, next_options)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
data
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns a boolean based on the HTTP response status of the given request.
|
|
145
|
+
#
|
|
146
|
+
# @param method [Symbol] HTTP verb (e.g. +:get+, +:head+)
|
|
147
|
+
# @param path [String] API path
|
|
148
|
+
# @param options [Hash] query / header options
|
|
149
|
+
# @return [Boolean] +true+ when the response is 2xx
|
|
150
|
+
def boolean_from_response?(method, path, options = {})
|
|
151
|
+
request(method, path, nil, options)
|
|
152
|
+
status = last_response&.status
|
|
153
|
+
|
|
154
|
+
(200..299).cover?(status)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Core request helper used by all HTTP verb methods.
|
|
158
|
+
#
|
|
159
|
+
# @param method [Symbol] HTTP verb
|
|
160
|
+
# @param path [String] API path
|
|
161
|
+
# @param data [Object, nil] optional request body
|
|
162
|
+
# @param options [Hash] query / header options
|
|
163
|
+
# @return [Object] parsed response body
|
|
164
|
+
def request(method, path, data = nil, options = {})
|
|
165
|
+
query, headers = parse_query_and_convenience_headers(options)
|
|
166
|
+
|
|
167
|
+
response = agent.public_send(method) do |req|
|
|
168
|
+
req.url(path, query)
|
|
169
|
+
req.headers.update(headers) unless headers.empty?
|
|
170
|
+
req.body = data if data
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
self.last_response = response
|
|
174
|
+
response.body
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Splits a generic options hash into query params and headers.
|
|
178
|
+
#
|
|
179
|
+
# Supports explicit `:query` and `:headers` keys; otherwise treats
|
|
180
|
+
# remaining keys as query params.
|
|
181
|
+
#
|
|
182
|
+
# @param options [Hash] combined query/header options
|
|
183
|
+
# @return [Array(Hash, Hash)] a two-element array of `[query, headers]`
|
|
184
|
+
def parse_query_and_convenience_headers(options)
|
|
185
|
+
return [{}, {}] if options.nil? || options.empty?
|
|
186
|
+
|
|
187
|
+
opts = options.dup
|
|
188
|
+
explicit_query = opts.delete(:query)
|
|
189
|
+
explicit_headers = opts.delete(:headers) || {}
|
|
190
|
+
|
|
191
|
+
query = explicit_query || opts
|
|
192
|
+
|
|
193
|
+
[query || {}, explicit_headers]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/retry'
|
|
5
|
+
|
|
6
|
+
require 'toc_doc/http/middleware/raise_error'
|
|
7
|
+
|
|
8
|
+
module TocDoc
|
|
9
|
+
# Provides sensible default values for every configurable option.
|
|
10
|
+
#
|
|
11
|
+
# Each value can be overridden per-environment via the corresponding `TOCDOC_*`
|
|
12
|
+
# environment variable (see individual methods).
|
|
13
|
+
#
|
|
14
|
+
# @see TocDoc::Configurable
|
|
15
|
+
module Default
|
|
16
|
+
# @return [String] the default API base URL
|
|
17
|
+
API_ENDPOINT = 'https://www.doctolib.fr'
|
|
18
|
+
|
|
19
|
+
# @return [String] the default User-Agent header
|
|
20
|
+
USER_AGENT = "TocDoc Ruby Gem #{TocDoc::VERSION}".freeze
|
|
21
|
+
|
|
22
|
+
# @return [String] the default Accept / Content-Type media type
|
|
23
|
+
MEDIA_TYPE = 'application/json'
|
|
24
|
+
|
|
25
|
+
# @return [Integer] the default number of results per page
|
|
26
|
+
PER_PAGE = 15
|
|
27
|
+
|
|
28
|
+
# @return [Integer] the hard upper limit for per_page
|
|
29
|
+
MAX_PER_PAGE = 15
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] whether to auto-paginate by default
|
|
32
|
+
AUTO_PAGINATE = false
|
|
33
|
+
|
|
34
|
+
# @return [Integer] the default maximum number of retries
|
|
35
|
+
MAX_RETRY = 3
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Returns a hash of all default configuration values, suitable for
|
|
39
|
+
# passing to {TocDoc::Configurable#reset!}.
|
|
40
|
+
#
|
|
41
|
+
# @return [Hash{Symbol => Object}]
|
|
42
|
+
def options
|
|
43
|
+
{
|
|
44
|
+
api_endpoint: api_endpoint,
|
|
45
|
+
user_agent: user_agent,
|
|
46
|
+
default_media_type: default_media_type,
|
|
47
|
+
per_page: per_page,
|
|
48
|
+
auto_paginate: auto_paginate,
|
|
49
|
+
middleware: middleware,
|
|
50
|
+
connection_options: connection_options
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The base API endpoint URL.
|
|
55
|
+
#
|
|
56
|
+
# Falls back to the `TOCDOC_API_ENDPOINT` environment variable, then
|
|
57
|
+
# {API_ENDPOINT}.
|
|
58
|
+
#
|
|
59
|
+
# @return [String]
|
|
60
|
+
def api_endpoint
|
|
61
|
+
ENV.fetch('TOCDOC_API_ENDPOINT', API_ENDPOINT)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The User-Agent header sent with every request.
|
|
65
|
+
#
|
|
66
|
+
# Falls back to the `TOCDOC_USER_AGENT` environment variable, then
|
|
67
|
+
# {USER_AGENT}.
|
|
68
|
+
#
|
|
69
|
+
# @return [String]
|
|
70
|
+
def user_agent
|
|
71
|
+
ENV.fetch('TOCDOC_USER_AGENT', USER_AGENT)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The Accept / Content-Type media type.
|
|
75
|
+
#
|
|
76
|
+
# Falls back to the `TOCDOC_MEDIA_TYPE` environment variable, then
|
|
77
|
+
# {MEDIA_TYPE}.
|
|
78
|
+
#
|
|
79
|
+
# @return [String]
|
|
80
|
+
def default_media_type
|
|
81
|
+
ENV.fetch('TOCDOC_MEDIA_TYPE', MEDIA_TYPE)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Number of results per page, clamped to {MAX_PER_PAGE}.
|
|
85
|
+
#
|
|
86
|
+
# Falls back to the `TOCDOC_PER_PAGE` environment variable, then
|
|
87
|
+
# {PER_PAGE}.
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
def per_page
|
|
91
|
+
[Integer(ENV.fetch('TOCDOC_PER_PAGE', PER_PAGE), 10), MAX_PER_PAGE].min
|
|
92
|
+
rescue ArgumentError
|
|
93
|
+
PER_PAGE
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether to follow pagination automatically.
|
|
97
|
+
#
|
|
98
|
+
# Falls back to the `TOCDOC_AUTO_PAGINATE` environment variable (set to
|
|
99
|
+
# `"true"` to enable), then {AUTO_PAGINATE}.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def auto_paginate
|
|
103
|
+
env_val = ENV.fetch('TOCDOC_AUTO_PAGINATE', nil)
|
|
104
|
+
return AUTO_PAGINATE if env_val.nil?
|
|
105
|
+
|
|
106
|
+
env_val.casecmp('true').zero?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The default Faraday middleware stack.
|
|
110
|
+
#
|
|
111
|
+
# Includes retry logic, error raising, JSON parsing, and the default
|
|
112
|
+
# adapter.
|
|
113
|
+
#
|
|
114
|
+
# @return [Faraday::RackBuilder]
|
|
115
|
+
def middleware
|
|
116
|
+
@middleware ||= build_middleware
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Default Faraday connection options (empty by default).
|
|
120
|
+
#
|
|
121
|
+
# @return [Hash]
|
|
122
|
+
def connection_options
|
|
123
|
+
@connection_options ||= {}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def build_middleware
|
|
129
|
+
Faraday::RackBuilder.new do |builder|
|
|
130
|
+
builder.request :retry, retry_options
|
|
131
|
+
builder.use TocDoc::Middleware::RaiseError
|
|
132
|
+
builder.response :raise_error
|
|
133
|
+
builder.response :json, content_type: /\bjson$/
|
|
134
|
+
builder.adapter Faraday.default_adapter
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def retry_options
|
|
139
|
+
{
|
|
140
|
+
max: Integer(ENV.fetch('TOCDOC_RETRY_MAX', MAX_RETRY), 10),
|
|
141
|
+
interval: 0.5,
|
|
142
|
+
interval_randomness: 0.5,
|
|
143
|
+
backoff_factor: 2,
|
|
144
|
+
retry_statuses: [429, 500, 502, 503, 504],
|
|
145
|
+
methods: %i[get head options]
|
|
146
|
+
}
|
|
147
|
+
rescue ArgumentError
|
|
148
|
+
retry_options_fallback
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def retry_options_fallback
|
|
152
|
+
{
|
|
153
|
+
max: MAX_RETRY,
|
|
154
|
+
interval: 0.5,
|
|
155
|
+
interval_randomness: 0.5,
|
|
156
|
+
backoff_factor: 2,
|
|
157
|
+
retry_statuses: [429, 500, 502, 503, 504],
|
|
158
|
+
methods: %i[get head options]
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Base error class for all TocDoc errors.
|
|
5
|
+
#
|
|
6
|
+
# Inherits from +StandardError+ so consumers can rescue `TocDoc::Error`
|
|
7
|
+
# without any knowledge of Faraday or other internal HTTP details.
|
|
8
|
+
#
|
|
9
|
+
# @example Rescuing TocDoc errors
|
|
10
|
+
# begin
|
|
11
|
+
# TocDoc.availabilities(visit_motive_ids: 0, agenda_ids: 0)
|
|
12
|
+
# rescue TocDoc::Error => e
|
|
13
|
+
# puts e.message
|
|
14
|
+
# end
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# URL building helpers for Doctolib API parameters.
|
|
5
|
+
#
|
|
6
|
+
# Doctolib expects certain ID list parameters to be dash-joined strings
|
|
7
|
+
# rather than standard repeated/bracket array notation. Include this module
|
|
8
|
+
# into endpoint modules and call +dashed_ids+ explicitly for each such param:
|
|
9
|
+
#
|
|
10
|
+
# module TocDoc::Client::Availabilities
|
|
11
|
+
# include TocDoc::UriUtils
|
|
12
|
+
#
|
|
13
|
+
# def availabilities(visit_motive_ids:, agenda_ids:, **opts)
|
|
14
|
+
# get('/availabilities.json', query: {
|
|
15
|
+
# visit_motive_ids: dashed_ids(visit_motive_ids),
|
|
16
|
+
# agenda_ids: dashed_ids(agenda_ids),
|
|
17
|
+
# **opts
|
|
18
|
+
# })
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
module UriUtils
|
|
22
|
+
# Joins one or many IDs into the dash-separated format expected by Doctolib.
|
|
23
|
+
#
|
|
24
|
+
# @param ids [Integer, String, Array] one or more IDs
|
|
25
|
+
# @return [String] e.g. "1234-5678-9012"
|
|
26
|
+
def dashed_ids(ids)
|
|
27
|
+
Array(ids).flatten.compact.map(&:to_s).reject(&:empty?).join('-')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module TocDoc
|
|
6
|
+
# @api private
|
|
7
|
+
module Middleware
|
|
8
|
+
# Faraday middleware that translates Faraday HTTP errors into
|
|
9
|
+
# {TocDoc::Error}, keeping Faraday as an internal implementation detail.
|
|
10
|
+
#
|
|
11
|
+
# Registered in the default middleware stack built by {TocDoc::Default}.
|
|
12
|
+
#
|
|
13
|
+
# @see TocDoc::Error
|
|
14
|
+
class RaiseError < Faraday::Middleware
|
|
15
|
+
# Executes the request and re-raises any +Faraday::Error+ as a
|
|
16
|
+
# {TocDoc::Error}.
|
|
17
|
+
#
|
|
18
|
+
# @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
|
|
21
|
+
def call(env)
|
|
22
|
+
@app.call(env)
|
|
23
|
+
rescue Faraday::Error => e
|
|
24
|
+
raise TocDoc::Error, e.message
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# Represents a single availability date entry returned by the Doctolib API.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# avail = TocDoc::Availability.new('date' => '2026-02-28', 'slots' => ['2026-02-28T10:00:00.000+01:00'])
|
|
8
|
+
# avail.date #=> "2026-02-28"
|
|
9
|
+
# avail.slots #=> ["2026-02-28T10:00:00.000+01:00"]
|
|
10
|
+
class Availability < Resource
|
|
11
|
+
# @return [String] date in ISO 8601 format (e.g. "2026-02-28")
|
|
12
|
+
def date
|
|
13
|
+
@attrs['date']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Array<String>] ISO 8601 datetime strings for each available slot
|
|
17
|
+
def slots
|
|
18
|
+
@attrs['slots'] || []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TocDoc
|
|
4
|
+
# A lightweight wrapper providing dot-notation access to response fields.
|
|
5
|
+
# Backed by a Hash, with +method_missing+ for attribute access and +#to_h+ for
|
|
6
|
+
# round-tripping back to a plain Hash.
|
|
7
|
+
#
|
|
8
|
+
# Unlike Sawyer, TocDoc does not use hypermedia relations, so this wrapper
|
|
9
|
+
# intentionally stays minimal.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# resource = TocDoc::Resource.new('date' => '2026-02-28', 'slots' => [])
|
|
13
|
+
# resource.date #=> "2026-02-28"
|
|
14
|
+
# resource[:date] #=> "2026-02-28"
|
|
15
|
+
# resource.to_h #=> { "date" => "2026-02-28", "slots" => [] }
|
|
16
|
+
class Resource
|
|
17
|
+
# @param attrs [Hash] the raw attribute hash (string or symbol keys)
|
|
18
|
+
def initialize(attrs = {})
|
|
19
|
+
@attrs = attrs.transform_keys(&:to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Read an attribute by name.
|
|
23
|
+
#
|
|
24
|
+
# @param key [String, Symbol] attribute name
|
|
25
|
+
# @return [Object, nil] the attribute value, or +nil+ if not present
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# resource[:date] #=> "2026-02-28"
|
|
29
|
+
def [](key)
|
|
30
|
+
@attrs[key.to_s]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Write an attribute by name.
|
|
34
|
+
#
|
|
35
|
+
# @param key [String, Symbol] attribute name
|
|
36
|
+
# @param value [Object] the value to set
|
|
37
|
+
# @return [Object] the value
|
|
38
|
+
def []=(key, value)
|
|
39
|
+
@attrs[key.to_s] = value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Return a plain Hash representation (shallow copy).
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{String => Object}]
|
|
45
|
+
def to_h
|
|
46
|
+
@attrs.dup
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Equality comparison.
|
|
50
|
+
#
|
|
51
|
+
# Two {Resource} instances are equal when their attribute hashes match.
|
|
52
|
+
# A {Resource} is also equal to a plain +Hash+ with equivalent keys.
|
|
53
|
+
#
|
|
54
|
+
# @param other [Resource, Hash, Object]
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def ==(other)
|
|
57
|
+
case other
|
|
58
|
+
when Resource then @attrs == other.to_h
|
|
59
|
+
when Hash then @attrs == other.transform_keys(&:to_s)
|
|
60
|
+
else false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @!visibility private
|
|
65
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
66
|
+
@attrs.key?(method_name.to_s) || super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Provides dot-notation access to response fields.
|
|
70
|
+
#
|
|
71
|
+
# Any method call whose name matches a key in the underlying attribute
|
|
72
|
+
# hash is dispatched here.
|
|
73
|
+
#
|
|
74
|
+
# @param method_name [Symbol] the method name
|
|
75
|
+
# @return [Object] the attribute value
|
|
76
|
+
# @raise [NoMethodError] when the key does not exist
|
|
77
|
+
def method_missing(method_name, *_args)
|
|
78
|
+
key = method_name.to_s
|
|
79
|
+
@attrs.key?(key) ? @attrs[key] : super
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|