../docxide-template

Docxide-template: Type safe docx templates with code generation in Rust

Short version: I built a Rust proc-macro library that generates type-safe structs from Word .docx templates, turning placeholders into compile-time checked fields.

Long version

My previous job was as a software consultant, and at one of the places I was deployed they handled lots of legal documents. These documents would be generated by an in-house case management software written in Java. The templates for these documents were MS Word .docx files and were pulled in from a separate repo at build time. The separation here was for organizational purposes, but the legal people who managed the templates were trained in using git so they could deal with all template-related things. But the software that essentially filled out these templates needed to know about where to insert things like names, addresses and legal details. This was done via some XML-files containing information about the placeholders in the corresponding .docx, and whatever Java-details needed to link this together. This was tedious to work with, and error prone. An update to either file and not the other could result in errors, like runtime faults at best, or placeholders finding their way into a finished document at worst. It was also essentially "stringly typed", no type system guarantee that things would work at all.

A case for Rust

I enjoy Rust for many reasons, and the strong type system is one of them. The notion that "bugs shouldn't compile" resonates with me. And while the chances I could convert a Java shop in the public sector to Rust were slim, I figured that making a type-safe .docx templating library would be a fun project that someone out there might find useful. The ecosystem of a language is an important factor when choosing a technology stack. If I can move the needle a little bit on the "corporate friendly" scale that would be a small win for Rust adoption.

My design goals were pretty straightforward:

Macros

One of Rust's super powers is metaprogramming with macros. From the official book:

Procedural macros allow you to run code at compile time that operates over Rust syntax, both consuming and producing Rust syntax.
 

Not having worked with macros before, it felt a bit daunting, but I figured the best way to learn to swim is to jump in. After a few cold starts, some research into what other templating libraries do, a complete rewrite, and a long hiatus, I released version 1.0.0 of Docxide-template.

So how does it work? The consumer places .docx template files in a folder, and points the macro to it. The macro is executed at compile time, and traverses the files. It generates structs where template placeholders become struct fields. The file name becomes the PascalCased struct name, and the placeholders become snake-cased field names. Like so, for a HelloWorld.docx template:

use docxide_template::generate_templates;
generate_templates!("path/to/templates");

fn main() {
    let doc = HelloWorld {
        first_name: "Alice".into(),
        company: "Acme Corp".into(),
    };

    doc.save("output/greeting").unwrap();

    // alternatively, output the filled template as bytes:
    doc.to_bytes()
}

The generated structs expose two methods, save() that saves the filled out template as a .docx file, and to_bytes() which outputs the raw bytes if the document is to be further processed before saving. Like converted to a PDF, or streamed to another service.

Baked in bytes

One way things can break with this setup is that the template files can be changed after compilation, like on the server or in whichever production environment it is deployed. Depending on how the build and distribution pipeline of a theoretical consumer of this library is set up, this may or may not be a problem. But the template files will have to be distributed with the compiled binary in order to work.

My second goal was for the .docx-files to essentially be a part of the source code. So as an alternative feature that can be toggled, the processed templates can also be baked directly into the binary. This ensures that the templates can't be changed accidentally (or maliciously), and makes the built application more portable. The only reason I have this as opt-in rather than opt-out is that someone might have thousands of template files, and that can balloon the binary size quite fast.