Maintain/Build/JsonEdit.rs
1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/JsonEdit.rs
3//=============================================================================//
4// Module: JsonEdit
5//
6// Brief Description: Implements JSON/JSON5 file editing for Tauri
7// configuration.
8//
9// RESPONSIBILITIES:
10// ================
11//
12// Primary:
13// - Dynamically modify fields in tauri.conf.json/tauri.conf.json5 files
14// - Update product name, bundle identifier, version, and sidecar path
15// - Support both JSON and JSON5 formats
16//
17// Secondary:
18// - Provide detailed logging of changes made
19// - Return whether any modifications occurred
20//
21// ARCHITECTURAL ROLE:
22// ===================
23//
24// Position:
25// - Infrastructure/File manipulation layer
26// - JavaScriptObjectNotation editing functionality
27//
28// Dependencies (What this module requires):
29// - External crates: serde_json, json5, serde (Serialize), std (fs, log, io)
30// - Internal modules: Error::BuildError
31// - Traits implemented: None
32//
33// Dependents (What depends on this module):
34// - Build orchestration functions
35// - Process function
36//
37// IMPLEMENTATION DETAILS:
38// =======================
39//
40// Design Patterns:
41// - Builder pattern (via serde)
42// - Functional pattern
43//
44// Performance Considerations:
45// - Complexity: O(n) - parsing and modifying based on file size
46// - Memory usage patterns: In-memory document manipulation
47// - Hot path optimizations: None needed
48//
49// Thread Safety:
50// - Thread-safe: No (not designed for concurrent access to files)
51// - Synchronization mechanisms used: None
52// - Interior mutability considerations: None
53//
54// Error Handling:
55// - Error types returned: BuildError (Json, Jsonfive, Io types)
56// - Recovery strategies: Propagate error up; Guard restores original file
57//
58// EXAMPLES:
59// =========
60//
61// Example 1: Full configuration update
62use std::{fs, path::Path};
63
64use log::{debug, info};
65use serde::Serialize;
66use serde_json::Value as JsonValue;
67
68/// ```rust
69/// use crate::Maintain::Source::Build::JsonEdit;
70/// let config_path = PathBuf::from("tauri.conf.json");
71/// let modified = JsonEdit(
72/// &config_path,
73/// "Debug_Mountain",
74/// "land.editor.binary.debug.mountain",
75/// "1.0.0",
76/// Some("Binary/node"),
77/// )?;
78/// ```
79// Example 2: Version and identifier update only
80/// ```rust
81/// use crate::Maintain::Source::Build::JsonEdit;
82/// let modified =
83/// JsonEdit(&config_path, "Mountain", "land.editor.binary.mountain", "1.0.0", None)?;
84/// ```
85//
86//=============================================================================//
87// IMPLEMENTATION
88//=============================================================================//
89use crate::Build::Error::Error as BuildError;
90
91/// Dynamically modifies fields in a `tauri.conf.json` or `tauri.conf.json5`
92/// file, including the sidecar path.
93///
94/// This function updates the following fields in the Tauri configuration:
95/// - `version` - The application version
96/// - `productName` - The product name displayed to users
97/// - `identifier` - The bundle identifier (reverse domain format)
98/// - `bundle.externalBin` - Adds the sidecar binary path if provided
99///
100/// The function automatically detects and handles both JSON and JSON5 formats,
101/// ensuring compatibility with different Tauri configuration styles.
102///
103/// # Parameters
104///
105/// * `File` - Path to the Tauri configuration file
106/// * `Product` - The product name to set (displayed to users)
107/// * `Id` - The bundle identifier to set (reverse domain format)
108/// * `Version` - The version string to set
109/// * `SidecarPath` - Optional path to the sidecar binary to bundle
110///
111/// # Returns
112///
113/// Returns a `Result<bool>` indicating:
114/// - `Ok(true)` - The file was modified and saved
115/// - `Ok(false)` - No changes were needed (all values already match)
116/// - `Err(BuildError)` - An error occurred during modification
117///
118/// # Errors
119///
120/// * `BuildError::Io` - If the file cannot be read or written
121/// * `BuildError::Json` - If JSON parsing or serialization fails
122/// * `BuildError::Jsonfive` - If JSON5 parsing fails
123/// * `BuildError::Utf` - If UTF-8 conversion fails
124///
125/// # Behavior
126///
127/// - Only modifies fields that don't match the specified values
128/// - Creates nested structures (`bundle`, `externalBin`) as needed
129/// - Writes output with tab indentation for human-readable formatting
130/// - Logs configuration changes at INFO level
131///
132/// # JSON5 Support
133///
134/// JSON5 is a superset of JSON that allows:
135/// - Trailing commas
136/// - Unquoted property names
137/// - Comments
138/// - Multi-line strings
139///
140/// The function automatically detects JSON5 files by their `.json5` extension
141/// and uses the appropriate parser.
142///
143/// # Example
144///
145/// ```no_run
146/// use crate::Maintain::Source::Build::JsonEdit;
147/// let path = PathBuf::from("tauri.conf.json");
148/// let modified = JsonEdit(
149/// &path,
150/// "Debug_Mountain",
151/// "land.editor.binary.debug.mountain",
152/// "1.0.0",
153/// Some("Binary/node"),
154/// )?;
155/// ```
156pub fn JsonEdit(File:&Path, Product:&str, Id:&str, Version:&str, SidecarPath:Option<&str>) -> Result<bool, BuildError> {
157 debug!(target: "Build::Json", "Attempting to modify JSON file: {}", File.display());
158
159 let Data = fs::read_to_string(File)?;
160
161 let mut Parsed:JsonValue = if File.extension().and_then(|s| s.to_str()) == Some("json5") {
162 json5::from_str(&Data)?
163 } else {
164 serde_json::from_str(&Data)?
165 };
166
167 let mut Modified = false;
168
169 let Root = Parsed.as_object_mut().ok_or_else(|| {
170 BuildError::Io(std::io::Error::new(
171 std::io::ErrorKind::InvalidData,
172 "JSON root is not an object",
173 ))
174 })?;
175
176 // Update version
177 if Root.get("version").and_then(JsonValue::as_str) != Some(Version) {
178 Root.insert("version".to_string(), JsonValue::String(Version.to_string()));
179
180 Modified = true;
181 }
182
183 // Update productName
184 if Root.get("productName").and_then(JsonValue::as_str) != Some(Product) {
185 Root.insert("productName".to_string(), JsonValue::String(Product.to_string()));
186
187 Modified = true;
188 }
189
190 // Update identifier
191 if Root.get("identifier").and_then(JsonValue::as_str) != Some(Id) {
192 Root.insert("identifier".to_string(), JsonValue::String(Id.to_string()));
193
194 Modified = true;
195 }
196
197 // Add sidecar path if provided
198 if let Some(Path) = SidecarPath {
199 let Bundle = Root
200 .entry("bundle")
201 .or_insert_with(|| JsonValue::Object(Default::default()))
202 .as_object_mut()
203 .unwrap();
204
205 let Bins = Bundle
206 .entry("externalBin")
207 .or_insert_with(|| JsonValue::Array(Default::default()))
208 .as_array_mut()
209 .unwrap();
210
211 Bins.push(JsonValue::String(Path.to_string()));
212
213 Modified = true;
214 }
215
216 // Write the file if any changes were made
217 if Modified {
218 let mut Buffer = Vec::new();
219
220 let Formatter = serde_json::ser::PrettyFormatter::with_indent(b"\t");
221
222 let mut Serializer = serde_json::Serializer::with_formatter(&mut Buffer, Formatter);
223
224 Parsed.serialize(&mut Serializer)?;
225
226 fs::write(File, String::from_utf8(Buffer)?)?;
227
228 info!(target: "Build::Json", "Dynamically configured {}", File.display());
229 }
230
231 Ok(Modified)
232}