Files
EasyTier/easytier-proto/build/flags.rs
T
Luna YaoandClaude Code 250c2ad8d6 refactor(config): preserve explicit flags with FlagsPatch and derive CLI arguments from schema (#2585)
This PR refactors EasyTier's configuration flags system across protobuf definitions, TOML/API persistence, runtime reconciliation, and the CLI.

It introduces a synthesized FlagsPatch protobuf message with optionize and pbjson support to track field presence reliably, replaces manual CLI flag boilerplate with dynamic schema-driven clap argument generation, and
establishes Flags::resolve(patch) as the single source of truth for runtime defaults.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-10 22:11:08 +08:00

150 lines
5.7 KiB
Rust

//! Reads the `(easytier.flag)` annotations off `common.Flags` and writes them
//! out as Rust the crate compiles in.
//!
//! Each annotation carries what the core runs for a flag a configuration does
//! not state, the name the management API spells it with, and whether a config
//! form offers it. Nothing here is a second list: the field, its type and its
//! presence are the schema's, and an unannotated field fails the build.
use std::{
fmt::Write as _,
path::{Path, PathBuf},
};
use anyhow::{Context as _, bail, ensure};
use prost_reflect::{DescriptorPool, DynamicMessage, Kind, Value};
pub fn write(descriptor_set: &[u8], out: &Path) -> anyhow::Result<PathBuf> {
let pool = DescriptorPool::decode(descriptor_set)?;
let message = pool
.get_message_by_name("common.Flags")
.context("the compiled schema declares no common.Flags")?;
let extension = pool
.get_extension_by_name("easytier.flag")
.context("annotations.proto declares no easytier.flag")?;
// A form reads and writes the management API, so a flag it manages has to
// be one the API carries.
let api = pool
.get_message_by_name("api.manage.NetworkConfig")
.context("the compiled schema declares no api.manage.NetworkConfig")?;
let api_carries = |name: &str| api.get_field_by_name(name).is_some();
let mut defaults: Vec<String> = Vec::new();
let mut form = String::new();
for field in message.fields() {
let name = field.name();
let options = field.options();
ensure!(
options.has_extension(&extension),
"common.Flags.{name} carries no (easytier.flag) annotation"
);
let Value::Message(meta) = options.get_extension(&extension).into_owned() else {
bail!("easytier.flag on common.Flags.{name} is not a message");
};
// The value the flag runs with, written the way its type reads; JSON
// needs quotes around text, and an optional flag that states nothing
// is the one that runs unset. What the value *means* is protobuf's to
// say when the crate reads this document back.
let value = match text(&meta, "default", name)? {
Some(text) => match field.kind() {
Kind::String | Kind::Enum(_) => serde_json::to_string(&text)?,
_ => text,
},
None if field.supports_presence() => "null".to_owned(),
None => bail!("common.Flags.{name} states no default"),
};
defaults.push(format!(" {name:?}: {value}"));
// The name the management API uses, where the flag's own name is not it.
let spelling = match meta.get_field_by_name("api").as_deref() {
Some(Value::Message(api)) => match api.get_field_by_name("field").as_deref() {
Some(Value::String(field)) if !field.is_empty() => Some(field.clone()),
_ => None,
},
Some(_) => bail!("common.Flags.{name}: the api spelling is not a message"),
_ => None,
};
if let Some(spelling) = &spelling {
ensure!(
api_carries(spelling),
"common.Flags.{name} is spelled {spelling} by the management API, \
which carries no such field"
);
}
let deprecated = is_set(&meta, "deprecated");
ensure!(
deprecated
== matches!(
options.get_field_by_name("deprecated").as_deref(),
Some(Value::Bool(true))
),
"common.Flags.{name}: the annotation says deprecated = {deprecated}, \
but the field says the contrary"
);
// The schema field names a config form owns, used as TOML keys.
//
// Only what the API carries can be a key here: the merge drops a key
// the form produces nothing for, so a flag the API cannot express
// would be deleted from a stored configuration on every save.
let api_name = spelling.as_deref().unwrap_or(name);
if is_set(&meta, "form") {
ensure!(
api_carries(api_name),
"common.Flags.{name} says a form has a control for it, but \
api.manage.NetworkConfig carries no {api_name}"
);
}
if !deprecated
&& api_carries(api_name)
&& (matches!(field.kind(), Kind::Bool) || is_set(&meta, "form"))
{
writeln!(form, " {name:?},")?;
}
}
let defaults = defaults.join(",\n");
let generated = format!(
"\
// @generated by easytier-proto's build script from the (easytier.flag)
// annotations on proto/common.proto. Do not edit.
/// The values a network runs with when a configuration states no flag, as the
/// schema declares them in protobuf JSON.
pub const DEFAULTS: &str = r##\"{{
{defaults}}}\"##;
/// The schema field names of flags managed by the config form.
pub const FORM: &[&str] = &[
{form}];
"
);
let path = out.join("flags.rs");
std::fs::write(&path, generated)?;
Ok(path)
}
/// A text field of the annotation, absent when the annotation leaves it out.
fn text(meta: &DynamicMessage, field: &str, flag: &str) -> anyhow::Result<Option<String>> {
if !meta.has_field_by_name(field) {
return Ok(None);
}
match meta.get_field_by_name(field).as_deref() {
Some(Value::String(text)) => Ok(Some(text.clone())),
Some(other) => bail!("common.Flags.{flag}: {field} is {other:?}, expected text"),
None => Ok(None),
}
}
fn is_set(meta: &DynamicMessage, field: &str) -> bool {
matches!(
meta.get_field_by_name(field).as_deref(),
Some(Value::Bool(true))
)
}