use std::future::Future;
pub struct SeedEntry {
pub key: serde_json::Value,
pub data: serde_json::Value,
}
#[derive(Default)]
pub struct QuerySeed {
entries: Vec<SeedEntry>,
}
impl QuerySeed {
pub fn new() -> Self {
Self::default()
}
pub async fn seed(mut self, entry: impl Future<Output = SeedEntry>) -> Self {
self.entries.push(entry.await);
self
}
pub fn to_script_tag(&self) -> String {
let json = serde_json::Value::Array(
self.entries
.iter()
.map(|e| {
serde_json::json!({
"key": e.key,
"data": e.data,
})
})
.collect(),
);
let safe = json.to_string().replace('<', "\\u003c");
format!(
r#"<script type="application/json" id="__nx_seeds__">{}</script>"#,
safe
)
}
}
pub fn seed_key(url: &str, params: Option<serde_json::Value>) -> serde_json::Value {
match params {
Some(p) => serde_json::json!([url, p]),
None => serde_json::json!([url]),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seed_key_shapes_match_generated_client() {
assert_eq!(
seed_key("/api/todos", None).to_string(),
r#"["/api/todos"]"#
);
assert_eq!(
seed_key("/api/todos", Some(serde_json::json!({"status": "open"}))).to_string(),
r#"["/api/todos",{"status":"open"}]"#
);
}
#[tokio::test]
async fn script_tag_escapes_angle_brackets() {
let seed = QuerySeed::new()
.seed(async {
SeedEntry {
key: seed_key("/api/x", None),
data: serde_json::json!({"html": "</script><script>alert(1)</script>"}),
}
})
.await;
let tag = seed.to_script_tag();
assert!(tag.starts_with(r#"<script type="application/json" id="__nx_seeds__">"#));
let inner = &tag[tag.find('>').unwrap() + 1..tag.rfind("</script>").unwrap()];
assert!(!inner.contains('<'), "unescaped < in payload: {}", inner);
let parsed: serde_json::Value = serde_json::from_str(inner).unwrap();
assert_eq!(
parsed[0]["data"]["html"],
serde_json::json!("</script><script>alert(1)</script>")
);
}
#[tokio::test]
async fn entries_accumulate_in_order() {
let seed = QuerySeed::new()
.seed(async {
SeedEntry {
key: seed_key("/a", None),
data: serde_json::json!(1),
}
})
.await
.seed(async {
SeedEntry {
key: seed_key("/b", None),
data: serde_json::json!(2),
}
})
.await;
let tag = seed.to_script_tag();
assert!(tag.find("/a").unwrap() < tag.find("/b").unwrap());
}
}