use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU64;
use std::path::PathBuf;
use serde::Deserialize;
use serde_json::Value;
use super::support::{Fixture, request};
use crate::serve::protocol::{QueryRequest, Selection, Version};
type Error = Box<dyn std::error::Error>;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Group {
pub group: String,
pub entries: Vec<Entry>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
pub name: String,
pub sentence: String,
pub query: String,
pub change: String,
pub defaults: BTreeMap<String, Default>,
pub selection: Option<Selected>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Selected {
branch: String,
version: String,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Default {
Literal(String),
Captured(Context),
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Context {
context: String,
}
pub fn load() -> Result<Vec<Group>, Error> {
let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/serve/query/library");
let mut paths = Vec::new();
for entry in std::fs::read_dir(&directory)? {
let path = entry?.path();
if path
.extension()
.is_some_and(|extension| extension == "json")
{
paths.push(path);
}
}
paths.sort();
if paths.is_empty() {
return Err(format!("library {} has no groups", directory.display()).into());
}
let mut groups = Vec::new();
for path in paths {
let bytes = std::fs::read(&path)?;
let group = serde_json::from_slice(&bytes)
.map_err(|error| format!("library {}: {error}", path.display()))?;
groups.push(group);
}
Ok(groups)
}
pub fn render(entry: &Entry, captured: &Value) -> Result<QueryRequest, Error> {
render_inner(entry, captured).map_err(|error| format!("entry {}: {error}", entry.name).into())
}
fn render_inner(entry: &Entry, captured: &Value) -> Result<QueryRequest, Error> {
for (kind, text) in [
("name", &entry.name),
("sentence", &entry.sentence),
("change", &entry.change),
] {
if text.trim().is_empty() {
return Err(format!("missing {kind}").into());
}
}
let mut defaults = BTreeMap::new();
for (part, default) in &entry.defaults {
if part.is_empty()
|| !part
.chars()
.all(|character| character.is_ascii_lowercase() || character == '_')
{
return Err(format!("invalid placeholder {part:?}").into());
}
let value = match default {
Default::Literal(value) => value.clone(),
Default::Captured(context) if context.context == "selection.version" => {
let value = captured
.get("version")
.ok_or("missing selection.version default")?;
let version: Version = serde_json::from_value(value.clone())?;
serde_json::to_string(&version)?
}
Default::Captured(context) => {
return Err(format!("unknown default context {}", context.context).into());
}
};
if value.is_empty() || value.contains("${") {
return Err(format!("placeholder {part} has an empty or unresolved default").into());
}
defaults.insert(part.clone(), value);
}
let mut used = BTreeSet::new();
let query = expand(&entry.query, &defaults, &mut used)?;
crate::serve::query::parser::pipeline(&query)
.map_err(|refusal| format!("{}: {refusal}", refusal.code()))?;
expand(&entry.change, &defaults, &mut used)?;
let mut ask = request(&query, NonZeroU64::MIN.saturating_add(3));
if let Some(selected) = &entry.selection {
let branch = expand(&selected.branch, &defaults, &mut used)?;
let version = expand(&selected.version, &defaults, &mut used)?;
ask.selection = Selection {
branch,
version: Some(serde_json::from_str(&version)?),
};
}
let declared: BTreeSet<_> = defaults.keys().cloned().collect();
if used != declared {
return Err(format!(
"unused defaults: {:?}",
declared.difference(&used).collect::<Vec<_>>()
)
.into());
}
Ok(ask)
}
fn expand(
text: &str,
defaults: &BTreeMap<String, String>,
used: &mut BTreeSet<String>,
) -> Result<String, Error> {
let mut remaining = text;
let mut expanded = String::new();
while let Some((before, after)) = remaining.split_once("${") {
expanded.push_str(before);
let (part, rest) = after.split_once('}').ok_or("unclosed placeholder")?;
let default = defaults
.get(part)
.ok_or_else(|| format!("placeholder {part} has no default"))?;
used.insert(part.to_owned());
expanded.push_str(default);
remaining = rest;
}
expanded.push_str(remaining);
Ok(expanded)
}
pub fn run_entry(fixture: &Fixture, entry: &Entry, captured: &Value) -> Result<Value, Error> {
let ask = render(entry, captured)?;
let answer = fixture.run(&ask, &[]).map_err(|failure| {
format!(
"entry {}: {}: {}",
entry.name, failure.code, failure.message
)
})?;
let rows = answer
.get("rows")
.and_then(Value::as_array)
.ok_or_else(|| format!("entry {}: query.run returned no page rows", entry.name))?;
for row in rows {
if row.get("value").is_none() || row.get("origin").is_none() {
return Err(format!(
"entry {}: query.run returned a row without value or origin",
entry.name
)
.into());
}
}
let selection = answer
.get("selection")
.ok_or_else(|| format!("entry {}: query.run returned no selection", entry.name))?;
let mut selected = selection
.as_object()
.ok_or_else(|| format!("entry {}: page selection is not an object", entry.name))?
.clone();
let roots = selected
.remove("roots")
.ok_or_else(|| format!("entry {}: page selection has no captured roots", entry.name))?;
if !roots.is_array() {
return Err(format!("entry {}: captured roots are not an array", entry.name).into());
}
serde_json::from_value::<Selection>(Value::Object(selected))
.map_err(|error| format!("entry {}: invalid page selection: {error}", entry.name))?;
Ok(answer)
}