lunchmoney 1.4.0 → 1.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +7 -0
  3. data/.github/workflows/build_and_publish_yard_docs.yml +4 -4
  4. data/.github/workflows/ci.yml +9 -10
  5. data/.github/workflows/rbi-updater.yml +1 -1
  6. data/.github/workflows/release_pipeline.yml +1 -1
  7. data/.rubocop.yml +1 -1
  8. data/.ruby-version +1 -1
  9. data/.simplecov +1 -0
  10. data/.toys/.toys.rb +8 -0
  11. data/Gemfile +3 -3
  12. data/Gemfile.lock +102 -78
  13. data/README.md +0 -2
  14. data/SECURITY.md +151 -0
  15. data/bin/check_vcr_version +94 -0
  16. data/lib/lunchmoney/api.rb +26 -38
  17. data/lib/lunchmoney/calls/assets.rb +10 -13
  18. data/lib/lunchmoney/calls/base.rb +59 -7
  19. data/lib/lunchmoney/calls/budgets.rb +22 -25
  20. data/lib/lunchmoney/calls/categories.rb +28 -38
  21. data/lib/lunchmoney/calls/crypto.rb +7 -9
  22. data/lib/lunchmoney/calls/plaid_accounts.rb +7 -9
  23. data/lib/lunchmoney/calls/recurring_expenses.rb +4 -5
  24. data/lib/lunchmoney/calls/tags.rb +3 -4
  25. data/lib/lunchmoney/calls/transactions.rb +28 -37
  26. data/lib/lunchmoney/calls/users.rb +3 -4
  27. data/lib/lunchmoney/configuration.rb +20 -0
  28. data/lib/lunchmoney/deprecate.rb +35 -0
  29. data/lib/lunchmoney/objects/asset.rb +6 -1
  30. data/lib/lunchmoney/objects/object.rb +4 -9
  31. data/lib/lunchmoney/objects/plaid_account.rb +6 -1
  32. data/lib/lunchmoney/validators.rb +8 -6
  33. data/lib/lunchmoney/version.rb +1 -1
  34. data/lib/lunchmoney.rb +3 -3
  35. data/lunchmoney.gemspec +1 -1
  36. data/sorbet/rbi/annotations/activesupport.rbi +40 -0
  37. data/sorbet/rbi/dsl/active_support/callbacks.rbi +0 -2
  38. data/sorbet/rbi/gems/{activesupport@7.2.1.rbi → activesupport@8.0.2.1.rbi} +1431 -1028
  39. data/sorbet/rbi/gems/{ast@2.4.2.rbi → ast@2.4.3.rbi} +4 -3
  40. data/sorbet/rbi/gems/{base64@0.2.0.rbi → base64@0.3.0.rbi} +76 -39
  41. data/sorbet/rbi/gems/benchmark@0.4.1.rbi +619 -0
  42. data/sorbet/rbi/gems/bigdecimal@3.2.2.rbi +275 -0
  43. data/sorbet/rbi/gems/{concurrent-ruby@1.3.4.rbi → concurrent-ruby@1.3.5.rbi} +44 -32
  44. data/sorbet/rbi/gems/{connection_pool@2.4.1.rbi → connection_pool@2.5.3.rbi} +1 -0
  45. data/sorbet/rbi/gems/{dotenv@3.1.2.rbi → dotenv@3.1.8.rbi} +21 -29
  46. data/sorbet/rbi/gems/{drb@2.2.1.rbi → drb@2.2.3.rbi} +503 -188
  47. data/sorbet/rbi/gems/{erubi@1.13.0.rbi → erubi@1.13.1.rbi} +14 -9
  48. data/sorbet/rbi/gems/{faraday-net_http@3.1.1.rbi → faraday-net_http@3.4.1.rbi} +34 -34
  49. data/sorbet/rbi/gems/{faraday@2.10.1.rbi → faraday@2.13.4.rbi} +507 -171
  50. data/sorbet/rbi/gems/{hashdiff@1.1.1.rbi → hashdiff@1.2.0.rbi} +5 -3
  51. data/sorbet/rbi/gems/{i18n@1.14.5.rbi → i18n@1.14.7.rbi} +80 -80
  52. data/sorbet/rbi/gems/{json@2.7.2.rbi → json@2.13.2.rbi} +988 -226
  53. data/sorbet/rbi/gems/{kramdown@2.4.0.rbi → kramdown@2.5.1.rbi} +316 -234
  54. data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
  55. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +240 -0
  56. data/sorbet/rbi/gems/{logger@1.6.0.rbi → logger@1.7.0.rbi} +136 -76
  57. data/sorbet/rbi/gems/{minitest@5.25.1.rbi → minitest@5.25.5.rbi} +227 -220
  58. data/sorbet/rbi/gems/{mocha@2.4.5.rbi → mocha@2.7.1.rbi} +154 -118
  59. data/sorbet/rbi/gems/{net-http@0.4.1.rbi → net-http@0.6.0.rbi} +360 -181
  60. data/sorbet/rbi/gems/{parser@3.3.4.2.rbi → parser@3.3.9.0.rbi} +326 -308
  61. data/sorbet/rbi/gems/{prism@0.30.0.rbi → prism@1.4.0.rbi} +12440 -9920
  62. data/sorbet/rbi/gems/{rack@3.1.7.rbi → rack@3.2.1.rbi} +752 -579
  63. data/sorbet/rbi/gems/{rake@13.2.1.rbi → rake@13.3.0.rbi} +238 -227
  64. data/sorbet/rbi/gems/rbi@0.3.6.rbi +5162 -0
  65. data/sorbet/rbi/gems/rbs@4.0.0.dev.4.rbi +7895 -0
  66. data/sorbet/rbi/gems/{regexp_parser@2.9.2.rbi → regexp_parser@2.11.2.rbi} +1124 -1013
  67. data/sorbet/rbi/gems/require-hooks@0.2.2.rbi +110 -0
  68. data/sorbet/rbi/gems/{rexml@3.3.6.rbi → rexml@3.4.2.rbi} +755 -318
  69. data/sorbet/rbi/gems/{rubocop-ast@1.32.1.rbi → rubocop-ast@1.46.0.rbi} +1287 -899
  70. data/sorbet/rbi/gems/{rubocop-minitest@0.35.1.rbi → rubocop-minitest@0.38.2.rbi} +133 -97
  71. data/sorbet/rbi/gems/{rubocop-rails@2.26.0.rbi → rubocop-rails@2.33.3.rbi} +9874 -6597
  72. data/sorbet/rbi/gems/{rubocop-shopify@2.15.1.rbi → rubocop-shopify@2.17.1.rbi} +1 -0
  73. data/sorbet/rbi/gems/{rubocop-sorbet@0.8.5.rbi → rubocop-sorbet@0.10.5.rbi} +804 -83
  74. data/sorbet/rbi/gems/{rubocop@1.65.1.rbi → rubocop@1.80.1.rbi} +10688 -5103
  75. data/sorbet/rbi/gems/{securerandom@0.3.1.rbi → securerandom@0.4.1.rbi} +7 -5
  76. data/sorbet/rbi/gems/{spoom@1.4.2.rbi → spoom@1.7.6.rbi} +1939 -1039
  77. data/sorbet/rbi/gems/{tapioca@0.16.1.rbi → tapioca@0.17.7.rbi} +765 -821
  78. data/sorbet/rbi/gems/{thor@1.3.1.rbi → thor@1.4.0.rbi} +139 -91
  79. data/sorbet/rbi/gems/unicode-display_width@3.1.5.rbi +132 -0
  80. data/sorbet/rbi/gems/unicode-emoji@4.0.4.rbi +251 -0
  81. data/sorbet/rbi/gems/{uri@0.13.0.rbi → uri@1.0.3.rbi} +278 -256
  82. data/sorbet/rbi/gems/{vcr@6.3.1.rbi → vcr@6.3.1-ce35c236fe48899f02ddf780973b44cdb756c0ee.rbi} +140 -123
  83. data/sorbet/rbi/gems/{webmock@3.23.1.rbi → webmock@3.25.1.rbi} +101 -78
  84. data/sorbet/rbi/gems/{yard@0.9.36.rbi → yard@0.9.37.rbi} +394 -235
  85. metadata +55 -53
  86. data/sorbet/rbi/gems/bigdecimal@3.1.8.rbi +0 -78
  87. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +0 -14237
  88. data/sorbet/rbi/gems/rbi@0.1.14.rbi +0 -3305
  89. data/sorbet/rbi/gems/strscan@3.1.0.rbi +0 -9
  90. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +0 -65
  91. /data/sorbet/rbi/gems/{parallel@1.26.3.rbi → parallel@1.27.0.rbi} +0 -0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+ require "json"
6
+ require "uri"
7
+
8
+ # This script checks if a newer version of VCR has been released beyond 6.3.1
9
+ # If a newer version is found, it raises an error indicating that we should
10
+ # switch back to using the released gem instead of the git commit.
11
+
12
+ CURRENT_VCR_VERSION = "6.3.1"
13
+ VCR_COMMIT_SHA = "ce35c236fe48899f02ddf780973b44cdb756c0ee"
14
+
15
+ def fetch_latest_vcr_version
16
+ uri = URI("https://rubygems.org/api/v1/gems/vcr.json")
17
+ response = Net::HTTP.get_response(uri)
18
+
19
+ unless response.is_a?(Net::HTTPSuccess)
20
+ puts "Warning: Could not fetch VCR version information from RubyGems API"
21
+ puts "Response: #{response.code} #{response.message}"
22
+ return nil
23
+ end
24
+
25
+ gem_info = JSON.parse(response.body)
26
+ gem_info["version"]
27
+ rescue StandardError => e
28
+ puts "Warning: Error fetching VCR version information: #{e.message}"
29
+ nil
30
+ end
31
+
32
+ def version_greater?(version1, version2)
33
+ # Simple version comparison - splits by dots and compares numerically
34
+ v1_parts = version1.split(".").map(&:to_i)
35
+ v2_parts = version2.split(".").map(&:to_i)
36
+
37
+ # Pad shorter version with zeros
38
+ max_length = [v1_parts.length, v2_parts.length].max
39
+ v1_parts += [0] * (max_length - v1_parts.length)
40
+ v2_parts += [0] * (max_length - v2_parts.length)
41
+
42
+ v1_parts.zip(v2_parts).each do |v1, v2|
43
+ return true if v1 > v2
44
+ return false if v1 < v2
45
+ end
46
+
47
+ false # versions are equal
48
+ end
49
+
50
+ def main
51
+ puts "Checking for newer VCR releases..."
52
+ puts "Current pinned version: #{CURRENT_VCR_VERSION}"
53
+ puts "Using commit: #{VCR_COMMIT_SHA}"
54
+ puts ""
55
+
56
+ latest_version = fetch_latest_vcr_version
57
+
58
+ if latest_version.nil?
59
+ puts "Could not determine latest VCR version. Skipping check."
60
+ exit 0
61
+ end
62
+
63
+ puts "Latest released version: #{latest_version}"
64
+
65
+ if version_greater?(latest_version, CURRENT_VCR_VERSION)
66
+ puts ""
67
+ puts "🚨 NEWER VCR VERSION AVAILABLE! 🚨"
68
+ puts ""
69
+ puts "A newer version of VCR (#{latest_version}) has been released!"
70
+ puts "This is likely to include the Ruby 3.5+ compatibility fix from commit #{VCR_COMMIT_SHA}."
71
+ puts ""
72
+ puts "ACTION REQUIRED:"
73
+ puts "1. Update Gemfile to use the released version:"
74
+ puts " gem \"vcr\", \"~> #{latest_version}\", require: false"
75
+ puts ""
76
+ puts "2. Remove the git reference and commit SHA"
77
+ puts ""
78
+ puts "3. Run 'bundle update vcr' to update to the new version"
79
+ puts ""
80
+ puts "4. Test to ensure everything works with the released version"
81
+ puts ""
82
+ puts "5. Consider removing this version check script once updated"
83
+ puts ""
84
+
85
+ exit 1
86
+ else
87
+ puts "No newer version available. Continuing to use commit #{VCR_COMMIT_SHA}."
88
+ exit 0
89
+ end
90
+ end
91
+
92
+ if __FILE__ == $0
93
+ main
94
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require_relative "exceptions"
5
5
  require_relative "configuration"
6
+ require_relative "deprecate"
6
7
 
7
8
  require_relative "calls/base"
8
9
  require_relative "objects/object"
@@ -29,7 +30,7 @@ module LunchMoney
29
30
 
30
31
  sig { params(api_key: T.nilable(String)).void }
31
32
  def initialize(api_key: nil)
32
- @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
33
+ @api_key = T.let(api_key || LunchMoney.configuration.api_key, T.nilable(String))
33
34
  end
34
35
 
35
36
  delegate :me, to: :user_calls
@@ -40,9 +41,7 @@ module LunchMoney
40
41
  # api.me
41
42
  sig { returns(LunchMoney::Calls::Base) }
42
43
  def user_calls
43
- with_valid_api_key do
44
- @user_calls ||= T.let(LunchMoney::Calls::Users.new(api_key:), T.nilable(LunchMoney::Calls::Users))
45
- end
44
+ memoized_call_instance(:@user_calls, LunchMoney::Calls::Users)
46
45
  end
47
46
 
48
47
  delegate :categories,
@@ -83,9 +82,7 @@ module LunchMoney
83
82
  # api.force_delete_category(1234567)
84
83
  sig { returns(LunchMoney::Calls::Base) }
85
84
  def category_calls
86
- with_valid_api_key do
87
- @category_calls ||= T.let(LunchMoney::Calls::Categories.new(api_key:), T.nilable(LunchMoney::Calls::Categories))
88
- end
85
+ memoized_call_instance(:@category_calls, LunchMoney::Calls::Categories)
89
86
  end
90
87
 
91
88
  delegate :tags, to: :tag_calls
@@ -96,9 +93,7 @@ module LunchMoney
96
93
  # api.tags
97
94
  sig { returns(LunchMoney::Calls::Base) }
98
95
  def tag_calls
99
- with_valid_api_key do
100
- @tag_calls ||= T.let(LunchMoney::Calls::Tags.new(api_key:), T.nilable(LunchMoney::Calls::Tags))
101
- end
96
+ memoized_call_instance(:@tag_calls, LunchMoney::Calls::Tags)
102
97
  end
103
98
 
104
99
  delegate :transactions,
@@ -159,12 +154,7 @@ module LunchMoney
159
154
  # api.delete_transaction_group(905483362)
160
155
  sig { returns(LunchMoney::Calls::Base) }
161
156
  def transaction_calls
162
- with_valid_api_key do
163
- @transaction_calls ||= T.let(
164
- LunchMoney::Calls::Transactions.new(api_key:),
165
- T.nilable(LunchMoney::Calls::Transactions),
166
- )
167
- end
157
+ memoized_call_instance(:@transaction_calls, LunchMoney::Calls::Transactions)
168
158
  end
169
159
 
170
160
  delegate :recurring_expenses, to: :recurring_expense_calls
@@ -175,12 +165,7 @@ module LunchMoney
175
165
  # api.recurring_expenses
176
166
  sig { returns(LunchMoney::Calls::Base) }
177
167
  def recurring_expense_calls
178
- with_valid_api_key do
179
- @recurring_expense_calls ||= T.let(
180
- LunchMoney::Calls::RecurringExpenses.new(api_key:),
181
- T.nilable(LunchMoney::Calls::RecurringExpenses),
182
- )
183
- end
168
+ memoized_call_instance(:@recurring_expense_calls, LunchMoney::Calls::RecurringExpenses)
184
169
  end
185
170
 
186
171
  delegate :budgets, :upsert_budget, :remove_budget, to: :budget_calls
@@ -192,14 +177,12 @@ module LunchMoney
192
177
  # @example [Upsert Budget](https://lunchmoney.dev/#upsert-budget)
193
178
  # api = LunchMoney::Api.new
194
179
  # api.upsert_budget(start_date: "2023-01-01", category_id: 777052, amount: 400.99)
195
- # @example [Remove Budget(https://lunchmoney.dev/#remove-budget)
180
+ # @example [Remove Budget](https://lunchmoney.dev/#remove-budget)
196
181
  # api = LunchMoney::Api.new
197
182
  # api.remove_budget(start_date: "2023-01-01", category_id: 777052)
198
183
  sig { returns(LunchMoney::Calls::Base) }
199
184
  def budget_calls
200
- with_valid_api_key do
201
- @budget_calls ||= T.let(LunchMoney::Calls::Budgets.new(api_key:), T.nilable(LunchMoney::Calls::Budgets))
202
- end
185
+ memoized_call_instance(:@budget_calls, LunchMoney::Calls::Budgets)
203
186
  end
204
187
 
205
188
  delegate :assets, :create_asset, :update_asset, to: :asset_calls
@@ -220,9 +203,7 @@ module LunchMoney
220
203
  # api.update_asset(93746, balance: "99.99")
221
204
  sig { returns(LunchMoney::Calls::Base) }
222
205
  def asset_calls
223
- with_valid_api_key do
224
- @asset_calls ||= T.let(LunchMoney::Calls::Assets.new(api_key:), T.nilable(LunchMoney::Calls::Assets))
225
- end
206
+ memoized_call_instance(:@asset_calls, LunchMoney::Calls::Assets)
226
207
  end
227
208
 
228
209
  delegate :plaid_accounts, :plaid_accounts_fetch, to: :plaid_account_calls
@@ -236,12 +217,7 @@ module LunchMoney
236
217
  # api.plaid_accounts_fetch
237
218
  sig { returns(LunchMoney::Calls::Base) }
238
219
  def plaid_account_calls
239
- with_valid_api_key do
240
- @plaid_account_calls ||= T.let(
241
- LunchMoney::Calls::PlaidAccounts.new(api_key:),
242
- T.nilable(LunchMoney::Calls::PlaidAccounts),
243
- )
244
- end
220
+ memoized_call_instance(:@plaid_account_calls, LunchMoney::Calls::PlaidAccounts)
245
221
  end
246
222
 
247
223
  delegate :crypto, :update_crypto, to: :crypto_calls
@@ -255,9 +231,7 @@ module LunchMoney
255
231
  # api.update_crypto(1234567, name: "New Crypto Name")
256
232
  sig { returns(LunchMoney::Calls::Base) }
257
233
  def crypto_calls
258
- with_valid_api_key do
259
- @crypto_calls ||= T.let(LunchMoney::Calls::Crypto.new(api_key:), T.nilable(LunchMoney::Calls::Crypto))
260
- end
234
+ memoized_call_instance(:@crypto_calls, LunchMoney::Calls::Crypto)
261
235
  end
262
236
 
263
237
  private
@@ -268,5 +242,19 @@ module LunchMoney
268
242
 
269
243
  yield
270
244
  end
245
+
246
+ sig do
247
+ type_parameters(:T)
248
+ .params(
249
+ ivar_name: Symbol,
250
+ klass: T.class_of(LunchMoney::Calls::Base),
251
+ )
252
+ .returns(LunchMoney::Calls::Base)
253
+ end
254
+ def memoized_call_instance(ivar_name, klass)
255
+ with_valid_api_key do
256
+ instance_variable_get(ivar_name) || instance_variable_set(ivar_name, klass.new(api_key:))
257
+ end
258
+ end
271
259
  end
272
260
  end
@@ -11,11 +11,10 @@ module LunchMoney
11
11
  def assets
12
12
  response = get("assets")
13
13
 
14
- api_errors = errors(response)
15
- return api_errors if api_errors.present?
16
-
17
- response.body[:assets].map do |asset|
18
- LunchMoney::Objects::Asset.new(**asset)
14
+ handle_api_response(response) do |body|
15
+ body[:assets].map do |asset|
16
+ LunchMoney::Objects::Asset.new(**asset)
17
+ end
19
18
  end
20
19
  end
21
20
 
@@ -50,10 +49,9 @@ module LunchMoney
50
49
 
51
50
  response = post("assets", params)
52
51
 
53
- api_errors = errors(response)
54
- return api_errors if api_errors.present?
55
-
56
- LunchMoney::Objects::Asset.new(**response.body)
52
+ handle_api_response(response) do |body|
53
+ LunchMoney::Objects::Asset.new(**body)
54
+ end
57
55
  end
58
56
 
59
57
  sig do
@@ -88,10 +86,9 @@ module LunchMoney
88
86
 
89
87
  response = put("assets/#{asset_id}", params)
90
88
 
91
- api_errors = errors(response)
92
- return api_errors if api_errors.present?
93
-
94
- LunchMoney::Objects::Asset.new(**response.body)
89
+ handle_api_response(response) do |body|
90
+ LunchMoney::Objects::Asset.new(**body)
91
+ end
95
92
  end
96
93
  end
97
94
  end
@@ -22,14 +22,15 @@ module LunchMoney
22
22
 
23
23
  sig { params(api_key: T.nilable(String)).void }
24
24
  def initialize(api_key: nil)
25
- @api_key = T.let((api_key || LunchMoney.configuration.api_key), T.nilable(String))
25
+ @api_key = T.let(api_key || LunchMoney.configuration.api_key, T.nilable(String))
26
+ @connections = T.let({}, T::Hash[Symbol, Faraday::Connection])
26
27
  end
27
28
 
28
29
  private
29
30
 
30
31
  sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
31
32
  def get(endpoint, query_params: nil)
32
- connection = request(flat_params: true)
33
+ connection = connection_for(:flat_params)
33
34
 
34
35
  if query_params.present?
35
36
  connection.get(BASE_URL + endpoint, query_params)
@@ -40,19 +41,19 @@ module LunchMoney
40
41
 
41
42
  sig { params(endpoint: String, params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
42
43
  def post(endpoint, params)
43
- request(json_request: true).post(BASE_URL + endpoint, params)
44
+ connection_for(:json).post(BASE_URL + endpoint, params)
44
45
  end
45
46
 
46
47
  sig { params(endpoint: String, body: T::Hash[Symbol, T.untyped]).returns(Faraday::Response) }
47
48
  def put(endpoint, body)
48
- request(json_request: true).put(BASE_URL + endpoint) do |req|
49
+ connection_for(:json).put(BASE_URL + endpoint) do |req|
49
50
  req.body = body
50
51
  end
51
52
  end
52
53
 
53
54
  sig { params(endpoint: String, query_params: T.nilable(T::Hash[Symbol, T.untyped])).returns(Faraday::Response) }
54
55
  def delete(endpoint, query_params: nil)
55
- connection = request(flat_params: true)
56
+ connection = connection_for(:flat_params)
56
57
 
57
58
  if query_params.present?
58
59
  connection.delete(BASE_URL + endpoint, query_params)
@@ -61,12 +62,24 @@ module LunchMoney
61
62
  end
62
63
  end
63
64
 
65
+ sig { params(connection_type: Symbol).returns(Faraday::Connection) }
66
+ def connection_for(connection_type)
67
+ @connections[connection_type] ||= case connection_type
68
+ when :json
69
+ build_connection(json_request: true)
70
+ when :flat_params
71
+ build_connection(flat_params: true)
72
+ else
73
+ build_connection
74
+ end
75
+ end
76
+
64
77
  sig { params(json_request: T::Boolean, flat_params: T::Boolean).returns(Faraday::Connection) }
65
- def request(json_request: false, flat_params: false)
78
+ def build_connection(json_request: false, flat_params: false)
66
79
  Faraday.new do |conn|
67
80
  conn.request(:authorization, "Bearer", @api_key)
68
81
  conn.request(:json) if json_request
69
- # conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
82
+ conn.options.params_encoder = Faraday::FlatParamsEncoder if flat_params
70
83
  conn.response(:json, content_type: /json$/, parser_options: { symbolize_names: true })
71
84
  end
72
85
  end
@@ -113,6 +126,45 @@ module LunchMoney
113
126
  def clean_params(params)
114
127
  params.reject! { |_key, value| value.nil? }
115
128
  end
129
+
130
+ sig do
131
+ type_parameters(:T)
132
+ .params(
133
+ response: Faraday::Response,
134
+ block: T.proc.params(body: T.untyped).returns(T.type_parameter(:T)),
135
+ )
136
+ .returns(T.any(T.type_parameter(:T), LunchMoney::Errors))
137
+ end
138
+ def handle_api_response(response, &block)
139
+ api_errors = errors(response)
140
+ return api_errors if api_errors.present?
141
+
142
+ yield(response.body)
143
+ end
144
+
145
+ sig do
146
+ type_parameters(:T)
147
+ .params(
148
+ response: Faraday::Response,
149
+ collection_key: Symbol,
150
+ lazy: T::Boolean,
151
+ block: T.proc.params(item: T.untyped).returns(T.type_parameter(:T)),
152
+ )
153
+ .returns(T.any(T::Enumerable[T.type_parameter(:T)], T::Array[T.type_parameter(:T)], LunchMoney::Errors))
154
+ end
155
+ def handle_collection_response(response, collection_key, lazy: false, &block)
156
+ api_errors = errors(response)
157
+ return api_errors if api_errors.present?
158
+
159
+ collection = response.body[collection_key]
160
+ return [] unless collection
161
+
162
+ if lazy
163
+ collection.lazy.map(&block)
164
+ else
165
+ collection.map(&block)
166
+ end
167
+ end
116
168
  end
117
169
  end
118
170
  end
@@ -18,29 +18,28 @@ module LunchMoney
18
18
  params = clean_params({ start_date:, end_date:, currency: })
19
19
  response = get("budgets", query_params: params)
20
20
 
21
- api_errors = errors(response)
22
- return api_errors if api_errors.present?
21
+ handle_api_response(response) do |body|
22
+ body.map do |budget|
23
+ if budget[:data]
24
+ data_keys = budget[:data].keys
25
+ data_keys.each do |data_key|
26
+ budget[:data][data_key] = LunchMoney::Objects::Data.new(**budget[:data][data_key])
27
+ end
28
+ end
23
29
 
24
- response.body.map do |budget|
25
- if budget[:data]
26
- data_keys = budget[:data].keys
27
- data_keys.each do |data_key|
28
- budget[:data][data_key] = LunchMoney::Objects::Data.new(**budget[:data][data_key])
30
+ if budget[:config]
31
+ config_keys = budget[:config].keys
32
+ config_keys.each do |config_key|
33
+ budget[:config][config_key] = LunchMoney::Objects::Data.new(**budget[:config][config_key])
34
+ end
29
35
  end
30
- end
31
36
 
32
- if budget[:config]
33
- config_keys = budget[:config].keys
34
- config_keys.each do |config_key|
35
- budget[:config][config_key] = LunchMoney::Objects::Data.new(**budget[:config][config_key])
37
+ if budget[:recurring]
38
+ budget[:recurring][:list]&.map! { |recurring| LunchMoney::Objects::RecurringExpenseBase.new(**recurring) }
36
39
  end
37
- end
38
40
 
39
- if budget[:recurring]
40
- budget[:recurring][:list]&.map! { |recurring| LunchMoney::Objects::RecurringExpenseBase.new(**recurring) }
41
+ LunchMoney::Objects::Budget.new(**budget)
41
42
  end
42
-
43
- LunchMoney::Objects::Budget.new(**budget)
44
43
  end
45
44
  end
46
45
 
@@ -59,10 +58,9 @@ module LunchMoney
59
58
  params = clean_params({ start_date:, category_id:, amount:, currency: })
60
59
  response = put("budgets", params)
61
60
 
62
- api_errors = errors(response)
63
- return api_errors if api_errors.present?
64
-
65
- response.body
61
+ handle_api_response(response) do |body|
62
+ body
63
+ end
66
64
  end
67
65
 
68
66
  sig { params(start_date: String, category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
@@ -74,10 +72,9 @@ module LunchMoney
74
72
 
75
73
  response = delete("budgets", query_params: params)
76
74
 
77
- api_errors = errors(response)
78
- return api_errors if api_errors.present?
79
-
80
- response.body
75
+ handle_api_response(response) do |body|
76
+ body
77
+ end
81
78
  end
82
79
  end
83
80
  end
@@ -7,7 +7,7 @@ module LunchMoney
7
7
  module Calls
8
8
  # https://lunchmoney.dev/#categories
9
9
  class Categories < LunchMoney::Calls::Base
10
- # Valid query parameter formets for categories
10
+ # Valid query parameter formats for categories
11
11
  VALID_FORMATS = T.let(
12
12
  [
13
13
  "flattened",
@@ -24,13 +24,12 @@ module LunchMoney
24
24
  def categories(format: nil)
25
25
  response = get("categories", query_params: categories_params(format:))
26
26
 
27
- api_errors = errors(response)
28
- return api_errors if api_errors.present?
27
+ handle_api_response(response) do |body|
28
+ body[:categories].map do |category|
29
+ category[:children]&.map! { |child_category| LunchMoney::Objects::Category.new(**child_category) }
29
30
 
30
- response.body[:categories].map do |category|
31
- category[:children]&.map! { |child_category| LunchMoney::Objects::Category.new(**child_category) }
32
-
33
- LunchMoney::Objects::Category.new(**category)
31
+ LunchMoney::Objects::Category.new(**category)
32
+ end
34
33
  end
35
34
  end
36
35
 
@@ -38,12 +37,11 @@ module LunchMoney
38
37
  def category(category_id)
39
38
  response = get("categories/#{category_id}")
40
39
 
41
- api_errors = errors(response)
42
- return api_errors if api_errors.present?
43
-
44
- response.body[:children]&.map! { |child_category| LunchMoney::Objects::ChildCategory.new(**child_category) }
40
+ handle_api_response(response) do |body|
41
+ body[:children]&.map! { |child_category| LunchMoney::Objects::ChildCategory.new(**child_category) }
45
42
 
46
- LunchMoney::Objects::Category.new(**response.body)
43
+ LunchMoney::Objects::Category.new(**body)
44
+ end
47
45
  end
48
46
 
49
47
  sig do
@@ -70,10 +68,9 @@ module LunchMoney
70
68
  })
71
69
  response = post("categories", params)
72
70
 
73
- api_errors = errors(response)
74
- return api_errors if api_errors.present?
75
-
76
- response.body
71
+ handle_api_response(response) do |body|
72
+ body
73
+ end
77
74
  end
78
75
 
79
76
  sig do
@@ -89,7 +86,6 @@ module LunchMoney
89
86
  end
90
87
  def create_category_group(name:, description: nil, is_income: false, exclude_from_budget: false,
91
88
  exclude_from_totals: false, category_ids: [], new_categories: [])
92
-
93
89
  params = {
94
90
  name:,
95
91
  description:,
@@ -102,10 +98,9 @@ module LunchMoney
102
98
 
103
99
  response = post("categories/group", params)
104
100
 
105
- api_errors = errors(response)
106
- return api_errors if api_errors.present?
107
-
108
- response.body
101
+ handle_api_response(response) do |body|
102
+ body
103
+ end
109
104
  end
110
105
 
111
106
  sig do
@@ -122,7 +117,6 @@ module LunchMoney
122
117
  end
123
118
  def update_category(category_id, name: nil, description: nil, is_income: nil, exclude_from_budget: nil,
124
119
  exclude_from_totals: nil, archived: nil, group_id: nil)
125
-
126
120
  params = clean_params({
127
121
  name:,
128
122
  description:,
@@ -134,10 +128,9 @@ module LunchMoney
134
128
  })
135
129
  response = put("categories/#{category_id}", params)
136
130
 
137
- api_errors = errors(response)
138
- return api_errors if api_errors.present?
139
-
140
- response.body
131
+ handle_api_response(response) do |body|
132
+ body
133
+ end
141
134
  end
142
135
 
143
136
  sig do
@@ -155,30 +148,27 @@ module LunchMoney
155
148
 
156
149
  response = post("categories/group/#{group_id}/add", params)
157
150
 
158
- api_errors = errors(response)
159
- return api_errors if api_errors.present?
160
-
161
- LunchMoney::Objects::Category.new(**response.body)
151
+ handle_api_response(response) do |body|
152
+ LunchMoney::Objects::Category.new(**body)
153
+ end
162
154
  end
163
155
 
164
156
  sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
165
157
  def delete_category(category_id)
166
158
  response = delete("categories/#{category_id}")
167
159
 
168
- api_errors = errors(response)
169
- return api_errors if api_errors.present?
170
-
171
- response.body
160
+ handle_api_response(response) do |body|
161
+ body
162
+ end
172
163
  end
173
164
 
174
165
  sig { params(category_id: Integer).returns(T.any(T::Boolean, LunchMoney::Errors)) }
175
166
  def force_delete_category(category_id)
176
167
  response = delete("categories/#{category_id}/force")
177
168
 
178
- api_errors = errors(response)
179
- return api_errors if api_errors.present?
180
-
181
- response.body
169
+ handle_api_response(response) do |body|
170
+ body
171
+ end
182
172
  end
183
173
 
184
174
  private
@@ -11,11 +11,10 @@ module LunchMoney
11
11
  def crypto
12
12
  response = get("crypto")
13
13
 
14
- api_errors = errors(response)
15
- return api_errors if api_errors.present?
16
-
17
- response.body[:crypto].map do |crypto|
18
- LunchMoney::Objects::Crypto.new(**crypto)
14
+ handle_api_response(response) do |body|
15
+ body[:crypto].map do |crypto|
16
+ LunchMoney::Objects::Crypto.new(**crypto)
17
+ end
19
18
  end
20
19
  end
21
20
 
@@ -40,10 +39,9 @@ module LunchMoney
40
39
 
41
40
  response = put("crypto/manual/#{crypto_id}", params)
42
41
 
43
- api_errors = errors(response)
44
- return api_errors if api_errors.present?
45
-
46
- LunchMoney::Objects::CryptoBase.new(**response.body)
42
+ handle_api_response(response) do |body|
43
+ LunchMoney::Objects::CryptoBase.new(**body)
44
+ end
47
45
  end
48
46
  end
49
47
  end
@@ -11,11 +11,10 @@ module LunchMoney
11
11
  def plaid_accounts
12
12
  response = get("plaid_accounts")
13
13
 
14
- api_errors = errors(response)
15
- return api_errors if api_errors.present?
16
-
17
- response.body[:plaid_accounts].map do |plaid_account|
18
- LunchMoney::Objects::PlaidAccount.new(**plaid_account)
14
+ handle_api_response(response) do |body|
15
+ body[:plaid_accounts].map do |plaid_account|
16
+ LunchMoney::Objects::PlaidAccount.new(**plaid_account)
17
+ end
19
18
  end
20
19
  end
21
20
 
@@ -30,10 +29,9 @@ module LunchMoney
30
29
  params = clean_params({ start_date:, end_date:, plaid_account_id: })
31
30
  response = post("plaid_accounts/fetch", params)
32
31
 
33
- api_errors = errors(response)
34
- return api_errors if api_errors.present?
35
-
36
- response.body
32
+ handle_api_response(response) do |body|
33
+ body
34
+ end
37
35
  end
38
36
  end
39
37
  end