GenObject (GenObject v0.4.0)
View SourceA library for creating stateful objects backed by GenServer processes with inheritance support.
GenObject provides a macro-based DSL for defining object-like structures that maintain state in GenServer processes. Objects support field access, updates, lazy operations, and merging. The library integrates with the Inherit library to provide inheritance modeling capabilities.
Features
- Stateful Objects: Objects backed by GenServer processes with automatic lifecycle management
- Field Operations: get, set, and merge operations with both synchronous and asynchronous variants
- Lazy Operations: Functions that compute values based on current object state
- Inheritance Support: Integration with the Inherit library for object inheritance patterns
- Process Safety: All operations are process-safe through GenServer messaging
Quick Start
defmodule Person do
use GenObject, [
name: "",
age: nil,
email: nil
]
end
# Create a new person object
person = Person.new(name: "Alice", age: 30)
# Access fields
Person.get(person, :first_name) # "Alice"
# Update a single field
person = Person.set(person, :age, 31)
# Update multiple fields
person = Person.merge(person, %{name: "Alice Smith", email: "alice@example.com"})
# Lazy updates based on current state
person = Person.set_lazy(person, :age, fn p -> p.age + 1 end)
Inheritance with the Inherit Library
GenObject integrates seamlessly with the Inherit library to provide object inheritance patterns. The Inherit library allows you to define parent-child relationships between objects and inherit fields and behaviors.
defmodule Animal do
use GenObject, [
name: "",
species: ""
]
end
defmodule Dog do
use Animal, [
breed: "",
trained: false
]
end
# Dog inherits all fields from Animal plus its own
dog = Dog.new(name: "Rex", species: "Canis lupus", breed: "Labrador")
Summary
Functions
Retrieves the complete current state of an object.
Retrieves the value of a specific field or multiple fields from an object.
Handles getting a field value from an object.
Handles merging multiple field values into an object.
Handles setting a field value in an object.
Merges multiple fields into an object and returns the updated object struct.
Merges multiple fields into an object asynchronously, returning :ok
immediately.
Merges multiple fields using a function that computes values based on current object state.
Merges multiple fields using a function asynchronously, returning :ok
immediately.
Create a new object with the specified fields
Updates a specific field in an object and returns the updated object struct.
Updates a specific field in an object asynchronously and returns :ok
immediately.
Updates a specific field using a function that computes the new value based on current object state.
Updates a specific field using a function asynchronously, returning :ok
immediately.
Gracefully stops the curent object's GenServer
Functions
Retrieves the complete current state of an object.
Returns the full object struct containing all fields and their current values.
Accepts either a PID directly or a struct containing a :pid
field.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
field
Examples
# Using the object struct
person = Person.new(name: "Alice", age: 30)
current_state = GenObject.get(person)
# Returns: %Person{name: "Alice", age: 30, pid: #PID<...>}
# Using the PID directly
current_state = GenObject.get(person.pid)
# Returns: %Person{name: "Alice", age: 30, pid: #PID<...>}
Retrieves the value of a specific field or multiple fields from an object.
When given a single field (atom), returns the current value of that field. When given a list of fields, returns a list of values in the same order as requested. This is more efficient than retrieving the entire object struct when you only need specific fields.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfield_or_fields
- Either an atom representing a single field name, or a list of atoms for multiple fields
Examples
person = Person.new(first_name: "Alice", last_name: "Smith", age: 30)
# Get a single field
first_name = Person.get(person, :first_name)
# Returns: "Alice"
# Get multiple fields at once
[first_name, age] = Person.get(person, [:first_name, :age])
# Returns: ["Alice", 30]
# Works with PIDs too
[last_name, age] = Person.get(person.pid, [:last_name, :age])
# Returns: ["Smith", 30]
# Virtual attributes work with lists too
[name, age] = Person.get(person, [:name, :age])
# Returns: ["Alice Smith", 30]
Handles getting a field value from an object.
This function is called whenever a field is accessed via get/2
. Override this
function to implement virtual attributes that compute values dynamically instead
of storing them directly in the object state.
Parameters
field
- The atom representing the field name being accessedobject
- The current object struct
Returns
The value for the requested field. For regular fields, this returns the stored value. For virtual attributes, this can return any computed value.
Examples
# Define a virtual attribute that combines first and last name
def handle_get(:name, %Person{} = person) do
"#{person.first_name} #{person.last_name}"
end
# Fall back to default behavior for other fields
def handle_get(field, object) do
super(field, object)
end
Handles merging multiple field values into an object.
This function is called whenever multiple fields are updated via merge/2
, merge!/2
,
or their lazy variants. The default implementation processes each field individually
through handle_set/2
, which means virtual attributes work automatically with merge
operations.
Override this function if you need custom logic for processing multiple fields together, such as validating field combinations or applying transformations that depend on multiple input values.
Parameters
fields
- A map of field-value pairs to merge into the objectobject
- The current object struct
Returns
The updated object struct with all field changes applied.
Examples
# Custom merge logic that validates address fields together
def handle_merge(%{street: _, city: _, zip: _} = address_fields, %Person{} = person) do
# Custom validation logic here
if valid_address?(address_fields) do
super(address_fields, person)
else
raise "Invalid address combination"
end
end
# Fall back to default behavior for other merges
def handle_merge(fields, object) do
super(fields, object)
end
Handles setting a field value in an object.
This function is called whenever a field is updated via put/3
, put!/3
, or their
lazy variants. Override this function to implement virtual attributes that can parse
or transform input values into multiple real fields.
Parameters
pair
- A tuple of{field, value}
representing the field name and new valueobject
- The current object struct
Returns
The updated object struct with the field changes applied.
Examples
# Define a virtual attribute that splits a full name into parts
def handle_set({:name, full_name}, %Person{} = person) do
[first_name, last_name] = String.split(full_name, " ", parts: 2)
Map.merge(person, %{first_name: first_name, last_name: last_name})
end
# Fall back to default behavior for other fields
def handle_set(pair, object) do
super(pair, object)
end
Merges multiple fields into an object and returns the updated object struct.
This synchronous operation updates multiple fields simultaneously, similar to struct/2
but for live GenObject processes. More efficient than multiple individual set/3
calls
when updating several fields at once.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfields
- A map of field-value pairs to merge into the object
Examples
person = Person.new(name: "Alice", age: 30)
# Merge multiple fields using the object struct
updated_person = GenObject.merge(person, %{
name: "Alice Smith",
age: 31,
email: "alice.smith@example.com"
})
# Returns: %Person{name: "Alice Smith", age: 31, email: "alice.smith@example.com", pid: #PID<...>}
# Merge using the PID directly
updated_person = GenObject.merge(person.pid, %{age: 32, location: "New York"})
Merges multiple fields into an object asynchronously, returning :ok
immediately.
This is the asynchronous version of merge/2
. It sends a cast message to update
multiple fields simultaneously but returns immediately without waiting for the
operation to complete. Use this for better performance when you don't need the updated struct.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfields
- A map of field-value pairs to merge into the object
Examples
person = Person.new(name: "Alice", age: 30)
# Async merge multiple fields
:ok = GenObject.merge!(person, %{
name: "Alice Smith",
age: 31,
email: "alice.smith@example.com"
})
# Verify the updates were applied
updated_person = GenObject.get(person)
# Returns: %Person{name: "Alice Smith", age: 31, email: "alice.smith@example.com", pid: #PID<...>}
Merges multiple fields using a function that computes values based on current object state.
This synchronous operation allows you to merge multiple fields using a function that receives the current object state and returns a map of field-value pairs to merge. Useful for complex updates that depend on multiple fields or computed values.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfunc
- A function that takes the current object struct and returns a map of field-value pairs to merge
Examples
person = Person.new(name: "Alice", age: 30)
# Merge fields based on current state
updated_person = GenObject.merge_lazy(person, fn p ->
%{
name: p.name <> " Smith",
age: p.age + 1,
display_name: p.name <> " (" <> Integer.to_string(p.age + 1) <> ")"
}
end)
# Returns: %Person{name: "Alice Smith", age: 31, display_name: "Alice (31)", pid: #PID<...>}
# Complex computation based on multiple fields
updated_person = GenObject.merge_lazy(person.pid, fn p ->
age_group = if p.age < 18, do: "minor", else: "adult"
%{age_group: age_group, can_vote: p.age >= 18}
end)
Merges multiple fields using a function asynchronously, returning :ok
immediately.
This is the asynchronous version of merge_lazy/2
. It sends a cast message to merge
multiple fields using a function that computes values based on current object state,
but returns immediately without waiting for the operation to complete.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfunc
- A function that takes the current object struct and returns a map of field-value pairs to merge
Examples
person = Person.new(name: "Alice", age: 30)
# Async merge fields based on current state
:ok = GenObject.merge_lazy!(person, fn p ->
%{
name: p.name <> " Smith",
age: p.age + 1,
display_name: p.name <> " (" <> Integer.to_string(p.age + 1) <> ")"
}
end)
# Verify the updates were applied
updated_person = GenObject.get(person)
# Returns: %Person{name: "Alice Smith", age: 31, display_name: "Alice (31)", pid: #PID<...>}
Create a new object with the specified fields
Updates a specific field in an object and returns the updated object struct.
This is a synchronous operation that updates the field value and returns the complete updated object struct. The operation is atomic and thread-safe.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfield
- The atom representing the field name to updatevalue
- The new value to set for the field
Examples
person = Person.new(name: "Alice", age: 30)
# Update using the object struct
updated_person = GenObject.set(person, :age, 31)
# Returns: %Person{name: "Alice", age: 31, pid: #PID<...>}
# Update using the PID directly
updated_person = GenObject.set(person.pid, :first_name, "Alice Smith")
# Returns: %Person{name: "Alice Smith", age: 31, pid: #PID<...>}
Updates a specific field in an object asynchronously and returns :ok
immediately.
This is an asynchronous operation that sends a cast message to update the field and returns immediately without waiting for confirmation. Use this when you don't need the updated object struct and want better performance.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfield
- The atom representing the field name to updatevalue
- The new value to set for the field
Examples
person = Person.new(name: "Alice", age: 30)
# Async update using the object struct
:ok = GenObject.set!(person, :age, 31)
# Async update using the PID directly
:ok = GenObject.set!(person.pid, :first_name, "Alice Smith")
# Verify the update was applied
updated_person = GenObject.get(person)
# Returns: %Person{name: "Alice Smith", age: 31, pid: #PID<...>}
Updates a specific field using a function that computes the new value based on current object state.
This synchronous operation allows you to update a field using a function that receives the current object state and returns the new value for the field. Useful for updates that depend on the current state of the object.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfield
- The atom representing the field name to updatefunc
- A function that takes the current object struct and returns the new value for the field
Examples
person = Person.new(name: "Alice", age: 30)
# Increment age based on current value
updated_person = GenObject.set_lazy(person, :age, fn p -> p.age + 1 end)
# Returns: %Person{name: "Alice", age: 31, pid: #PID<...>}
# Modify name based on current state
updated_person = GenObject.set_lazy(person.pid, :first_name, fn p ->
p.name <> " (" <> Integer.to_string(p.age) <> ")"
end)
# Returns: %Person{name: "Alice (30)", age: 30, pid: #PID<...>}
Updates a specific field using a function asynchronously, returning :ok
immediately.
This is the asynchronous version of set_lazy/3
. It sends a cast message to update
the field using a function that computes the new value based on current object state,
but returns immediately without waiting for the operation to complete.
Parameters
pid_or_object
- The PID of the GenObject process, or an object struct containing a:pid
fieldfield
- The atom representing the field name to updatefunc
- A function that takes the current object struct and returns the new value for the field
Examples
person = Person.new(name: "Alice", age: 30)
# Async increment age based on current value
:ok = GenObject.set_lazy!(person, :age, fn p -> p.age + 1 end)
# Async modify name based on current state
:ok = GenObject.set_lazy!(person.pid, :first_name, fn p ->
p.name <> " (" <> Integer.to_string(p.age) <> ")"
end)
# Verify the updates were applied
updated_person = GenObject.get(person)
Gracefully stops the curent object's GenServer