use super::trailer::TrailerKind;
pub use camel_dsl::embedded_store::{
STORE_SCHEMA, SourcePlan, StoreDocument, StoreEntry, StoreEntryKind, StoreError, StoreIndex,
VirtualDocumentStore, validate_path,
};
impl From<TrailerKind> for StoreEntryKind {
fn from(kind: TrailerKind) -> Self {
match kind {
TrailerKind::Route => Self::Route,
TrailerKind::Job => Self::Job,
}
}
}
impl TryFrom<StoreEntryKind> for TrailerKind {
type Error = ();
fn try_from(kind: StoreEntryKind) -> Result<Self, Self::Error> {
match kind {
StoreEntryKind::Route => Ok(Self::Route),
StoreEntryKind::Job => Ok(Self::Job),
_ => Err(()),
}
}
}
pub(crate) fn validate_typed_references(
index: &StoreIndex,
kind: TrailerKind,
) -> Result<(), StoreError> {
let kind_of = |path: &str| -> Result<StoreEntryKind, StoreError> {
index
.entries
.iter()
.find(|entry| entry.path == path)
.map(|entry| entry.kind)
.ok_or_else(|| StoreError::MissingReference(path.to_string()))
};
let mismatch =
|path: &str, expected: &'static str, got: StoreEntryKind| StoreError::KindMismatch {
expected,
got: got.as_str(),
path: path.to_string(),
};
let artifact_kind = StoreEntryKind::from(kind);
let entry_kind = kind_of(&index.entry_point)?;
if entry_kind != artifact_kind {
return Err(mismatch(
&index.entry_point,
artifact_kind.as_str(),
entry_kind,
));
}
for path in &index.config_references {
let got = kind_of(path)?;
if !matches!(
got,
StoreEntryKind::Config | StoreEntryKind::Include | StoreEntryKind::Profile
) {
return Err(mismatch(path, "config, include, or profile", got));
}
}
for path in &index.source_plan.references {
let got = kind_of(path)?;
if !matches!(got, StoreEntryKind::Route | StoreEntryKind::Job) {
return Err(mismatch(path, "route or job", got));
}
}
Ok(())
}
#[cfg(test)]
fn index_json(entries: &str, store_schema: u64, entry_point: &str, plan: &str) -> String {
format!(
"{{\"config_references\":[],\"entry_point\":\"{entry_point}\",\
\"entries\":[{entries}],\
\"source_plan\":{{\"references\":[{plan}]}},\
\"store_schema\":{store_schema}}}"
)
}
#[cfg(test)]
fn entry_json(kind: &str, offset: u64, length: u64, path: &str) -> String {
format!("{{\"kind\":\"{kind}\",\"length\":{length},\"offset\":{offset},\"path\":\"{path}\"}}")
}
#[cfg(test)]
const CONTENT_LEN: usize = 15;
#[test]
fn store_decoder_rejects_schema_ranges_references_and_order() {
let two_routes = format!(
"{},{}",
entry_json("route", 0, 5, "routes/a.yaml"),
entry_json("route", 5, 10, "routes/b.yaml")
);
let canonical = index_json(
&two_routes,
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/a.yaml\",\"routes/b.yaml\"",
);
assert!(StoreIndex::decode(canonical.as_bytes(), CONTENT_LEN).is_ok());
let json = index_json(&two_routes, 2, "routes/a.yaml", "\"routes/a.yaml\"");
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::UnsupportedStoreSchema(2))
);
let json = index_json(
&format!(
"{},{}",
entry_json("route", 0, 5, "routes/a.yaml"),
entry_json("route", 5, 99, "routes/b.yaml")
),
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/a.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::RangeOutOfBounds)
);
let json = index_json(
&format!(
"{},{}",
entry_json("route", 0, 10, "routes/a.yaml"),
entry_json("route", 4, 11, "routes/b.yaml")
),
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/a.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::OverlappingRange)
);
let json = index_json(
&format!(
"{},{}",
entry_json("route", 0, 5, "routes/a.yaml"),
entry_json("route", 5, 10, "routes/a.yaml")
),
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/a.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::DuplicatePath("routes/a.yaml".into()))
);
let json = index_json(
&two_routes,
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/missing.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::MissingReference("routes/missing.yaml".into()))
);
let json = index_json(
&entry_json("route", 0, 5, "routes/a.yaml"),
STORE_SCHEMA,
"routes/a.yaml",
"\"routes/a.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::UnreferencedContent)
);
let json = index_json(
&format!(
"{},{}",
entry_json("route", 0, 5, "routes/b.yaml"),
entry_json("route", 5, 10, "routes/a.yaml")
),
STORE_SCHEMA,
"routes/b.yaml",
"\"routes/b.yaml\",\"routes/a.yaml\"",
);
assert_eq!(
StoreIndex::decode(json.as_bytes(), CONTENT_LEN),
Err(StoreError::NoncanonicalOrder)
);
}
#[test]
fn canonical_index_json_matches_embedded_store_bytes() {
let index = StoreIndex {
config_references: vec!["Camel.toml".into(), "includes/base.yaml".into()],
entry_point: "routes/a.yaml".into(),
entries: vec![
StoreEntry {
kind: StoreEntryKind::Config,
length: 4,
offset: 0,
path: "Camel.toml".into(),
},
StoreEntry {
kind: StoreEntryKind::Include,
length: 4,
offset: 4,
path: "includes/base.yaml".into(),
},
StoreEntry {
kind: StoreEntryKind::Route,
length: 4,
offset: 8,
path: "routes/a.yaml".into(),
},
StoreEntry {
kind: StoreEntryKind::Route,
length: 3,
offset: 12,
path: "routes/b.yaml".into(),
},
],
source_plan: SourcePlan {
references: vec!["routes/a.yaml".into(), "routes/b.yaml".into()],
},
store_schema: STORE_SCHEMA,
};
let bytes = index.encode_canonical().expect("index encodes");
let expected = concat!(
r#"{"config_references":["Camel.toml","includes/base.yaml"],"#,
r#""entries":["#,
r#"{"kind":"config","length":4,"offset":0,"path":"Camel.toml"},"#,
r#"{"kind":"include","length":4,"offset":4,"path":"includes/base.yaml"},"#,
r#"{"kind":"route","length":4,"offset":8,"path":"routes/a.yaml"},"#,
r#"{"kind":"route","length":3,"offset":12,"path":"routes/b.yaml"}],"#,
r#""entry_point":"routes/a.yaml","#,
r#""source_plan":{"references":["routes/a.yaml","routes/b.yaml"]},"#,
r#""store_schema":1}"#,
);
assert_eq!(String::from_utf8(bytes).expect("utf-8"), expected);
}