congress_gov 0.1.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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +156 -0
  5. data/lib/congress_gov/client.rb +125 -0
  6. data/lib/congress_gov/configuration.rb +48 -0
  7. data/lib/congress_gov/error.rb +46 -0
  8. data/lib/congress_gov/resources/amendment.rb +107 -0
  9. data/lib/congress_gov/resources/base.rb +74 -0
  10. data/lib/congress_gov/resources/bill.rb +199 -0
  11. data/lib/congress_gov/resources/bound_congressional_record.rb +48 -0
  12. data/lib/congress_gov/resources/clerk_vote.rb +125 -0
  13. data/lib/congress_gov/resources/committee.rb +136 -0
  14. data/lib/congress_gov/resources/committee_meeting.rb +37 -0
  15. data/lib/congress_gov/resources/committee_print.rb +50 -0
  16. data/lib/congress_gov/resources/committee_report.rb +65 -0
  17. data/lib/congress_gov/resources/congress_info.rb +32 -0
  18. data/lib/congress_gov/resources/crs_report.rb +17 -0
  19. data/lib/congress_gov/resources/daily_congressional_record.rb +48 -0
  20. data/lib/congress_gov/resources/hearing.rb +37 -0
  21. data/lib/congress_gov/resources/house_communication.rb +51 -0
  22. data/lib/congress_gov/resources/house_requirement.rb +36 -0
  23. data/lib/congress_gov/resources/house_vote.rb +127 -0
  24. data/lib/congress_gov/resources/law.rb +53 -0
  25. data/lib/congress_gov/resources/member.rb +151 -0
  26. data/lib/congress_gov/resources/nomination.rb +74 -0
  27. data/lib/congress_gov/resources/senate_communication.rb +51 -0
  28. data/lib/congress_gov/resources/summary.rb +27 -0
  29. data/lib/congress_gov/resources/treaty.rb +75 -0
  30. data/lib/congress_gov/response.rb +109 -0
  31. data/lib/congress_gov/version.rb +6 -0
  32. data/lib/congress_gov.rb +180 -0
  33. metadata +237 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e7b32276ee034d37d47c0fbe21485df9fd922f51fc56623cccd567e1d3884ad
4
+ data.tar.gz: 5f4986720a61399f43c8c7bfe6ed14c9e64db870f6f281e766193a215f8014a2
5
+ SHA512:
6
+ metadata.gz: 8d1bc89489d8626316023c668ea5b131531405ae1ba0e13a2ac142d6186470e5e333c32da42cbb4b217c7cacdb02ab33b7dd0912dad8434b4fa44c43fa0281e6
7
+ data.tar.gz: d7ae6017db8987dd0b224434aee920e46a3c0e14bc000efa420ba2e3c5f6f78c2cf4b6206800aa00dad6be9766f052ce783cff7b9f035f05f41a9f583da8792f
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2025-09-20
4
+
5
+ ### Added
6
+ - Initial release
7
+ - Member lookup by district, bioguide ID, sponsored/cosponsored legislation
8
+ - Bill detail, actions, subjects, summaries, cosponsors
9
+ - House roll call vote listing, detail, and per-member vote positions
10
+ - Clerk of the House XML fallback parser for vote records
11
+ - Committee listing and membership
12
+ - Full error hierarchy with retry support for rate limits
13
+ - VCR-based test suite with real Clerk XML fixture
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 ThePublicTab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # congress_gov
2
+
3
+ Ruby client for the [Congress.gov REST API v3](https://api.congress.gov/). Access member data, roll call votes, bill details, committee information, and more. Includes a fallback parser for Clerk of the House XML vote records.
4
+
5
+ The first Ruby client targeting the current v3 API.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem "congress_gov"
11
+ ```
12
+
13
+ Then `bundle install`, or install directly:
14
+
15
+ ```
16
+ gem install congress_gov
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Get an API key at https://api.congress.gov/sign-up/
22
+
23
+ ```ruby
24
+ CongressGov.configure do |config|
25
+ config.api_key = ENV["CONGRESS_GOV_API_KEY"]
26
+ config.timeout = 30 # optional, default 30s
27
+ config.retries = 3 # optional, default 3 (handles 429s with exponential backoff)
28
+ config.logger = Logger.new($stdout) # optional
29
+ end
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Member Lookup
35
+
36
+ ```ruby
37
+ # Get the current rep for a congressional district
38
+ rep = CongressGov.member.current_for_district(state: "VA", district: 8)
39
+ rep["bioguideId"] #=> "B001292"
40
+
41
+ # Full member profile
42
+ profile = CongressGov.member.get("B001292")
43
+ profile["member"]["directOrderName"] #=> "Donald S. Beyer"
44
+
45
+ # Sponsored legislation
46
+ CongressGov.member.sponsored_legislation("B001292", limit: 50)
47
+
48
+ # List all current members (paginated, max 250 per page)
49
+ CongressGov.member.list(current: true, limit: 250)
50
+ ```
51
+
52
+ ### Roll Call Votes
53
+
54
+ ```ruby
55
+ # How did a member vote on a specific roll call?
56
+ position = CongressGov.house_vote.position_for_member(
57
+ congress: 119, session: 1, roll_call: 281, bioguide_id: "B001292"
58
+ )
59
+ position #=> "Nay"
60
+
61
+ # All member votes as a hash keyed by bioguide ID
62
+ votes = CongressGov.house_vote.member_votes_by_bioguide(
63
+ congress: 119, session: 1, roll_call: 281
64
+ )
65
+ # { "B001292" => "Nay", "A000055" => "Aye", ... }
66
+ ```
67
+
68
+ ### Bills
69
+
70
+ ```ruby
71
+ # Bill detail
72
+ bill = CongressGov.bill.get(119, "hr", 5371)
73
+
74
+ # Find House roll call votes associated with a bill
75
+ refs = CongressGov.bill.house_vote_references(119, "hr", 5371)
76
+ # [{ "rollNumber" => 281, "chamber" => "House", "url" => "https://clerk.house.gov/..." }]
77
+
78
+ # Actions, subjects, summaries, cosponsors
79
+ CongressGov.bill.actions(119, "hr", 5371)
80
+ CongressGov.bill.subjects(119, "hr", 5371)
81
+ CongressGov.bill.summaries(119, "hr", 5371)
82
+ CongressGov.bill.cosponsors(119, "hr", 5371)
83
+ ```
84
+
85
+ ### Clerk of the House XML (Fallback)
86
+
87
+ Parse vote records directly from Clerk XML — no API key needed:
88
+
89
+ ```ruby
90
+ vote = CongressGov.clerk_vote.fetch(year: 2025, roll_call: 281)
91
+ vote[:result] #=> "Passed"
92
+ vote[:totals][:yea] #=> 217
93
+ vote[:members]["B001292"] #=> { name: "Beyer", party: "D", state: "VA", vote: "Nay" }
94
+
95
+ # Or from a URL returned by bill actions
96
+ vote = CongressGov.clerk_vote.fetch_by_url("https://clerk.house.gov/evs/2025/roll281.xml")
97
+ ```
98
+
99
+ ### Committees
100
+
101
+ ```ruby
102
+ CongressGov.committee.list(chamber: "house")
103
+ CongressGov.committee.get("senate", "ssap00")
104
+ CongressGov.committee.for_member("B001292")
105
+ ```
106
+
107
+ ### Error Handling
108
+
109
+ ```ruby
110
+ begin
111
+ CongressGov.member.get("Z999999")
112
+ rescue CongressGov::NotFoundError
113
+ puts "Member not found"
114
+ rescue CongressGov::AuthenticationError
115
+ puts "Check your API key"
116
+ rescue CongressGov::RateLimitError
117
+ puts "Rate limited (5,000 req/hour)"
118
+ rescue CongressGov::ParseError => e
119
+ puts "Clerk XML parse failed: #{e.message}"
120
+ rescue CongressGov::ConnectionError => e
121
+ puts "Network issue: #{e.message}"
122
+ end
123
+ ```
124
+
125
+ ## The Data Chain
126
+
127
+ This gem enables crossreferencing congressional votes with federal spending:
128
+
129
+ ```
130
+ District
131
+ -> CongressGov.member.current_for_district(state:, district:)
132
+ -> bioguide_id
133
+ -> CongressGov.bill.house_vote_references(congress, type, number)
134
+ -> roll_call numbers
135
+ -> CongressGov.house_vote.position_for_member(congress:, session:, roll_call:, bioguide_id:)
136
+ -> "Yea" or "Nay"
137
+ ```
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ bundle install
143
+ bundle exec rspec
144
+ bundle exec rubocop
145
+ ```
146
+
147
+ To record VCR cassettes against the live API:
148
+
149
+ ```bash
150
+ export CONGRESS_GOV_API_KEY="your_real_key"
151
+ VCR_RECORD=new_episodes bundle exec rspec
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT License. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'faraday/http_cache'
6
+ require 'json'
7
+
8
+ module CongressGov
9
+ # Low-level HTTP client that wraps Faraday for Congress.gov API requests.
10
+ # Handles authentication, retries, timeout, and error mapping.
11
+ class Client
12
+ # @return [CongressGov::Configuration]
13
+ attr_reader :config
14
+
15
+ # Creates a new client and validates the configuration.
16
+ #
17
+ # @param config [CongressGov::Configuration] configuration to use.
18
+ def initialize(config = CongressGov.configuration)
19
+ @config = config
20
+ config.validate!
21
+ end
22
+
23
+ # Returns a memoized Faraday connection configured for the Congress.gov API.
24
+ #
25
+ # @return [Faraday::Connection]
26
+ def connection
27
+ @connection ||= Faraday.new(url: config.base_url) do |f|
28
+ f.options.timeout = config.timeout
29
+ f.options.open_timeout = 10
30
+
31
+ # Inject API key into every request as a query param
32
+ f.request :url_encoded
33
+ f.response :json, content_type: /\bjson$/
34
+
35
+ if config.cache_store
36
+ f.use :http_cache,
37
+ store: config.cache_store,
38
+ shared_cache: false
39
+ end
40
+
41
+ f.request :retry,
42
+ max: config.retries,
43
+ interval: 1.0,
44
+ interval_randomness: 0.5,
45
+ backoff_factor: 2,
46
+ retry_statuses: [429, 500, 502, 503, 504],
47
+ exceptions: [
48
+ Faraday::TimeoutError,
49
+ Faraday::ConnectionFailed,
50
+ Faraday::RetriableResponse
51
+ ]
52
+
53
+ f.response :logger, config.logger if config.logger
54
+ f.adapter config.adapter
55
+ end
56
+ end
57
+
58
+ # Plain HTTP client for fetching Clerk XML — no auth, no JSON parsing.
59
+ def clerk_connection
60
+ @clerk_connection ||= Faraday.new(url: config.clerk_base_url) do |f|
61
+ f.options.timeout = config.timeout
62
+ f.options.open_timeout = 10
63
+ f.adapter config.adapter
64
+ end
65
+ end
66
+
67
+ # GET request — automatically appends api_key to params.
68
+ #
69
+ # @param path [String]
70
+ # @param params [Hash]
71
+ # @return [CongressGov::Response]
72
+ def get(path, params = {})
73
+ response = connection.get(path, params.merge(api_key: config.api_key, format: 'json'))
74
+ handle_response(response)
75
+ rescue Faraday::TimeoutError => e
76
+ raise ConnectionError, "Request timed out: #{e.message}"
77
+ rescue Faraday::ConnectionFailed => e
78
+ raise ConnectionError, "Connection failed: #{e.message}"
79
+ end
80
+
81
+ # Fetch raw XML from clerk.house.gov. Returns the raw body string.
82
+ # Does not raise on HTTP errors — callers handle nil/blank responses.
83
+ #
84
+ # @param path [String] e.g. "2025/roll281.xml"
85
+ # @return [String, nil]
86
+ def get_clerk_xml(path)
87
+ response = clerk_connection.get(path)
88
+ return nil unless response.status == 200
89
+
90
+ response.body
91
+ rescue Faraday::Error
92
+ nil
93
+ end
94
+
95
+ private
96
+
97
+ # Maps an HTTP response to a {Response} or raises an appropriate error.
98
+ #
99
+ # @param response [Faraday::Response]
100
+ # @return [CongressGov::Response]
101
+ # @raise [CongressGov::Error] on non-2xx status codes.
102
+ def handle_response(response)
103
+ case response.status
104
+ when 200..299
105
+ Response.new(response.body, response.status)
106
+ when 403
107
+ raise AuthenticationError.new(
108
+ 'Invalid or missing API key',
109
+ status: 403,
110
+ body: response.body
111
+ )
112
+ when 404
113
+ raise NotFoundError.new(status: 404, body: response.body)
114
+ when 429
115
+ raise RateLimitError.new('Rate limit exceeded', status: 429, body: response.body)
116
+ when 400..499
117
+ raise ClientError.new(status: response.status, body: response.body)
118
+ when 500..599
119
+ raise ServerError.new(status: response.status, body: response.body)
120
+ else
121
+ raise Error, "Unexpected HTTP status: #{response.status}"
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongressGov
4
+ # Stores runtime settings for the CongressGov client.
5
+ # Use {CongressGov.configure} to modify these values.
6
+ class Configuration
7
+ # Default base URL for the Congress.gov API.
8
+ DEFAULT_BASE_URL = 'https://api.congress.gov/v3/'
9
+ # Default HTTP timeout in seconds.
10
+ DEFAULT_TIMEOUT = 30
11
+ # Default number of automatic retries on transient failures.
12
+ DEFAULT_RETRIES = 3
13
+ # Base URL for the House Clerk electronic voting system.
14
+ CLERK_BASE_URL = 'https://clerk.house.gov/evs/'
15
+
16
+ # @return [String, nil] API key used to authenticate requests.
17
+ # @return [String] base URL for the Congress.gov API.
18
+ # @return [String] base URL for the House Clerk voting site.
19
+ # @return [Integer] HTTP timeout in seconds.
20
+ # @return [Integer] number of automatic retries on transient failures.
21
+ # @return [Logger, nil] optional logger for request/response debugging.
22
+ # @return [Symbol] Faraday adapter to use for HTTP requests.
23
+ attr_accessor :api_key, :base_url, :clerk_base_url,
24
+ :timeout, :retries, :logger, :adapter,
25
+ :cache_store
26
+
27
+ # Initializes a new Configuration with sensible defaults.
28
+ # Reads +CONGRESS_GOV_API_KEY+ from the environment when available.
29
+ def initialize
30
+ @api_key = ENV.fetch('CONGRESS_GOV_API_KEY', nil)
31
+ @base_url = DEFAULT_BASE_URL
32
+ @clerk_base_url = CLERK_BASE_URL
33
+ @timeout = DEFAULT_TIMEOUT
34
+ @retries = DEFAULT_RETRIES
35
+ @logger = nil
36
+ @adapter = Faraday.default_adapter
37
+ @cache_store = nil
38
+ end
39
+
40
+ # Raises {ConfigurationError} unless a valid API key is present.
41
+ #
42
+ # @return [void]
43
+ # @raise [ConfigurationError] if api_key is nil or empty.
44
+ def validate!
45
+ raise ConfigurationError, 'api_key is required' if api_key.nil? || api_key.empty?
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongressGov
4
+ # Base error class. All gem errors inherit from this.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when api_key is missing or blank.
8
+ class ConfigurationError < Error; end
9
+
10
+ # Raised on 4xx responses (excluding 429).
11
+ class ClientError < Error
12
+ attr_reader :status, :body
13
+
14
+ def initialize(message = nil, status: nil, body: nil)
15
+ @status = status
16
+ @body = body
17
+ super(message || "HTTP #{status}: #{body}")
18
+ end
19
+ end
20
+
21
+ # Raised on 403 — usually means invalid or missing API key.
22
+ class AuthenticationError < ClientError; end
23
+
24
+ # Raised on 404.
25
+ class NotFoundError < ClientError; end
26
+
27
+ # Raised on 429 after retries are exhausted.
28
+ class RateLimitError < ClientError; end
29
+
30
+ # Raised on 5xx responses.
31
+ class ServerError < Error
32
+ attr_reader :status, :body
33
+
34
+ def initialize(message = nil, status: nil, body: nil)
35
+ @status = status
36
+ @body = body
37
+ super(message || "Server error HTTP #{status}")
38
+ end
39
+ end
40
+
41
+ # Raised on network-level failures (timeout, connection refused).
42
+ class ConnectionError < Error; end
43
+
44
+ # Raised when Clerk XML cannot be parsed or is malformed.
45
+ class ParseError < Error; end
46
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongressGov
4
+ module Resources
5
+ # Access amendment data from the Congress.gov API.
6
+ class Amendment < Base
7
+ # Valid amendment type codes: samdt (Senate), hamdt (House), suamdt (Senate unprinted).
8
+ AMENDMENT_TYPES = %w[samdt hamdt suamdt].freeze
9
+
10
+ # List amendments with optional congress and type filters.
11
+ #
12
+ # @param congress [Integer, nil] e.g. 119
13
+ # @param amendment_type [String, nil] one of AMENDMENT_TYPES
14
+ # @param limit [Integer]
15
+ # @param offset [Integer]
16
+ # @return [CongressGov::Response]
17
+ def list(congress: nil, amendment_type: nil, limit: 20, offset: 0)
18
+ params = { limit: limit, offset: offset }
19
+
20
+ if congress && amendment_type
21
+ validate_type!(amendment_type)
22
+ client.get("amendment/#{congress}/#{amendment_type.downcase}", params)
23
+ elsif congress
24
+ client.get("amendment/#{congress}", params)
25
+ else
26
+ client.get('amendment', params)
27
+ end
28
+ end
29
+
30
+ # Fetch a single amendment record.
31
+ #
32
+ # @param congress [Integer]
33
+ # @param type [String] one of AMENDMENT_TYPES
34
+ # @param number [Integer]
35
+ # @return [CongressGov::Response]
36
+ def get(congress, type, number)
37
+ validate_type!(type)
38
+ client.get("amendment/#{congress}/#{type.downcase}/#{number}")
39
+ end
40
+
41
+ # Actions taken on an amendment.
42
+ #
43
+ # @param congress [Integer]
44
+ # @param type [String]
45
+ # @param number [Integer]
46
+ # @param limit [Integer]
47
+ # @param offset [Integer]
48
+ # @return [CongressGov::Response]
49
+ def actions(congress, type, number, limit: 20, offset: 0)
50
+ validate_type!(type)
51
+ client.get("amendment/#{congress}/#{type.downcase}/#{number}/actions",
52
+ { limit: limit, offset: offset })
53
+ end
54
+
55
+ # Sub-amendments to an amendment.
56
+ #
57
+ # @param congress [Integer]
58
+ # @param type [String]
59
+ # @param number [Integer]
60
+ # @param limit [Integer]
61
+ # @param offset [Integer]
62
+ # @return [CongressGov::Response]
63
+ def amendments(congress, type, number, limit: 20, offset: 0)
64
+ validate_type!(type)
65
+ client.get("amendment/#{congress}/#{type.downcase}/#{number}/amendments",
66
+ { limit: limit, offset: offset })
67
+ end
68
+
69
+ # Cosponsors of an amendment.
70
+ #
71
+ # @param congress [Integer]
72
+ # @param type [String]
73
+ # @param number [Integer]
74
+ # @param limit [Integer]
75
+ # @param offset [Integer]
76
+ # @return [CongressGov::Response]
77
+ def cosponsors(congress, type, number, limit: 20, offset: 0)
78
+ validate_type!(type)
79
+ client.get("amendment/#{congress}/#{type.downcase}/#{number}/cosponsors",
80
+ { limit: limit, offset: offset })
81
+ end
82
+
83
+ # Text versions of an amendment.
84
+ #
85
+ # @param congress [Integer]
86
+ # @param type [String]
87
+ # @param number [Integer]
88
+ # @param limit [Integer]
89
+ # @param offset [Integer]
90
+ # @return [CongressGov::Response]
91
+ def text(congress, type, number, limit: 20, offset: 0)
92
+ validate_type!(type)
93
+ client.get("amendment/#{congress}/#{type.downcase}/#{number}/text",
94
+ { limit: limit, offset: offset })
95
+ end
96
+
97
+ private
98
+
99
+ def validate_type!(type)
100
+ return if AMENDMENT_TYPES.include?(type.to_s.downcase)
101
+
102
+ raise ArgumentError,
103
+ "amendment type must be one of: #{AMENDMENT_TYPES.join(', ')}"
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongressGov
4
+ # Namespace for all Congress.gov API resource classes.
5
+ module Resources
6
+ # Abstract base class for all Congress.gov resource endpoints.
7
+ # Provides a shared client reference, a convenience +get+ wrapper,
8
+ # and generic auto-pagination via {#each_page} and {#paginate}.
9
+ class Base
10
+ # Creates a new resource bound to the given client.
11
+ #
12
+ # @param client [CongressGov::Client] the HTTP client to use.
13
+ def initialize(client = CongressGov.client)
14
+ @client = client
15
+ end
16
+
17
+ # Iterate through every page of a paginated endpoint.
18
+ # Yields each {CongressGov::Response} page.
19
+ #
20
+ # @param path [String] API path.
21
+ # @param params [Hash] query parameters (should include :limit).
22
+ # @yield [CongressGov::Response] each page of results.
23
+ # @return [Enumerator] if no block given.
24
+ #
25
+ # @example
26
+ # CongressGov.bill.each_page('bill/119', limit: 250) do |page|
27
+ # page.results.each { |bill| process(bill) }
28
+ # end
29
+ def each_page(path, params = {}, &block)
30
+ return enum_for(:each_page, path, params) unless block
31
+
32
+ limit = params.fetch(:limit, 20)
33
+ offset = params.fetch(:offset, 0)
34
+
35
+ loop do
36
+ response = client.get(path, params.merge(limit: limit, offset: offset))
37
+ yield response
38
+ break unless response.has_next_page?
39
+
40
+ offset += limit
41
+ end
42
+ end
43
+
44
+ # Collect all results across all pages of a paginated endpoint.
45
+ # Returns a flat Array of all result hashes.
46
+ #
47
+ # @param path [String] API path.
48
+ # @param params [Hash] query parameters.
49
+ # @return [Array<Hash>] all results across all pages.
50
+ #
51
+ # @example
52
+ # all_bills = CongressGov.bill.paginate('bill/119/hr', limit: 250)
53
+ def paginate(path, params = {})
54
+ results = []
55
+ each_page(path, params) { |page| results.concat(page.results) }
56
+ results
57
+ end
58
+
59
+ private
60
+
61
+ # @return [CongressGov::Client]
62
+ attr_reader :client
63
+
64
+ # Shorthand for {Client#get}.
65
+ #
66
+ # @param path [String] API path relative to the base URL.
67
+ # @param params [Hash] optional query parameters.
68
+ # @return [CongressGov::Response]
69
+ def get(path, params = {})
70
+ client.get(path, params)
71
+ end
72
+ end
73
+ end
74
+ end