Skip to main content

bake/
output.rs

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