@@ -15,6 +15,12 @@ import 'dart:io';
1515/// // Merge a single key (idempotent overwrite)
1616/// JsonEditor.mergeKey('/path/to/manifest.json', 'gcm_sender_id', '482941778795');
1717///
18+ /// // Deep-merge two maps (nested translation files, configs, etc.)
19+ /// final merged = JsonEditor.deepMerge(existing, incoming);
20+ ///
21+ /// // Merge an incoming JSON file into an existing one (idempotent)
22+ /// JsonEditor.mergeJsonFile('/app/assets/lang/en.json', '/stubs/en.json');
23+ ///
1824/// // Write a full map
1925/// JsonEditor.writeJson('/path/to/output.json', {'key': 'value'});
2026///
@@ -102,6 +108,133 @@ class JsonEditor {
102108 writeJson (path, data);
103109 }
104110
111+ /// Deep-merge [source] into [target] , returning a new map.
112+ ///
113+ /// When both [target] and [source] contain the same key and both values
114+ /// are `Map<String, dynamic>` , the merge recurses into that key. Otherwise
115+ /// the [source] value wins (overwrite semantics).
116+ ///
117+ /// Neither [target] nor [source] are mutated — a fresh map is returned.
118+ ///
119+ /// ### Example
120+ ///
121+ /// ```dart
122+ /// final target = {'auth': {'login': 'Login', 'logout': 'Logout'}};
123+ /// final source = {'auth': {'login': 'Sign In', 'register': 'Sign Up'}};
124+ /// final result = JsonEditor.deepMerge(target, source);
125+ /// // {'auth': {'login': 'Sign In', 'logout': 'Logout', 'register': 'Sign Up'}}
126+ /// ```
127+ ///
128+ /// @param target The base map (existing data).
129+ /// @param source The incoming map whose values take precedence.
130+ /// @return A new [Map<String, dynamic>] containing the merged result.
131+ static Map <String , dynamic > deepMerge (
132+ Map <String , dynamic > target,
133+ Map <String , dynamic > source,
134+ ) {
135+ final Map <String , dynamic > result = Map <String , dynamic >.from (target);
136+
137+ for (final entry in source.entries) {
138+ final existing = result[entry.key];
139+
140+ // Both sides are maps → recurse.
141+ if (existing is Map <String , dynamic > &&
142+ entry.value is Map <String , dynamic >) {
143+ result[entry.key] = deepMerge (
144+ existing,
145+ entry.value as Map <String , dynamic >,
146+ );
147+ } else {
148+ // Source wins — overwrite or add.
149+ result[entry.key] = entry.value;
150+ }
151+ }
152+
153+ return result;
154+ }
155+
156+ /// Merge an incoming JSON file into an existing target JSON file.
157+ ///
158+ /// If the [targetPath] file does not exist, the [sourcePath] content is
159+ /// written as-is. If the target already exists, a recursive deep-merge
160+ /// is performed — existing keys the user may have customised are preserved
161+ /// while new keys from [sourcePath] are added.
162+ ///
163+ /// When [force] is `true` , the [sourcePath] content overwrites [targetPath]
164+ /// entirely — no merge is performed.
165+ ///
166+ /// ### Example
167+ ///
168+ /// ```dart
169+ /// // Merge plugin translations into host app (idempotent)
170+ /// JsonEditor.mergeJsonFile(
171+ /// '/app/assets/lang/en.json',
172+ /// '/stubs/en.json',
173+ /// );
174+ ///
175+ /// // Force-overwrite with stub content
176+ /// JsonEditor.mergeJsonFile(
177+ /// '/app/assets/lang/en.json',
178+ /// '/stubs/en.json',
179+ /// force: true,
180+ /// );
181+ /// ```
182+ ///
183+ /// @param targetPath The destination JSON file (host app's file).
184+ /// @param sourcePath The incoming JSON file (stub / plugin file).
185+ /// @param force When `true` , skip merge and overwrite entirely.
186+ ///
187+ /// @throws [FileSystemException] if [sourcePath] does not exist.
188+ /// @throws [FormatException] if either file contains invalid JSON.
189+ static void mergeJsonFile (
190+ String targetPath,
191+ String sourcePath, {
192+ bool force = false ,
193+ }) {
194+ final sourceData = readJson (sourcePath);
195+ final targetFile = File (targetPath);
196+
197+ // 1. Force mode or target missing → write source as-is.
198+ if (force || ! targetFile.existsSync ()) {
199+ writeJson (targetPath, sourceData);
200+ return ;
201+ }
202+
203+ // 2. Deep-merge source into existing target.
204+ final targetData = readJson (targetPath);
205+ final merged = deepMerge (targetData, sourceData);
206+
207+ writeJson (targetPath, merged);
208+ }
209+
210+ /// Merge an in-memory [sourceData] map into an existing JSON file.
211+ ///
212+ /// Convenience variant of [mergeJsonFile] when the source content is
213+ /// already loaded (e.g. from a stub string rather than a file on disk).
214+ ///
215+ /// @param targetPath The destination JSON file.
216+ /// @param sourceData The incoming map to merge.
217+ /// @param force When `true` , skip merge and overwrite entirely.
218+ static void mergeJsonData (
219+ String targetPath,
220+ Map <String , dynamic > sourceData, {
221+ bool force = false ,
222+ }) {
223+ final targetFile = File (targetPath);
224+
225+ // 1. Force mode or target missing → write source as-is.
226+ if (force || ! targetFile.existsSync ()) {
227+ writeJson (targetPath, sourceData);
228+ return ;
229+ }
230+
231+ // 2. Deep-merge source into existing target.
232+ final targetData = readJson (targetPath);
233+ final merged = deepMerge (targetData, sourceData);
234+
235+ writeJson (targetPath, merged);
236+ }
237+
105238 // -------------------------------------------------------------------------
106239 // Inspection
107240 // -------------------------------------------------------------------------
0 commit comments