Skip to content

Commit

Permalink
refactor: CSpell-IO to use Service Bus (#3303)
Browse files Browse the repository at this point in the history
* refactor: CSpell-IO to use Service Bus
  The goal is to have CSpell-IO support both node and web.
* Update index.test.ts.snap
* Add some test helpers
* Have tests use the test helper
* Use URLs in the handlers instead of string.
* wire up readFile
* Support reading http files.
  • Loading branch information
Jason3S committed Jul 28, 2022
1 parent 252c738 commit f89c101
Show file tree
Hide file tree
Showing 47 changed files with 700 additions and 219 deletions.
3 changes: 3 additions & 0 deletions cspell.code-workspace
Expand Up @@ -31,6 +31,9 @@
{
"path": "packages/cspell-grammar"
},
{
"path": "packages/cspell-io"
},
{
"path": "packages/cspell-json-reporter"
},
Expand Down
14 changes: 14 additions & 0 deletions packages/cspell-io/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cspell-io/package.json
Expand Up @@ -46,6 +46,7 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"@cspell/cspell-service-bus": "^6.4.2",
"@types/node-fetch": "^2.6.2",
"node-fetch": "^2.6.7"
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cspell-io/src/CSpellIO.ts
@@ -0,0 +1,10 @@
import type { Stats } from './models';

export interface CSpellIO {
readFile(uriOrFilename: string): Promise<string>;
readFileSync(uriOrFilename: string): string;
writeFile(uriOrFilename: string, content: string): Promise<void>;
getStat(uriOrFilename: string): Promise<Stats>;
getStatSync(uriOrFilename: string): Stats;
compareStats(left: Stats, right: Stats): number;
}
75 changes: 75 additions & 0 deletions packages/cspell-io/src/CSpellIONode.test.ts
@@ -0,0 +1,75 @@
import { CSpellIONode } from './CSpellIONode';
import { pathToSample as ps } from './test/helper';

const sc = expect.stringContaining;
const oc = expect.objectContaining;

describe('CSpellIONode', () => {
test('constructor', () => {
const cspellIo = new CSpellIONode();
expect(cspellIo).toBeDefined();
});

test.each`
filename | expected
${__filename} | ${sc('This bit of text')}
${ps('cities.txt')} | ${sc('San Francisco\n')}
${ps('cities.txt.gz')} | ${sc('San Francisco\n')}
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/packages/cspell-io/samples/cities.txt'} | ${sc('San Francisco\n')}
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/packages/cspell-io/samples/cities.txt.gz'} | ${sc('San Francisco\n')}
`('readFile $filename', async ({ filename, expected }) => {
const cspellIo = new CSpellIONode();
await expect(cspellIo.readFile(filename)).resolves.toEqual(expected);
});

test.each`
filename | expected
${ps('cities.not_found.txt')} | ${oc({ code: 'ENOENT' })}
${ps('cities.not_found.txt.gz')} | ${oc({ code: 'ENOENT' })}
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/packages/cspell-io/samples/cities.not_found.txt'} | ${oc({ code: 'ENOENT' })}
${'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/packages/cspell-io/not_found/cities.txt.gz'} | ${oc({ code: 'ENOENT' })}
`('readFile not found $filename', async ({ filename, expected }) => {
const cspellIo = new CSpellIONode();
await expect(cspellIo.readFile(filename)).rejects.toEqual(expected);
});

test.each`
filename | expected
${__filename} | ${sc('This bit of text')}
${ps('cities.txt')} | ${sc('San Francisco\n')}
${ps('cities.txt.gz')} | ${sc('San Francisco\n')}
`('readFileSync $filename', ({ filename, expected }) => {
const cspellIo = new CSpellIONode();
expect(cspellIo.readFileSync(filename)).toEqual(expected);
});

const stats = {
urlA: { eTag: 'W/"10c5e3c7c73159515d4334813d6ba0255230270d92ebfdbd37151db7a0db5918"', mtimeMs: 0, size: -1 },
urlB: { eTag: 'W/"10c5e3c7c73159515d4334813d6ba0255230270d92ebfdbd37151db7a0dbffff"', mtimeMs: 0, size: -1 },
file1: { mtimeMs: 1658757408444.0342, size: 1886 },
file2: { mtimeMs: 1658757408444.0342, size: 2886 },
file3: { mtimeMs: 1758757408444.0342, size: 1886 },
};

test.each`
left | right | expected
${stats.urlA} | ${stats.urlA} | ${0}
${stats.urlA} | ${stats.file1} | ${1}
${stats.file1} | ${stats.file3} | ${-1}
${stats.file2} | ${stats.file3} | ${1}
`('getStat $left <> $right', async ({ left, right, expected }) => {
const cspellIo = new CSpellIONode();
const r = cspellIo.compareStats(left, right);
expect(r).toEqual(expected);
});

// writeFile(_uriOrFilename: string, _content: string): Promise<void> {
// throw new ErrorNotImplemented('writeFile');
// }
// getStat(_uriOrFilename: string): Promise<Stats> {
// throw new ErrorNotImplemented('getStat');
// }
// getStatSync(_uriOrFilename: string): Stats {
// throw new ErrorNotImplemented('getStatSync');
// }
});
68 changes: 68 additions & 0 deletions packages/cspell-io/src/CSpellIONode.ts
@@ -0,0 +1,68 @@
import { isServiceResponseSuccess, ServiceBus } from '@cspell/cspell-service-bus';
import { compareStats } from './common/stat';
import { CSpellIO } from './CSpellIO';
import { ErrorNotImplemented } from './errors/ErrorNotImplemented';
import { registerHandlers } from './handlers/node/file';
import { Stats } from './models/Stats';
import { toURL } from './node/file/util';
import {
RequestFsReadFile,
RequestFsReadFileSync,
RequestFsStat,
RequestFsStatSync,
RequestFsWriteFile,
} from './requests';

export class CSpellIONode implements CSpellIO {
constructor(readonly serviceBus = new ServiceBus()) {
registerHandlers(serviceBus);
}

readFile(uriOrFilename: string): Promise<string> {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsReadFile.create({ url }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'readFile');
}
return res.value;
}
readFileSync(uriOrFilename: string): string {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsReadFileSync.create({ url }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'readFileSync');
}
return res.value;
}
writeFile(uriOrFilename: string, _content: string): Promise<void> {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsWriteFile.create({ url }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'writeFile');
}
return res.value;
}
getStat(uriOrFilename: string): Promise<Stats> {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsStat.create({ url }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'getStat');
}
return res.value;
}
getStatSync(uriOrFilename: string): Stats {
const url = toURL(uriOrFilename);
const res = this.serviceBus.dispatch(RequestFsStatSync.create({ url }));
if (!isServiceResponseSuccess(res)) {
throw genError(res.error, 'getStatSync');
}
return res.value;
}
compareStats(left: Stats, right: Stats): number {
return compareStats(left, right);
}
}

function genError(err: Error | undefined, alt: string): Error {
return err || new ErrorNotImplemented(alt);
}
25 changes: 25 additions & 0 deletions packages/cspell-io/src/CSpellIOWeb.ts
@@ -0,0 +1,25 @@
import { CSpellIO } from './CSpellIO';
import { ErrorNotImplemented } from './errors/ErrorNotImplemented';
import { compareStats } from './common/stat';
import { Stats } from './models/Stats';

export class CSpellIOWeb implements CSpellIO {
readFile(_uriOrFilename: string): Promise<string> {
throw new ErrorNotImplemented('readFile');
}
readFileSync(_uriOrFilename: string): string {
throw new ErrorNotImplemented('readFileSync');
}
writeFile(_uriOrFilename: string, _content: string): Promise<void> {
throw new ErrorNotImplemented('writeFile');
}
getStat(_uriOrFilename: string): Promise<Stats> {
throw new ErrorNotImplemented('getStat');
}
getStatSync(_uriOrFilename: string): Stats {
throw new ErrorNotImplemented('getStatSync');
}
compareStats(left: Stats, right: Stats): number {
return compareStats(left, right);
}
}
27 changes: 27 additions & 0 deletions packages/cspell-io/src/common/stat.test.ts
@@ -0,0 +1,27 @@
import { compareStats } from './stat';

describe('stat', () => {
const stats = {
urlA: { eTag: 'W/"10c5e3c7c73159515d4334813d6ba0255230270d92ebfdbd37151db7a0db5918"', mtimeMs: 0, size: -1 },
urlB: { eTag: 'W/"10c5e3c7c73159515d4334813d6ba0255230270d92ebfdbd37151db7a0dbffff"', mtimeMs: 0, size: -1 },
file1: { mtimeMs: 1658757408444.0342, size: 1886 },
file2: { mtimeMs: 1658757408444.0342, size: 2886 },
file3: { mtimeMs: 1758757408444.0342, size: 1886 },
};

test.each`
left | right | expected
${stats.urlA} | ${stats.urlA} | ${0}
${stats.urlA} | ${stats.urlB} | ${-1}
${stats.urlA} | ${stats.file1} | ${1}
${stats.file1} | ${stats.file1} | ${0}
${stats.file1} | ${stats.file2} | ${-1}
${stats.file1} | ${stats.file3} | ${-1}
${stats.file2} | ${stats.file1} | ${1}
${stats.file3} | ${stats.file1} | ${1}
${stats.file2} | ${stats.file3} | ${1}
`('getStat $left <> $right', async ({ left, right, expected }) => {
const r = compareStats(left, right);
expect(r).toEqual(expected);
});
});
14 changes: 14 additions & 0 deletions packages/cspell-io/src/common/stat.ts
@@ -0,0 +1,14 @@
import { Stats } from '../models/Stats';

/**
* Compare two Stats to see if they have the same value.
* @param left - Stats
* @param right - Stats
* @returns 0 - equal; 1 - left > right; -1 left < right
*/
export function compareStats(left: Stats, right: Stats): number {
if (left === right) return 0;
if (left.eTag || right.eTag) return left.eTag === right.eTag ? 0 : (left.eTag || '') < (right.eTag || '') ? -1 : 1;
const diff = left.size - right.size || left.mtimeMs - right.mtimeMs;
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
}
5 changes: 5 additions & 0 deletions packages/cspell-io/src/errors/ErrorNotImplemented.ts
@@ -0,0 +1,5 @@
export class ErrorNotImplemented extends Error {
constructor(readonly method: string) {
super(`Method ${method} is not supported.`);
}
}
9 changes: 0 additions & 9 deletions packages/cspell-io/src/file/fetch.ts

This file was deleted.

60 changes: 0 additions & 60 deletions packages/cspell-io/src/file/fileWriter.test.ts

This file was deleted.

7 changes: 3 additions & 4 deletions packages/cspell-io/src/file/index.ts
@@ -1,4 +1,3 @@
export { readFile, readFileSync } from './fileReader';
export { writeToFile, writeToFileIterable, writeToFileIterableP } from './fileWriter';
export { getStat, getStatSync } from './stat';
export type { Stats } from './stat';
export { readFile, readFileSync } from './../node/file';
export { writeToFile, writeToFileIterable, writeToFileIterableP } from './../node/file';
export { getStat, getStatSync } from './../node/file';

0 comments on commit f89c101

Please sign in to comment.