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