vrl/docs/
mod.rs

1#![deny(warnings, clippy::pedantic)]
2pub mod cmd;
3
4pub use cmd::{Opts, docs};
5
6use crate::compiler::Function;
7use crate::compiler::value::kind;
8use crate::core::Value;
9use crate::prelude::function::EnumVariant;
10use crate::prelude::{Example, Parameter};
11use indexmap::IndexMap;
12use serde::Serialize;
13use std::path::Path;
14use std::{fs, io};
15use tracing::{debug, info};
16
17#[derive(Serialize)]
18pub struct FunctionDoc {
19    pub anchor: String,
20    pub name: String,
21    pub category: String,
22    pub description: String,
23    pub arguments: Vec<ArgumentDoc>,
24    pub r#return: ReturnDoc,
25    #[serde(skip_serializing_if = "Vec::is_empty")]
26    pub internal_failure_reasons: Vec<String>,
27    #[serde(skip_serializing_if = "Vec::is_empty")]
28    pub examples: Vec<ExampleDoc>,
29    #[serde(skip_serializing_if = "Vec::is_empty")]
30    pub notices: Vec<String>,
31    pub pure: bool,
32}
33
34#[derive(Serialize)]
35pub struct ArgumentDoc {
36    pub name: String,
37    pub description: String,
38    pub required: bool,
39    pub r#type: Vec<String>,
40    #[serde(skip_serializing_if = "IndexMap::is_empty")]
41    pub r#enum: IndexMap<String, String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub default: Option<String>,
44}
45
46#[derive(Serialize)]
47pub struct ReturnDoc {
48    pub types: Vec<String>,
49    #[serde(skip_serializing_if = "Vec::is_empty")]
50    pub rules: Vec<String>,
51}
52
53#[derive(Serialize)]
54pub struct ExampleDoc {
55    pub title: String,
56    pub source: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub input: Option<serde_json::Value>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub r#return: Option<serde_json::Value>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub raises: Option<String>,
63}
64
65/// Writes function documentation files into `output_dir`
66///
67/// # Errors
68/// - Failed to create `output_dir`.
69/// - Failed to write or create file in `output_dir`.
70/// - JSON serialization error.
71///
72/// # Panics
73/// Will panic if any function's example has an input that is not valid JSON
74pub fn document_functions_to_dir(
75    functions: &[Box<dyn Function>],
76    output_dir: &Path,
77    extension: &str,
78) -> io::Result<()> {
79    // Ensure output directory exists
80    fs::create_dir_all(output_dir)?;
81
82    // Remove existing files with the target extension so that renamed/deleted
83    // functions don't leave stale files behind.
84    let ext_match = std::ffi::OsStr::new(extension);
85    for entry in fs::read_dir(output_dir)? {
86        let path = entry?.path();
87        if path.extension() == Some(ext_match) {
88            fs::remove_file(&path)?;
89        }
90    }
91
92    for doc in build_functions_doc(functions) {
93        let filename = format!("{}.{extension}", doc.name);
94        let filepath = output_dir.join(&filename);
95        let mut json = serde_json::to_string_pretty(&doc)?;
96        json.push('\n');
97
98        fs::write(&filepath, json)?;
99
100        debug!(path = ?filepath.display(), "Generated file");
101    }
102
103    info!("VRL documentation generation complete.");
104    Ok(())
105}
106
107/// # Panics
108/// Will panic if any function's example has an input that is not valid JSON
109#[must_use]
110pub fn build_functions_doc(functions: &[Box<dyn Function>]) -> Vec<FunctionDoc> {
111    functions
112        .iter()
113        .map(|f| build_function_doc(f.as_ref()))
114        .collect()
115}
116
117/// # Panics
118/// Will panic if any function's example has an input that is not valid JSON
119pub fn build_function_doc(func: &dyn Function) -> FunctionDoc {
120    let name = func.identifier().to_string();
121
122    let arguments: Vec<ArgumentDoc> = func
123        .parameters()
124        .iter()
125        .map(|param| {
126            let Parameter {
127                keyword,
128                kind,
129                required,
130                description,
131                default,
132                enum_variants,
133            } = param;
134
135            let name = keyword.trim().to_string();
136            let description = description.trim().to_string();
137            let default = default.map(pretty_value);
138            let r#type = kind_to_types(*kind);
139            let r#enum = enum_variants
140                .unwrap_or_default()
141                .iter()
142                .map(|EnumVariant { value, description }| {
143                    (value.to_string(), description.to_string())
144                })
145                .collect();
146
147            ArgumentDoc {
148                name,
149                description,
150                required: *required,
151                r#type,
152                default,
153                r#enum,
154            }
155        })
156        .collect();
157
158    let examples: Vec<ExampleDoc> = func
159        .examples()
160        .iter()
161        .map(|example| {
162            let Example {
163                title,
164                source,
165                result,
166                input,
167                file: _,
168                line: _,
169                deterministic: _,
170            } = example;
171
172            let (r#return, raises) = match result {
173                Ok(result) => {
174                    // Try to parse as JSON, otherwise treat as string
175                    let value = serde_json::from_str(result)
176                        .unwrap_or_else(|_| serde_json::Value::String(result.to_string()));
177                    (Some(value), None)
178                }
179                Err(error) => (None, Some(error.to_string())),
180            };
181
182            let source = source.to_string();
183            let title = title.to_string();
184            let input = input
185                .map(|s| serde_json::from_str(s).expect("VRL example input must be valid JSON"));
186            ExampleDoc {
187                title,
188                source,
189                input,
190                r#return,
191                raises,
192            }
193        })
194        .collect();
195
196    FunctionDoc {
197        anchor: name.clone(),
198        name,
199        category: func.category().to_string(),
200        description: trim_str(func.usage()),
201        arguments,
202        r#return: ReturnDoc {
203            types: kind_to_types(func.return_kind()),
204            rules: trim_slice(func.return_rules()),
205        },
206        internal_failure_reasons: trim_slice(func.internal_failure_reasons()),
207        examples,
208        notices: trim_slice(func.notices()),
209        pure: func.pure(),
210    }
211}
212
213fn kind_to_types(kind_bits: u16) -> Vec<String> {
214    // All type bits combined
215    if (kind_bits & kind::ANY) == kind::ANY {
216        return vec!["any".to_string()];
217    }
218
219    let mut types = Vec::new();
220
221    if (kind_bits & kind::BYTES) == kind::BYTES {
222        types.push("string".to_string());
223    }
224    if (kind_bits & kind::INTEGER) == kind::INTEGER {
225        types.push("integer".to_string());
226    }
227    if (kind_bits & kind::FLOAT) == kind::FLOAT {
228        types.push("float".to_string());
229    }
230    if (kind_bits & kind::BOOLEAN) == kind::BOOLEAN {
231        types.push("boolean".to_string());
232    }
233    if (kind_bits & kind::OBJECT) == kind::OBJECT {
234        types.push("object".to_string());
235    }
236    if (kind_bits & kind::ARRAY) == kind::ARRAY {
237        types.push("array".to_string());
238    }
239    if (kind_bits & kind::TIMESTAMP) == kind::TIMESTAMP {
240        types.push("timestamp".to_string());
241    }
242    if (kind_bits & kind::REGEX) == kind::REGEX {
243        types.push("regex".to_string());
244    }
245    if (kind_bits & kind::NULL) == kind::NULL {
246        types.push("null".to_string());
247    }
248
249    assert!(!types.is_empty(), "kind_bits {kind_bits} produced no types");
250
251    types
252}
253
254fn pretty_value(v: &Value) -> String {
255    if let Value::Bytes(b) = v {
256        str::from_utf8(b).map_or_else(|_| v.to_string(), String::from)
257    } else {
258        v.to_string()
259    }
260}
261
262fn trim_str(s: &'static str) -> String {
263    s.trim().to_string()
264}
265
266fn trim_slice(slice: &'static [&'static str]) -> Vec<String> {
267    slice.iter().map(|s| s.trim().to_string()).collect()
268}