bridgeapi_client 1.0.1

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +2 -0
  3. data/.github/workflows/ci-analysis.yml +24 -0
  4. data/.github/workflows/rubocop-analysis.yml +24 -0
  5. data/.gitignore +11 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +50 -0
  8. data/CHANGELOG.md +24 -0
  9. data/Gemfile +23 -0
  10. data/Gemfile.lock +186 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +229 -0
  13. data/Rakefile +12 -0
  14. data/bin/bundle +114 -0
  15. data/bin/coderay +27 -0
  16. data/bin/console +21 -0
  17. data/bin/htmldiff +27 -0
  18. data/bin/ldiff +27 -0
  19. data/bin/pry +27 -0
  20. data/bin/racc +27 -0
  21. data/bin/rake +27 -0
  22. data/bin/rspec +27 -0
  23. data/bin/rubocop +27 -0
  24. data/bin/ruby-parse +27 -0
  25. data/bin/ruby-rewrite +27 -0
  26. data/bin/setup +8 -0
  27. data/bridge_api.gemspec +40 -0
  28. data/lib/bridge_api/account.rb +46 -0
  29. data/lib/bridge_api/api/client.rb +206 -0
  30. data/lib/bridge_api/api/error.rb +48 -0
  31. data/lib/bridge_api/api/resource.rb +25 -0
  32. data/lib/bridge_api/authorization.rb +44 -0
  33. data/lib/bridge_api/bank.rb +18 -0
  34. data/lib/bridge_api/bridge_object.rb +120 -0
  35. data/lib/bridge_api/category.rb +39 -0
  36. data/lib/bridge_api/configuration.rb +38 -0
  37. data/lib/bridge_api/connect.rb +59 -0
  38. data/lib/bridge_api/insight.rb +27 -0
  39. data/lib/bridge_api/item.rb +91 -0
  40. data/lib/bridge_api/object_types.rb +28 -0
  41. data/lib/bridge_api/payment.rb +260 -0
  42. data/lib/bridge_api/provider.rb +40 -0
  43. data/lib/bridge_api/resources.rb +15 -0
  44. data/lib/bridge_api/stock.rb +45 -0
  45. data/lib/bridge_api/transaction.rb +79 -0
  46. data/lib/bridge_api/transfer.rb +42 -0
  47. data/lib/bridge_api/user.rb +98 -0
  48. data/lib/bridge_api/version.rb +5 -0
  49. data/lib/bridge_api.rb +22 -0
  50. data/lib/bridgeapi_client.rb +3 -0
  51. metadata +97 -0
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ require "bridge_api/api/error"
8
+
9
+ module BridgeApi
10
+ module API
11
+ #
12
+ # Allows to request the Bridge API using Ruby native net/http library
13
+ #
14
+ class Client
15
+ HTTP_VERBS_MAP = {
16
+ get: Net::HTTP::Get,
17
+ post: Net::HTTP::Post,
18
+ put: Net::HTTP::Put,
19
+ delete: Net::HTTP::Delete
20
+ }.freeze
21
+
22
+ attr_accessor :access_token
23
+
24
+ #
25
+ # Handles a GET request
26
+ #
27
+ # @param [String] path the API endpoint PATH to query
28
+ # @param [Hash] params any params that might be required (or optional) to communicate with the API
29
+ #
30
+ # @return [Hash] the parsed API response
31
+ #
32
+ # @raise [API::Error] expectation if API responding with any error
33
+ #
34
+ def get(path, **params)
35
+ request :get, path, params
36
+ end
37
+
38
+ #
39
+ # Handles a POST request
40
+ #
41
+ # @param (see #get)
42
+ #
43
+ # @return (see #get)
44
+ #
45
+ # @raise (see #get)
46
+ #
47
+
48
+ def post(path, **params)
49
+ request :post, path, params
50
+ end
51
+
52
+ #
53
+ # Handles a PUT request
54
+ #
55
+ # @param (see #get)
56
+ #
57
+ # @return (see #get)
58
+ #
59
+ # @raise (see #get)
60
+ #
61
+ def put(path, **params)
62
+ request :put, path, params
63
+ end
64
+
65
+ #
66
+ # Handles a DELETE request
67
+ #
68
+ # @param (see #get)
69
+ #
70
+ # @return (see #get)
71
+ #
72
+ # @raise (see #get)
73
+ #
74
+ def delete(path, **params)
75
+ request :delete, path, params
76
+ end
77
+
78
+ private
79
+
80
+ def request(method, path, params = {})
81
+ make_http_request do
82
+ if !method.in?(%i(get delete))
83
+ HTTP_VERBS_MAP[method].new(path, headers).tap do |request|
84
+ request.body = params.to_json
85
+ end
86
+ else
87
+ HTTP_VERBS_MAP[method].new(encode_path(path, params), headers)
88
+ end
89
+ end
90
+ end
91
+
92
+ def make_http_request
93
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
94
+ http.set_debug_output($stdout) if debug?
95
+
96
+ request = yield
97
+
98
+ if debug?
99
+ puts "\n--- BRIDGE API REQUEST ---"
100
+ puts "#{request.method} #{uri}#{request.path}"
101
+ request.each_header { |k, v| puts " #{k}: #{v}" }
102
+ puts " Body: #{request.body.inspect}" if request.body
103
+ puts "--------------------------\n"
104
+ end
105
+
106
+ api_response = http.request(request)
107
+
108
+ if debug?
109
+ puts "--- BRIDGE API RESPONSE #{api_response.code} ---"
110
+ puts api_response.body
111
+ puts "-------------------------------\n"
112
+ end
113
+
114
+ case api_response.code
115
+ when "200", "201"
116
+ data = parse_response_body(api_response.body)
117
+
118
+ if data.dig(:pagination, :next_uri) && follow_pages
119
+ handle_paging(data)
120
+ else
121
+ data
122
+ end
123
+ when "204", "202"
124
+ {}
125
+ else
126
+ handle_error(api_response)
127
+ end
128
+ end
129
+ end
130
+
131
+ def parse_response_body(json_response_body)
132
+ JSON.parse(json_response_body, symbolize_names: true)
133
+ end
134
+
135
+ def uri
136
+ @uri ||= URI.parse(BridgeApi.configuration.api_base_url)
137
+ end
138
+
139
+ def follow_pages
140
+ BridgeApi.configuration.follow_pages
141
+ end
142
+
143
+ def debug?
144
+ BridgeApi.configuration.debug
145
+ end
146
+
147
+ def handle_paging(data)
148
+ page_uri = URI.parse(data[:pagination][:next_uri])
149
+ params = URI.decode_www_form(page_uri.query).to_h
150
+
151
+ next_page_data = get(page_uri.path, **params)
152
+
153
+ next_page_data[:resources] = data[:resources] + next_page_data[:resources]
154
+ next_page_data
155
+ end
156
+
157
+ def headers
158
+ headers =
159
+ {
160
+ "Bridge-Version" => BridgeApi.configuration.api_version,
161
+ "Client-Id" => BridgeApi.configuration.api_client_id,
162
+ "Client-Secret" => BridgeApi.configuration.api_client_secret,
163
+ "Content-Type" => "application/json",
164
+ "Accept" => "application/json"
165
+ }
166
+
167
+ return headers unless access_token
168
+
169
+ headers.merge!("Authorization" => "Bearer #{access_token}")
170
+ end
171
+
172
+ def encode_path(path, params = nil)
173
+ URI::HTTP
174
+ .build(path: path, query: URI.encode_www_form(params))
175
+ .request_uri
176
+ end
177
+
178
+ def handle_error(api_response)
179
+ response_body = parse_response_body(api_response.body)
180
+
181
+ case api_response.code
182
+ when "400"
183
+ raise API::BadRequestError.new(api_response.code, response_body)
184
+ when "401"
185
+ raise API::UnauthorizedError.new(api_response.code, response_body)
186
+ when "403"
187
+ raise API::ForbiddenError.new(api_response.code, response_body)
188
+ when "404"
189
+ raise API::NotFoundError.new(api_response.code, response_body)
190
+ when "409"
191
+ raise API::ConflictError.new(api_response.code, response_body)
192
+ when "415"
193
+ raise API::UnsupportedMediaTypeError.new(api_response.code, response_body)
194
+ when "422"
195
+ raise API::UnprocessableEntityError.new(api_response.code, response_body)
196
+ when "429"
197
+ raise API::TooManyRequestsError.new(api_response.code, response_body)
198
+ when "500"
199
+ raise API::InternalServerError.new(api_response.code, response_body)
200
+ else
201
+ raise API::Error.new(api_response.code, response_body)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ module API
5
+ #
6
+ # Error is the base error from which all other more specific BridgeApi errors derive.
7
+ #
8
+ class Error < StandardError
9
+ #
10
+ # Initializes Error
11
+ #
12
+ # @param [String] code the HTTP code returned by the API
13
+ # @param [Hash] response_body the parsed API response
14
+ # @option response_body [String] :type the machine readable error message
15
+ # @option response_body [String] :message the human readable error message
16
+ # @option response_body [String] :documentation_url the optional link to documentation
17
+ #
18
+ def initialize(code, response_body = {})
19
+ @payload = response_body
20
+ @code = code
21
+ @type = payload[:type]
22
+ @documentation_url = payload[:documentation_url]
23
+
24
+ super(payload[:message])
25
+ end
26
+
27
+ attr_reader :payload, :code, :type, :documentation_url
28
+ end
29
+
30
+ class BadRequestError < Error; end
31
+
32
+ class UnauthorizedError < Error; end
33
+
34
+ class ForbiddenError < Error; end
35
+
36
+ class NotFoundError < Error; end
37
+
38
+ class ConflictError < Error; end
39
+
40
+ class UnsupportedMediaTypeError < Error; end
41
+
42
+ class UnprocessableEntityError < Error; end
43
+
44
+ class TooManyRequestsError < Error; end
45
+
46
+ class InternalServerError < Error; end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ module API
5
+ #
6
+ # Extend any resource class with API specific methods
7
+ #
8
+ module Resource
9
+ private
10
+
11
+ def protected_resource(access_token)
12
+ client = API::Client.new
13
+ client.access_token = access_token
14
+ Thread.current[:bridge_api_api_client] = client
15
+ yield
16
+ ensure
17
+ Thread.current[:bridge_api_api_client] = nil
18
+ end
19
+
20
+ def api_client
21
+ Thread.current[:bridge_api_api_client] || API::Client.new
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module BridgeApi
6
+ #
7
+ # User authentication & authorization
8
+ #
9
+ class Authorization
10
+ attr_reader :access_token, :expires_at
11
+
12
+ #
13
+ # Initializes Authorization
14
+ #
15
+ # @param [String] access_token access token the access token provided by the API
16
+ # @param [Time] expires_at the expiration time for the provided access token
17
+ #
18
+ def initialize(access_token, expires_at)
19
+ @access_token = access_token
20
+ @expires_at = Time.parse(expires_at)
21
+ end
22
+
23
+ class << self
24
+ include API::Resource
25
+
26
+ #
27
+ # Generate an access token for a Bridge user
28
+ #
29
+ # @param [String] user_uuid the Bridge user UUID
30
+ # @param [String] external_user_id the external user identifier (alternative to user_uuid)
31
+ #
32
+ # @return [Authorization] the authorization informations provided by the API
33
+ #
34
+ def generate_token(user_uuid: nil, external_user_id: nil)
35
+ params = {}
36
+ params[:user_uuid] = user_uuid if user_uuid
37
+ params[:external_user_id] = external_user_id if external_user_id
38
+
39
+ response = api_client.post("/v3/aggregation/authorization/token", **params)
40
+ new(response[:access_token], response[:expires_at])
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ #
5
+ # Bank resource is deprecated, using Provider instead
6
+ #
7
+ class Bank < Provider
8
+ def self.list(**params)
9
+ warn "BridgeApi::Bank is deprecated. Use BridgeApi::Provider instead."
10
+ super
11
+ end
12
+
13
+ def self.find(id:, **params)
14
+ warn "BridgeApi::Bank is deprecated. Use BridgeApi::Provider instead."
15
+ super
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ #
5
+ # BridgeObject is the base class from which all other more specific BridgeApi resources derive.
6
+ #
7
+ class BridgeObject
8
+ HIDDEN_ATTRIBUTES = %i[resource_type resource_uri].freeze
9
+
10
+ #
11
+ # Initializes BridgeObject
12
+ #
13
+ # @param [Hash] attrs any informations returned by the API as a valid response
14
+ #
15
+ def initialize(**attrs)
16
+ define_instance_variables(attrs)
17
+ end
18
+
19
+ class << self
20
+ #
21
+ # Convert any API response body with its corresponding resource object if exists
22
+ #
23
+ # @param [Hash] data parsed API response body
24
+ #
25
+ # @return [Account, Bank, Category, Item, Stock, Transaction, Transfer, User, BridgeObject] a resource object
26
+ #
27
+ def convert_to_bridge_object(data)
28
+ if data[:resources]
29
+ data[:resources].map { |resource| convert_to_bridge_object(resource) }
30
+ elsif data.is_a?(Array)
31
+ data.map { |val| convert_to_bridge_object(val) }
32
+ else
33
+ object_from_resource_type(data)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def object_classes
40
+ @object_classes ||= ObjectTypes.resource_types_to_classes
41
+ end
42
+
43
+ def object_from_resource_type(data)
44
+ object_classes.fetch(data[:resource_type], BridgeObject).new(**data)
45
+ end
46
+ end
47
+
48
+ def to_hash
49
+ instance_variables.each_with_object({}) do |var, hash|
50
+ hash[var.to_s.delete("@")] =
51
+ case instance_variable_get(var)
52
+ when BridgeObject
53
+ instance_variable_get(var).to_hash
54
+ when Array
55
+ instance_variable_get(var).map { |val| val.is_a?(BridgeObject) ? val.to_hash : val }
56
+ else
57
+ instance_variable_get(var)
58
+ end
59
+ end.transform_keys!(&:to_sym)
60
+ end
61
+
62
+ def to_json(*_args)
63
+ to_hash.to_json
64
+ end
65
+
66
+ def ==(other)
67
+ other.is_a?(BridgeObject) && to_hash == other.to_hash
68
+ end
69
+
70
+ private
71
+
72
+ def define_instance_variables(attrs)
73
+ attrs.each do |key, value|
74
+ next if HIDDEN_ATTRIBUTES.include?(key)
75
+
76
+ handle_values_types(key, value) do |parsed_value|
77
+ instance_variable_set(:"@#{key}", parsed_value)
78
+ self.class.class_eval { attr_reader key }
79
+ end
80
+ end
81
+ end
82
+
83
+ def handle_values_types(key, value)
84
+ yield(
85
+ case value
86
+ when Array
87
+ handle_array_values(value)
88
+ when Hash
89
+ handle_hash_values(value)
90
+ when String
91
+ handle_time_values(key, value)
92
+ else
93
+ value
94
+ end
95
+ )
96
+ end
97
+
98
+ def handle_array_values(array)
99
+ array.map do |value|
100
+ next value unless value.is_a?(Hash)
101
+
102
+ handle_hash_values(value)
103
+ end
104
+ end
105
+
106
+ def handle_hash_values(hash)
107
+ self.class.convert_to_bridge_object(hash)
108
+ end
109
+
110
+ def handle_time_values(key, value)
111
+ if key == :date
112
+ Date.parse(value)
113
+ elsif key.to_s.match?(/_at$/)
114
+ Time.parse(value)
115
+ else
116
+ value
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ #
5
+ # Category resource
6
+ #
7
+ class Category < BridgeObject
8
+ RESOURCE_TYPE = "category"
9
+
10
+ class << self
11
+ include API::Resource
12
+
13
+ #
14
+ # List all categories supported by the Bridge API
15
+ #
16
+ # @param [Hash] params any params that might be required (or optional) to communicate with the API
17
+ #
18
+ # @return [Array<Category>] the supported categories list
19
+ #
20
+ def list(**params)
21
+ data = api_client.get("/v3/aggregation/categories", **params)
22
+ convert_to_bridge_object(**data)
23
+ end
24
+
25
+ #
26
+ # Retrieve a single category
27
+ #
28
+ # @param [Integer] id the id of the requested resource
29
+ # @param [Hash] params any params that might be required (or optional) to communicate with the API
30
+ #
31
+ # @return [Category] the requested category
32
+ #
33
+ def find(id:, **params)
34
+ data = api_client.get("/v3/aggregation/categories/#{id}", **params)
35
+ convert_to_bridge_object(**data)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # BridgeApi module
5
+ #
6
+ module BridgeApi
7
+ def self.configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+
11
+ def self.configuration=(config)
12
+ @configuration = config
13
+ end
14
+
15
+ def self.configure
16
+ yield configuration
17
+ end
18
+
19
+ #
20
+ # Configurations setup
21
+ #
22
+ class Configuration
23
+ attr_reader :api_base_url, :api_version
24
+ attr_accessor :api_client_id, :api_client_secret, :follow_pages, :debug
25
+
26
+ #
27
+ # Initializes Configuration
28
+ #
29
+ def initialize
30
+ @api_base_url = "https://api.bridgeapi.io"
31
+ @api_version = "2025-01-15"
32
+ @api_client_id = ""
33
+ @api_client_secret = ""
34
+ @follow_pages = false
35
+ @debug = false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ #
5
+ # Connect resource
6
+ #
7
+ class Connect < BridgeObject
8
+ class << self
9
+ include API::Resource
10
+
11
+ #
12
+ # Create a Connect session (replaces all old connect methods)
13
+ #
14
+ # @param [String] access_token the access token provided during the user authentication
15
+ # @param [Hash] params any params that might be required (or optional) to communicate with the API
16
+ #
17
+ # @return [BridgeObject] Connect session with redirect_url
18
+ #
19
+ def create_session(access_token:, **params)
20
+ protected_resource(access_token) do
21
+ data = api_client.post("/v3/aggregation/connect-sessions", **params)
22
+ convert_to_bridge_object(**data)
23
+ end
24
+ end
25
+
26
+ # Deprecated methods - kept for backward compatibility
27
+
28
+ def connect_item(access_token:, **params)
29
+ warn "BridgeApi::Connect.connect_item is deprecated. Use BridgeApi::Connect.create_session instead."
30
+ create_session(access_token: access_token, **params)
31
+ end
32
+
33
+ def connect_item_with_iban(access_token:, **params)
34
+ warn "BridgeApi::Connect.connect_item_with_iban is deprecated. Use BridgeApi::Connect.create_session instead."
35
+ create_session(access_token: access_token, **params)
36
+ end
37
+
38
+ def edit_item(access_token:, **params)
39
+ warn "BridgeApi::Connect.edit_item is deprecated. Use BridgeApi::Connect.create_session instead."
40
+ create_session(access_token: access_token, **params)
41
+ end
42
+
43
+ def item_sync(access_token:, **params)
44
+ warn "BridgeApi::Connect.item_sync is deprecated. Use BridgeApi::Connect.create_session instead."
45
+ create_session(access_token: access_token, **params)
46
+ end
47
+
48
+ def validate_email(access_token:, **params)
49
+ warn "BridgeApi::Connect.validate_email is deprecated. Use BridgeApi::Connect.create_session instead."
50
+ create_session(access_token: access_token, **params)
51
+ end
52
+
53
+ def validate_pro_items(access_token:, **params)
54
+ warn "BridgeApi::Connect.validate_pro_items is deprecated. Use BridgeApi::Connect.create_session instead."
55
+ create_session(access_token: access_token, **params)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgeApi
4
+ #
5
+ # Insight resource
6
+ #
7
+ class Insight < BridgeObject
8
+ class << self
9
+ include API::Resource
10
+
11
+ #
12
+ # Categories statistics provided by Bridge
13
+ #
14
+ # @param [String] access_token the access token provided during the user authentication
15
+ # @param [Hash] params any params that might be required (or optional) to communicate with the API
16
+ #
17
+ # @return [Insight] the statistics generated by Bridge API
18
+ #
19
+ def categories_insights(access_token:, **params)
20
+ protected_resource(access_token) do
21
+ data = api_client.get("/v3/aggregation/insights/category", **params)
22
+ convert_to_bridge_object(**data)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end