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
65pub fn document_functions_to_dir(
75 functions: &[Box<dyn Function>],
76 output_dir: &Path,
77 extension: &str,
78) -> io::Result<()> {
79 fs::create_dir_all(output_dir)?;
81
82 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#[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
117pub 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 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 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}