use std::path::{Path, PathBuf};
use cratestack_core::{Field, Schema};
use cratestack_proto::PbLock;
pub(crate) fn pb_lock_path(schema_resolved: &Path) -> PathBuf {
schema_resolved.with_extension("pb.lock")
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PbLockValidationError {
Missing,
Malformed(String),
Stale,
}
impl PbLockValidationError {
pub(crate) fn message(&self, lock_path_display: &str, schema_path_display: &str) -> String {
match self {
PbLockValidationError::Missing => format!(
"no `{lock_path_display}` found; run `cratestack generate-proto --schema \
{schema_path_display} --package <name>` first"
),
PbLockValidationError::Malformed(reason) => {
format!("failed to parse `{lock_path_display}`: {reason}")
}
PbLockValidationError::Stale => format!(
"`{schema_path_display}` has changed since `{lock_path_display}` was last \
generated; run `cratestack generate-proto --schema {schema_path_display} \
--check` to see the drift, then re-run without `--check` to update the lock"
),
}
}
}
pub(crate) fn validate_pb_lock(
schema: &Schema,
lock_text: Option<&str>,
extra_messages: &std::collections::BTreeMap<String, Vec<Field>>,
) -> Result<PbLock, PbLockValidationError> {
let Some(lock_text) = lock_text else {
return Err(PbLockValidationError::Missing);
};
let lock = PbLock::from_toml(lock_text)
.map_err(|error| PbLockValidationError::Malformed(error.to_string()))?;
let changed = cratestack_proto::lock_would_change(schema, &lock, extra_messages)
.map_err(|error| PbLockValidationError::Malformed(error.to_string()))?;
if changed {
return Err(PbLockValidationError::Stale);
}
Ok(lock)
}
pub(crate) fn load_pb_lock(
schema: &Schema,
schema_resolved: &Path,
extra_messages: &std::collections::BTreeMap<String, Vec<Field>>,
) -> Result<PbLock, String> {
let lock_path = pb_lock_path(schema_resolved);
let lock_text = std::fs::read_to_string(&lock_path).ok();
validate_pb_lock(schema, lock_text.as_deref(), extra_messages).map_err(|error| {
error.message(
&lock_path.display().to_string(),
&schema_resolved.display().to_string(),
)
})
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
fn schema(source: &str) -> Schema {
cratestack_parser::parse_schema(source).expect("schema should parse")
}
#[test]
fn missing_lock_text_is_reported_as_missing() {
let schema = schema("model Widget {\n id Int @id\n}\n");
let result = validate_pb_lock(&schema, None, &BTreeMap::new());
assert_eq!(result.unwrap_err(), PbLockValidationError::Missing);
}
#[test]
fn malformed_toml_is_reported_as_malformed() {
let schema = schema("model Widget {\n id Int @id\n}\n");
let result = validate_pb_lock(&schema, Some("not valid toml {{{"), &BTreeMap::new());
assert!(matches!(
result.unwrap_err(),
PbLockValidationError::Malformed(_)
));
}
#[test]
fn matching_lock_validates_successfully() {
let schema = schema("model Widget {\n id Int @id\n name String\n}\n");
let extra = BTreeMap::new();
let built = cratestack_proto::build_lock(&schema, None, &extra).unwrap();
let mut with_package = built.clone();
with_package.package = Some("widgets".to_owned());
let result = validate_pb_lock(&schema, Some(&with_package.to_toml()), &extra);
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn stale_lock_is_reported_as_stale() {
let old_schema = schema("model Widget {\n id Int @id\n}\n");
let extra = BTreeMap::new();
let mut old_lock = cratestack_proto::build_lock(&old_schema, None, &extra).unwrap();
old_lock.package = Some("widgets".to_owned());
let new_schema = schema("model Widget {\n id Int @id\n name String\n}\n");
let result = validate_pb_lock(&new_schema, Some(&old_lock.to_toml()), &extra);
assert_eq!(result.unwrap_err(), PbLockValidationError::Stale);
}
}