Skip to main content

starbase_utils/
json.rs

1use crate::fs;
2use serde::Serialize;
3use serde::de::DeserializeOwned;
4use std::fmt::Debug;
5use std::path::Path;
6use tracing::{instrument, trace};
7
8pub use crate::json_error::JsonError;
9pub use serde_json;
10pub use serde_json::{Map as JsonMap, Number as JsonNumber, Value as JsonValue, json};
11
12/// Clean a JSON string by removing comments and trailing commas.
13#[inline]
14#[instrument(name = "clean_json", skip_all)]
15pub fn clean<T: AsRef<str>>(json: T) -> Result<String, std::io::Error> {
16    let mut json = json.as_ref().to_owned();
17
18    if !json.is_empty() {
19        json_strip_comments::strip(&mut json)?;
20    }
21
22    Ok(json)
23}
24
25/// Recursively merge [`JsonValue`] objects, with values from next overwriting previous.
26#[inline]
27#[instrument(name = "merge_json", skip_all)]
28pub fn merge(prev: &JsonValue, next: &JsonValue) -> JsonValue {
29    match (prev, next) {
30        (JsonValue::Object(prev_object), JsonValue::Object(next_object)) => {
31            let mut object = prev_object.clone();
32
33            for (key, value) in next_object.iter() {
34                if let Some(prev_value) = prev_object.get(key) {
35                    object.insert(key.to_owned(), merge(prev_value, value));
36                } else {
37                    object.insert(key.to_owned(), value.to_owned());
38                }
39            }
40
41            JsonValue::Object(object)
42        }
43        _ => next.to_owned(),
44    }
45}
46
47/// Parse a string and deserialize into the required type.
48#[inline]
49#[instrument(name = "parse_json", skip(data))]
50pub fn parse<D>(data: impl AsRef<str>) -> Result<D, JsonError>
51where
52    D: DeserializeOwned,
53{
54    trace!("Parsing JSON");
55
56    let contents = clean(data.as_ref()).map_err(|error| JsonError::Clean {
57        error: Box::new(error),
58    })?;
59
60    serde_json::from_str(&contents).map_err(|error| JsonError::Parse {
61        error: Box::new(error),
62    })
63}
64
65/// Format and serialize the provided value into a string.
66#[inline]
67#[instrument(name = "format_json", skip(data))]
68pub fn format<D>(data: &D, pretty: bool) -> Result<String, JsonError>
69where
70    D: ?Sized + Serialize,
71{
72    trace!("Formatting JSON");
73
74    if pretty {
75        serde_json::to_string_pretty(&data).map_err(|error| JsonError::Format {
76            error: Box::new(error),
77        })
78    } else {
79        serde_json::to_string(&data).map_err(|error| JsonError::Format {
80            error: Box::new(error),
81        })
82    }
83}
84
85/// Format and serialize the provided value into a string, with the provided
86/// indentation. This can be used to preserve the original indentation of a file.
87#[inline]
88#[instrument(name = "format_json_with_identation", skip(data))]
89pub fn format_with_identation<D>(data: &D, indent: &str) -> Result<String, JsonError>
90where
91    D: ?Sized + Serialize,
92{
93    use serde_json::Serializer;
94    use serde_json::ser::PrettyFormatter;
95
96    trace!(indent, "Formatting JSON with preserved indentation");
97
98    // Based on serde_json::to_string_pretty!
99    let mut writer = Vec::with_capacity(128);
100    let mut serializer =
101        Serializer::with_formatter(&mut writer, PrettyFormatter::with_indent(indent.as_bytes()));
102
103    data.serialize(&mut serializer)
104        .map_err(|error| JsonError::Format {
105            error: Box::new(error),
106        })?;
107
108    Ok(unsafe { String::from_utf8_unchecked(writer) })
109}
110
111/// Read a file at the provided path and deserialize into the required type.
112/// The path must already exist.
113#[inline]
114#[instrument(name = "read_json")]
115pub fn read_file<D>(path: impl AsRef<Path> + Debug) -> Result<D, JsonError>
116where
117    D: DeserializeOwned,
118{
119    let path = path.as_ref();
120    let contents = clean(fs::read_file(path)?).map_err(|error| JsonError::CleanFile {
121        path: path.to_owned(),
122        error: Box::new(error),
123    })?;
124
125    trace!(file = ?path, "Reading JSON file");
126
127    serde_json::from_str(&contents).map_err(|error| JsonError::ReadFile {
128        path: path.to_path_buf(),
129        error: Box::new(error),
130    })
131}
132
133/// Write a file and serialize the provided data to the provided path. If the parent directory
134/// does not exist, it will be created.
135///
136/// This function is primarily used internally for non-consumer facing files.
137#[inline]
138#[instrument(name = "write_json", skip(data))]
139pub fn write_file<D>(
140    path: impl AsRef<Path> + Debug,
141    data: &D,
142    pretty: bool,
143) -> Result<(), JsonError>
144where
145    D: ?Sized + Serialize,
146{
147    let path = path.as_ref();
148
149    trace!(file = ?path, "Writing JSON file");
150
151    let data = if pretty {
152        serde_json::to_string_pretty(&data).map_err(|error| JsonError::WriteFile {
153            path: path.to_path_buf(),
154            error: Box::new(error),
155        })?
156    } else {
157        serde_json::to_string(&data).map_err(|error| JsonError::WriteFile {
158            path: path.to_path_buf(),
159            error: Box::new(error),
160        })?
161    };
162
163    fs::write_file(path, data)?;
164
165    Ok(())
166}
167
168/// Write a file and serialize the provided data to the provided path, while taking the
169/// closest `.editorconfig` into account. If the parent directory does not exist,
170/// it will be created.
171///
172/// This function is used for consumer facing files, like configs.
173#[cfg(feature = "editor-config")]
174#[inline]
175#[instrument(name = "write_json_with_config", skip(data))]
176pub fn write_file_with_config<D>(
177    path: impl AsRef<Path> + Debug,
178    data: &D,
179    pretty: bool,
180) -> Result<(), JsonError>
181where
182    D: ?Sized + Serialize,
183{
184    if !pretty {
185        return write_file(path, &data, false);
186    }
187
188    trace!(file = ?path, "Writing JSON file with .editorconfig");
189
190    let path = path.as_ref();
191    let editor_config = fs::get_editor_config_props(path)?;
192
193    let mut data = format_with_identation(&data, &editor_config.indent)?;
194    editor_config.apply_eof(&mut data);
195
196    fs::write_file(path, data)?;
197
198    Ok(())
199}