Skip to content

Commit f06a217

Browse files
feat: add merge function
While `join` merges the content of an object into a file, `merge` merges the content of two files into a third, that can be a different file or one of the given two Closes #5
1 parent e940743 commit f06a217

File tree

4 files changed

+345
-14
lines changed

4 files changed

+345
-14
lines changed

README.md

+72-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![npm version](https://badge.fury.io/js/json-file-handler.svg)](https://badge.fury.io/js/json-file-handler)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
77

8-
Create or manipulate JSON files with asynchronous `read`, `write` and `join` (using an object) operations.
8+
Create or manipulate JSON files with asynchronous `read`, `write`, `merge` (two files into a third) and `join` (an object into a file) operations.
99

1010
## Table of Contents
1111

@@ -14,8 +14,9 @@ Create or manipulate JSON files with asynchronous `read`, `write` and `join` (us
1414
- [Things to know](#things-to-know)
1515
- [Error Handling](#error-handling)
1616
- [Examples](#examples)
17-
- [Reading](#reading)
18-
- [Joining and Overwriting](#joining-and-overwriting)
17+
- [Read](#read)
18+
- [Join and Overwrite](#join-and-overwrite)
19+
- [Merge](#merge)
1920
- [API Documentation](#api-documentation)
2021
- [FAQ](#faq)
2122

@@ -72,14 +73,14 @@ const relativeFilePath = '../config/settings.json';
7273
const absoluteFilePath = path.resolve(__dirname, relativeFilePath);
7374
```
7475

75-
Writing functions (`overwrite` and `join`) asks for a third and optional `indentationLevel` parameter, whose value determines how much space is used for indentation when the file is formatted.
76+
Writing functions (`overwrite`, `merge` and `join`) asks for an optional `indentationLevel` parameter, whose value determines how much space is used for indentation when the file is formatted.
7677
`JSON.stringify` is responsible for the formatting, so a value less than 1 indicates that no formatting is applied, and a value greater than 10 is ignored.
7778

7879
### Error Handling
7980

8081
When the functions fails they return a rejected promise with an error that can either be an instance of `JSONFileHandlerError`, if the error was caused by a misuse of the library, or of `Error` (actually is of [`SystemError`](https://nodejs.org/api/errors.html#errors_class_systemerror), but Node does not exposes the class so it can not be checked using `instanceof` operator), if it was caused by violating an operating system constraint, like reading a file that does not exist, or trying to write to a read-only file.
8182

82-
Both types of errors contains a `code` property that can be used to handle them.
83+
Both types of errors contain the properties `path` (useful when you want to know which file caused the error when you are merging two files or iterating) and `code`, that can be used to handle them.
8384
Bellow are the error codes that should be checked for each function, classified by kind:
8485

8586
- `read`
@@ -98,14 +99,23 @@ Bellow are the error codes that should be checked for each function, classified
9899
- `'EPERM'`
99100
- `'EMFILE'`
100101
- `'EISDIR'`
102+
- `merge`
103+
- `JSONFileHandlerError`
104+
- `'EMPTY_FILE'`
105+
- `'NOT_A_JSON'`
106+
- `SystemError`
107+
- `'ENOENT'`
108+
- `'EPERM'`
109+
- `'EMFILE'`
110+
- `'EISDIR'`
101111

102112
The error codes of `JSONFileHandlerError` are self explanatory, a more detailed explanation of `SystemError` error codes can be found inside [Node documentation](https://nodejs.org/api/errors.html#errors_common_system_errors). Also in the [examples section](#Examples).
103113

104114
## Examples
105115

106-
Functions can be executed using async/await or promises. `read` example uses promises while `join` example uses async/await.
116+
Functions can be used with async/await or promises. `read` example uses promises while `join` and `merge` examples use async/await.
107117

108-
### Reading
118+
### Read
109119

110120
```js
111121
import { read as readJSON } from 'json-file-handler';
@@ -142,7 +152,7 @@ readJSON(settingsFilePath)
142152
});
143153
```
144154

145-
### Joining and Overwriting
155+
### Join and Overwrite
146156

147157
```js
148158
import { join as joinJSON } from 'json-file-handler';
@@ -184,6 +194,60 @@ const newSettings = {
184194
updateSettingsFile(settingsFilePath, newSettings);
185195
```
186196

197+
### Merge
198+
199+
```js
200+
import { merge as mergeJSON } from 'json-file-handler';
201+
202+
const path = require('path');
203+
204+
const mergeCustomSettingsFileIntoSettingsFile = async (
205+
settingsFilePath,
206+
customSettingsFilePath
207+
) => {
208+
try {
209+
await mergeJSON(
210+
absoluteSettingsFilePath,
211+
absoluteCustomSettingsFilePath,
212+
absoluteSettingsFilePath
213+
);
214+
} catch (error) {
215+
switch (error.code) {
216+
case 'EMPTY_FILE':
217+
// Both files are empty
218+
break;
219+
case 'NOT_A_JSON':
220+
// The content of at least one of the two files can't be read as JSON
221+
break;
222+
case 'ENOENT':
223+
// At least one of the two files doesn't exist
224+
break;
225+
case 'EPERM':
226+
// At least one of the files requires elevated privileges to be read or
227+
// written
228+
break;
229+
case 'EMFILE':
230+
// There are too many open file descriptors, so the file can't be
231+
// written at this time
232+
break;
233+
case 'EISDIR':
234+
// At least one of the paths is the path of an existing directory
235+
break;
236+
}
237+
}
238+
};
239+
240+
const settingsFilePath = path.resolve(__dirname, '../config/settings.json');
241+
const customSettingsFilePath = path.resolve(
242+
__dirname,
243+
'../../custom-settings.json'
244+
);
245+
mergeCustomSettingsFileIntoSettingsFile(
246+
settingsFilePath,
247+
customSettingsFilePath
248+
);
249+
```
250+
187251
## API Documentation
188252

189253
API documentation can be read at [https://sebastian-altamirano.github.io/json-file-handler/](https://sebastian-altamirano.github.io/json-file-handler/).

src/index.ts

+76-5
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { JSONFileHandlerError } from './models/classes';
3636
import fs from 'fs';
3737
import path from 'path';
3838

39-
import merge from 'deepmerge';
39+
import deepMerge from 'deepmerge';
4040

4141
/**
4242
* Checks if the directory where the file is or will be located exists.
@@ -195,15 +195,15 @@ export const read = async (filePath: string): Promise<object> => {
195195
};
196196

197197
/**
198-
* Merges a given content with the content of a JSON file, or creates a new one
199-
* if it does not exists.
198+
* Joins a given content to the content of a JSON file, or creates a new one
199+
* if it doesn't exists.
200200
*
201201
* @param filePath - The absolute path where the file is or will be located
202202
* @param jsonContent - The object to be merged or written to the JSON file
203203
* @param indentationLevel - How much space to use for indentation when
204204
* formatting
205205
*
206-
* @returns A promise that joins or creates the file when resolved
206+
* @returns A promise that joins the content or creates the file when resolved
207207
*/
208208
export const join = async (
209209
filePath: string,
@@ -213,7 +213,7 @@ export const join = async (
213213
if (isAnObject(jsonContent)) {
214214
try {
215215
const currentJsonContent: object = await read(filePath);
216-
const newJsonContent = merge<object>(currentJsonContent, jsonContent);
216+
const newJsonContent = deepMerge<object>(currentJsonContent, jsonContent);
217217
return await write(filePath, newJsonContent, indentationLevel);
218218
} catch (error) {
219219
const fileIsEmpty = error.code === 'EMPTY_FILE';
@@ -233,8 +233,79 @@ export const join = async (
233233
return Promise.reject(notAValidObjectError);
234234
};
235235

236+
/**
237+
* Duplicates a JSON file.
238+
*
239+
* @param filePath - The absolute path of the file to be duplicated
240+
* @param duplicatedFilePath - The absolute path where the file will be
241+
* duplicated
242+
* @param indentationLevel - How much space to use for indentation when
243+
* formatting
244+
*
245+
* @returns A promise that duplicates the file when resolved
246+
*/
247+
const duplicate = async (
248+
filePath: string,
249+
duplicatedFilePath: string,
250+
indentationLevel: number
251+
): Promise<void> => {
252+
try {
253+
const fileContent: object = await read(filePath);
254+
return await write(duplicatedFilePath, fileContent, indentationLevel);
255+
} catch (error) {
256+
return Promise.reject(error);
257+
}
258+
};
259+
260+
/**
261+
* Merges the content of two JSON files into a third file, or duplicates one of
262+
* the files if the other one is empty.
263+
*
264+
* @param firstFilePath - The absolute path of the first file
265+
* @param secondFilePath - The absolute path of the second file
266+
* @param mergedFilePath - The absolute path where the file is or will be
267+
* located
268+
* @param indentationLevel - How much space to use for indentation when
269+
* formatting
270+
*
271+
* @returns A promise that merges the files, or duplicates one of them, when
272+
* resolved
273+
*/
274+
export const merge = async (
275+
firstFilePath: string,
276+
secondFilePath: string,
277+
mergedFilePath: string,
278+
indentationLevel = 2
279+
): Promise<void> => {
280+
let firstFileContent: object;
281+
try {
282+
firstFileContent = await read(firstFilePath);
283+
const secondFileContent: object = await read(secondFilePath);
284+
const mergedContent: object = deepMerge<object>(
285+
firstFileContent,
286+
secondFileContent
287+
);
288+
return await write(mergedFilePath, mergedContent, indentationLevel);
289+
} catch (error) {
290+
if (error.code === 'EMPTY_FILE' && firstFilePath !== secondFilePath) {
291+
if (error.path === secondFilePath) {
292+
// If the the second file is empty, then the first file has content, so
293+
// it's duplicated at `mergedFilePath` using `indentationLevel`
294+
return write(mergedFilePath, firstFileContent, indentationLevel);
295+
} else {
296+
// But if the first file is empty, it's not guaranteed that the second
297+
// file will have content, `duplicate` takes care of that condition
298+
return duplicate(secondFilePath, mergedFilePath, indentationLevel);
299+
}
300+
} else {
301+
return Promise.reject(error);
302+
}
303+
}
304+
};
305+
236306
module.exports = {
237307
read,
238308
overwrite,
239309
join,
310+
merge,
240311
};

0 commit comments

Comments
 (0)