use std::path::Path;
mod dart_types;
mod hash;
mod resolve;
pub use hash::{type_definitions, version_of_sources};
pub use resolve::Wire;
pub mod vocab {
pub const MATH: &[(&str, &str)] = &[("<plaza_vocab>/math.rs", include_str!("vocab/math.rs"))];
pub const APP_COMMON: &[(&str, &str)] = &[
("<plaza_vocab>/app_common_locking.rs", include_str!("vocab/app_common_locking.rs")),
("<plaza_vocab>/app_common_object_property_ops.rs", include_str!("vocab/app_common_object_property_ops.rs")),
("<plaza_vocab>/app_common_ordered_collection_ops.rs", include_str!("vocab/app_common_ordered_collection_ops.rs")),
("<plaza_vocab>/app_common_presence.rs", include_str!("vocab/app_common_presence.rs")),
("<plaza_vocab>/app_common_presence_fragments.rs", include_str!("vocab/app_common_presence_fragments.rs")),
];
}
pub const VOCAB_VERSION: u32 = include!(concat!(env!("OUT_DIR"), "/vocab_version.rs"));
pub fn version_of<P: AsRef<Path>>(sources: &[P]) -> u32 {
let contents: Vec<Vec<u8>> = sources
.iter()
.map(|source| {
let path = source.as_ref();
std::fs::read(path).unwrap_or_else(|e| panic!("wire version: cannot read {}: {e}", path.display()))
})
.collect();
version_of_sources(contents)
}
pub fn emit<P: AsRef<Path>>(sources: &[P]) {
for source in sources {
println!("cargo:rerun-if-changed={}", source.as_ref().display());
}
publish(version_of(sources));
}
pub(crate) fn publish(version: u32) {
println!("cargo:rustc-env=WIRE_PROTOCOL={version}");
let out_dir = std::env::var("OUT_DIR").expect("wire version: OUT_DIR is unset, so this is not running as a build script");
let path = Path::new(&out_dir).join("wire_protocol.rs");
let generated = format!(
"// Generated by plaza_wire::build. Do not edit; it is rewritten every build.\n\
/// The wire format's version, hashed from the sources that define it.\n\
pub const WIRE_PROTOCOL: u32 = {version};\n"
);
std::fs::write(&path, generated).unwrap_or_else(|e| panic!("wire version: cannot write {}: {e}", path.display()));
}
pub fn emit_dart<P: AsRef<Path>>(sources: &[P], dart_path: impl AsRef<Path>) {
for source in sources {
println!("cargo:rerun-if-changed={}", source.as_ref().display());
}
let dart_path = dart_path.as_ref();
println!("cargo:rerun-if-changed={}", dart_path.display());
write_dart(version_of(sources), dart_path);
}
pub(crate) fn write_dart(version: u32, dart_path: &Path) {
write_if_changed(dart_path, &dart_version_file(version));
}
pub(crate) fn write_if_changed(path: &Path, content: &str) {
match std::fs::read(path) {
Ok(existing) if existing == content.as_bytes() => {}
_ => {
std::fs::write(path, content).unwrap_or_else(|e| panic!("wire version: cannot write {}: {e}", path.display()));
}
}
}
pub fn assert_dart_protocol(dart_path: impl AsRef<Path>, expected: u32) {
let path = dart_path.as_ref();
let dart = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("the generated {} should be committed: {e}", path.display()));
let declared = dart
.lines()
.find_map(|line| line.strip_prefix("const int wireProtocol = ")?.strip_suffix(';')?.trim().parse::<u32>().ok())
.unwrap_or_else(|| panic!("{} carries no `const int wireProtocol = N;` line", path.display()));
assert_eq!(
declared, expected,
"the Dart client would announce protocol {declared} where this build speaks {expected}; rebuild the server \
crate to refresh {}",
path.display()
);
}
fn dart_version_file(version: u32) -> String {
format!(
"// Generated by plaza_wire::build from the paired server's wire sources.\n\
// Do not edit; the server's build script rewrites it when the wire changes.\n\n\
/// The wire format's version, hashed from the Rust sources that define it.\n\
const int wireProtocol = {version};\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_dart_file_carries_the_same_version_and_is_rewritten_only_on_change() {
let dir = std::env::temp_dir().join(format!("plaza_wire_emit_dart_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let source = dir.join("types.rs");
let dart = dir.join("wire_protocol.dart");
std::fs::write(&source, b"enum Op { Ping }").unwrap();
emit_dart(&[&source], &dart);
let written = std::fs::read_to_string(&dart).unwrap();
assert!(
written.contains(&format!("const int wireProtocol = {};", version_of(&[&source]))),
"the Dart const is the same number the Rust const gets: {written}"
);
let before = std::fs::metadata(&dart).unwrap().modified().unwrap();
emit_dart(&[&source], &dart);
assert_eq!(
std::fs::metadata(&dart).unwrap().modified().unwrap(),
before,
"an unchanged wire leaves the committed file untouched"
);
std::fs::write(&source, b"enum Op { Ping, Pong }").unwrap();
emit_dart(&[&source], &dart);
assert!(
std::fs::read_to_string(&dart).unwrap().contains(&format!("= {};", version_of(&[&source]))),
"a shape change rewrites it"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_same_sources_always_give_the_same_version() {
let sources = [&b"enum Op { Ping }"[..], &b"struct Packet;"[..]];
assert_eq!(version_of_sources(sources), version_of_sources(sources));
}
#[test]
fn a_shape_change_moves_the_version() {
let before = version_of_sources([&b"enum Op { Ping }"[..]]);
assert_ne!(before, version_of_sources([&b"enum Op { Ping, Pong }"[..]]), "a new variant");
assert_ne!(before, version_of_sources([&b"enum Op { Pong }"[..]]), "a renamed variant");
let s = &b"struct P { a: u8, b: u8 }"[..];
assert_ne!(
version_of_sources([s]),
version_of_sources([&b"struct P { b: u8, a: u8 }"[..]]),
"reordering, which changes any positional encoding"
);
assert_ne!(
version_of_sources([s]),
version_of_sources([&b"struct P { a: u8, b: u16 }"[..]]),
"a field's type"
);
assert_ne!(
version_of_sources([&b"enum Op { A = 0 }"[..]]),
version_of_sources([&b"enum Op { A = 7 }"[..]]),
"an explicit discriminant"
);
assert_ne!(
version_of_sources([&b"struct P { #[serde(rename = \"a\")] alpha: u8 }"[..]]),
version_of_sources([&b"struct P { #[serde(rename = \"z\")] alpha: u8 }"[..]]),
"a serde attribute, which changes the wire without changing the type"
);
}
#[test]
fn a_bug_fix_does_not_move_the_version() {
let before = version_of_sources([&b"enum Op { Ping }\nfn apply(x: u8) -> u8 { x + 1 }\n"[..]]);
let after = version_of_sources([&b"enum Op { Ping }\nfn apply(x: u8) -> u8 { x.saturating_add(1) }\n"[..]]);
assert_eq!(before, after, "a fix to a function sharing the file");
}
#[test]
fn comments_and_formatting_do_not_move_the_version() {
let plain = version_of_sources([&b"enum Op { Ping }"[..]]);
assert_eq!(plain, version_of_sources([&b"/// Docs.\nenum Op { Ping }"[..]]), "a doc comment");
assert_eq!(plain, version_of_sources([&b"enum Op {\n Ping,\n}"[..]].map(|s| s)), "reformatting");
assert_eq!(plain, version_of_sources([&b"enum Op { Ping } // trailing"[..]]), "a trailing comment");
assert_eq!(plain, version_of_sources([&b"use std::fmt;\nenum Op { Ping }"[..]]), "an added import");
}
#[test]
fn impls_are_not_part_of_the_shape() {
let before = version_of_sources([&b"struct P { a: u8 }"[..]]);
let after = version_of_sources([&b"struct P { a: u8 }\nimpl P { fn a(&self) -> u8 { self.a } }"[..]]);
assert_eq!(before, after);
}
#[test]
fn line_endings_do_not_change_the_version() {
let unix = version_of_sources([&b"enum Op {\n Ping,\n}\n"[..]]);
let dos = version_of_sources([&b"enum Op {\r\n Ping,\r\n}\r\n"[..]]);
assert_eq!(unix, dos);
}
#[test]
fn the_version_is_never_zero() {
assert_ne!(version_of_sources(Vec::<&[u8]>::new()), 0);
assert_ne!(version_of_sources([&b""[..]]), 0);
}
#[test]
fn every_source_in_the_list_counts() {
let one = version_of_sources([&b"struct A;"[..]]);
let both = version_of_sources([&b"struct A;"[..], &b"struct B;"[..]]);
assert_ne!(one, both);
}
}