Skip to main content

prost_protovalidate_build/
lib.rs

1//! Compile-time code generator for Protocol Buffer validation.
2//!
3//! Generates `impl prost_protovalidate::Validate` for messages that have
4//! **only** standard `buf.validate` rules (no CEL expressions). Validators
5//! run through monomorphized direct field access at runtime — no
6//! `prost-reflect` transcoding, no CEL interpreter on the hot path.
7//! Messages with any CEL rules are excluded and must use the runtime
8//! `prost_protovalidate::Validator` instead.
9//!
10//! # Usage
11//!
12//! In your `build.rs`:
13//!
14//! ```rust,no_run
15//! fn main() -> Result<(), Box<dyn std::error::Error>> {
16//!     // First, compile protos with prost-build (writes descriptor set)
17//!     let descriptor_path = std::path::PathBuf::from(std::env::var("OUT_DIR")?)
18//!         .join("file_descriptor_set.bin");
19//!     prost_build::Config::new()
20//!         .file_descriptor_set_path(&descriptor_path)
21//!         .compile_protos(&["proto/service.proto"], &["proto/"])?;
22//!
23//!     // Then generate validation impls
24//!     prost_protovalidate_build::Builder::new()
25//!         .file_descriptor_set_path(&descriptor_path)?
26//!         .compile()?;
27//!     Ok(())
28//! }
29//! ```
30//!
31//! Then include the generated code alongside the prost-generated code:
32//!
33//! ```rust,ignore
34//! include!(concat!(env!("OUT_DIR"), "/validate_impl.rs"));
35//! ```
36
37mod codegen;
38mod message;
39mod naming;
40mod rules;
41
42use std::fs;
43use std::path::{Path, PathBuf};
44
45use prost_reflect::DescriptorPool;
46
47/// Builder for configuring and running the validation code generator.
48#[derive(Default)]
49pub struct Builder {
50    file_descriptor_set_bytes: Option<Vec<u8>>,
51    out_dir: Option<PathBuf>,
52    extern_paths: Vec<(String, String)>,
53}
54
55impl Builder {
56    /// Create a new builder with default settings.
57    #[must_use]
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Set the file descriptor set bytes directly.
63    #[must_use]
64    pub fn file_descriptor_set_bytes(mut self, bytes: Vec<u8>) -> Self {
65        self.file_descriptor_set_bytes = Some(bytes);
66        self
67    }
68
69    /// Read the file descriptor set from a file path.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the file cannot be read.
74    pub fn file_descriptor_set_path(mut self, path: impl AsRef<Path>) -> Result<Self, Error> {
75        let bytes = fs::read(path.as_ref()).map_err(|e| Error::Io {
76            path: path.as_ref().to_path_buf(),
77            source: e,
78        })?;
79        self.file_descriptor_set_bytes = Some(bytes);
80        Ok(self)
81    }
82
83    /// Override the output directory (defaults to `OUT_DIR` env var).
84    #[must_use]
85    pub fn out_dir(mut self, path: impl Into<PathBuf>) -> Self {
86        self.out_dir = Some(path.into());
87        self
88    }
89
90    /// Map a proto package path to a Rust module path.
91    ///
92    /// This is equivalent to prost-build's `extern_path` and should match
93    /// your prost-build configuration.
94    ///
95    /// # Example
96    ///
97    /// ```rust,no_run
98    /// prost_protovalidate_build::Builder::new()
99    ///     .extern_path(".my.package", "::my_crate::my_package");
100    /// ```
101    #[must_use]
102    pub fn extern_path(
103        mut self,
104        proto_path: impl Into<String>,
105        rust_path: impl Into<String>,
106    ) -> Self {
107        self.extern_paths
108            .push((proto_path.into(), rust_path.into()));
109        self
110    }
111
112    /// Run the code generator.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the descriptor set is missing, cannot be parsed,
117    /// or the output file cannot be written.
118    pub fn compile(self) -> Result<(), Error> {
119        let fds_bytes = self
120            .file_descriptor_set_bytes
121            .ok_or(Error::MissingDescriptorSet)?;
122
123        // Decode the raw bytes directly into a prost-reflect DescriptorPool.
124        // Using prost_types::FileDescriptorSet::decode() first would strip
125        // extension data (buf.validate.field, etc.) from proto options since
126        // prost does not preserve unknown fields by default.
127        let pool = DescriptorPool::decode(fds_bytes.as_slice())
128            .map_err(|e| Error::Decode(e.to_string()))?;
129
130        let out_dir = match self.out_dir {
131            Some(dir) => dir,
132            None => PathBuf::from(std::env::var("OUT_DIR").map_err(|_| Error::MissingOutDir)?),
133        };
134
135        let naming_ctx = naming::NamingContext::new(&self.extern_paths);
136        let tokens = codegen::generate(&pool, &naming_ctx);
137
138        let file = syn::parse2(tokens).map_err(|e| Error::Codegen(e.to_string()))?;
139        let formatted = prettyplease::unparse(&file);
140
141        fs::create_dir_all(&out_dir).map_err(|e| Error::Io {
142            path: out_dir.clone(),
143            source: e,
144        })?;
145
146        let output_path = out_dir.join("validate_impl.rs");
147        fs::write(&output_path, formatted).map_err(|e| Error::Io {
148            path: output_path,
149            source: e,
150        })?;
151
152        Ok(())
153    }
154}
155
156/// Errors that can occur during code generation.
157#[derive(Debug, thiserror::Error)]
158#[non_exhaustive]
159pub enum Error {
160    /// No file descriptor set was provided.
161    #[error(
162        "no file descriptor set provided; call file_descriptor_set_bytes() or file_descriptor_set_path()"
163    )]
164    MissingDescriptorSet,
165
166    /// The `OUT_DIR` environment variable is not set.
167    #[error("OUT_DIR environment variable not set; call out_dir() or run from build.rs")]
168    MissingOutDir,
169
170    /// Failed to decode the file descriptor set.
171    #[error("failed to decode file descriptor set: {0}")]
172    Decode(String),
173
174    /// Code generation produced invalid tokens.
175    #[error("code generation error: {0}")]
176    Codegen(String),
177
178    /// I/O error reading or writing files.
179    #[error("I/O error at {path}: {source}")]
180    Io {
181        /// The path that caused the error.
182        path: PathBuf,
183        /// The underlying I/O error.
184        source: std::io::Error,
185    },
186
187    /// A constraint could not be decoded from proto extensions.
188    #[error("constraint decode error: {0}")]
189    ConstraintDecode(String),
190}