lib/attio/util/configuration.rb
# frozen_string_literal: true module Attio # Utility classes for the Attio gem module Util # Configuration management for the Attio gem class Configuration # Raised when configuration validation fails class ConfigurationError < ::Attio::InvalidRequestError; end # Settings that must be configured REQUIRED_SETTINGS = %i[api_key].freeze # Optional settings with defaults OPTIONAL_SETTINGS = %i[ api_base api_version timeout open_timeout max_retries logger debug ca_bundle_path verify_ssl_certs use_faraday won_statuses lost_statuses open_statuses in_progress_statuses ].freeze # All available configuration settings ALL_SETTINGS = (REQUIRED_SETTINGS + OPTIONAL_SETTINGS).freeze # Default values for optional settings DEFAULT_SETTINGS = { api_base: "https://api.attio.com", api_version: "v2", timeout: 30, open_timeout: 10, max_retries: 3, logger: nil, debug: false, ca_bundle_path: nil, verify_ssl_certs: true, use_faraday: true, won_statuses: ["Won 🎉"].freeze, lost_statuses: ["Lost"].freeze, open_statuses: ["Lead"].freeze, in_progress_statuses: ["In Progress"].freeze }.freeze attr_reader(*ALL_SETTINGS) def initialize @mutex = Mutex.new @configured = false reset_without_lock! end # Reset configuration to defaults # @return [void] def reset! @mutex.synchronize do reset_without_lock! @configured = false end end def configure raise ConfigurationError, "Configuration has already been finalized" if frozen? @mutex.synchronize do yield(self) if block_given? validate! @configured = true end end # Call this to make configuration immutable def finalize! @mutex.synchronize do validate! freeze unless frozen? end end def validate! REQUIRED_SETTINGS.each do |setting| value = instance_variable_get("@#{setting}") if value.nil? || (value.respond_to?(:empty?) && value.empty?) raise ConfigurationError, "#{setting} must be configured" end end raise ConfigurationError, "timeout must be positive" if @timeout && @timeout <= 0 raise ConfigurationError, "open_timeout must be positive" if @open_timeout && @open_timeout <= 0 raise ConfigurationError, "max_retries must be non-negative" if @max_retries&.negative? true end # Convert configuration to hash # @return [Hash] Configuration settings as a hash def to_h ALL_SETTINGS.each_with_object({}) do |setting, hash| hash[setting] = instance_variable_get("@#{setting}") end end def apply_env_vars! raise ConfigurationError, "Cannot modify frozen configuration" if frozen? @mutex.synchronize do @api_key = ENV.fetch("ATTIO_API_KEY", @api_key) @api_base = ENV.fetch("ATTIO_API_BASE", @api_base) @api_version = ENV.fetch("ATTIO_API_VERSION", @api_version) @timeout = ENV.fetch("ATTIO_TIMEOUT", @timeout).to_i if ENV.key?("ATTIO_TIMEOUT") @open_timeout = ENV.fetch("ATTIO_OPEN_TIMEOUT", @open_timeout).to_i if ENV.key?("ATTIO_OPEN_TIMEOUT") @max_retries = ENV.fetch("ATTIO_MAX_RETRIES", @max_retries).to_i if ENV.key?("ATTIO_MAX_RETRIES") @debug = ENV.fetch("ATTIO_DEBUG", @debug).to_s.downcase == "true" if ENV.key?("ATTIO_DEBUG") @ca_bundle_path = ENV.fetch("ATTIO_CA_BUNDLE_PATH", @ca_bundle_path) if ENV.key?("ATTIO_CA_BUNDLE_PATH") @verify_ssl_certs = ENV.fetch("ATTIO_VERIFY_SSL_CERTS", @verify_ssl_certs).to_s.downcase != "false" if ENV.key?("ATTIO_VERIFY_SSL_CERTS") @use_faraday = ENV.fetch("ATTIO_USE_FARADAY", @use_faraday).to_s.downcase != "false" if ENV.key?("ATTIO_USE_FARADAY") if ENV.key?("ATTIO_LOGGER") logger_class = ENV["ATTIO_LOGGER"] @logger = (logger_class == "STDOUT") ? Logger.new($stdout) : nil end end end # Create a new configuration with merged options # @param options [Hash] Options to merge # @return [Configuration] New configuration instance def merge(options) dup.tap do |config| options.each do |key, value| if ALL_SETTINGS.include?(key.to_sym) config.instance_variable_set("@#{key}", value) end end end end # Create a duplicate configuration # @return [Configuration] Duplicate configuration instance def dup self.class.new.tap do |config| ALL_SETTINGS.each do |setting| config.instance_variable_set("@#{setting}", instance_variable_get("@#{setting}")) end end end # Setters - only work before configuration is frozen ALL_SETTINGS.each do |setting| define_method("#{setting}=") do |value| raise ConfigurationError, "Cannot modify frozen configuration" if frozen? # Don't synchronize here - it's already synchronized in configure instance_variable_set("@#{setting}", value) end end private def reset_without_lock! DEFAULT_SETTINGS.each do |key, value| # For arrays, create a new copy to avoid frozen arrays actual_value = value.is_a?(Array) ? value.dup : value instance_variable_set("@#{key}", actual_value) end @api_key = nil end end end end