| Crates.io | dtox |
| lib.rs | dtox |
| version | 0.8.0 |
| created_at | 2025-12-19 12:21:08.494125+00 |
| updated_at | 2025-12-19 12:21:08.494125+00 |
| description | DTO-centric template engine (think T4) that expands placeholders, FOREACH blocks, and conditional sections |
| homepage | https://gitlab.com/hti-oss/rxdatasets/dtox |
| repository | https://gitlab.com/hti-oss/rxdatasets/dtox |
| max_upload_size | |
| id | 1994683 |
| size | 215,950 |
This tool automates the generation of source code and other artifacts from a set of Data Transfer Object (DTO) definition files. It uses a template-based approach, allowing for flexible code generation for any language or framework.
The generator discovers DTO files, processes templates by replacing placeholders with DTO-specific information, and handles both per-DTO file generation and aggregation of content into single files.
dtox is now distributed as a regular Rust CLI in addition to the Docker image. If you already have Rust installed, you can install the latest tagged release on macOS, Windows, or Linux with a single command:
cargo install --locked --git https://gitlab.com/hti-oss/rxdatasets/dtox.git --tag 0.7.0
Drop the --tag flag if you prefer tracking master, or add --force to upgrade an existing installation. Helper scripts live in scripts/install.sh (Bash) and scripts/install.ps1 (PowerShell) if you would rather not memorize the cargo install flags.
See INSTALL.md for platform-specific walkthroughs (Rust/Rustup prerequisites, PATH updates, and verification steps). Once installed, run dtox --version to verify the binary is on your PATH.
Here is a summary of the command-line options, which you can also view by running dtox --help.
| Option | Description | Default |
|---|---|---|
-i, --input-dir <path> |
Path to the directory containing DTO definition files and configuration CSVs. | |
-o, --output-dir <path> |
Path to the directory where generated files will be stored. | |
-t, --template-dir <path> |
Path to the directory containing templates. If not specified, the input directory is used. | |
-l, --list-file <path> |
Optional path to a file with a newline-separated list of DTO filenames to process. | |
-v, --verbose |
Enable verbose logging. | false |
--keep |
Do not clean the output directory before generation. | false |
--no-gitignore |
Do not respect .gitignore files when processing templates (includes all files). | false |
--generate-completion |
Generate a shell completion script for bash, powershell, etc. and print it to stdout. |
The generator operates by discovering DTOs, loading configuration, and processing templates.
The tool finds DTO definition files in the specified --input-dir.
[name].v[version].[extension]. For example, product.v2.dto or order.v1.proto.minor version 3. Suggestion is to use a format-specific comment, such as // minor version 3 for protobuf--list-file (e.g., dtos.lst) containing a newline-separated list of DTO filenames to process. If this is used, only the files listed will be processed - and in that order.You can provide DTO-specific values for placeholders using special CSV files in the input directory.
File Naming: Configuration files must be named __[placeholder_name]__.csv. For example, a file named __csharp_namespace__.csv will provide values for the __csharp_namespace__ placeholder.
File Format: The CSV file should contain two columns: the DTO identifier and the value. The DTO identifier is [name].v[version]. Comment lines or lines that are not valid CSV are ignored.
# C# Namespaces for DTOs
product.v2,MyCompany.Products.V2
order.v1,MyCompany.Billing.V1
The tool processes templates found in the --template-dir (which defaults to --input-dir). There are two main processing modes.
For each discovered DTO, the tool copies all subdirectories from the template directory into a temporary location. It then performs placeholder replacements on all filenames and file contents within that temporary directory before merging the result into the output directory.
For creating aggregate files (e.g., a registration file or an index), you can use special markers in any template file. These are lines containing the following strings:
FOREACH-DTO-STARTFOREACH-DTO-ENDThe block of text between these two markers will be duplicated for every DTO, with placeholders replaced accordingly. The marker lines themselves are removed - but it's suggested to use use format-specific comments such as eg. <!-- FOREACH-DTO-START --> in XML.
Example:
// FOREACH-DTO-START
builder.Services.AddDtoServer<__DtoName____DtoVersion__>();
// FOREACH-DTO-END
| Placeholder | Description | Example (from product.v2.dto) |
|---|---|---|
__dtoname__ |
The lowercase name of the DTO. | product |
__DtoName__ |
The PascalCase name of the DTO. | Product |
__dtoversion__ |
The version of the DTO. | v2 |
__DtoVersion__ |
The PascalCase version of the DTO. | V2 |
__dtominorversion__ |
The minor version from inside the DTO file. | 3 |
__dtofilepath__ |
The full path to the source DTO definition file. | /path/to/product.v2.dto |
__dtofilecontents__ |
Injects the entire content of the source DTO file. | #minor version 3\n... |
__custom__ |
Any custom placeholder defined in a __custom__.csv configuration file. |
MyCompany.Products.V2 |
You can also define custom placeholders directly in your DTO files using the pattern // __placeholder_name__ value. See the Multi-Value Placeholders section below for details.
You can extract multiple values for the same placeholder from a DTO file and iterate over them in templates using FOREACH-DTO-VALUE blocks.
In your DTO file, specify the same placeholder multiple times on different lines:
// minor version 1
// __indexname__ bysku
// __indexname__ byname
// __indexname__ bycategory
message Product {
string sku = 1;
string name = 2;
}
Create a template with FOREACH-DTO-VALUE-START and FOREACH-DTO-VALUE-END markers:
public class ProductIndexes {
public void RegisterIndexes() {
// FOREACH-DTO-VALUE-START __indexname__
RegisterIndex("__indexname__");
// FOREACH-DTO-VALUE-END
}
}
Generated Output:
public class ProductIndexes {
public void RegisterIndexes() {
RegisterIndex("bysku");
RegisterIndex("byname");
RegisterIndex("bycategory");
}
}
You can define multiple placeholders together on the same line, and they will stay paired during iteration:
DTO File:
// __aliasname__ __aliaspattern__ byitem item_sku
// __aliasname__ __aliaspattern__ bycustomer customer_id
Template:
// FOREACH-DTO-VALUE-START __aliasname__
AddAlias("__aliasname__", "__aliaspattern__");
// FOREACH-DTO-VALUE-END
Generated Output:
AddAlias("byitem", "item_sku");
AddAlias("bycustomer", "customer_id");
Key Features:
__name__ __pattern__ is different from __pattern__ __name____dtoname__) with multi-value placeholders in FOREACH-DTO-VALUE blocksYou can nest FOREACH-DTO-VALUE inside FOREACH-DTO for per-DTO multi-value generation:
// FOREACH-DTO-START
public class __DtoName__Indexes {
// FOREACH-DTO-VALUE-START __indexname__
RegisterIndex("__dtoname__", "__indexname__");
// FOREACH-DTO-VALUE-END
}
// FOREACH-DTO-END
Important: You cannot nest FOREACH-DTO inside FOREACH-DTO-VALUE.
When your DTO files contain duplicate values for multi-value placeholders, you can use FOREACH-UNIQUE-DTO-VALUE to iterate only over unique values, automatically filtering duplicates while preserving first-occurrence order.
A common scenario is generating enums from DTO metadata that may contain duplicates:
DTO File (order.v1.proto):
// minor version 1
// __statusvalue__ Pending
// __statusvalue__ Processing
// __statusvalue__ Pending // duplicate
// __statusvalue__ Completed
// __statusvalue__ Processing // duplicate
// __statusvalue__ Cancelled
Template:
public enum OrderStatus {
// FOREACH-UNIQUE-DTO-VALUE-START __statusvalue__
__statusvalue__,
// FOREACH-UNIQUE-DTO-VALUE-END __statusvalue__
}
Generated Output:
public enum OrderStatus {
Pending,
Processing,
Completed,
Cancelled,
}
Key Features:
FOREACH-DTO blocks for per-DTO unique iterationWhen using paired placeholders (Format B), uniqueness is determined by the primary (first) placeholder:
DTO File:
// __aliasname__ __aliaspath__ bysku product/sku
// __aliasname__ __aliaspath__ bysku product/id // duplicate primary
// __aliasname__ __aliaspath__ byname product/name
Template:
// FOREACH-UNIQUE-DTO-VALUE-START __aliasname__
AddUniqueAlias("__aliasname__", "__aliaspath__");
// FOREACH-UNIQUE-DTO-VALUE-END __aliasname__
Generated Output:
AddUniqueAlias("bysku", "product/sku"); // First occurrence kept
AddUniqueAlias("byname", "product/name");
The second bysku entry is filtered out because the primary placeholder (__aliasname__) is a duplicate, even though the secondary placeholder (__aliaspath__) has a different value.
You can use both in the same template when you need all values in one place and unique values in another:
public class ProductIndexes {
// Register all indexes (including duplicates for tracking)
public void RegisterAllIndexes() {
// FOREACH-DTO-VALUE-START __indexname__
RegisterIndex("__indexname__");
// FOREACH-DTO-VALUE-END __indexname__
}
// Register only unique indexes (for schema creation)
public void CreateUniqueIndexes() {
// FOREACH-UNIQUE-DTO-VALUE-START __indexname__
CreateIndex("__indexname__");
// FOREACH-UNIQUE-DTO-VALUE-END __indexname__
}
}
Use FOREACH-UNIQUE-DTO-VALUE when:
Use regular FOREACH-DTO-VALUE when:
The IFDEF-DTO-VALUE and IFANY-DTO-VALUE markers allow you to conditionally include blocks of content based on whether a placeholder is defined in the DTO file. Both marker names work identically and are completely interchangeable.
// IFDEF-DTO-VALUE-START __placeholder__
... content to include if __placeholder__ exists ...
// IFDEF-DTO-VALUE-END
Or equivalently:
// IFANY-DTO-VALUE-START __placeholder__
... content to include if __placeholder__ exists ...
// IFANY-DTO-VALUE-END
Important Notes:
__placeholder__ exists in the DTO (has at least one value), the content between the markers is included__placeholder__ doesn't exist, the entire block (including the marker lines) is removedFOREACH-DTO-VALUE, this does NOT iterate - content is included at most onceIFDEF and IFANY are completely interchangeable - use whichever reads better in your contextDTO with optional metadata (product.v1.proto):
// minor version 1
// __indexname__ bysku
// __cacheable__ true
message Product {
string sku = 1;
string name = 2;
}
DTO without optional metadata (order.v1.proto):
// minor version 1
// __indexname__ byid
message Order {
string id = 1;
}
Template with conditional block:
public class __DtoName____DtoVersion__ {
public string Name = "__dtoname__";
// IFDEF-DTO-VALUE-START __cacheable__
public bool IsCacheable = true;
public string CacheKey = "__DtoName___cache";
// IFDEF-DTO-VALUE-END
public List<string> Indexes = new List<string>();
}
Generated output for Product (has cacheable):
public class ProductV1 {
public string Name = "product";
public bool IsCacheable = true;
public string CacheKey = "Product_cache";
public List<string> Indexes = new List<string>();
}
Generated output for Order (no cacheable):
public class OrderV1 {
public string Name = "order";
public List<string> Indexes = new List<string>();
}
The caching-related fields only appear in the Product class because __cacheable__ is defined in its DTO file.
IFDEF-DTO-VALUE works seamlessly inside FOREACH-DTO blocks, allowing each DTO to have different optional content:
// FOREACH-DTO-START
public class __DtoName____DtoVersion__Config {
// IFDEF-DTO-VALUE-START __cacheable__
public TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
// IFDEF-DTO-VALUE-END
// IFDEF-DTO-VALUE-START __audit__
public bool EnableAuditLog = true;
// IFDEF-DTO-VALUE-END
}
// FOREACH-DTO-END
Each DTO will generate its own config class with only the features it declares.
Use IFDEF-DTO-VALUE when:
This is particularly useful for:
The IFNDEF-DTO-VALUE and IFNO-DTO-VALUE markers provide the opposite behavior of IFDEF/IFANY - they include content only when a placeholder is NOT defined in the DTO file. Both marker names work identically and are completely interchangeable.
// IFNDEF-DTO-VALUE-START __placeholder__
... content to include if __placeholder__ does NOT exist ...
// IFNDEF-DTO-VALUE-END
Or equivalently:
// IFNO-DTO-VALUE-START __placeholder__
... content to include if __placeholder__ does NOT exist ...
// IFNO-DTO-VALUE-END
Important Notes:
__placeholder__ does NOT exist in the DTO, the content between the markers is included__placeholder__ exists (has at least one value), the entire block is removedIFDEF/IFANYIFNDEF and IFNO are completely interchangeableDTO with custom cache (product.v1.proto):
// minor version 1
// __custom_cache__ redis
// __custom_timeout__ 3600
DTO without custom cache (order.v1.proto):
// minor version 1
Template:
// FOREACH-DTO-START
public class __DtoName____DtoVersion__Config {
public string Name = "__dtoname__";
// IFNDEF-DTO-VALUE-START __custom_cache__
public string CacheStrategy = "default";
// IFNDEF-DTO-VALUE-END
// IFNO-DTO-VALUE-START __custom_timeout__
public int TimeoutSeconds = 30;
// IFNO-DTO-VALUE-END
}
// FOREACH-DTO-END
Generated output for Product (has custom values):
public class ProductV1Config {
public string Name = "product";
}
Generated output for Order (no custom values):
public class OrderV1Config {
public string Name = "order";
public string CacheStrategy = "default";
public int TimeoutSeconds = 30;
}
The default values only appear when the custom placeholders are not defined.
You can mix both types of conditional markers in the same template:
// FOREACH-DTO-START
public class __DtoName____DtoVersion__ {
// IFDEF-DTO-VALUE-START __custom_validation__
public IValidator Validator = new CustomValidator();
// IFDEF-DTO-VALUE-END
// IFNDEF-DTO-VALUE-START __custom_validation__
public IValidator Validator = new DefaultValidator();
// IFNDEF-DTO-VALUE-END
}
// FOREACH-DTO-END
Use IFNDEF-DTO-VALUE when:
This is particularly useful for:
By default, dtox respects .gitignore files when processing templates. This helps prevent binary files, build artifacts, and other unwanted files from being included in the generated output.
Default Behavior (Gitignore Enabled):
.gitignore.gitignore, .git/info/exclude, global gitignore)Disabling Gitignore:
Use the --no-gitignore flag to include all files, even those normally ignored:
dtox --input-dir ./templates --output-dir ./output --no-gitignore
Common Use Cases:
--no-gitignore): When you need to include specific files that are gitignored, or when working with repositories that don't use gitjust (Task Runner)This project includes a justfile to simplify common development tasks. just is a command runner similar to make or invoke.
First, install just.
Then you can use the following commands from the project directory:
just build: Compiles the project.just run -- --input-dir <path>: Runs the generator. Pass arguments after --.just completion-bash: Generates the Bash completion script (dtox-completion.bash).just completion-pwsh: Generates the PowerShell completion script (_dtox.ps1).Running just with no arguments will list all available commands.
This tool can generate shell completion scripts for various shells like Bash and PowerShell. You can generate them manually or use the just recipes above.
Generate the completion script:
Run your compiled application with the --generate-completion bash flag and redirect the output to a file.
# Assuming your executable is in the default debug location
./target/debug/dtox --generate-completion bash > dtox-completion.bash
Load the script for the current session: Source the generated file to enable completion in your current terminal session.
source dtox-completion.bash
Load the script permanently:
To make completion available in all new shell sessions, you can either copy the script to a standard completion directory or source it from your shell's profile file (~/.bashrc or ~/.bash_profile).
Option A: Copy to completion directory (recommended on Linux):
# The exact path may vary based on your distribution
sudo cp dtox-completion.bash /etc/bash_completion.d/dtox
Option B: Source from .bashrc:
Add the following line to your ~/.bashrc file, replacing /path/to/ with the actual path to the script.
echo 'source /path/to/dtox-completion.bash' >> ~/.bashrc
You will need to restart your shell for the changes to take effect.
Check your execution policy:
PowerShell requires scripts to be signed or the execution policy to be changed. You can check your current policy with Get-ExecutionPolicy. If it's Restricted, you may need to change it.
# Run this command in a PowerShell terminal with administrator privileges
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Generate the completion script:
# Assuming your executable is in the default debug location
.\target\debug\dtox.exe --generate-completion powershell > _dtox.ps1
Load the script permanently:
To load the script every time you open PowerShell, add it to your profile. You can find your profile path by checking the $PROFILE variable.
# Open your profile in Notepad (it will be created if it doesn't exist)
notepad $PROFILE
Add the following line to the profile file, replacing C:\path\to\ with the actual path to the script.
. C:\path\to\_dtox.ps1
Save the file and restart your PowerShell session. You can now use tab-completion for the dtox command and its arguments.