1use crate::machine::Machine;
10use crate::{query, render, solve};
11use std::io::{self, Write};
12
13pub enum QueryResult {
18 Solutions,
20 ParseError(String),
22 RuntimeError(String),
24}
25
26pub fn run_query(m: &mut Machine, q: &str) -> QueryResult {
30 let goal = match query::parse_query(m, q) {
31 Ok(g) => g,
32 Err(e) => return QueryResult::ParseError(format!("Parse error: {e}")),
33 };
34 match solve::solve(m, goal) {
35 solve::Outcome::Error => {
36 let msg = m.error.take().map(|e| e.message).unwrap_or_default();
37 QueryResult::RuntimeError(format!("Runtime error: {msg}"))
38 }
39 solve::Outcome::Done => QueryResult::Solutions,
40 }
41}
42
43pub fn exhausted(m: &Machine) -> bool {
47 m.solution_limit.is_none_or(|l| m.solutions.len() < l)
48}
49
50pub fn write_error_json<W: Write>(w: &mut W, message: &str) -> io::Result<()> {
53 write!(w, "{{\"error\":\"{}\"}}", render::json_escape(message))
54}
55
56pub fn write_solutions_json<W: Write>(
63 w: &mut W,
64 m: &Machine,
65 exhausted: bool,
66 output: Option<&str>,
67) -> io::Result<()> {
68 write!(
69 w,
70 "{{\"count\":{},\"exhausted\":{}",
71 m.solutions.len(),
72 exhausted
73 )?;
74 if let Some(out) = output {
75 write!(w, ",\"output\":\"{}\"", render::json_escape(out))?;
76 }
77 w.write_all(b",\"solutions\":[")?;
78 for (i, sol) in m.solutions.iter().enumerate() {
79 if i > 0 {
80 w.write_all(b",")?;
81 }
82 w.write_all(b"{")?;
83 for (j, (name, json, _)) in sol.bindings.iter().enumerate() {
84 if j > 0 {
85 w.write_all(b",")?;
86 }
87 write!(w, "\"{}\":{}", render::json_escape(name), json)?;
88 }
89 w.write_all(b"}")?;
90 }
91 w.write_all(b"]}")
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use plg_shared::StringInterner;
98
99 fn machine() -> Box<Machine> {
100 Machine::new(StringInterner::new(), Vec::new())
101 }
102
103 fn bytes(f: impl FnOnce(&mut Vec<u8>) -> io::Result<()>) -> String {
104 let mut buf = Vec::new();
105 f(&mut buf).unwrap();
106 String::from_utf8(buf).unwrap()
107 }
108
109 #[test]
110 fn empty_success_matches_v1_shape() {
111 let m = machine();
112 assert_eq!(
113 bytes(|w| write_solutions_json(w, &m, true, None)),
114 "{\"count\":0,\"exhausted\":true,\"solutions\":[]}"
115 );
116 }
117
118 #[test]
119 fn output_field_sorts_between_exhausted_and_solutions() {
120 let m = machine();
121 assert_eq!(
122 bytes(|w| write_solutions_json(w, &m, false, Some("hi\n"))),
123 "{\"count\":0,\"exhausted\":false,\"output\":\"hi\\n\",\"solutions\":[]}"
124 );
125 }
126
127 #[test]
128 fn error_object_is_escaped() {
129 assert_eq!(
130 bytes(|w| write_error_json(w, "a\"b")),
131 "{\"error\":\"a\\\"b\"}"
132 );
133 }
134
135 #[test]
136 fn exhausted_follows_the_limit() {
137 let mut m = machine();
138 assert!(exhausted(&m), "no limit => exhausted");
139 m.solution_limit = Some(2);
140 assert!(exhausted(&m), "under the limit => exhausted");
141 m.solutions
142 .push(render::RenderedSolution { bindings: vec![] });
143 m.solutions
144 .push(render::RenderedSolution { bindings: vec![] });
145 assert!(!exhausted(&m), "limit hit exactly => not exhausted");
146 }
147}