1use std::path::{Path, PathBuf};
4
5use serde_json::Value;
6
7use crate::codes::Code;
8use crate::config::node::{self, Kind, Node};
9use crate::diag::Diagnostics;
10
11#[derive(Debug, Clone)]
13pub struct YamlFile {
14 pub display: PathBuf,
16 pub text: String,
17 pub value: Value,
19 pub node: Node,
21}
22
23impl YamlFile {
24 pub fn load(path: &Path, display: PathBuf, diags: &mut Diagnostics) -> Option<YamlFile> {
26 let text = match std::fs::read_to_string(path) {
27 Ok(t) => t,
28 Err(e) => {
29 diags.error(
30 Code::IoError,
31 Some(display),
32 None,
33 format!("cannot read file: {e}"),
34 );
35 return None;
36 }
37 };
38 Self::parse(text, display, diags)
39 }
40
41 pub fn parse(text: String, display: PathBuf, diags: &mut Diagnostics) -> Option<YamlFile> {
42 match node::parse(&text) {
43 Ok(node) => {
44 let value = node.to_json();
45 Some(YamlFile {
46 display,
47 text,
48 value,
49 node,
50 })
51 }
52 Err(e) => {
53 diags.error(
54 Code::YamlSyntax,
55 Some(display),
56 e.line,
57 format!("invalid YAML: {}", e.message),
58 );
59 None
60 }
61 }
62 }
63
64 pub fn line_of(&self, key: &str, after: Option<usize>) -> Option<usize> {
67 let start = after.unwrap_or(1);
68 self.text.lines().enumerate().skip(start - 1).find_map(|(i, l)| {
69 let t = l.trim_start().trim_start_matches("- ").trim_start();
70 let t = t
71 .strip_prefix('"')
72 .and_then(|t| t.strip_prefix(key).and_then(|r| r.strip_prefix('"')))
73 .or_else(|| {
74 t.strip_prefix('\'')
75 .and_then(|t| t.strip_prefix(key).and_then(|r| r.strip_prefix('\'')))
76 })
77 .or_else(|| t.strip_prefix(key));
78 match t {
79 Some(rest) if rest.trim_start().starts_with(':') => Some(i + 1),
80 _ => None,
81 }
82 })
83 }
84
85 pub fn line_containing(&self, needle: &str) -> Option<usize> {
87 self.text.lines().position(|l| l.contains(needle)).map(|i| i + 1)
88 }
89}
90
91pub fn to_node(v: &Value) -> Node {
94 let kind = match v {
95 Value::Null => Kind::Null,
96 Value::Bool(b) => Kind::Bool(*b),
97 Value::Number(n) => match (n.as_i64(), n.as_u64(), n.as_f64()) {
98 (Some(i), _, _) => Kind::Int(i),
99 (None, Some(u), _) => Kind::UInt(u),
100 (_, _, f) => Kind::Float(f.unwrap_or(f64::NAN)),
101 },
102 Value::String(s) => Kind::Str(s.clone()),
103 Value::Array(s) => Kind::Seq(s.iter().map(to_node).collect()),
104 Value::Object(m) => Kind::Map(
105 m.iter()
106 .map(|(k, v)| {
107 (
108 node::Key {
109 name: k.clone(),
110 line: 0,
111 },
112 to_node(v),
113 )
114 })
115 .collect(),
116 ),
117 };
118 Node {
119 kind,
120 line: 0,
121 column: 0,
122 }
123}
124
125pub fn scalar_str(v: &Value) -> Option<String> {
127 match v {
128 Value::String(s) => Some(s.clone()),
129 Value::Number(n) => Some(n.to_string()),
130 Value::Bool(b) => Some(b.to_string()),
131 _ => None,
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn parse(text: &str) -> (Option<YamlFile>, Diagnostics) {
140 let mut d = Diagnostics::default();
141 (
142 YamlFile::parse(text.to_string(), PathBuf::from("t.yml"), &mut d),
143 d,
144 )
145 }
146
147 #[test]
148 fn merge_keys_are_applied_and_explicit_keys_win() {
149 let (yf, _) = parse(
150 "base: &b {type: postgres, host: h, database: shop}\n\
151 other: &o {port: 5433, host: other}\n\
152 prod:\n <<: *b\n database: shop_prod\n\
153 both:\n <<: [*b, *o]\n",
154 );
155 let v = yf.unwrap().value;
156 assert_eq!(v["prod"]["type"], "postgres");
157 assert_eq!(v["prod"]["database"], "shop_prod");
158 assert!(v["prod"].get("<<").is_none());
159 assert_eq!(v["both"]["host"], "h");
161 assert_eq!(v["both"]["port"], 5433);
162 }
163
164 #[test]
165 fn merges_chain() {
166 let (yf, _) = parse(
167 "dev: &pg {type: postgres, host: h, sslmode: prefer}\n\
168 verify: &v {<<: *pg, sslmode: verify-full}\n\
169 wrong: {<<: *v, host: other}\n",
170 );
171 let v = yf.unwrap().value;
172 assert_eq!(v["wrong"]["type"], "postgres");
173 assert_eq!(v["wrong"]["sslmode"], "verify-full");
174 assert_eq!(v["wrong"]["host"], "other");
175 assert!(v["wrong"].get("<<").is_none());
176 let (yf, d) = parse("a: {<<: 3}\n");
177 assert!(yf.is_none() && format!("{d:?}").contains("must be a map"));
178 }
179
180 #[test]
181 fn scalar_keys_become_names() {
182 let (yf, _) = parse("output: {format: csv, null: NULL, true: 1, 2024: x}\n");
183 let v = yf.unwrap().value;
184 assert!(v["output"]["null"].is_null());
185 assert_eq!(v["output"]["true"], 1);
186 assert_eq!(v["output"]["2024"], "x");
187 let (yf, _) = parse("output: {null: \"NULL\"}\n");
188 assert_eq!(yf.unwrap().value["output"]["null"], "NULL");
189 }
190
191 #[test]
192 fn complex_keys_are_an_error() {
193 let (yf, d) = parse("? [a, b]\n: 1\n");
194 assert!(yf.is_none());
195 assert!(format!("{d:?}").contains("keys must be names"));
196 }
197}