toc_doc 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,13 +5,13 @@ module TocDoc
5
5
  #
6
6
  # Doctolib expects certain ID list parameters to be dash-joined strings
7
7
  # rather than standard repeated/bracket array notation. Include this module
8
- # into endpoint modules and call +dashed_ids+ explicitly for each such param:
8
+ # and call +dashed_ids+ explicitly for each such param:
9
9
  #
10
- # module TocDoc::Client::Availabilities
11
- # include TocDoc::UriUtils
10
+ # class TocDoc::Availability
11
+ # extend TocDoc::UriUtils
12
12
  #
13
- # def availabilities(visit_motive_ids:, agenda_ids:, **opts)
14
- # get('/availabilities.json', query: {
13
+ # def self.where(visit_motive_ids:, agenda_ids:, **opts)
14
+ # client.get('/availabilities.json', query: {
15
15
  # visit_motive_ids: dashed_ids(visit_motive_ids),
16
16
  # agenda_ids: dashed_ids(agenda_ids),
17
17
  # **opts
@@ -4,5 +4,5 @@ module TocDoc
4
4
  # The current version of the TocDoc gem.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '1.1.0'
7
+ VERSION = '1.3.0'
8
8
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'toc_doc/models/availability'
5
+
6
+ module TocDoc
7
+ class Availability
8
+ # An Enumerable collection of {TocDoc::Availability} instances returned
9
+ # by {TocDoc::Availability.where}.
10
+ #
11
+ # @example Iterate over available slots
12
+ # collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)
13
+ # collection.each { |avail| puts avail.date }
14
+ #
15
+ # @example Access metadata
16
+ # collection.total #=> 5
17
+ # collection.next_slot #=> "2026-02-28T10:00:00.000+01:00"
18
+ class Collection
19
+ include Enumerable
20
+
21
+ attr_reader :path, :query
22
+
23
+ # @param data [Hash] parsed first-page response body
24
+ # @param query [Hash] original query params (used to build next-page requests)
25
+ # @param path [String] API path for subsequent requests
26
+ def initialize(data, query: {}, path: '/availabilities.json')
27
+ @data = data.dup
28
+ @query = query
29
+ @path = path
30
+ end
31
+
32
+ # Iterates over {TocDoc::Availability} instances that have at least one slot.
33
+ #
34
+ # @yieldparam availability [TocDoc::Availability]
35
+ # @return [Enumerator] if no block given
36
+ def each(&)
37
+ filtered_entries.each(&)
38
+ end
39
+
40
+ # The total number of available slots in the collection.
41
+ #
42
+ # @return [Integer]
43
+ def total
44
+ @data['total']
45
+ end
46
+
47
+ # The nearest available appointment slot.
48
+ #
49
+ # Returns the +next_slot+ value from the API when present (which only
50
+ # occurs when none of the loaded dates have any slots). Otherwise
51
+ # returns the first slot of the first date that has one.
52
+ #
53
+ # @return [String, nil] ISO 8601 datetime string, or +nil+ when unavailable
54
+ def next_slot
55
+ return @data['next_slot'] if @data.key?('next_slot')
56
+
57
+ Array(@data['availabilities']).each do |entry|
58
+ slots = Array(entry['slots'])
59
+ return slots.first unless slots.empty?
60
+ end
61
+
62
+ nil
63
+ end
64
+
65
+ # All date entries — including those with no slots — as {TocDoc::Availability}
66
+ # objects.
67
+ #
68
+ # @return [Array<TocDoc::Availability>]
69
+ def raw_availabilities
70
+ Array(@data['availabilities']).map { |entry| TocDoc::Availability.new(entry) }
71
+ end
72
+
73
+ # Returns a plain Hash representation of the collection.
74
+ #
75
+ # The +availabilities+ key contains only dates with slots (filtered),
76
+ # serialised back to plain Hashes.
77
+ #
78
+ # @return [Hash{String => Object}]
79
+ def to_h
80
+ @data.merge('availabilities' => filtered_entries.map(&:to_h))
81
+ end
82
+
83
+ # Fetches the next window of availabilities (starting the day after the
84
+ # last date in the current collection) and merges them in.
85
+ #
86
+ # @param page_data [Hash] parsed response body to merge into this collection
87
+ # @return [self]
88
+ def merge_page!(page_data)
89
+ @data['availabilities'] = @data.fetch('availabilities', []) + page_data.fetch('availabilities', [])
90
+ @data['total'] = @data.fetch('total', 0) + page_data.fetch('total', 0)
91
+ self
92
+ end
93
+
94
+ private
95
+
96
+ def filtered_entries
97
+ Array(@data['availabilities'])
98
+ .select { |entry| Array(entry['slots']).any? }
99
+ .map { |entry| TocDoc::Availability.new(entry) }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'date'
4
+ require 'toc_doc/core/uri_utils'
4
5
 
5
6
  module TocDoc
6
7
  # Represents a single availability date entry returned by the Doctolib API.
@@ -12,9 +13,77 @@ module TocDoc
12
13
  # avail.slots #=> [#<DateTime: 2026-02-28T10:00:00.000+01:00>]
13
14
  # avail.raw_slots #=> ["2026-02-28T10:00:00.000+01:00"]
14
15
  class Availability < Resource
16
+ extend TocDoc::UriUtils
17
+
15
18
  attr_reader :date, :slots
16
19
 
17
- # @param attrs [Hash] raw attributes from the API response, expected to include
20
+ PATH = '/availabilities.json'
21
+
22
+ class << self
23
+ # Fetches availabilities from the API and returns an {Availability::Collection}.
24
+ #
25
+ # When the API response contains a +next_slot+ key — indicating that no
26
+ # date in the current window has available slots — a second request is
27
+ # made automatically from that date before the collection is returned.
28
+ #
29
+ # @param visit_motive_ids [Integer, String, Array<Integer>]
30
+ # one or more visit-motive IDs (dash-joined for the API)
31
+ # @param agenda_ids [Integer, String, Array<Integer>]
32
+ # one or more agenda IDs (dash-joined for the API)
33
+ # @param start_date [Date, String]
34
+ # earliest date to search from (default: +Date.today+)
35
+ # @param limit [Integer]
36
+ # maximum availability dates per page (default: +TocDoc.per_page+)
37
+ # @param options [Hash]
38
+ # additional query params forwarded verbatim to the API
39
+ # @return [TocDoc::Availability::Collection]
40
+ #
41
+ # @example
42
+ # TocDoc::Availability.where(
43
+ # visit_motive_ids: 7_767_829,
44
+ # agenda_ids: [1_101_600],
45
+ # start_date: Date.today
46
+ # ).each { |avail| puts avail.date }
47
+ def where(visit_motive_ids:, agenda_ids:, start_date: Date.today,
48
+ limit: TocDoc.per_page, **options)
49
+ client = TocDoc.client
50
+ query = build_query(visit_motive_ids, agenda_ids, start_date, limit, options)
51
+ data = client.get(PATH, query: query)
52
+
53
+ merge_next_page(client, query, data) if data['next_slot']
54
+
55
+ Collection.new(data, query: query, path: PATH)
56
+ end
57
+
58
+ private
59
+
60
+ def merge_next_page(client, query, current_page)
61
+ next_date = Date.parse(current_page['next_slot']).to_s
62
+ next_page = client.get(PATH, query: query.merge(start_date: next_date))
63
+ merge_page_data(current_page, next_page)
64
+ end
65
+
66
+ def merge_page_data(current_page, next_page)
67
+ current_page['availabilities'] =
68
+ current_page.fetch('availabilities', []) + next_page.fetch('availabilities', [])
69
+ current_page['total'] = current_page.fetch('total', 0) + next_page.fetch('total', 0)
70
+ current_page.delete('next_slot')
71
+ current_page['next_slot'] = next_page['next_slot'] if next_page.key?('next_slot')
72
+ end
73
+
74
+ def build_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
75
+ {
76
+ visit_motive_ids: dashed_ids(visit_motive_ids),
77
+ agenda_ids: dashed_ids(agenda_ids),
78
+ start_date: start_date.to_s,
79
+ limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
80
+ **extra
81
+ }
82
+ end
83
+ end
84
+
85
+ # @param attrs [Hash] raw attributes from the API response; expected to include
86
+ # a +date+ key (ISO 8601 date string) and a +slots+ key (Array of ISO 8601 datetime strings)
18
87
  def initialize(*attrs)
19
88
  super
20
89
  raw = build_raw(@attrs)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ class Profile
5
+ # An organization profile.
6
+ class Organization < Profile; end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ class Profile
5
+ # A practitioner profile (raw +owner_type: "Account"+).
6
+ class Practitioner < Profile; end
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ # Represents a search profile result (practitioner or organization).
5
+ # Inherits dot-notation attribute access from +TocDoc::Resource+.
6
+ #
7
+ # Use +Profile.build+ to obtain the correctly typed subclass instance.
8
+ #
9
+ # @example
10
+ # profile = TocDoc::Profile.build('owner_type' => 'Account', 'name' => 'Dr Smith')
11
+ # profile.class #=> TocDoc::Profile::Practitioner
12
+ # profile.practitioner? #=> true
13
+ # profile.name #=> "Dr Smith"
14
+ class Profile < Resource
15
+ # Factory — returns a +Profile::Practitioner+ or +Profile::Organization+
16
+ # depending on the +owner_type+ field of the raw attribute hash.
17
+ #
18
+ # @param attrs [Hash] raw attribute hash from the API response
19
+ # @return [Profile::Practitioner, Profile::Organization]
20
+ def self.build(attrs = {})
21
+ case attrs['owner_type'] || attrs[:owner_type]
22
+ when 'Account'
23
+ Practitioner.new(attrs)
24
+ else
25
+ Organization.new(attrs)
26
+ end
27
+ end
28
+
29
+ # @return [Boolean] true when this profile is a practitioner
30
+ def practitioner?
31
+ is_a?(Practitioner)
32
+ end
33
+
34
+ # @return [Boolean] true when this profile is an organization
35
+ def organization?
36
+ is_a?(Organization)
37
+ end
38
+ end
39
+ end
40
+
41
+ require 'toc_doc/models/profile/practitioner'
42
+ require 'toc_doc/models/profile/organization'
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'toc_doc/models/profile'
4
+ require 'toc_doc/models/speciality'
5
+
6
+ module TocDoc
7
+ class Search
8
+ # Envelope returned by {TocDoc::Search.where} when no +type:+ filter is given.
9
+ #
10
+ # Wraps the raw API response and exposes typed collections for profiles and
11
+ # specialities. Unlike {TocDoc::Availability::Collection} this class does
12
+ # NOT include +Enumerable+ — the dual-type nature of the result does not
13
+ # lend itself to a single iteration interface.
14
+ #
15
+ # @example
16
+ # result = TocDoc::Search.where(query: 'dentiste')
17
+ # result.profiles #=> [#<TocDoc::Profile::Practitioner>, ...]
18
+ # result.specialities #=> [#<TocDoc::Speciality>, ...]
19
+ class Result
20
+ # @param data [Hash] raw parsed response body from the autocomplete endpoint
21
+ def initialize(data)
22
+ @profiles = build_profiles(data['profiles'])
23
+ @specialities = build_specialities(data['specialities'])
24
+ end
25
+
26
+ # All profile results, typed as {TocDoc::Profile::Practitioner} or
27
+ # {TocDoc::Profile::Organization} via {TocDoc::Profile.build}.
28
+ #
29
+ # @return [Array<TocDoc::Profile::Practitioner, TocDoc::Profile::Organization>]
30
+ attr_reader :profiles
31
+
32
+ # All speciality results as {TocDoc::Speciality} instances.
33
+ #
34
+ # @return [Array<TocDoc::Speciality>]
35
+ attr_reader :specialities
36
+
37
+ # Returns a subset of results narrowed to the given type.
38
+ #
39
+ # @param type [String] one of +'profile'+, +'practitioner'+, +'organization'+, +'speciality'+
40
+ # @return [Array<TocDoc::Profile>, Array<TocDoc::Speciality>]
41
+ def filter_by_type(type)
42
+ case type
43
+ when 'profile' then profiles
44
+ when 'practitioner' then profiles.select(&:practitioner?)
45
+ when 'organization' then profiles.select(&:organization?)
46
+ when 'speciality' then specialities
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def build_profiles(raw)
53
+ Array(raw).map { |attrs| TocDoc::Profile.build(attrs) }
54
+ end
55
+
56
+ def build_specialities(raw)
57
+ Array(raw).map { |attrs| TocDoc::Speciality.new(attrs) }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'toc_doc/models/search/result'
4
+
5
+ module TocDoc
6
+ # Entry point for the autocomplete / search endpoint.
7
+ #
8
+ # Unlike {TocDoc::Availability}, +Search+ is not itself a resource — it is a
9
+ # plain service class that wraps the API call and returns a typed result.
10
+ #
11
+ # @example Fetch everything
12
+ # result = TocDoc::Search.where(query: 'dentiste')
13
+ # result #=> #<TocDoc::Search::Result>
14
+ # result.profiles #=> [#<TocDoc::Profile::Practitioner>, ...]
15
+ #
16
+ # @example Filter by type
17
+ # TocDoc::Search.where(query: 'dentiste', type: 'practitioner')
18
+ # #=> [#<TocDoc::Profile::Practitioner>, ...]
19
+ class Search
20
+ PATH = '/api/searchbar/autocomplete.json'
21
+ VALID_TYPES = %w[profile practitioner organization speciality].freeze
22
+
23
+ class << self
24
+ # Queries the autocomplete endpoint and returns a {Search::Result} or a
25
+ # filtered array.
26
+ #
27
+ # The +type:+ keyword is handled client-side only — it is never forwarded
28
+ # to the API. The full response is always fetched; narrowing happens after.
29
+ #
30
+ # @param query [String] the search term
31
+ # @param type [String, nil] optional filter; one of +'profile'+,
32
+ # +'practitioner'+, +'organization'+, +'speciality'+
33
+ # @param options [Hash] additional query params forwarded verbatim to the API
34
+ # @return [Search::Result] when +type:+ is +nil+
35
+ # @return [Array<TocDoc::Profile>] when +type:+ is +'profile'+, +'practitioner'+,
36
+ # or +'organization'+
37
+ # @return [Array<TocDoc::Speciality>] when +type:+ is +'speciality'+
38
+ # @raise [ArgumentError] if +type:+ is not +nil+ and not in {VALID_TYPES}
39
+ #
40
+ # @example
41
+ # TocDoc::Search.where(query: 'derma', type: 'speciality')
42
+ # #=> [#<TocDoc::Speciality name="Dermatologue">, ...]
43
+ def where(query:, type: nil, **options)
44
+ if !type.nil? && !VALID_TYPES.include?(type)
45
+ raise ArgumentError, "Invalid type #{type.inspect}. Must be one of: #{VALID_TYPES.join(', ')}"
46
+ end
47
+
48
+ data = TocDoc.client.get(PATH, query: { search: query, **options })
49
+ result = Result.new(data)
50
+
51
+ type.nil? ? result : result.filter_by_type(type)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TocDoc
4
+ # Represents a speciality returned by the autocomplete endpoint.
5
+ #
6
+ # All fields (+value+, +slug+, +name+) are primitives and are accessed via
7
+ # dot-notation inherited from {TocDoc::Resource}.
8
+ #
9
+ # @example
10
+ # speciality = TocDoc::Speciality.new('value' => 228, 'slug' => 'homeopathe', 'name' => 'Homéopathe')
11
+ # speciality.value #=> 228
12
+ # speciality.slug #=> "homeopathe"
13
+ # speciality.name #=> "Homéopathe"
14
+ class Speciality < Resource
15
+ end
16
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'toc_doc/models/resource'
4
+ require 'toc_doc/models/search'
5
+
4
6
  require 'toc_doc/models/availability'
7
+ require 'toc_doc/models/availability/collection'
5
8
 
6
- require 'toc_doc/models/response/availability'
9
+ require 'toc_doc/models/profile'
10
+ require 'toc_doc/models/speciality'
data/lib/toc_doc.rb CHANGED
@@ -16,13 +16,9 @@ require 'toc_doc/client'
16
16
  # Configuration can be set at the module level and will be inherited by every
17
17
  # {TocDoc::Client} instance created via {.client} or {.setup}.
18
18
  #
19
- # Any method available on {TocDoc::Client} can be called directly on `TocDoc`
20
- # and will be forwarded to the memoized {.client}.
21
- #
22
19
  # @example Quick start
23
20
  # TocDoc.setup do |config|
24
21
  # config.api_endpoint = 'https://www.doctolib.de'
25
- # config.auto_paginate = true
26
22
  # end
27
23
  #
28
24
  # TocDoc.availabilities(
@@ -70,6 +66,29 @@ module TocDoc
70
66
  client
71
67
  end
72
68
 
69
+ # Returns available appointment slots.
70
+ #
71
+ # Delegates to {TocDoc::Availability.where} — see that method for full
72
+ # parameter documentation.
73
+ #
74
+ # @return [TocDoc::Availability::Collection]
75
+ def availabilities(**)
76
+ TocDoc::Availability.where(**)
77
+ end
78
+
79
+ # Queries the autocomplete / search endpoint.
80
+ #
81
+ # Delegates to {TocDoc::Search.where} — see that method for full
82
+ # parameter documentation.
83
+ #
84
+ # @return [TocDoc::Search::Result] when called without +type:+
85
+ # @return [Array<TocDoc::Profile>] when +type:+ is +'profile'+,
86
+ # +'practitioner'+, or +'organization'+
87
+ # @return [Array<TocDoc::Speciality>] when +type:+ is +'speciality'+
88
+ def search(**)
89
+ TocDoc::Search.where(**)
90
+ end
91
+
73
92
  # @!visibility private
74
93
  def method_missing(method_name, ...)
75
94
  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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 01max
@@ -57,12 +57,12 @@ files:
57
57
  - CHANGELOG.md
58
58
  - CODE_OF_CONDUCT.md
59
59
  - LICENSE.md
60
+ - POTENTIAL_ENDPOINTS.md
60
61
  - README.md
61
62
  - Rakefile
62
63
  - TODO.md
63
64
  - lib/toc_doc.rb
64
65
  - lib/toc_doc/client.rb
65
- - lib/toc_doc/client/availabilities.rb
66
66
  - lib/toc_doc/core/authentication.rb
67
67
  - lib/toc_doc/core/configurable.rb
68
68
  - lib/toc_doc/core/connection.rb
@@ -75,8 +75,14 @@ files:
75
75
  - lib/toc_doc/middleware/.keep
76
76
  - lib/toc_doc/models.rb
77
77
  - lib/toc_doc/models/availability.rb
78
+ - lib/toc_doc/models/availability/collection.rb
79
+ - lib/toc_doc/models/profile.rb
80
+ - lib/toc_doc/models/profile/organization.rb
81
+ - lib/toc_doc/models/profile/practitioner.rb
78
82
  - lib/toc_doc/models/resource.rb
79
- - lib/toc_doc/models/response/availability.rb
83
+ - lib/toc_doc/models/search.rb
84
+ - lib/toc_doc/models/search/result.rb
85
+ - lib/toc_doc/models/speciality.rb
80
86
  - sig/toc_doc.rbs
81
87
  homepage: https://github.com/01max/toc_doc
82
88
  licenses:
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
-
5
- module TocDoc
6
- class Client
7
- # Endpoint module for the Doctolib availabilities API.
8
- #
9
- # Included into {TocDoc::Client}; methods delegate to
10
- # {TocDoc::Connection#get} via the enclosing client instance.
11
- #
12
- # @see https://www.doctolib.fr/availabilities.json Doctolib availability endpoint
13
- module Availabilities
14
- # Returns available appointment slots for the given visit motives and
15
- # agendas.
16
- #
17
- # When +auto_paginate+ is enabled, all pages of results are
18
- # fetched and merged automatically.
19
- #
20
- # @param visit_motive_ids [Integer, String, Array<Integer>]
21
- # one or more visit-motive IDs (dash-joined for the API)
22
- # @param agenda_ids [Integer, String, Array<Integer>]
23
- # one or more agenda IDs (dash-joined for the API)
24
- # @param start_date [Date, String]
25
- # earliest date to search from (default: +Date.today+)
26
- # @param limit [Integer]
27
- # maximum number of availability dates per page
28
- # (default: +per_page+ config)
29
- # @param options [Hash]
30
- # additional query params forwarded verbatim to the API
31
- # (e.g. +practice_ids:+, +telehealth:+)
32
- # @return [TocDoc::Response::Availability] structured response object
33
- #
34
- # @example Fetch availabilities for a single practitioner
35
- # client.availabilities(
36
- # visit_motive_ids: 7_767_829,
37
- # agenda_ids: [1_101_600],
38
- # practice_ids: 377_272,
39
- # telehealth: false
40
- # )
41
- #
42
- # @example Via the module-level shortcut
43
- # TocDoc.availabilities(visit_motive_ids: 123, agenda_ids: 456)
44
- def availabilities(visit_motive_ids:, agenda_ids:, start_date: Date.today, limit: per_page, **options)
45
- base_query = build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, options)
46
-
47
- response = paginate('/availabilities.json', query: base_query) do |acc, last_resp|
48
- paginate_availability_page(acc, last_resp, base_query)
49
- end
50
-
51
- TocDoc::Response::Availability.new(response)
52
- end
53
-
54
- private
55
-
56
- # Builds the query hash sent to the availabilities endpoint.
57
- #
58
- # @param visit_motive_ids [Integer, String, Array] raw motive IDs
59
- # @param agenda_ids [Integer, String, Array] raw agenda IDs
60
- # @param start_date [Date, String] earliest search date
61
- # @param limit [Integer] page size
62
- # @param extra [Hash] additional query params
63
- # @return [Hash{Symbol => Object}] ready-to-send query hash
64
- def build_availability_query(visit_motive_ids, agenda_ids, start_date, limit, extra)
65
- {
66
- visit_motive_ids: dashed_ids(visit_motive_ids),
67
- agenda_ids: dashed_ids(agenda_ids),
68
- start_date: start_date.to_s,
69
- limit: [limit.to_i, TocDoc::Default::MAX_PER_PAGE].min,
70
- **extra
71
- }
72
- end
73
-
74
- # Merges the latest page body into the accumulator and returns options
75
- # for the next page, or +nil+ to halt pagination.
76
- #
77
- # On the first yield +acc+ *is* the first-page body (identical object),
78
- # so the merge step is skipped.
79
- #
80
- # @param acc [Hash] growing accumulator
81
- # @param last_resp [Faraday::Response] the most-recent raw response
82
- # @param base_query [Hash] the original query hash
83
- # @return [Hash, nil] options for the next page, or +nil+ to stop
84
- def paginate_availability_page(acc, last_resp, base_query)
85
- latest = last_resp.body
86
-
87
- merge_availability_page(acc, latest) unless acc.equal?(latest)
88
- availability_next_page_options(latest, base_query)
89
- end
90
-
91
- # Merges a new page body into the running accumulator.
92
- #
93
- # @param acc [Hash] the accumulator hash
94
- # @param latest [Hash] the most-recent page body
95
- # @return [void]
96
- def merge_availability_page(acc, latest)
97
- acc['availabilities'] = (acc['availabilities'] || []) + (latest['availabilities'] || [])
98
- acc['total'] = (acc['total'] || 0) + (latest['total'] || 0)
99
- acc['next_slot'] = latest['next_slot']
100
- end
101
-
102
- # Determines the options for the next page of availabilities, or +nil+
103
- # if pagination should stop.
104
- #
105
- # @param latest [Hash] the most-recent page body
106
- # @param base_query [Hash] the original query hash
107
- # @return [Hash, nil] next-page options, or +nil+ to halt
108
- def availability_next_page_options(latest, base_query)
109
- avails = latest['availabilities'] || []
110
- last_date_str = avails.last&.dig('date')
111
- return unless last_date_str && latest['next_slot']
112
-
113
- next_start = (Date.parse(last_date_str) + 1).to_s
114
- { query: base_query.merge(start_date: next_start) }
115
- end
116
- end
117
- end
118
- end