use serde_json::{Value, json};
use textlane_core::{Allocator, Error, File, Lines};
use crate::models::pipeline::{Api, Pipeline, read_tree, write_tree};
use crate::models::run::{Outcome, Progress};
use crate::utils::testing::hosts::{read_behaviour, run_host};
use crate::utils::testing::plugins::take_trace;
pub fn run_case(case: &Value) -> Value {
take_trace();
let path = case.get("path").and_then(Value::as_str).map(str::to_owned);
let mut file = File {
path: path.clone(),
..File::new()
};
let result = match read_case(case) {
Ok((pipeline, _, Some(operations))) => {
run_operations(pipeline, operations, &mut file)
}
Ok((pipeline, hosts, None)) if !hosts.is_empty() => {
run_hosted(case, pipeline, &hosts, &mut file)
}
Ok((pipeline, _, None)) => run_document(case, pipeline, &mut file),
Err(error) => Err(error),
};
match result {
Ok(mut result) => {
result["messages"] = write_messages(&file);
result["fileData"] = Value::Object(file.data);
result["trace"] = json!(take_trace());
result
}
Err(error) => {
json!({ "error": { "code": error.code, "message": error.message } })
}
}
}
type Case<'a> = (Pipeline, Vec<String>, Option<&'a Vec<Value>>);
fn read_case(case: &Value) -> Result<Case<'_>, Error> {
let entries = case
.get("pipeline")
.and_then(Value::as_array)
.ok_or_else(|| Error::new("invalid-case", "a case needs a `pipeline` array"))?;
let mut pipeline = Pipeline::new();
let mut hosts: Vec<String> = Vec::new();
for entry in entries {
let name = entry.get(0).cloned().unwrap_or(Value::Null);
let value = match name.as_str() {
Some(host) if host.starts_with("host:") => {
let id = match hosts.iter().position(|known| known == host) {
Some(id) => id,
None => {
hosts.push(host.to_string());
hosts.len() - 1
}
};
json!({ "host": id })
}
_ => name,
};
pipeline.use_value(&value, entry.get(1).cloned())?;
}
Ok((
pipeline,
hosts,
case.get("operations").and_then(Value::as_array),
))
}
fn run_hosted(
case: &Value,
mut pipeline: Pipeline,
hosts: &[String],
file: &mut File,
) -> Result<Value, Error> {
let options = |id: u32| pipeline.host_options(id).unwrap_or(Value::Null);
let mut recorded = Vec::new();
for id in pipeline.hosts() {
let name = hosts
.get(id as usize)
.map(String::as_str)
.unwrap_or_default();
if read_behaviour(name) == "recordOptions" {
recorded.push(options(id));
}
}
let host_options: Vec<Value> = (0..hosts.len() as u32).map(options).collect();
if !recorded.is_empty() {
pipeline.set_data("recorded", Value::Array(recorded))?;
}
let input = case
.get("input")
.and_then(Value::as_str)
.unwrap_or_default();
let start = File {
path: file.path.clone(),
..File::new()
};
let mut progress = pipeline.start_process(input, start, Api::Async)?;
loop {
match progress {
Progress::Paused(mut pause) => {
let mut tree = pause.tree().clone();
for id in pause.hosts().to_vec() {
let name = hosts
.get(id as usize)
.map(String::as_str)
.unwrap_or_default();
let options = host_options.get(id as usize).unwrap_or(&Value::Null);
tree = run_host(name, options, tree, pause.file_mut())?;
}
progress = pause.resume(&tree)?;
}
Progress::Finished(finished) => {
*file = finished.file;
let value = match finished.output {
Outcome::Text(text) => text,
Outcome::Tree(tree) => tree.to_string(),
};
let mut result = json!({ extension_of(&mut pipeline)?: value });
result["data"] = pipeline.data(None);
return Ok(result);
}
}
}
}
fn run_document(
case: &Value,
mut pipeline: Pipeline,
file: &mut File,
) -> Result<Value, Error> {
let processor = pipeline.freeze()?;
let alloc = Allocator::new();
let input = case.get("input").and_then(Value::as_str);
let lines = input.map(|input| Lines::new(processor.source(input)));
let tree = match (input, case.get("tree")) {
(Some(input), _) if processor.has_parser() => {
processor.parse(input, file, &alloc)?
}
(_, Some(tree)) => read_tree(tree, processor.input_kind(), &alloc)?,
(Some(_), None) => {
return Err(Error::new(
"missing-parser",
"Cannot `parse` without `parser`",
));
}
(None, None) => {
return Err(Error::new(
"invalid-case",
"a case needs either `input` or `tree`",
));
}
};
let tree = processor.run_sync(tree, file, &alloc)?;
let mut result = match processor.extension() {
Some(extension) => json!({ extension: processor.stringify(&tree, file)? }),
None => json!({ "tree": write_tree(&tree, lines.as_ref())? }),
};
result["data"] = pipeline.data(None);
Ok(result)
}
fn run_operations(
mut pipeline: Pipeline,
operations: &[Value],
file: &mut File,
) -> Result<Value, Error> {
let mut result = json!({});
for operation in operations {
let name = operation.get(0).and_then(Value::as_str).unwrap_or_default();
let argument = |index: usize| operation.get(index).cloned();
let api = match operation.get(2).and_then(|options| options.get("api")) {
Some(Value::String(api)) if api == "async" => Api::Async,
_ => Api::Sync,
};
result = match name {
"use" => {
let value = argument(1).unwrap_or(Value::Null);
pipeline.use_value(&value, argument(2))?;
json!({})
}
"data" => match (argument(1), argument(2)) {
(Some(Value::String(key)), Some(value)) => {
pipeline.set_data(&key, value)?;
json!({})
}
(Some(Value::String(key)), None) => {
json!({ "data": pipeline.data(Some(&key)) })
}
_ => json!({ "data": pipeline.data(None) }),
},
"freeze" => {
pipeline.freeze()?;
json!({})
}
"parse" => {
let input = argument(1).unwrap_or(Value::Null);
json!({ "tree": pipeline.parse(input.as_str().unwrap_or_default(), file)? })
}
"run" => {
let tree = argument(1).unwrap_or(Value::Null);
json!({ "tree": pipeline.run(&tree, file, api)? })
}
"stringify" => {
let tree = argument(1).unwrap_or(Value::Null);
let value = pipeline.stringify(&tree, file)?;
json!({ extension_of(&mut pipeline)?: value })
}
"process" => {
let input = argument(1).unwrap_or(Value::Null);
let value =
pipeline.process(input.as_str().unwrap_or_default(), file, api)?;
json!({ extension_of(&mut pipeline)?: value })
}
_ => {
return Err(Error::new(
"invalid-case",
format!("not an operation: {operation}"),
));
}
};
}
if result.get("data").is_none() {
result["data"] = pipeline.data(None);
}
Ok(result)
}
fn extension_of(pipeline: &mut Pipeline) -> Result<&'static str, Error> {
Ok(pipeline.freeze()?.extension().unwrap_or("txt"))
}
fn write_messages(file: &File) -> Value {
let messages = file.messages.iter();
let write_point = |point: &textlane_core::UnistPoint| json!({ "line": point.line, "column": point.column, "offset": point.offset });
Value::Array(
messages
.map(|message| {
json!({
"ruleId": message.rule,
"source": message.source,
"place": message.place.as_ref().map(|place| json!({
"start": write_point(&place.start),
"end": write_point(&place.end),
})),
})
})
.collect(),
)
}
#[cfg(test)]
#[path = "run_unit.rs"]
mod unit;