class

Sepia::Object

Inherits Reference < Object

Base class for all objects managed by Sepia.

Provides generation tracking functionality for optimistic concurrency control, automatic ID generation with UUIDs, and core persistence methods.

All classes that use Sepia's serialization features must inherit from this class.

Example

class MyDocument < Sepia::Object
  include Sepia::Serializable

  property content : String

  def initialize(@content = "")
  end

  def to_sepia : String
    @content
  end

  def self.from_sepia(sepia_string : String) : self
    new(sepia_string)
  end
end

doc = MyDocument.new("Hello, World!")
doc.save # Saves to storage with auto-generated UUID

Constructors

load(id : String, path : String | Nil = nil) : self

Loads an object from storage with transparent generation handling.

Generation-transparent loading:

  • If you specify a generation (e.g., "note-123.2"), loads that specific generation
  • If you specify base ID only (e.g., "note-123"), automatically loads the latest generation
  • If no generations exist, loads the base object

This makes generations completely transparent - users get the latest content by default without needing to think about versioning.

# Load the latest version automatically (most common case)
doc = MyDocument.load("doc-uuid")

# Load a specific generation (rare, for version control)
old_doc = MyDocument.load("doc-uuid.1")

# Load from custom path (still respects latest generation)
doc = MyDocument.load("doc-uuid", "/custom/path")
Source

Class methods

exists?(id : String) : Bool

Check if an object with the given ID exists in storage.

Returns true if an object of this class with the specified ID exists, false otherwise. This is useful for checking existence before loading or verifying if a specific generation exists.

# Check if a specific version exists
if Document.exists?("doc-123.2")
  puts "Version 2 exists"
else
  puts "Version 2 does not exist"
end

# Check for existence of legacy object (generation 0)
Document.exists?("legacy-doc") # => true if exists

# Common pattern: check before creating new generation
unless Document.exists?("doc-123.3")
  # Safe to create version 3
end
Source
generation_separator

Separator character used between base ID and generation number.

Default is "." which creates IDs like "note-123.1", "note-123.2". Can be overridden per class if needed.

class CustomNote < Sepia::Object
  class_property generation_separator = "_"
end

note = CustomNote.new
note.save_with_generation # ID becomes "note-uuid_1"
Source
generation_separator=(generation_separator : String)

Separator character used between base ID and generation number.

Default is "." which creates IDs like "note-123.1", "note-123.2". Can be overridden per class if needed.

class CustomNote < Sepia::Object
  class_property generation_separator = "_"
end

note = CustomNote.new
note.save_with_generation # ID becomes "note-uuid_1"
Source
latest(base_id : String) : self | Nil

Find the latest version of an object by its base ID.

Returns the object with the highest generation number, or nil if no versions exist. This is useful for always retrieving the most recent version of an object.

# Get the latest version of a document
latest_doc = Document.latest("doc-123")
if latest_doc
  puts "Latest version: #{latest_doc.generation}"
  puts "Content: #{latest_doc.content}"
end

# Always returns nil for non-existent base IDs
Document.latest("non-existent") # => nil
Source
versions(base_id : String) : Array(self)

Find all versions of an object by its base ID.

Returns an array of all object versions, sorted by generation number in ascending order. This allows you to access the complete version history of an object.

# Get all versions of a document
all_versions = Document.versions("doc-123")

# Print version history
all_versions.each do |version|
  puts "Version #{version.generation}: #{version.created_at}"
end

# versions are sorted by generation
all_versions.first.generation # => 0 (oldest)
all_versions.last.generation  # => 2 (newest)

Note: For FileStorage, this scans the directory and may be slow with many versions. Consider caching or cleanup strategies for long-running applications.

Source

Instance methods

backup_supported?

Checks if this object can be backed up with the current storage backend.

Returns

true if backup is supported, false otherwise

Example

if document.backup_supported?
  puts "Can create backup of this document"
else
  puts "Switch to FileStorage to enable backup"
end
Source
backup_to(output_path : String) : String

Creates a backup of this object and all its referenced objects.

This is a convenience method that creates a backup containing this object as the root and automatically includes all objects it references.

Parameters

  • output_path : Path where the backup tar file will be created

Returns

The path to the created backup file

Example

# Backup a project and all its documents
project = Project.load("project-123")
backup_path = project.backup_to("project_backup.tar")

# Auto-generate timestamped backup name
backup_path = user.backup_to("user_backup_#{Time.utc.to_unix}.tar")

Raises

  • Sepia::Backup::BackendNotSupportedError if storage doesn't support backups
  • Sepia::Backup::BackupCreationError if backup creation fails
Source
base_id

Returns the base ID without the generation suffix.

The base ID is the unique identifier that remains constant across all generations.

obj.sepia_id = "note-123.2"
obj.base_id # => "note-123"

obj.sepia_id = "legacy-note"
obj.base_id # => "legacy-note"
Source
canonical_path

Returns the canonical path for this object in storage.

The canonical path follows the pattern: {storage_path}/{ClassName}/{sepia_id}. This is where Serializable objects are stored by default.

doc = MyDocument.new
doc.sepia_id = "my-doc"
doc.canonical_path # => "/tmp/storage/MyDocument/my-doc"
Source
create_backup(backup_dir : String = ".", include_timestamp : Bool = true) : String

Creates a backup with an auto-generated timestamped filename.

Convenience method that generates a backup filename based on the object's class name and ID with a timestamp.

Parameters

  • backup_dir : Directory where the backup will be created (defaults to current directory)
  • include_timestamp : Whether to include timestamp in filename (default: true)

Returns

The path to the created backup file

Example

# Create timestamped backup in current directory
backup_path = document.create_backup
# => "Document_doc123_backup_1703123456.tar"

# Create backup in specific directory without timestamp
backup_path = project.create_backup("/backups", include_timestamp: false)
# => "/backups/Project_project456_backup.tar"
Source
delete

Deletes the object from storage.

Removes the object's file or directory from storage. For Container objects, also cleans up any nested objects and references.

doc = MyDocument.load("doc-uuid")
doc.delete # Removes the document from storage
Source
generation

Returns the generation number extracted from the object's ID.

For IDs without a generation suffix, returns 0.

obj.sepia_id = "note-123.2"
obj.generation # => 2

obj.sepia_id = "legacy-note"
obj.generation # => 0
Source
save(path : String | Nil = nil)

Saves the object to storage.

For Serializable objects, serializes the object using its to_sepia method. For Container objects, creates a directory structure and saves all nested objects.

The optional path parameter specifies where to save the object. If not provided, uses the canonical path based on the object's class and sepia_id.

doc = MyDocument.new("Hello")
doc.save # Saves to default location

# Save to specific path
doc.save("/custom/path")
Source
save(metadata = nil) : self

Saves the object to storage with optional metadata.

This is a convenience method that delegates to the global Storage API. It automatically detects whether to create a new object or update an existing one.

Parameters

  • metadata : Optional JSON-serializable metadata for event logging

Returns

The object itself (for method chaining)

Example

note = Note.new("Hello World")
note.sepia_id = "my-note"

# Simple save
note.save

# Save with metadata
note.save(metadata: {"user" => "alice"})
Source
save(*, force_new_generation : Bool, metadata = nil)

Saves the object with forced new generation.

This method always creates a new version regardless of whether the object already exists in storage. Equivalent to save_with_generation but follows the same method signature pattern as save().

Parameters

  • force_new_generation : Always increment generation (set to true)
  • metadata : Optional JSON-serializable metadata for event logging

Returns

The object itself (for method chaining)

Example

note = Note.new("Hello World")
note.sepia_id = "my-note"

# Always creates new version
note.save(force_new_generation: true, metadata: {"user" => "alice"})
Source
save_with_generation(metadata = nil) : self

Creates a new version of this object with an incremented generation number.

Returns a new object instance with the same attributes but a new ID containing the next generation number. The original object is not modified.

obj.sepia_id = "note-123.2"
new_obj = obj.save_with_generation
new_obj.sepia_id # => "note-123.3"

NOTE: This method requires the class to implement to_sepia and from_sepia for Serializable objects. For Container objects, override this method.

Source
sepia_id

Unique identifier for this object.

Defaults to a randomly generated UUIDv4 string. Can be manually set for specific use cases or when restoring objects with known IDs.

The ID format may include generation suffixes for version tracking:

  • Without generation: "note-123e4567-e89b-12d3-a456-426614174000"
  • With generation: "note-123e4567-e89b-12d3-a456-426614174000.1"
obj = MyClass.new
obj.sepia_id # => "myclass-uuid-string"

# Manually set ID
obj.sepia_id = "custom-id"
Source
sepia_id=(id : String)

Sets the unique identifier for this object.

Use this when you need to control the object's ID, such as when restoring from external data or maintaining specific naming conventions.

Note: Forward slashes ("/") are automatically replaced with underscores ("_") to prevent unintended subdirectory creation in the filesystem storage.

obj = MyClass.new
obj.sepia_id = "document-2024-001"
obj.sepia_id = "path/to/document" # Becomes "path_to_document"
Source
stale?(expected_generation : Int32) : Bool

Checks if a newer version of this object exists.

Returns true if an object with ID base_id.(expected_generation + 1) exists. This is useful for optimistic concurrency control.

obj.sepia_id = "note-123.2"
obj.stale?(2) # => true if "note-123.3" exists
Source