SOFA is a flexible code generation framework that transforms Avro schemas into various target formats using customizable templates. It provides a powerful way to generate code, documentation, or any text-based output while maintaining complex relationships between Avro records.
- Template-Based Generation: Uses Pebble templating engine for flexible code generation
- Multiple Output Formats: Can generate multiple outputs from the same schema
- Relationship Awareness: Understands and preserves record relationships and dependencies
- Type System Support: Built-in type converters for various target platforms:
- Java
- Flatbuffers
- LiquidBase
- Apache Connect
- Customizable Naming: Configurable naming strategies for namespaces, classes, and files
- Filtering: Supports white/black listing of entities for selective generation
- Post-Generation Hooks: Ability to run commands after generation
Add the following dependency to your project:
<dependency>
<groupId>art.limitium.sofa</groupId>
<artifactId>sofa</artifactId>
<version>1.0.0</version>
</dependency>
- Create a YAML configuration file defining your generation rules:
schemas:
- path/to/schema1.avsc
- path/to/schema2.avsc
values:
packageName: "com.example"
version: "1.0.0"
generators:
- path: "generators/java"
templates:
namespace: "{{packageName}}"
name: "{{schema.name}}"
fullname: "{{namespace}}.{{name}}"
folder: "src/main/java/{{namespace | replace('.', '/')}}"
filename: "{{name}}.java"
filters:
white:
- "com.example.User"
- "com.example.Order"
- Run the generator:
java -jar sofa.jar path/to/config.yaml
SOFA uses different templates to handle various entity relationships and types. Each template serves a specific purpose in the code generation process:
Used for generating root record entities that have no parent dependencies. Root records typically represent:
- Top-level domain objects
- Aggregate roots in DDD terms
- Entry points for object graphs
Example use case: Generating main entity classes that own other entities.
// Example root template usage
public class {{name}} {
private final String id;
{% for owned in entity.dependencies %}
private final List<{{owned.name}}> {{owned.name | toSnakeCase}}s;
{% endfor %}
}
The framework distinguishes between two key concepts:
- Messages: Self-contained documents that include all related data inline. Messages are designed for data transfer and typically denormalized, making them ideal for event-driven systems and API payloads. When a message includes related data, it embeds the complete related object directly in the message structure.
In this model:
- One-to-one relationships are embedded directly in the parent record
- One-to-many relationships are represented as arrays within the parent record
- All related data is included in a single document
- Ideal for event-driven systems and message passing architectures
- Entities: Database-oriented structures that follow relational database normalization principles. Entities use references (typically through primary keys) to establish relationships between objects, rather than embedding the complete related data. This approach is optimized for data storage and maintains referential integrity through foreign key relationships.
In this model:
- One-to-one relationships are embedded in the parent entity
- One-to-many relationships are extracted into separate entities with an owner_id reference
- Relationships are maintained through foreign key references
- Ideal for relational databases and systems requiring normalized data
For example, consider an Order with LineItems:
a message:
{
"orderId": "123",
"items": [
{ "productId": "A1", "quantity": 2 },
{ "productId": "B2", "quantity": 1 }
]
}
the same as the entities
Order {
id: "123"
}
OrderItem {
id: "item1",
order_id: "123",
product_id: "A1",
quantity: 2
}
OrderItem {
id: "item2",
order_id: "123",
product_id: "B2",
quantity: 1
}
The schema generator can handle both approaches, choosing the appropriate template based on whether the record is marked as an owner (containing arrays) or dependent (owned by another record).
Used for records that are neither root nor involved in one-to-many relationships. Child records are typically:
- Value objects
- Component parts of larger entities
- Supporting data structures
Example use case: Generating embedded/component classes.
// Example child template usage
public class {{name}} {
{% for field in entity.fields %}
private {{field.type | javaType}} {{field.name}};
{% endfor %}
}
Used for records that contain one-to-many relationships with other records. Owner records:
- Manage collections of other entities
- Control lifecycle of dependent entities
- Implement parent-side of relationships
Example use case: Generating container classes with collection management.
// Example owner template usage
public class {{name}} {
{% for field in entity.fields | recordLists %}
private List<{{field.type.elementType | javaType}}> {{field.name}};
public void add{{field.name | capitalize}}({{field.type.elementType | javaType}} item) {
{{field.name}}.add(item);
}
{% endfor %}
}
Used for records that are owned by other records in one-to-many relationships. Dependent records:
- Belong to parent entities
- Have their lifecycle managed by owners
- Implement child-side of relationships
Example use case: Generating entities that are always part of a collection.
// Example dependent template usage
public class {{name}} {
private final {{entity.owners[0].name}} owner;
public {{name}}({{entity.owners[0].name}} owner) {
this.owner = owner;
}
{% for field in entity.fields %}
private {{field.type | javaType}} {{field.name}};
{% endfor %}
}
Used as a fallback template for any record type that doesn't match more specific templates. This template is:
- The most generic template type
- Used when no other template matches
- Suitable for basic record generation regardless of relationships
Record templates typically handle:
- Basic field generation
- Common methods (getters/setters)
- Standard class structure
Example use case: Generating standard data classes or when relationship-specific templates are not needed.
// Example record template usage
public class {{name}} {
{% for field in entity.fields %}
private {{field.type | javaType}} {{field.name}};
{% endfor %}
public {{name}}() {}
{% for field in entity.fields %}
public {{field.type | javaType}} get{{field.name | capitalize}}() {
return {{field.name}};
}
public void set{{field.name | capitalize}}({{field.type | javaType}} {{field.name}}) {
this.{{field.name}} = {{field.name}};
}
{% endfor %}
{% if entity.fields | recordLists %}
// Collection management methods
{% for field in entity.fields | recordLists %}
public void add{{field.name | capitalize | singular}}({{field.type.elementType | javaType}} item) {
if (this.{{field.name}} == null) {
this.{{field.name}} = new ArrayList<>();
}
this.{{field.name}}.add(item);
}
{% endfor %}
{% endif %}
}
Used for generating enum types. Supports:
- Basic enum generation
- Enum with additional properties
- Enum with aliases/descriptions
Example use case: Generating type-safe enumeration classes.
// Example enum template usage
public enum {{name}} {
{% for symbol in symbols %}
{{symbol}}{% if not loop.last %},{% endif %}
{% endfor %}
}
When multiple templates are available, SOFA selects the most specific template in this order:
enum.peb
for enum typesroot.peb
for root recordsowner.peb
for records with collectionschild.peb
for non-root recordsdependent.peb
for records owned by othersrecord.peb
as final fallback for any record type
SOFA provides various template filters to help with code generation:
- Case conversion:
toSnakeCase
,toCamelCase
- Type conversion:
javaType
,fbType
,liquidBaseType
- Dependency traversal:
dependenciesRecursiveAll
,dependenciesRecursiveUpToClosestDependent
- Structure flattening:
flattenFields
,flattenRecords
,flattenOwners
- Entity filtering:
enums
,recordLists
,noRecordLists
Given an Avro schema:
{
"type": "record",
"name": "User",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "string", "logicalType": "uuid"},
{"name": "name", "type": "string"},
{"name": "status", "type": "enum", "name": "UserStatus", "symbols": ["ACTIVE", "INACTIVE"]}
]
}
And a Java template:
package {{namespace}};
public class {{name}} {
{% for field in entity.fields %}
private {{field.type | javaType}} {{field.name}};
{% endfor %}
}
SOFA will generate:
package com.example;
public class User {
private String id;
private String name;
private UserStatus status;
}
Contributions are welcome! Please feel free to submit a Pull Request.
- External conditions per template generation
- Smart override detection for unchanged entities
- Extension loading from classpath
- Gradle plugin/script integration
- Example tests
- Comprehensive documentation
This project is licensed under the MIT License - see the LICENSE file for details.