#![cfg(all(
feature = "swift",
feature = "kotlin",
feature = "typescript",
feature = "csharp"
))]
use std::{
fs,
path::{Path, PathBuf},
};
use facet::Facet;
use facet_generate::{
Registry,
generation::{bincode::BincodePlugin, csharp, kotlin, swift, typescript},
reflection::RegistryBuilder,
};
#[derive(Facet)]
struct Point {
x: f64,
y: f64,
}
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum Shape {
Circle {
centre: Point,
radius: f64,
},
Rectangle {
position: Point,
width: f64,
height: f64,
},
}
struct Block {
marker: &'static str,
fence: &'static str,
code: String,
}
#[test]
fn readme_examples_are_up_to_date() {
let registry = RegistryBuilder::new()
.add_type::<Shape>()
.unwrap()
.build()
.unwrap();
let blocks = vec![
Block {
marker: "swift",
fence: "swift",
code: generate_swift(®istry),
},
Block {
marker: "kotlin",
fence: "kotlin",
code: generate_kotlin(®istry),
},
Block {
marker: "typescript",
fence: "typescript",
code: generate_typescript(®istry),
},
Block {
marker: "csharp",
fence: "csharp",
code: generate_csharp(®istry),
},
];
let readme_path = readme_path();
let original = fs::read_to_string(&readme_path).expect("failed to read README.md");
let mut updated = original.clone();
for block in &blocks {
updated = replace_block(&updated, block);
}
if updated == original {
return;
}
if update_enabled() {
fs::write(&readme_path, updated).expect("failed to write README.md");
} else {
panic!(
"README.md generated-code blocks are out of date.\n\
Run `UPDATE_EXPECT=1 cargo test -p facet_generate --test readme` to update them."
);
}
}
fn update_enabled() -> bool {
std::env::var_os("UPDATE_EXPECT").is_some() || std::env::var_os("UPDATE_README").is_some()
}
fn readme_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../README.md")
}
fn generate_swift(registry: &Registry) -> String {
let dir = tempfile::tempdir().unwrap();
swift::Installer::new("Example", dir.path())
.plugin(BincodePlugin)
.generate(registry)
.unwrap();
let source = find_source_with_token(dir.path(), "Point").expect("Swift module not found");
strip_preamble(&source)
}
fn generate_kotlin(registry: &Registry) -> String {
let dir = tempfile::tempdir().unwrap();
kotlin::Installer::new("com.example", dir.path())
.plugin(BincodePlugin)
.generate(registry)
.unwrap();
let source = find_source_with_token(dir.path(), "Point").expect("Kotlin module not found");
strip_preamble(&source)
}
fn generate_typescript(registry: &Registry) -> String {
let dir = tempfile::tempdir().unwrap();
typescript::Installer::new("example", dir.path())
.plugin(BincodePlugin)
.generate(registry)
.unwrap();
let source = find_source_with_token(dir.path(), "Point").expect("TypeScript module not found");
strip_preamble(&source)
}
fn generate_csharp(registry: &Registry) -> String {
let dir = tempfile::tempdir().unwrap();
csharp::Installer::new("Example", dir.path())
.plugin(BincodePlugin)
.generate(registry)
.unwrap();
let source = find_source_with_token(dir.path(), "Point").expect("C# module not found");
strip_preamble(&source)
}
fn strip_preamble(source: &str) -> String {
source
.lines()
.skip_while(|line| {
let t = line.trim();
t.is_empty()
|| t.starts_with("import ")
|| t.starts_with("package ")
|| t.starts_with("using ")
})
.collect::<Vec<_>>()
.join("\n")
.trim_start()
.to_string()
}
fn find_source_with_token(dir: &Path, token: &str) -> Option<String> {
let mut stack = vec![dir.to_path_buf()];
while let Some(path) = stack.pop() {
let entries = fs::read_dir(&path).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if let Ok(contents) = fs::read_to_string(&path)
&& contents.contains(token)
{
return Some(contents);
}
}
}
None
}
fn replace_block(readme: &str, block: &Block) -> String {
let start_marker = format!("<!-- generated:{}:start -->", block.marker);
let end_marker = format!("<!-- generated:{}:end -->", block.marker);
let start = readme
.find(&start_marker)
.unwrap_or_else(|| panic!("missing `{start_marker}` in README.md"));
let end = readme
.find(&end_marker)
.unwrap_or_else(|| panic!("missing `{end_marker}` in README.md"));
assert!(
end > start,
"`{end_marker}` appears before `{start_marker}` in README.md"
);
let replacement = format!(
"{start_marker}\n\n```{fence}\n{code}\n```\n\n",
fence = block.fence,
code = block.code,
);
let mut out = String::with_capacity(readme.len());
out.push_str(&readme[..start]);
out.push_str(&replacement);
out.push_str(&readme[end..]);
out
}