class Attio::Attribute

Attributes define the schema for data stored on records
Represents a custom attribute on an Attio object

def self.resource_path

Returns:
  • (String) - The API path
def self.resource_path
  "attributes"
end

def archive(**opts)

Archive this attribute
def archive(**opts)
  raise InvalidRequestError, "Cannot archive an attribute without an ID" unless persisted?
  response = self.class.send(:execute_request, :POST, "#{resource_path}/archive", {}, opts)
  update_from(response[:data] || response)
  self
end

def archived?

def archived?
  @is_archived == true
end

def create(params = {}, **opts)

Override create to handle object-specific attributes
def create(params = {}, **opts)
  object = params[:object]
  validate_object_identifier!(object)
  prepared_params = prepare_params_for_create(params)
  response = execute_request(:POST, "objects/#{object}/attributes", prepared_params, opts)
  new(response["data"] || response, opts)
end

def for_object(object, params = {}, **)

List attributes for a specific object
def for_object(object, params = {}, **)
  list(params.merge(object: object), **)
end

def has_default?

def has_default?
  is_default_value_enabled == true
end

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

def initialize(attributes = {}, opts = {})
  super
  normalized_attrs = normalize_attributes(attributes)
  @api_slug = normalized_attrs[:api_slug]
  @type = normalized_attrs[:type]
  @attio_object_id = normalized_attrs[:object_id]
  @object_api_slug = normalized_attrs[:object_api_slug]
  @parent_object_id = normalized_attrs[:parent_object_id]
  @created_by_actor = normalized_attrs[:created_by_actor]
  @is_archived = normalized_attrs[:is_archived] || false
  @archived_at = parse_timestamp(normalized_attrs[:archived_at])
  @title = normalized_attrs[:title]
end

def list(params = {}, **opts)

Override list to handle object-specific attributes
def list(params = {}, **opts)
  if params[:object]
    object = params.delete(:object)
    validate_object_identifier!(object)
    response = execute_request(:GET, "objects/#{object}/attributes", params, opts)
    APIResource::ListObject.new(response, self, params.merge(object: object), opts)
  else
    raise ArgumentError, "Attributes must be listed for a specific object. Use Attribute.for_object(object_slug) or pass object: parameter"
  end
end

def prepare_options(options)

def prepare_options(options)
  return nil unless options
  case options
  when Array
    options.map do |opt|
      case opt
      when String
        {title: opt}
      when Hash
        opt
      else
        {title: opt.to_s}
      end
    end
  else
    options
  end
end

def prepare_params_for_create(params)

Override create to handle validation and object parameter
def prepare_params_for_create(params)
  validate_type!(params[:type])
  validate_type_config!(params)
  # Generate api_slug from name if not provided
  api_slug = params[:api_slug] || params[:name].downcase.gsub(/[^a-z0-9]+/, "_")
  {
    data: {
      title: params[:name] || params[:title],
      api_slug: api_slug,
      type: params[:type],
      description: params[:description],
      is_required: params[:is_required] || false,
      is_unique: params[:is_unique] || false,
      is_multiselect: params[:is_multiselect] || false,
      default_value: params[:default_value],
      config: params[:config] || {}
    }.compact
  }
end

def prepare_params_for_update(params)

Override update params preparation
def prepare_params_for_update(params)
  # Only certain fields can be updated
  updateable_fields = %i[
    name
    title
    description
    is_required
    is_unique
    default_value
    options
  ]
  update_params = params.slice(*updateable_fields)
  update_params[:options] = prepare_options(update_params[:options]) if update_params[:options]
  # Wrap in data for API
  {
    data: update_params
  }
end

def required?

def required?
  is_required == true
end

def resource_path

def resource_path
  raise InvalidRequestError, "Cannot generate path without an ID" unless persisted?
  attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
  "#{self.class.resource_path}/#{attribute_id}"
end

def retrieve(id, **opts)

Override retrieve to handle object-scoped attributes
def retrieve(id, **opts)
  # Extract simple ID if it's a nested hash
  attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
  validate_id!(attribute_id)
  # For attributes, we need the object context - check if it's in the nested ID
  if id.is_a?(Hash) && id["object_id"]
    object_id = id["object_id"]
    response = execute_request(:GET, "objects/#{object_id}/attributes/#{attribute_id}", {}, opts)
  else
    # Fall back to regular attributes endpoint
    response = execute_request(:GET, "#{resource_path}/#{attribute_id}", {}, opts)
  end
  new(response["data"] || response, opts)
end

def save(**)

Override save to handle nested ID
def save(**)
  raise InvalidRequestError, "Cannot save an attribute without an ID" unless persisted?
  return self unless changed?
  # Pass the full ID (including object context) to update method
  self.class.update(id, changed_attributes, **)
end

def to_h

Returns:
  • (Hash) - Attribute data as a hash
def to_h
  super.merge(
    api_slug: api_slug,
    name: name,
    description: description,
    type: type,
    is_required: is_required,
    is_unique: is_unique,
    is_default_value_enabled: is_default_value_enabled,
    default_value: default_value,
    options: options,
    object_id: attio_object_id,
    object_api_slug: object_api_slug,
    parent_object_id: parent_object_id,
    created_by_actor: created_by_actor,
    is_archived: is_archived,
    archived_at: archived_at&.iso8601
  ).compact
end

def unarchive(**opts)

Unarchive this attribute
def unarchive(**opts)
  raise InvalidRequestError, "Cannot unarchive an attribute without an ID" unless persisted?
  response = self.class.send(:execute_request, :POST, "#{resource_path}/unarchive", {}, opts)
  update_from(response[:data] || response)
  self
end

def unique?

def unique?
  is_unique == true
end

def update(id, params = {}, **opts)

Override update to handle object-scoped attributes
def update(id, params = {}, **opts)
  # Extract simple ID if it's a nested hash
  attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
  validate_id!(attribute_id)
  # For attributes, we need the object context
  if id.is_a?(Hash) && id["object_id"]
    object_id = id["object_id"]
    prepared_params = prepare_params_for_update(params)
    response = execute_request(:PATCH, "objects/#{object_id}/attributes/#{attribute_id}", prepared_params, opts)
  else
    # Fall back to regular attributes endpoint
    prepared_params = prepare_params_for_update(params)
    response = execute_request(:PATCH, "#{resource_path}/#{attribute_id}", prepared_params, opts)
  end
  new(response["data"] || response, opts)
end

def validate_object_identifier!(object)

def validate_object_identifier!(object)
  raise ArgumentError, "Object identifier is required" if object.nil? || object.to_s.empty?
end

def validate_type!(type)

def validate_type!(type)
  raise ArgumentError, "Attribute type is required" if type.nil? || type.to_s.empty?
  unless TYPES.include?(type.to_s)
    raise ArgumentError, "Invalid attribute type: #{type}. Valid types: #{TYPES.join(", ")}"
  end
end

def validate_type_config!(params)

def validate_type_config!(params)
  type = params[:type]
  config = TYPE_CONFIGS[type.to_s]
  return unless config
  # Check required options
  if config[:requires_options]
    options = params[:options]
    if options.nil? || (options.is_a?(Array) && options.empty?)
      raise ArgumentError, "Attribute type '#{type}' requires options"
    end
  end
  # Check required target object
  if config[:requires_target_object]
    target = params[:target_object]
    if target.nil? || target.to_s.empty?
      raise ArgumentError, "Attribute type '#{type}' requires target_object"
    end
  end
  # Validate unsupported features
  if params[:is_unique] && !config[:supports_unique]
    raise ArgumentError, "Attribute type '#{type}' does not support unique constraint"
  end
  if params[:is_required] && !config[:supports_required]
    raise ArgumentError, "Attribute type '#{type}' does not support required constraint"
  end
  if params[:is_default_value_enabled] && !config[:supports_default]
    raise ArgumentError, "Attribute type '#{type}' does not support default values"
  end
end