diff --git a/.gitignore b/.gitignore index c2ae678..134c196 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ config/private.json config/keys.json stripe-ruby/ *.md +/exemples +/exemples diff --git a/Gemfile b/Gemfile index 60a5926..0a2214c 100644 --- a/Gemfile +++ b/Gemfile @@ -16,3 +16,5 @@ group :development do gem 'webmock', '~> 3.26' gem 'yard', '~> 0.9.37' end + +gem 'dotenv', '~> 3.1' diff --git a/exe/bridge_api b/exe/bridge_api deleted file mode 100755 index 7f8db28..0000000 --- a/exe/bridge_api +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'bridge_api' - -puts "Bridge API Ruby Gem v#{BridgeApi::VERSION}" -puts 'For more information, visit: https://github.com/ScionX/bridge_api' diff --git a/lib/bridge_api.rb b/lib/bridge_api.rb index 118acd3..edefe55 100644 --- a/lib/bridge_api.rb +++ b/lib/bridge_api.rb @@ -73,3 +73,10 @@ def default_base_url require_relative 'bridge_api/resources/total_balance' require_relative 'bridge_api/models/wallets_collection' require_relative 'bridge_api/client' +require_relative 'bridge_api/services/base_service' +require_relative 'bridge_api/services/kyc_link_service' +require_relative 'bridge_api/services/customer_service' +require_relative 'bridge_api/services/wallet_service' +require_relative 'bridge_api/services/external_account_service' +require_relative 'bridge_api/services/virtual_account_service' +require_relative 'bridge_api/services/webhook_service' diff --git a/lib/bridge_api/api_operations.rb b/lib/bridge_api/api_operations.rb index 83820a8..c606225 100644 --- a/lib/bridge_api/api_operations.rb +++ b/lib/bridge_api/api_operations.rb @@ -13,8 +13,10 @@ def list(client, params = {}) response = client.request(:get, resource_path, params) return response unless response.success? - # Create a list response - for now return as is but can be enhanced later - response + BridgeApi::Util.convert_to_bridged_object( + response, + resource_hint: self::OBJECT_NAME, + ) end end end diff --git a/lib/bridge_api/base_resource.rb b/lib/bridge_api/base_resource.rb index 9bbe1a2..deac78f 100644 --- a/lib/bridge_api/base_resource.rb +++ b/lib/bridge_api/base_resource.rb @@ -121,11 +121,27 @@ def set_attribute(key, value) @unsaved_values.add(key) end + # Dynamic method handling for all attributes in @values + # Only allows attribute access without arguments, raises error if arguments are provided + def method_missing(method_name, *args) + if args.empty? && @values.key?(method_name) + @values[method_name] + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @values.key?(method_name) || super + end + # Convert values to appropriate types when possible def convert_value(value) case value when Array value.map { |v| convert_value(v) } + when Hash + value.each_with_object({}) { |(k, v), hash| hash[k.to_sym] = convert_value(v) } else value end diff --git a/lib/bridge_api/client.rb b/lib/bridge_api/client.rb index c47ab10..7f8f85a 100644 --- a/lib/bridge_api/client.rb +++ b/lib/bridge_api/client.rb @@ -77,7 +77,12 @@ def build_request_options(method, payload, idempotency_key: nil) if %i[get delete].include?(method) { query: payload } else - headers = { 'Idempotency-Key' => idempotency_key || SecureRandom.uuid } + # Only add Idempotency-Key if explicitly provided or for POST/PATCH + # PUT requests (like webhook updates) don't support idempotency keys + headers = {} + if idempotency_key || %i[post patch].include?(method) + headers['Idempotency-Key'] = idempotency_key || SecureRandom.uuid + end { body: payload.to_json, headers: headers } end end @@ -92,11 +97,85 @@ def retry_request(method, endpoint, payload, retries, response) request(method, endpoint, payload, retries + 1) end + # --- Resource Accessor Support --- + class ResourceAccessor + def initialize(client, resource_name) + @client = client + @resource_name = resource_name + @singular_resource_name = resource_name.to_s.sub(/s$/, '') + end + + def list(params = {}) + @client.send("list_#{@resource_name}", params) + end + + def get(id) + @client.send("get_#{@singular_resource_name}", id) + end + + def retrieve(id) + @client.send("get_#{@singular_resource_name}", id) + end + + def create(params = {}, idempotency_key: nil) + method_name = if idempotency_key + "create_#{@singular_resource_name}_with_idempotency" + else + "create_#{@singular_resource_name}" + end + if @client.respond_to?(method_name) + @client.send(method_name, params, idempotency_key: idempotency_key) + else + @client.send(:request, :post, @resource_name, params) + end + end + + def update(id, params = {}, idempotency_key: nil) + method_name = if idempotency_key + "update_#{@singular_resource_name}_with_idempotency" + else + "update_#{@singular_resource_name}" + end + if @client.respond_to?(method_name) + @client.send(method_name, id, params, idempotency_key: idempotency_key) + else + # Use PUT for webhooks, PATCH for others + http_method = @resource_name == :webhooks ? :put : :patch + @client.send(:request, http_method, "#{@resource_name}/#{id}", params) + end + end + + def delete(id) + method_name = "delete_#{@singular_resource_name}" + if @client.respond_to?(method_name) + @client.send(method_name, id) + else + @client.send(:request, :delete, "#{@resource_name}/#{id}", {}) + end + end + + private + + def method_missing(method_name, *, &) + # Delegate other methods to the client that start with the resource name + if @client.respond_to?(method_name) + @client.send(method_name, *) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @client.respond_to?(method_name) || super + end + end + # --- Class Methods --- class << self def define_dynamic_resource_methods (RESOURCES + READ_ONLY_RESOURCES).each do |resource| define_resource_methods(resource) + define_resource_accessor(resource) end end @@ -111,6 +190,16 @@ def define_resource_methods(resource) define_special_methods(resource) unless READ_ONLY_RESOURCES.include?(resource) end + def define_resource_accessor(resource) + define_method(resource) do + instance_variable_name = "@#{resource}_accessor" + unless instance_variable_defined?(instance_variable_name) + instance_variable_set(instance_variable_name, ResourceAccessor.new(self, resource)) + end + instance_variable_get(instance_variable_name) + end + end + def define_special_methods(resource) send("define_#{resource}_methods") if respond_to?("define_#{resource}_methods", true) end @@ -200,7 +289,7 @@ def define_webhooks_methods end end - private :define_resource_methods, :define_special_methods, + private :define_resource_methods, :define_special_methods, :define_resource_accessor, :define_wallets_methods, :define_customers_methods, :define_customers_wallet_methods, :define_customers_virtual_account_methods, :define_webhooks_methods diff --git a/lib/bridge_api/resources/customer.rb b/lib/bridge_api/resources/customer.rb index a0a02a2..8c24475 100644 --- a/lib/bridge_api/resources/customer.rb +++ b/lib/bridge_api/resources/customer.rb @@ -19,32 +19,26 @@ def self.resource_path include BridgeApi::APIOperations::Delete def self.get_customer_wallets(client, customer_id, params = {}) - # Use the client's public API to make the request client.get_customer_wallets(customer_id, params) end def self.create_wallet_for_customer(client, customer_id, chain, idempotency_key: nil) - # Use the client's public API to make the request client.create_customer_wallet(customer_id, chain, idempotency_key: idempotency_key) end def self.get_customer_wallet(client, customer_id, wallet_id) - # Use the client's public API to make the request client.get_customer_wallet(customer_id, wallet_id) end def self.get_customer_virtual_accounts(client, customer_id, params = {}) - # Use the client's public API to make the request client.list_customer_virtual_accounts(customer_id, params) end def self.get_customer_virtual_account(client, customer_id, virtual_account_id) - # Use the client's public API to make the request client.get_customer_virtual_account(customer_id, virtual_account_id) end def self.create_customer_virtual_account(client, customer_id, params, idempotency_key: nil) - # Use the client's public API to make the request client.create_customer_virtual_account(customer_id, params, idempotency_key: idempotency_key) end @@ -52,23 +46,7 @@ def initialize(attributes = {}) super end - # Specific accessor methods for convenience - def id - @values[:id] - end - - def email - @values[:email] - end - - def first_name - @values[:first_name] - end - - def last_name - @values[:last_name] - end - + # Override datetime accessors to return parsed Time objects def created_at parse_datetime(@values[:created_at]) end @@ -101,6 +79,7 @@ def create_virtual_account(client, params, idempotency_key: nil) self.class.create_customer_virtual_account(client, id, params, idempotency_key: idempotency_key) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/kyc_link.rb b/lib/bridge_api/resources/kyc_link.rb index a07ac46..486cb4e 100644 --- a/lib/bridge_api/resources/kyc_link.rb +++ b/lib/bridge_api/resources/kyc_link.rb @@ -69,6 +69,7 @@ def created_at parse_datetime(@values[:created_at]) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/reward_rate.rb b/lib/bridge_api/resources/reward_rate.rb index 0c275a7..9946800 100644 --- a/lib/bridge_api/resources/reward_rate.rb +++ b/lib/bridge_api/resources/reward_rate.rb @@ -20,6 +20,7 @@ def expires_at parse_datetime(@values[:expires_at]) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/transaction_history.rb b/lib/bridge_api/resources/transaction_history.rb index 0ce0a37..943d8c8 100644 --- a/lib/bridge_api/resources/transaction_history.rb +++ b/lib/bridge_api/resources/transaction_history.rb @@ -36,6 +36,7 @@ def destination @values[:destination] end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/virtual_account.rb b/lib/bridge_api/resources/virtual_account.rb index 277510d..faae576 100644 --- a/lib/bridge_api/resources/virtual_account.rb +++ b/lib/bridge_api/resources/virtual_account.rb @@ -52,6 +52,7 @@ def balances @values[:balances] || [] end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/wallet.rb b/lib/bridge_api/resources/wallet.rb index 56591b8..f55eb5f 100644 --- a/lib/bridge_api/resources/wallet.rb +++ b/lib/bridge_api/resources/wallet.rb @@ -55,6 +55,7 @@ def self.get_for_customer(client, customer_id, wallet_id) client.get_customer_wallet(customer_id, wallet_id) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook.rb b/lib/bridge_api/resources/webhook.rb index 9546ab8..ce9fa37 100644 --- a/lib/bridge_api/resources/webhook.rb +++ b/lib/bridge_api/resources/webhook.rb @@ -21,7 +21,7 @@ def self.retrieve(client, id, _params = {}) end def self.create(client, params = {}) - client.create_webhook(params) + super end # Specific accessor methods for convenience @@ -49,6 +49,7 @@ def created_at parse_datetime(@values[:created_at]) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook_event.rb b/lib/bridge_api/resources/webhook_event.rb index bb109b0..3668191 100644 --- a/lib/bridge_api/resources/webhook_event.rb +++ b/lib/bridge_api/resources/webhook_event.rb @@ -52,6 +52,7 @@ def event_created_at parse_datetime(@values[:event_created_at]) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/resources/webhook_event_delivery_log.rb b/lib/bridge_api/resources/webhook_event_delivery_log.rb index 2a8ed5b..b5b3be9 100644 --- a/lib/bridge_api/resources/webhook_event_delivery_log.rb +++ b/lib/bridge_api/resources/webhook_event_delivery_log.rb @@ -24,6 +24,7 @@ def created_at parse_datetime(@values[:created_at]) end + private # Parse a datetime string to a Time object diff --git a/lib/bridge_api/services/base_service.rb b/lib/bridge_api/services/base_service.rb new file mode 100644 index 0000000..e075002 --- /dev/null +++ b/lib/bridge_api/services/base_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class BaseService + def initialize(client) + @client = client + end + + protected + + def request(method, endpoint, params = {}, idempotency_key: nil) + if idempotency_key + @client.send(:request_with_idempotency, method, endpoint, params, idempotency_key) + else + @client.send(:request, method, endpoint, params) + end + end + end + end +end diff --git a/lib/bridge_api/services/customer_service.rb b/lib/bridge_api/services/customer_service.rb new file mode 100644 index 0000000..c6815f3 --- /dev/null +++ b/lib/bridge_api/services/customer_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class CustomerService < BaseService + def create(params = {}, idempotency_key: nil) + request(:post, 'customers', params, idempotency_key: idempotency_key) + end + + def retrieve(id, params = {}) + response = request(:get, "customers/#{id}", params) + return response unless response.success? + + # Return a Customer object directly if the request was successful + BridgeApi::Resources::Customer.construct_from(response.data) + end + + def list(params = {}) + request(:get, 'customers', params) + end + + def update(id, params = {}, idempotency_key: nil) + request(:patch, "customers/#{id}", params, idempotency_key: idempotency_key) + end + end + end +end diff --git a/lib/bridge_api/services/external_account_service.rb b/lib/bridge_api/services/external_account_service.rb new file mode 100644 index 0000000..5c39d5e --- /dev/null +++ b/lib/bridge_api/services/external_account_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling External Account-related operations + class ExternalAccountService < BaseService + def initialize(client) + super + @resource_class = BridgeApi::ExternalAccount + end + + # Get an external account by ID + # @param external_account_id [String] The ID of the external account + # @return [BridgeApi::ExternalAccount] The external account object + def get(external_account_id) + resource = @resource_class.new(@client) + resource.retrieve(external_account_id) + end + + # List all external accounts + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of external account objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end diff --git a/lib/bridge_api/services/kyc_link_service.rb b/lib/bridge_api/services/kyc_link_service.rb new file mode 100644 index 0000000..b15d9cc --- /dev/null +++ b/lib/bridge_api/services/kyc_link_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module BridgeApi + module Services + class KycLinkService < BaseService + def create(params = {}, idempotency_key: nil) + request(:post, 'kyc_links', params, idempotency_key: idempotency_key) + end + + def retrieve(id, params = {}) + request(:get, "kyc_links/#{id}", params) + end + + def list(params = {}) + request(:get, 'kyc_links', params) + end + end + end +end diff --git a/lib/bridge_api/services/virtual_account_service.rb b/lib/bridge_api/services/virtual_account_service.rb new file mode 100644 index 0000000..a297116 --- /dev/null +++ b/lib/bridge_api/services/virtual_account_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../base_resource' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling Virtual Account-related operations + class VirtualAccountService < BaseService + def initialize(client) + super + @resource_class = BridgeApi::VirtualAccount + end + + # Get a virtual account by ID + # @param virtual_account_id [String] The ID of the virtual account + # @return [BridgeApi::VirtualAccount] The virtual account object + def get(virtual_account_id) + resource = @resource_class.new(@client) + resource.retrieve(virtual_account_id) + end + + # List all virtual accounts + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of virtual account objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end diff --git a/lib/bridge_api/services/wallet_service.rb b/lib/bridge_api/services/wallet_service.rb new file mode 100644 index 0000000..0d249fa --- /dev/null +++ b/lib/bridge_api/services/wallet_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module BridgeApi + module Services + # Service class for handling Wallet-related operations + class WalletService < BaseService + def initialize(client) + super + @resource_class = BridgeApi::Wallet + end + + # Get a wallet by ID + # @param wallet_id [String] The ID of the wallet + # @return [BridgeApi::Wallet] The wallet object + def get(wallet_id) + resource = @resource_class.new(@client) + resource.retrieve(wallet_id) + end + + # List all wallets + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of wallet objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end diff --git a/lib/bridge_api/services/webhook_service.rb b/lib/bridge_api/services/webhook_service.rb new file mode 100644 index 0000000..050a469 --- /dev/null +++ b/lib/bridge_api/services/webhook_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative 'base_service' +require_relative '../client' + +module BridgeApi + module Services + # Service class for handling Webhook-related operations + class WebhookService < BaseService + def initialize(client) + super + @resource_class = BridgeApi::Webhook + end + + # Get a webhook by ID + # @param webhook_id [String] The ID of the webhook + # @return [BridgeApi::Webhook] The webhook object + def get(webhook_id) + @resource_class.retrieve(@client, webhook_id) + end + + # List all webhooks + # @param options [Hash] Optional parameters for filtering + # @return [Array] Array of webhook objects + def list(options = {}) + @resource_class.list(@client, options) + end + end + end +end diff --git a/lib/bridge_api/util.rb b/lib/bridge_api/util.rb index 2040160..84bf8d5 100644 --- a/lib/bridge_api/util.rb +++ b/lib/bridge_api/util.rb @@ -2,145 +2,120 @@ module BridgeApi module Util - # Map of object names to their corresponding classes for automatic conversion - def self.object_classes - { - 'wallet' => BridgeApi::Resources::Wallet, - 'customer' => BridgeApi::Resources::Customer, - 'transaction_history' => BridgeApi::Resources::TransactionHistory, - 'reward_rate' => BridgeApi::Resources::RewardRate, - 'kyc_link' => BridgeApi::Resources::KycLink, - 'webhook' => BridgeApi::Resources::Webhook, - 'webhook_event' => BridgeApi::Resources::WebhookEvent, - 'webhook_event_delivery_log' => BridgeApi::Resources::WebhookEventDeliveryLog, - 'virtual_account' => BridgeApi::Resources::VirtualAccount, - } - end + OBJECT_CLASS_NAMES = { + 'wallet' => 'BridgeApi::Resources::Wallet', + 'customer' => 'BridgeApi::Resources::Customer', + 'transaction_history' => 'BridgeApi::Resources::TransactionHistory', + 'reward_rate' => 'BridgeApi::Resources::RewardRate', + 'kyc_link' => 'BridgeApi::Resources::KycLink', + 'webhook' => 'BridgeApi::Resources::Webhook', + 'webhook_event' => 'BridgeApi::Resources::WebhookEvent', + 'webhook_event_delivery_log' => 'BridgeApi::Resources::WebhookEventDeliveryLog', + 'virtual_account' => 'BridgeApi::Resources::VirtualAccount', + }.freeze - # Convert API response data to appropriate resource objects - def self.convert_to_bridged_object(data, opts = {}) - resource_hint = opts[:resource_hint] + RESOURCE_PATTERNS = { + 'wallet' => { all: %i[id chain address] }, + 'customer' => { all: %i[id], any: %i[email name customer_type] }, + 'virtual_account' => { all: %i[id account_number] }, + 'transaction_history' => { all: %i[id], any: %i[amount transaction_date] }, + 'kyc_link' => { all: %i[id redirect_url] }, + 'webhook' => { all: %i[id url status] }, + }.freeze - case data - when Array - data.map { |item| convert_to_bridged_object(item, opts) } - when Hash - # Check if this is a list object (has 'data' key, optional 'count') - if data.key?('data') || data.key?(:data) - # This looks like a list response, convert to ListObject - list_data = symbolize_keys(data) - # Convert the data array items recursively if data exists - if list_data[:data].is_a?(Array) - list_data[:data] = list_data[:data].map do |item| - convert_to_bridged_object(item, opts) - end - end - BridgeApi::ListObject.new(list_data) - # Check if this is a resource object with an 'object' field - elsif (object_name = data['object'] || data[:object]) && object_classes.key?(object_name.to_s) - construct_resource(object_name.to_s, symbolize_keys(data), opts) - elsif resource_hint && object_classes.key?(resource_hint.to_s) - # Use hint if no object field present - construct_resource(resource_hint.to_s, symbolize_keys(data), opts) - elsif likely_resource_type(data) - # Auto-detect resource type based on common fields - object_name = likely_resource_type(data) - construct_resource(object_name, symbolize_keys(data), opts) + class << self + # Convert API response data to appropriate resource objects + def convert_to_bridged_object(data, opts = {}) + # Detect if data is a Client::Response and unwrap it + data = unwrap_client_response(data) + + case data + when Array + data.map { |item| convert_to_bridged_object(item, opts) } + when Hash + process_hash_data(data, opts) else - # For non-resource objects, return as is or convert nested objects - convert_nested_objects(symbolize_keys(data)) + data end - else - data end - end - # Convert hash keys to symbols recursively - def self.symbolize_keys(obj) - case obj - when Hash - obj.each_with_object({}) do |(key, value), new_hash| - new_hash[key.to_sym] = symbolize_keys(value) + private + + def unwrap_client_response(data) + # Detect if data is a Client::Response and unwrap it + return data unless data.is_a?(BridgeApi::Client::Response) + + # Extract payload from response (prefer response.data if present, otherwise response.body) + if data.data + data.data + elsif data.body + data.body + elsif data.respond_to?(:to_h) + data.to_h + else + data end - when Array - obj.map { |item| symbolize_keys(item) } - else - obj end - end - def self.construct_resource(object_name, data, opts) - klass = object_classes[object_name] - if klass.respond_to?(:construct_from) - klass.construct_from(data, opts) - else - klass.new(data) + def process_hash_data(data, opts) + if data.key?('data') || data.key?(:data) + convert_list_object(data, opts) + elsif (object_name = detect_resource_type_from_object_field(data)) + construct_resource(object_name, symbolize_keys(data), opts) + elsif (resource_hint = opts[:resource_hint]) && OBJECT_CLASS_NAMES.key?(resource_hint.to_s) + construct_resource(resource_hint.to_s, symbolize_keys(data), opts) + elsif (detected_type = detect_resource_type(data)) + construct_resource(detected_type, symbolize_keys(data), opts) + else + symbolize_keys(data).transform_values { |value| convert_to_bridged_object(value, opts) } + end end - end - # Attempt to detect resource type based on common identifying fields - def self.likely_resource_type(data) - return false unless data.is_a?(Hash) - - # Search for the first matching resource type - resource_checks = { - 'wallet' => ->(d) { all_keys?(d, %i[id chain address]) }, - 'customer' => ->(d) { all_keys?(d, %i[id]) && any_key?(d, %i[email name customer_type]) }, - 'virtual_account' => ->(d) { all_keys?(d, %i[id account_number]) }, - 'transaction_history' => ->(d) { all_keys?(d, %i[id]) && any_key?(d, %i[amount transaction_date]) }, - 'kyc_link' => ->(d) { all_keys?(d, %i[id redirect_url]) }, - } - - resource_checks.each do |resource_type, check_proc| - return resource_type if check_proc.call(data) && object_classes.key?(resource_type) + def detect_resource_type_from_object_field(data) + object_name = data['object'] || data[:object] + object_name && OBJECT_CLASS_NAMES.key?(object_name.to_s) ? object_name.to_s : nil end - false - end + def convert_list_object(data, opts) + list_data = symbolize_keys(data) + if list_data[:data].is_a?(Array) + list_data[:data] = list_data[:data].map do |item| + convert_to_bridged_object(item, opts) + end + end + BridgeApi::ListObject.new(list_data) + end - # Helper method to check if data has all the specified keys - def self.all_keys?(data, keys) - keys.all? { |key| data.key?(key) } - end + def detect_resource_type(data) + return nil unless data.is_a?(Hash) - # Helper method to check if data has at least one of the specified keys - def self.any_key?(data, keys) - keys.any? { |key| data.key?(key) } - end + RESOURCE_PATTERNS.each do |resource_type, pattern| + all_keys_present = Array(pattern[:all]).all? { |key| data.key?(key) || data.key?(key.to_s) } + any_key_present = !pattern[:any] || Array(pattern[:any]).any? { |key| data.key?(key) || data.key?(key.to_s) } - # Recursively convert nested objects that look like API resources - def self.convert_nested_objects(data) - case data - when Hash - # Check if this is a likely resource object (has id, object type, etc.) - if data[:id] && data[:object] - convert_resource_object(data) - else - # Recursively check nested values - data.transform_values do |value| - convert_nested_objects(value) - end + return resource_type if all_keys_present && any_key_present && OBJECT_CLASS_NAMES.key?(resource_type) end - when Array - data.map { |item| convert_nested_objects(item) } - else - data + + nil end - end - private_class_method def self.convert_resource_object(data) - object_name = data[:object] - if object_classes.key?(object_name.to_s) - klass = object_classes[object_name.to_s] + def construct_resource(object_name, data, opts) + klass = Object.const_get(OBJECT_CLASS_NAMES[object_name]) if klass.respond_to?(:construct_from) - klass.construct_from(data) + klass.construct_from(data, opts) else klass.new(data) end - else - # Recursively check nested values - data.transform_values do |value| - convert_nested_objects(value) + end + + def symbolize_keys(obj) + case obj + when Hash + obj.transform_keys(&:to_sym).transform_values { |value| symbolize_keys(value) } + when Array + obj.map { |item| symbolize_keys(item) } + else + obj end end end