class Attio::APIResource
Provides standard CRUD operations in a clean, Ruby-like way
Base class for all API resources
def self.resource_name
-
(String)
- The lowercase resource name
def self.resource_name name.split("::").last.downcase end
def self.resource_path
-
(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)
def ==(other) other.is_a?(self.class) && id == other.id && @attributes == other.instance_variable_get(:@attributes) end
def [](key)
-
(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)
-
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)
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)
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
-
(Array
- Array of changed attribute names as strings)
def changed @changed_attributes.map(&:to_s) end
def changed?
def changed? !@changed_attributes.empty? end
def changed_attributes
-
(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
-
(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(**)
def destroy(**) raise InvalidRequestError, "Cannot destroy a resource without an ID" unless persisted? self.class.delete(id, **) true end
def each(&)
def each(&) return enum_for(:each) unless block_given? @attributes.each(&) end
def execute_request(method, path, params = {}, opts = {})
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)
-
(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
-
(Integer)
- Hash code based on class, ID, and attributes
def hash [self.class, id, @attributes].hash end
def id_param_name(id = nil)
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
-
(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
-
(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?
def persisted? !id.nil? end
def prepare_params_for_create(params)
def prepare_params_for_create(params) params end
def prepare_params_for_update(params)
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!
-
(void)
-
def reset_changes! @changed_attributes.clear @original_attributes = deep_copy(@attributes) end
def resource_path
-
(String)
- The full API path including the resource ID
def resource_path "#{self.class.resource_path}/#{id}" end
def revert!
-
(void)
-
def revert! @attributes = deep_copy(@original_attributes) @changed_attributes.clear end
def save(**)
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
def to_h { id: id, created_at: created_at&.iso8601, **@attributes }.compact end
def to_json(*opts)
-
(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)
def update_attributes(attributes) attributes.each do |key, value| self[key] = value end self end
def update_from(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)
def validate_id!(id) raise ArgumentError, "ID is required" if id.nil? || id.to_s.empty? end
def values
-
(Array)
- Array of attribute values
def values @attributes.values end