Skip to content

Commit

Permalink
Format JSON file contents to allow for easier editing (#525)
Browse files Browse the repository at this point in the history
* Add formatting to JSON.stringify when running sync util

* Adjust unit tests based on formatting of JSON file contents

* Add prepare script for building on install

* Add ConfigOptions to Config class to allow formatting to be set

* Update factory to accept options to pass to Config

* Update utils.sync to accept options for formatting file on write

* Update unit tests to reflect new config options changes

* Update README with new ConfigOptions changes

* Add JSDoc comments to ConfigOptions and DEFAULT_CONFIG

* Split ConfigOption into a separate PrettyJSONConfig type

* Change util.sync to only send in prettyJson config

* Removing redundant options? check as options cannot be undefined (it has a default)
  • Loading branch information
kelfish authored Sep 21, 2023
1 parent 5c73bd8 commit d94af02
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 32 deletions.
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ A `key` can be :
- an array of string representing a multi level key
eg: `['foo', 'bar']`

### ConfigOptions

```ts
type ConfigOptions = {
prettyJson?: {
enabled: boolean;
indentSize?: number;
};
};
```

Options for creating and storing Config contents.
Currently supports formatting the JSON file contents in a pretty, indented
format for easy readability or editability.

`prettyJson.identSize` defaults to `2` if this option is `enabled`.


### Storable

Expand All @@ -72,7 +89,7 @@ interface Storable {
```


### `factory(file?: string, key?: string): Conf`
### `factory(file?: string, key?: string, options?: ConfigOptions): Conf`

**Description:**
Create an instance of [Config] and returns it.
Expand All @@ -99,13 +116,19 @@ factory('/data/test.json', 'test');
// file: app.getPath('userData') + '/config.json'
// key: 'test'
factory(undefined, 'test');

// file: app.getPath('userData') + '/config.json'
// key: 'test'
// JSON stored in readable, indented format (with default 2 space tab)
factory(undefined, 'test', { prettyJson: { enabled: true }});
```

**Parameters:**
| Name | Type | Default |
| ------- | -------- | ------------------------------------------ |
| `file?` | [string] | `app.getPath('userData') + '/config.json'` |
| `key?` | [string] | `key || file || 'userData'` |
| Name | Type | Default |
| ---------- | --------------- | ------------------------------------------ |
| `file?` | [string] | `app.getPath('userData') + '/config.json'` |
| `key?` | [string] | `key || file || 'userData'` |
| `options?` | [ConfigOptions] | `{ prettyJson: { enabled: false }}` |

**Returns:** void

Expand All @@ -114,12 +137,13 @@ factory(undefined, 'test');

The config class is a set of wrappers and helpers providing access to configuration and file synchronization.

#### `new Config(file: string, data: Storable): Config`
#### `new Config(file: string, data: Storable, options?: ConfigOptions): Config`
**Parameters:**
| Name | Type |
| ------ | ---------- |
| `file` | [string] |
| `data` | [Storable] |
| Name | Type |
| ---------- | --------------- |
| `file` | [string] |
| `data` | [Storable] |
| `options?` | [ConfigOptions] |

**Returns:** [Config]

Expand Down Expand Up @@ -214,6 +238,7 @@ If `key` is provided, returns an array containing all sub keys in the key object


[Key]: #key
[ConfigOptions]: #configOptions
[Storable]: #storable
[Config]: #config
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"test": "electron-mocha -r ts-node/register '**/*.spec.ts'",
"test:coverage": "nyc npm run test",
"test:docker": "xvfb-run --server-args=\"-screen 0 1024x780x24\" npm run test",
"build": "tsc"
"build": "tsc",
"prepare": "npm run build"
},
"license": "BSD-2-Clause",
"devDependencies": {
Expand Down
41 changes: 40 additions & 1 deletion src/Config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { app } from 'electron';
import { expect } from 'chai';
import { join } from 'path';
import { unlinkSync, readFileSync } from 'fs';
import Config from './Config';
import Config, { DEFAULT_CONFIG } from './Config';

const tmpFile = join(app.getPath('temp'), 'test.json');

Expand Down Expand Up @@ -48,6 +48,26 @@ describe('Config.set', () => {
});
});

describe('Config.set (with pretty JSON)', () => {
const config = new Config(tmpFile, {}, { prettyJson: { enabled: true }});

it('adds a value under a top level key', () => {
config.set('foo', 'bar');
expect((config as any)._data?.foo).to.equals('bar');
});

it('adds a value under a nested key', () => {
config.set('the.answer', 42);
expect((config as any)._data?.the?.answer).to.equals(42);
});

it('syncs the file on call', () => {
config.set('isSynced', 'sure');
const content = readFileSync(tmpFile).toString();
expect(content).to.equals(JSON.stringify((config as any)._data, null, 2));
});
});

describe('Config.setBulk', () => {
const config = new Config(tmpFile, {});

Expand All @@ -67,6 +87,25 @@ describe('Config.setBulk', () => {
});
});

describe('Config.setBulk (with pretty JSON)', () => {
const config = new Config(tmpFile, {}, { prettyJson: { enabled: true }});

it('adds multiple values in a single call', () => {
config.setBulk({
'foo': 'bar',
'the.answer': 42,
});
expect((config as any)._data?.foo).to.equals('bar');
expect((config as any)._data?.the?.answer).to.equals(42);
});

it('syncs the file on call', () => {
config.set('isSynced', 'sure');
const content = readFileSync(tmpFile).toString();
expect(content).to.equals(JSON.stringify((config as any)._data, null, 2));
});
});

describe('Config.get', () => {
const config = new Config(
tmpFile,
Expand Down
36 changes: 34 additions & 2 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,40 @@ function sync(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
descriptor.value = function (this: any, ...args: Array<any>): any {
const res = originalMethod.apply(this, args);
util.sync(this._file, this._data);
util.sync(this._file, this._data, this._options.prettyJson);

return res;
};

return descriptor;
}

/**
* Type to support configuration of pretty JSON when writing JSON file
*/
export type PrettyJSONConfig = {
enabled: boolean;
indentSize?: number;
};

/**
* Options that can be passed to Config for writing and storing data
*
* Currently supports pretty JSON format for storing indented,
* multi-line in file
*/
export type ConfigOptions = {
prettyJson?: PrettyJSONConfig;
};
/**
* Default config, used for optional `options` args throughout
*/
export const DEFAULT_CONFIG = {
prettyJson: {
enabled: false,
},
};

/**
* A Key can be:
* - a simple string: 'foo'
Expand All @@ -31,10 +57,16 @@ export type Key = string | Array<string>;
export default class Config {
private _file: string;
private _data: Storable;
private _options: ConfigOptions;

public constructor(file: string, data: Storable) {
public constructor(
file: string,
data: Storable,
options: ConfigOptions = DEFAULT_CONFIG,
) {
this._file = file;
this._data = data;
this._options = options;
}

public get file(): string {
Expand Down
10 changes: 7 additions & 3 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { app } from 'electron';
import { join } from 'path';
import { read } from './utils';
import Conf from './Config';
import Conf, { ConfigOptions, DEFAULT_CONFIG } from './Config';

const defaultFile = join(app.getPath('userData'), 'config.json');
const defaultKey = 'userData';

const instances: Map<string, Conf> = new Map();


export function factory(file?: string, key?: string): Conf {
export function factory(
file?: string,
key?: string,
options: ConfigOptions = DEFAULT_CONFIG,
): Conf {
const actualKey = key || file || defaultKey;

if (!instances.has(actualKey)) {
const actualFile = file || defaultFile;

instances.set(
actualKey,
new Conf(actualFile, read(actualFile)),
new Conf(actualFile, read(actualFile), options),
);
}

Expand Down
31 changes: 31 additions & 0 deletions src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ function unlinkTmpFiles() {
}

describe('utils.sync', () => {
afterEach(unlinkTmpFiles);

it('updates the file with given object', () => {
const path = join(tmpDir, 'iexist');
const data: Storable = {
Expand All @@ -40,6 +42,35 @@ describe('utils.sync', () => {
const content = readFileSync(path).toString();
expect(content).to.equals(JSON.stringify(data));
});
it('updates the file with given object - pretty JSON (indent default)', () => {
const path = join(tmpDir, 'iexist');
const data: Storable = {
first: 'level',
deep: { nested: 'value' },
};

const defaultIndent = 2;

utils.sync(path, data, { enabled: true });

const content = readFileSync(path).toString();
expect(content).to.equals(JSON.stringify(data, null, defaultIndent));
});

it('updates the file with given object - pretty JSON (indent 4)', () => {
const path = join(tmpDir, 'iexist');
const data: Storable = {
first: 'level',
deep: { nested: 'value' },
};

const indent = 4;

utils.sync(path, data, { enabled: true, indentSize: indent });

const content = readFileSync(path).toString();
expect(content).to.equals(JSON.stringify(data, null, indent));
});
});

describe('utils.read', () => {
Expand Down
34 changes: 19 additions & 15 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { readFileSync, writeFileSync } from 'fs';
import { Buffer } from 'buffer';
import { readFileSync, writeFileSync } from "fs";
import { Buffer } from "buffer";
import Storable from "./Storable";
import { Key } from './Config';
import { DEFAULT_CONFIG, Key, PrettyJSONConfig } from "./Config";

export function sync(file: string, data: Record<string, unknown>): void {
writeFileSync(file, JSON.stringify(data));
export function sync(
file: string,
data: Record<string, unknown>,
options: PrettyJSONConfig = DEFAULT_CONFIG["prettyJson"],
): void {
if (options.enabled) {
writeFileSync(file, JSON.stringify(data, null, options.indentSize || 2));
} else {
writeFileSync(file, JSON.stringify(data));
}
}

export function read(file: string): Storable {
Expand All @@ -13,12 +21,12 @@ export function read(file: string): Storable {
// See: https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback
const data = readFileSync(file) as Buffer;
return JSON.parse(data.toString());
} catch(err) {
} catch (err) {
if (
err instanceof Error &&
(err as NodeJS.ErrnoException).code === 'ENOENT'
(err as NodeJS.ErrnoException).code === "ENOENT"
) {
writeFileSync(file, '{}');
writeFileSync(file, "{}");
return {};
}
throw err;
Expand All @@ -30,11 +38,11 @@ export function pathiffy(key: Key): Array<string> {
return key;
}

return key.split('.');
return key.split(".");
}

export function search<T>(data: Storable, key: Key): T | undefined {
const path = pathiffy(key)
const path = pathiffy(key);

for (let i = 0; i < path.length; i++) {
if (data[path[i]] === undefined) {
Expand All @@ -46,11 +54,7 @@ export function search<T>(data: Storable, key: Key): T | undefined {
return data as T;
}

export function set<T>(
data: Storable,
key: Key,
value: Storable | T,
): void {
export function set<T>(data: Storable, key: Key, value: Storable | T): void {
const path = pathiffy(key);
let i;

Expand Down

0 comments on commit d94af02

Please sign in to comment.