class Sepia::Object

Overview

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

Defined in:

sepia/object.cr

Constructors

Class Method Summary

Instance Method Summary

Constructor Detail

def self.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")

[View source]

Class Method Detail

def self.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

[View source]
def self.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"

[View source]
def self.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"

[View source]
def self.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

[View source]
def self.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.


[View source]

Instance Method Detail

def base_id : String #

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"

[View source]
def canonical_path : String #

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"

[View source]
def 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

[View source]
def generation : Int32 #

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

[View source]
def 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")

[View source]
def 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"})

[View source]
def 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"})

[View source]
def 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.


[View source]
def sepia_id : String #

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"

[View source]
def 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.

obj = MyClass.new
obj.sepia_id = "document-2024-001"

[View source]
def 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

[View source]