use std::path::Path;
use loftspec::codec::{self, Site, Sites};
use loftspec::document::Document;
use loftspec::ids::ElementId;
use serde_json::{Map, Value, json};
use crate::finish::Finished;
use crate::json;
use crate::out::{errln, outln};
use crate::stop;
const SCHEMA: &str = "loft-find/1";
struct Match {
key: String,
label: Option<String>,
kind: &'static str,
site: Site,
name: String,
}
pub fn run(root: &Path, query: &str, as_json: bool) -> Finished {
let read = stop::read_files(root).and_then(|files| Ok(codec::read_sited(&files)?));
let (document, sites) = match read {
Ok(read) => read,
Err(stop) => return stop.exit(SCHEMA, as_json),
};
let found = find(&document, &sites, query);
let outcome = if found.is_empty() { "none" } else { "found" };
if as_json {
let matches: Vec<Value> = found.iter().map(match_json).collect();
let fields = json!({ "matches": matches });
json::print(&json::result(SCHEMA, outcome, fields, Vec::new()));
} else {
for each in &found {
outln!("{}", line(each));
}
}
if found.is_empty() {
errln!("nothing matches \"{query}\"");
return Finished::failure(outcome);
}
Finished::success(outcome)
}
fn find(document: &Document, sites: &Sites, query: &str) -> Vec<Match> {
let wanted = query.to_lowercase();
let mut found = Vec::new();
let mut consider = |element: ElementId, kind: &'static str, slug: Option<&str>, name: &str| {
let key = element.key();
let text = key.to_string();
let label = document
.bindings
.label_of(element)
.map(|label| label.to_string());
let identified = label
.as_deref()
.is_some_and(|label| label.eq_ignore_ascii_case(query))
|| slug == Some(query)
|| text == wanted
|| name.to_lowercase().contains(&wanted);
if identified {
found.push(Match {
key: text,
label,
kind,
site: sites
.get(key)
.cloned()
.expect("every element read from a tree has a site"),
name: name.to_owned(),
});
}
};
document.walk(&mut |node| {
if let Some(id) = node.id() {
let slug = node.slug().map(loftspec::slug::Slug::as_str);
consider(id.into(), node.kind().as_str(), slug, &node.name());
}
});
for decision in &document.decisions {
let slug = Some(decision.slug.as_str());
consider(decision.id.into(), "decision", slug, &decision.title);
}
found
}
fn line(found: &Match) -> String {
let label = found.label.as_deref().unwrap_or("-");
format!(
"{} {label} {} {} {}",
found.key, found.kind, found.site, found.name
)
}
fn match_json(found: &Match) -> Value {
let mut fields = Map::new();
fields.insert("key".to_owned(), json!(found.key));
fields.insert("label".to_owned(), json!(found.label));
fields.insert("kind".to_owned(), json!(found.kind));
fields.extend(json::site(&found.site));
fields.insert("name".to_owned(), json!(found.name));
Value::Object(fields)
}