Class: Conductor::Http::ApiClient

Inherits:
Object
  • Object
show all
Defined in:
lib/conductor/http/api_client.rb

Overview

ApiClient handles HTTP communication and serialization/deserialization for Conductor API. It manages authentication tokens with automatic refresh and exponential backoff on failures.

Constant Summary collapse

PRIMITIVE_TYPES =
[String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
NATIVE_TYPE_MAPPING =
{
  'String' => String,
  'Integer' => Integer,
  'Float' => Float,
  'Boolean' => :boolean,
  'DateTime' => DateTime,
  'Date' => Date,
  'Time' => Time,
  'Object' => Object
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration: nil, default_headers: {}) ⇒ ApiClient

Initialize ApiClient

Parameters:

  • configuration (Configuration) (defaults to: nil)

    Configuration object

  • default_headers (Hash) (defaults to: {})

    Optional default headers



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/conductor/http/api_client.rb', line 35

def initialize(configuration: nil, default_headers: {})
  @configuration = configuration || Configuration.new
  @rest_client = RestClient.new(@configuration)
  @default_headers = get_default_headers.merge(default_headers)

  # Token refresh backoff tracking
  @token_refresh_failures = 0
  @last_token_refresh_attempt = 0
  @max_token_refresh_failures = 5

  # Mutex for thread-safe token refresh
  @token_refresh_mutex = Mutex.new

  # Initial token fetch
  refresh_auth_token
end

Instance Attribute Details

#configurationObject (readonly)

Returns the value of attribute configuration.



29
30
31
# File 'lib/conductor/http/api_client.rb', line 29

def configuration
  @configuration
end

#default_headersObject

Returns the value of attribute default_headers.



30
31
32
# File 'lib/conductor/http/api_client.rb', line 30

def default_headers
  @default_headers
end

#last_responseObject (readonly)

Returns the value of attribute last_response.



29
30
31
# File 'lib/conductor/http/api_client.rb', line 29

def last_response
  @last_response
end

#rest_clientObject (readonly)

Returns the value of attribute rest_client.



29
30
31
# File 'lib/conductor/http/api_client.rb', line 29

def rest_client
  @rest_client
end

Instance Method Details

#call_api(resource_path, method, opts = {}) ⇒ Array, Object

Main API call method with automatic retry on auth failures

Parameters:

  • resource_path (String)

    The resource path

  • method (String)

    HTTP method (GET, POST, PUT, DELETE, PATCH)

  • opts (Hash) (defaults to: {})

    Optional parameters

Options Hash (opts):

  • :path_params (Hash)

    Path parameters

  • :query_params (Hash)

    Query parameters

  • :header_params (Hash)

    Header parameters

  • :body (Object)

    Request body

  • :return_type (String)

    Expected return type

  • :return_http_data_only (Boolean)

    Return only data (default: false)

Returns:

  • (Array, Object)

    Response data (and status/headers if return_http_data_only is false)



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/conductor/http/api_client.rb', line 63

def call_api(resource_path, method, opts = {})
  call_api_with_retry(resource_path, method, opts)
rescue AuthorizationError => e
  if e.token_expired? || e.invalid_token?
    token_status = e.token_expired? ? 'expired' : 'invalid'
    logger.info("Authentication token is #{token_status}, renewing token... (request: #{method} #{resource_path})")

    if force_refresh_auth_token
      logger.debug('Authentication token successfully renewed')
      # Retry the request once after successful token refresh
      return call_api_no_retry(resource_path, method, opts)
    else
      logger.error('Failed to renew authentication token. Please check your credentials.')
    end
  end
  raise
end

#deserialize(response, return_type) ⇒ Object

Deserialize HTTP response body into object

Parameters:

  • response (RestResponse)

    HTTP response

  • return_type (String)

    Expected return type (e.g., 'String', 'Array', 'Hash<String, Object>')

Returns:

  • (Object)

    Deserialized object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/conductor/http/api_client.rb', line 122

def deserialize(response, return_type)
  return nil if response.nil? || return_type.nil?

  body = response.body
  return nil if body.nil? || body.empty?

  # For String return type, return the raw body directly
  # (many Conductor APIs return plain text, e.g. workflow ID)
  return body.to_s.strip.delete_prefix('"').delete_suffix('"') if return_type == 'String'

  # Parse response body as JSON for complex types
  data = response.json
  if data.nil?
    # JSON parsing failed — try to use raw body
    return body
  end

  deserialize_data(data, return_type)
rescue StandardError => e
  logger.error("Failed to deserialize data into #{return_type}: #{e.message}")
  nil
end

#force_refresh_auth_tokenBoolean

Force refresh authentication token (called on 401/403 errors)

Returns:

  • (Boolean)

    true if token was successfully refreshed, false otherwise



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/conductor/http/api_client.rb', line 147

def force_refresh_auth_token
  return false unless @configuration.auth_configured?

  @token_refresh_mutex.synchronize do
    # Skip backoff for legitimate token renewal (credentials should be valid)
    token = get_new_token(skip_backoff: true)
    if token
      @configuration.update_token(token)
      return true
    end

    # Check if auth was disabled during token refresh (404 response)
    unless @configuration.auth_configured?
      logger.info('Authentication was disabled (no auth endpoint found)')
      return false
    end

    false
  end
end

#get_authentication_headersHash?

Get authentication headers for requests

Returns:

  • (Hash, nil)

    Headers hash with X-Authorization or nil



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/conductor/http/api_client.rb', line 170

def get_authentication_headers
  return nil unless @configuration.auth_token

  now_ms = (Time.now.to_f * 1000).round
  time_since_last_update = now_ms - @configuration.token_update_time

  # Proactively refresh token if TTL expired
  if time_since_last_update > @configuration.auth_token_ttl_msec
    @token_refresh_mutex.synchronize do
      logger.info('Authentication token TTL expired, renewing token...')
      token = get_new_token(skip_backoff: true)
      @configuration.update_token(token) if token
      logger.debug('Authentication token successfully renewed') if token
    end
  end

  { 'X-Authorization' => @configuration.auth_token }
end

#sanitize_for_serialization(obj) ⇒ Object

Sanitize object for serialization to JSON

Parameters:

  • obj (Object)

    Object to sanitize

Returns:

  • (Object)

    Sanitized object ready for JSON serialization



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/conductor/http/api_client.rb', line 84

def sanitize_for_serialization(obj)
  return nil if obj.nil?
  return obj if PRIMITIVE_TYPES.any? { |type| obj.is_a?(type) }

  case obj
  when Array
    obj.map { |item| sanitize_for_serialization(item) }
  when Hash
    obj.transform_values do |val|
      sanitize_for_serialization(val)
    end
  when DateTime, Date, Time
    obj.iso8601
  else
    # Handle model objects with ATTRIBUTE_MAP and SWAGGER_TYPES
    if obj.class.const_defined?(:ATTRIBUTE_MAP) && obj.class.const_defined?(:SWAGGER_TYPES)
      attr_map = obj.class.const_get(:ATTRIBUTE_MAP)
      swagger_types = obj.class.const_get(:SWAGGER_TYPES)

      swagger_types.each_with_object({}) do |(attr, _type), hash|
        value = obj.send(attr)
        next if value.nil?

        json_key = attr_map[attr]
        hash[json_key] = sanitize_for_serialization(value)
      end
    elsif obj.respond_to?(:to_h)
      sanitize_for_serialization(obj.to_h)
    else
      obj.to_s
    end
  end
end