class Attio::APIResource

Provides standard CRUD operations in a clean, Ruby-like way
Base class for all API resources

def self.resource_name

Returns:
  • (String) - The lowercase resource name
def self.resource_name
  name.split("::").last.downcase
end

def self.resource_path

Raises:
  • (NotImplementedError) - Must be implemented by subclasses

Returns:
  • (String) - The API path (e.g., "/v2/objects")
def self.resource_path
  raise NotImplementedError, "Subclasses must implement resource_path"
end

def ==(other)

Comparison
def ==(other)
  other.is_a?(self.class) && id == other.id && @attributes == other.instance_variable_get(:@attributes)
end

def [](key)

Returns:
  • (Object) - The value of the attribute

Parameters:
  • key (String, Symbol) -- The attribute key to retrieve
def [](key)
  @attributes[key.to_sym]
end

def []=(key, value)

Parameters:
  • value (Object) -- The value to set
  • key (String, Symbol) -- The attribute key to set
def []=(key, value)
  key = key.to_sym
  old_value = @attributes[key]
  new_value = process_attribute_value(value)
  return if old_value == new_value
  @attributes[key] = new_value
  @changed_attributes.add(key)
end

def api_operations(*operations)

Example: api_operations :list, :create, :retrieve, :update, :delete
Define which operations this resource supports
def api_operations(*operations)
  @supported_operations = operations
  operations.each do |operation|
    case operation
    when :list
      define_list_operation
    when :create
      define_create_operation
    when :retrieve
      define_retrieve_operation
    when :update
      define_update_operation
    when :delete
      define_delete_operation
    else
      raise ArgumentError, "Unknown operation: #{operation}"
    end
  end
end

def attr_attio(*attributes)

Define attribute accessors for known attributes
def attr_attio(*attributes)
  attributes.each do |attr|
    # Reader method
    define_method(attr) do
      self[attr]
    end
    # Writer method
    define_method("#{attr}=") do |value|
      self[attr] = value
    end
    # Predicate method
    define_method("#{attr}?") do
      !!self[attr]
    end
  end
end

def changed

Returns:
  • (Array) - Array of changed attribute names as strings
def changed
  @changed_attributes.map(&:to_s)
end

def changed?

Dirty tracking
def changed?
  !@changed_attributes.empty?
end

def changed_attributes

Returns:
  • (Hash) - Hash of changed attributes with their current values
def changed_attributes
  @changed_attributes.each_with_object({}) do |key, hash|
    hash[key] = @attributes[key]
  end
end

def changes

Returns:
  • (Hash) - Hash mapping attribute names to [old_value, new_value] arrays
def changes
  @changed_attributes.each_with_object({}) do |key, hash|
    hash[key.to_s] = [@original_attributes[key], @attributes[key]]
  end
end

def deep_copy(obj)

def deep_copy(obj)
  case obj
  when Hash
    obj.transform_values { |v| deep_copy(v) }
  when Array
    obj.map { |v| deep_copy(v) }
  when Set
    Set.new(obj.map { |v| deep_copy(v) })
  else
    begin
      obj.dup
    rescue
      obj
    end
  end
end

def define_create_operation

def define_create_operation
  define_singleton_method :create do |params = {}, **opts|
    prepared_params = prepare_params_for_create(params)
    response = execute_request(:POST, resource_path, prepared_params, opts)
    new(response["data"] || response, opts)
  end
end

def define_delete_operation

def define_delete_operation
  define_singleton_method :delete do |id, **opts|
    validate_id!(id)
    execute_request(:DELETE, "#{resource_path}/#{id}", {}, opts)
    true
  end
  singleton_class.send(:alias_method, :destroy, :delete)
end

def define_list_operation

def define_list_operation
  define_singleton_method :list do |params = {}, **opts|
    response = execute_request(:GET, resource_path, params, opts)
    ListObject.new(response, self, params, opts)
  end
  singleton_class.send(:alias_method, :all, :list)
end

def define_retrieve_operation

def define_retrieve_operation
  define_singleton_method :retrieve do |id, **opts|
    validate_id!(id)
    response = execute_request(:GET, "#{resource_path}/#{id}", {}, opts)
    new(response["data"] || response, opts)
  end
  singleton_class.send(:alias_method, :get, :retrieve)
  singleton_class.send(:alias_method, :find, :retrieve)
end

def define_update_operation

def define_update_operation
  define_singleton_method :update do |id, params = {}, **opts|
    validate_id!(id)
    prepared_params = prepare_params_for_update(params)
    response = execute_request(:PATCH, "#{resource_path}/#{id}", prepared_params, opts)
    new(response[:data] || response, opts)
  end
end

def destroy(**)

Default destroy implementation
def destroy(**)
  raise InvalidRequestError, "Cannot destroy a resource without an ID" unless persisted?
  self.class.delete(id, **)
  true
end

def each(&)

Enumerable support
def each(&)
  return enum_for(:each) unless block_given?
  @attributes.each(&)
end

def execute_request(method, path, params = {}, opts = {})

Execute HTTP request
def execute_request(method, path, params = {}, opts = {})
  client = Attio.client(api_key: opts[:api_key])
  case method
  when :GET
    client.get(path, params)
  when :POST
    client.post(path, params)
  when :PUT
    client.put(path, params)
  when :PATCH
    client.patch(path, params)
  when :DELETE
    client.delete(path)
  else
    raise ArgumentError, "Unsupported method: #{method}"
  end
end

def extract_id(id_value)

def extract_id(id_value)
  case id_value
  when Hash
    # Handle Attio's nested ID structure
    # Objects have { workspace_id: "...", object_id: "..." }
    # Records have { workspace_id: "...", object_id: "...", record_id: "..." }
  when String
    # Simple string ID
  end
  id_value
end

def fetch(key, default = nil)

Returns:
  • (Object) - The attribute value or default

Parameters:
  • default (Object) -- The default value if key is not found
  • key (String, Symbol) -- The attribute key to fetch
def fetch(key, default = nil)
  @attributes.fetch(key.to_sym, default)
end

def hash

Returns:
  • (Integer) - Hash code based on class, ID, and attributes
def hash
  [self.class, id, @attributes].hash
end

def id_param_name(id = nil)

Get the ID parameter name (usually "id", but sometimes needs prefix)
def id_param_name(id = nil)
  :id
end

def initialize(attributes = {}, opts = {})

def initialize(attributes = {}, opts = {})
  @attributes = {}
  @original_attributes = {}
  @changed_attributes = Set.new
  @opts = opts
  @metadata = {}
  # Normalize attributes to use symbol keys
  normalized_attrs = normalize_attributes(attributes)
  # Extract metadata and system fields
  if normalized_attrs.is_a?(Hash)
    # Handle Attio's nested ID structure
    @id = extract_id(normalized_attrs[:id])
    @created_at = parse_timestamp(normalized_attrs[:created_at])
    @metadata = normalized_attrs[:_metadata] || {}
    # Process all attributes
    normalized_attrs.each do |key, value|
      next if SKIP_KEYS.include?(key)
      @attributes[key] = process_attribute_value(value)
      @original_attributes[key] = deep_copy(process_attribute_value(value))
    end
  end
end

def inspect

Returns:
  • (String) - Inspection string with class name, ID, and attributes
def inspect
  attrs = @attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
  "#<#{self.class.name}:#{object_id} id=#{id.inspect} #{attrs}>"
end

def key?(key)

def key?(key)
  @attributes.key?(key.to_sym)
end

def keys

Returns:
  • (Array) - Array of attribute keys as symbols
def keys
  @attributes.keys
end

def normalize_attributes(attributes)

def normalize_attributes(attributes)
  return attributes unless attributes.is_a?(Hash)
  attributes.transform_keys(&:to_sym)
end

def parse_timestamp(value)

def parse_timestamp(value)
  return nil if value.nil?
  case value
  when Time
    value
  when String
    Time.parse(value)
  when Integer
    Time.at(value)
  end
rescue ArgumentError
  nil
end

def persisted?

Check if resource has been persisted
def persisted?
  !id.nil?
end

def prepare_params_for_create(params)

Hook for subclasses to prepare params before create
def prepare_params_for_create(params)
  params
end

def prepare_params_for_update(params)

Hook for subclasses to prepare params before update
def prepare_params_for_update(params)
  params
end

def process_attribute_value(value)

def process_attribute_value(value)
  case value
  when Hash
    if value.key?(:value) || value.key?("value")
      # Handle Attio attribute format
      value[:value] || value["value"]
    else
      # Regular hash
      value.transform_keys(&:to_sym)
    end
  when Array
    value.map { |v| process_attribute_value(v) }
  else
    value
  end
end

def reset_changes!

Returns:
  • (void) -
def reset_changes!
  @changed_attributes.clear
  @original_attributes = deep_copy(@attributes)
end

def resource_path

Returns:
  • (String) - The full API path including the resource ID
def resource_path
  "#{self.class.resource_path}/#{id}"
end

def revert!

Returns:
  • (void) -
def revert!
  @attributes = deep_copy(@original_attributes)
  @changed_attributes.clear
end

def save(**)

Default save implementation
def save(**)
  if persisted?
    self.class.update(id, changed_attributes, **)
  else
    raise InvalidRequestError, "Cannot save a resource without an ID"
  end
end

def to_h

Serialization
def to_h
  {
    id: id,
    created_at: created_at&.iso8601,
    **@attributes
  }.compact
end

def to_json(*opts)

Returns:
  • (String) - JSON representation of the resource

Parameters:
  • opts (Hash) -- Options to pass to JSON.generate
def to_json(*opts)
  JSON.generate(to_h, *opts)
end

def update_attributes(attributes)

Update attributes
def update_attributes(attributes)
  attributes.each do |key, value|
    self[key] = value
  end
  self
end

def update_from(response)

Update from API response
def update_from(response)
  normalized = normalize_attributes(response)
  @id = normalized[:id] if normalized[:id]
  @created_at = parse_timestamp(normalized[:created_at]) if normalized[:created_at]
  normalized.each do |key, value|
    next if SKIP_KEYS.include?(key)
    @attributes[key] = process_attribute_value(value)
  end
  reset_changes!
  self
end

def validate_id!(id)

Validate an ID parameter
def validate_id!(id)
  raise ArgumentError, "ID is required" if id.nil? || id.to_s.empty?
end

def values

Returns:
  • (Array) - Array of attribute values
def values
  @attributes.values
end