Skip to content

Commit

Permalink
feat: tidy up based on initial feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
cassels committed May 5, 2020
1 parent bf919b0 commit 70fd0f3
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 84 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ The default export of the `dotpref` module is a singleton [instance](#Instance)
import Pref from 'dotpref';
```

#### `createInstance`
#### `createPref`

Creates a custom [instance](#Instance) of dotpref with custom configuration. This method can be used if you need multiple configurations.

```js
import { createInstance } from 'dotpref';
import { createPref } from 'dotpref';
```

#### Instance
Expand Down
158 changes: 96 additions & 62 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { join } from 'path';
import { createInstance, Pref as Pref1 } from './index';
import { createPref, Pref } from './index';
import * as io from './utils/io';

const mockRead = jest.spyOn(io, 'readFromDisk').mockImplementation();
const mockWrite = jest.spyOn(io, 'writeToDisk').mockImplementation();

afterAll(() => {
jest.restoreAllMocks();
});

beforeEach(() => {
mockRead.mockImplementation();
mockWrite.mockImplementation();
});

describe('exports', () => {
it('exports expected items', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand All @@ -12,10 +24,59 @@ describe('exports', () => {
});

describe('singleton instance', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('Pref is a singleton instance', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Pref2 = require('./index').Pref;
expect(Pref1).toBe(Pref2);
expect(Pref).toBe(Pref2);
});

it('sets, gets, resets', () => {
expect(Pref.get('foo')).toBe(undefined);

Pref.set('foo', 'bar');
expect(mockWrite).toHaveBeenLastCalledWith(
Pref.filePath.replace('/config.pref', ''),
'config.pref',
expect.any(String)
);

expect(Pref.get('foo')).toBe('bar');

Pref.reset('foo');
expect(mockWrite).toHaveBeenLastCalledWith(
Pref.filePath.replace('/config.pref', ''),
'config.pref',
expect.any(String)
);

expect(Pref.get('foo')).toBe(undefined);

expect(mockWrite).toBeCalledTimes(2);
expect(mockRead).toBeCalledTimes(1);
});

it('writes, reads', () => {
let file: string;
mockWrite.mockImplementation((dir, name, text) => {
file = text;
});

Pref.write();
expect(mockWrite).toBeCalledTimes(1);
expect(mockWrite).toBeCalledWith(
Pref.filePath.replace('/config.pref', ''),
'config.pref',
expect.any(String)
);

mockRead.mockReturnValueOnce(file);
Pref.read();
expect(mockRead).toBeCalledTimes(1);
expect(mockRead).toReturnWith(expect.any(String));
});
});

Expand All @@ -30,58 +91,52 @@ describe('create instance', () => {
decoder,
};

afterEach(() => {
beforeEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
jest.resetAllMocks();
});

const createMockExists = (hasFile = false) =>
jest.spyOn(io, 'existsOnDisk').mockImplementation(() => hasFile);
const createMockRead = <S>(s?: S) => {
const mock = jest.spyOn(io, 'readFromDisk');
return s
? mock.mockImplementation(() => JSON.stringify(s))
: mock.mockImplementation();
};
const createMockWrite = () =>
jest.spyOn(io, 'writeToDisk').mockImplementation();

it('does not throw error', () => {
createMockExists();
createMockRead();
createMockWrite();
expect(() =>
createInstance({
createPref({
defaults: { a: 'memory' },
...SAFE_OPTIONS,
})
).not.toThrow();
});

it('creates defaults and gets defaults but does not read or write', () => {
createMockExists();
const mockRead = createMockRead();
const mockWrite = createMockWrite();

const { get } = createInstance({
it('creates defaults and gets defaults', () => {
mockRead.mockImplementationOnce(() => {
throw new Error();
});
const { get } = createPref({
defaults: { a: 'a' },
...SAFE_OPTIONS,
});

expect(mockRead).not.toHaveBeenCalled();
expect(mockRead).toHaveBeenCalledTimes(1);
expect(mockRead).not.toReturn();
expect(mockWrite).not.toHaveBeenCalled();

expect(get('a')).toBe('a');
});

it('falls back on defaults if error', () => {
mockRead.mockReturnValueOnce('not encrypted text');

const { get } = createPref({
defaults: { a: 'a' },
...SAFE_OPTIONS,
});
expect(mockRead).toHaveBeenCalled();
expect(mockWrite).not.toHaveBeenCalled();

expect(get('a')).toBe('a');
});

it('reads and extends defaults', () => {
createMockExists(true);
const mockRead = createMockRead({ a: 'disk' });
mockRead.mockReturnValueOnce(JSON.stringify({ a: 'disk' }));

const { get } = createInstance({
const { get } = createPref({
defaults: { a: 'default', b: 'b' },
...SAFE_OPTIONS,
});
Expand All @@ -92,11 +147,7 @@ describe('create instance', () => {
});

it("dose not write if state doesn't change", () => {
createMockExists();
createMockRead();
const mockWrite = createMockWrite();

const { set } = createInstance({
const { set } = createPref({
defaults: { a: 'a' },
...SAFE_OPTIONS,
});
Expand All @@ -109,11 +160,7 @@ describe('create instance', () => {
});

it('writes once state has changed', () => {
createMockExists();
createMockRead();
const mockWrite = createMockWrite();

const { set } = createInstance({
const { set } = createPref({
defaults: { a: 'a' },
...SAFE_OPTIONS,
});
Expand All @@ -131,10 +178,7 @@ describe('create instance', () => {
});

it('filePath exists and is overwritten', () => {
createMockExists();
createMockRead();

const { filePath } = createInstance({
const { filePath } = createPref({
defaults: { a: 'a' },
...SAFE_OPTIONS,
});
Expand All @@ -143,16 +187,12 @@ describe('create instance', () => {
});

it('gets, sets, and resets key from state', () => {
createMockExists();
const mockRead = createMockRead();
const mockWrite = createMockWrite();

const { get, set, reset } = createInstance({
const { get, set, reset } = createPref({
defaults: { a: 'default' },
...SAFE_OPTIONS,
});

expect(mockRead).not.toHaveBeenCalled();
expect(mockRead).toHaveBeenCalledTimes(1);
expect(mockWrite).not.toHaveBeenCalled();
expect(get('a')).toEqual('default');

Expand All @@ -165,7 +205,7 @@ describe('create instance', () => {
JSON.stringify({ a: 'disk' })
);
expect(get('a')).toEqual('disk');
expect(mockRead).not.toHaveBeenCalled();
expect(mockRead).toHaveBeenCalledTimes(1);

reset('a');

Expand All @@ -176,14 +216,11 @@ describe('create instance', () => {
JSON.stringify({ a: 'default' })
);
expect(get('a')).toEqual('default');
expect(mockRead).not.toHaveBeenCalled();
expect(mockRead).toHaveBeenCalledTimes(1);
});

it('never writes when equality is `false`', () => {
createMockExists();
createMockRead();
const mockWrite = createMockWrite();
const { set, reset } = createInstance({
const { set, reset } = createPref({
defaults: { a: 'default' },
...SAFE_OPTIONS,
equality: false,
Expand All @@ -199,10 +236,7 @@ describe('create instance', () => {
});

it('always writes when equality is `true`', () => {
createMockExists();
createMockRead();
const mockWrite = createMockWrite();
const { set, reset } = createInstance({
const { set, reset } = createPref({
defaults: { a: 'default' },
...SAFE_OPTIONS,
equality: true,
Expand Down
36 changes: 28 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dirname, join } from 'path';
import { AnyState, DotPrefOptions } from './types';
import { existsOnDisk, readFromDisk, writeToDisk } from './utils/io';
import { AnyState, DotPref, DotPrefOptions } from './types';
import { readFromDisk, writeToDisk } from './utils/io';
import { getOptions } from './utils/options';
import { PartialPick } from './utils/types';
import { getPackageData, normalizeId, shouldWrite } from './utils/utils';
Expand All @@ -22,20 +22,40 @@ const getModuleParentDir = () => {
return dirname((module.parent && module.parent.filename) || '.');
};

let instance: DotPref<AnyState>;
const getInstance = (): DotPref<AnyState> => {
if (!instance) {
instance = createPref<AnyState>({
defaults: {},
});
}
return instance;
};

/**
* Default instance of .pref
*/
export const Pref = createInstance<AnyState>({
defaults: {},
export const Pref: DotPref<AnyState> = {
get: key => getInstance().get(key),
set: (key, value) => getInstance().set(key, value),
reset: key => getInstance().reset(key),
read: () => getInstance().read(),
write: () => getInstance().write(),
filePath: '',
};

Object.defineProperty(Pref, 'filePath', {
enumerable: true,
get: () => getInstance().filePath,
});

/**
* Create custom instance of .pref.
*/
export function createInstance<S extends AnyState>({
export function createPref<S extends AnyState>({
name,
...options
}: PartialPick<DotPrefOptions<S>, 'defaults'>) {
}: PartialPick<DotPrefOptions<S>, 'defaults'>): DotPref<S> {
const parentPackageData = getPackageData(getModuleParentDir());
const defaultName = parentPackageData.name;

Expand Down Expand Up @@ -71,12 +91,12 @@ export function createInstance<S extends AnyState>({
};

const read = () => {
if (existsOnDisk(dirPath, filename)) {
try {
const encryptedData = readFromDisk(dirPath, filename);
const serializedData = decoder(encryptedData);
state = { ...defaults, ...deserializer(serializedData) };
// TODO: notify READ
} else {
} catch (e) {
state = { ...defaults };
}
};
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@ export interface DotPrefOptions<S extends AnyState> {
// TODO: `watch`;
// watch: boolean;
}

export interface DotPref<S extends AnyState> {
get: <K extends keyof S>(key: K) => S[K];
set: <K extends keyof S>(key: K, value: S[K]) => void;
reset: <K extends keyof S>(key: K) => void;
read: () => void;
write: () => void;
filePath: string;
}
7 changes: 1 addition & 6 deletions src/utils/io.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as atomic from 'write-file-atomic';
import { existsOnDisk, readFromDisk, writeToDisk } from './io';
import { readFromDisk, writeToDisk } from './io';

describe('io', () => {
afterEach(() => {
Expand All @@ -16,11 +16,6 @@ describe('io', () => {
expect(readFromDisk('./tmp', 'test')).toBe('test contents');
});

it('checks existence of file', () => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
expect(existsOnDisk('./tmp', 'test')).toBe(true);
});

it('writes to file', () => {
const mkdirMock = jest.spyOn(fs, 'mkdirSync').mockImplementation();
const atomicMock = jest.spyOn(atomic, 'sync').mockImplementation();
Expand Down
7 changes: 1 addition & 6 deletions src/utils/io.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readFileSync } from 'fs';
import { mkdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { sync as writeAtomic } from 'write-file-atomic';

Expand All @@ -16,8 +16,3 @@ export const readFromDisk = (dirPath: string, filename: string): string => {
const filePath = join(dirPath, filename);
return readFileSync(filePath, 'utf8');
};

export const existsOnDisk = (dirPath: string, filename: string): boolean => {
const filePath = join(dirPath, filename);
return existsSync(filePath);
};

0 comments on commit 70fd0f3

Please sign in to comment.