Skip to main content

bake/
output.rs

1use crate::{Context, Error, Result, Value};
2use std::fmt;
3use std::fs;
4use std::path::Path;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8/// Built-in result encodings accepted by the default `output` task.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum Format {
11    /// Plain text for strings and pretty JSON for structured values.
12    Raw,
13    /// Indented JSON.
14    Json,
15    /// One compact JSON value per line; the input must be an array.
16    Ndjson,
17}
18
19impl FromStr for Format {
20    type Err = Error;
21
22    fn from_str(value: &str) -> Result<Self> {
23        match value {
24            "raw" => Ok(Self::Raw),
25            "json" => Ok(Self::Json),
26            "ndjson" => Ok(Self::Ndjson),
27            _ => Err(Error::new(format!(
28                "unknown output format {value:?}; use raw, json, or ndjson"
29            ))),
30        }
31    }
32}
33
34impl fmt::Display for Format {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        formatter.write_str(match self {
37            Self::Raw => "raw",
38            Self::Json => "json",
39            Self::Ndjson => "ndjson",
40        })
41    }
42}
43
44fn format_value(value: &Value, format: Format) -> Result<String> {
45    let mut output = match format {
46        Format::Raw => match value {
47            Value::String(text) => text.clone(),
48            Value::Null => String::new(),
49            value => serde_json::to_string_pretty(value)?,
50        },
51        Format::Json => serde_json::to_string_pretty(value)?,
52        Format::Ndjson => {
53            let Value::Array(values) = value else {
54                return Err(Error::new("ndjson output requires an array result"));
55            };
56            let mut output = String::new();
57            for value in values {
58                output.push_str(&serde_json::to_string(value)?);
59                output.push('\n');
60            }
61            return Ok(output);
62        }
63    };
64
65    if !output.is_empty() && !output.ends_with('\n') {
66        output.push('\n');
67    }
68    Ok(output)
69}
70
71fn inferred_format(file: &Path) -> Option<Format> {
72    match file.extension()?.to_str()?.to_ascii_lowercase().as_str() {
73        "json" => Some(Format::Json),
74        "ndjson" => Some(Format::Ndjson),
75        "txt" | "text" => Some(Format::Raw),
76        _ => None,
77    }
78}
79
80/// Format or write the value returned by the previous task.
81///
82/// With no explicit `format`, JSON and NDJSON filename extensions select those
83/// formats. Other destinations use raw output. The input value is returned so
84/// further chained tasks can inspect it.
85#[crate::task(output, builtin)]
86pub fn output(
87    context: &mut Context,
88    #[bake(input)] input: Value,
89    file: Option<PathBuf>,
90    format: Option<Format>,
91) -> Result<Value> {
92    let format = format
93        .or_else(|| context.default_format())
94        .or_else(|| file.as_deref().and_then(inferred_format))
95        .unwrap_or(Format::Raw);
96    let contents = format_value(&input, format)?;
97
98    if let Some(file) = file {
99        let path = context.root().join(file);
100        fs::write(&path, contents.as_bytes())
101            .map_err(|error| Error::new(format!("{}: {error}", path.display())))?;
102    } else {
103        context.write_output(&contents);
104    }
105
106    Ok(input)
107}
108
109/// Consume a previous result without writing it.
110#[crate::task(output, builtin)]
111pub fn null(#[bake(input)] input: Value) -> Result<Value> {
112    Ok(input)
113}
114
115pub(crate) fn builtins() -> Vec<crate::Task> {
116    vec![output_task().builtin(), null_task().builtin()]
117}