mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 00:11:02 -05:00
Made base classes for zip-based metadata readers
This commit is contained in:
parent
917a5130f1
commit
a8cce57c4b
2 changed files with 215 additions and 0 deletions
115
src/loaders/zipped-loader-metadata-reader.ts
Normal file
115
src/loaders/zipped-loader-metadata-reader.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { Awaitable } from "@/utils/types";
|
||||||
|
import { StreamZipAsync, async as ZipArchive } from "node-stream-zip";
|
||||||
|
import { PathLike } from "node:fs";
|
||||||
|
import { LoaderMetadata } from "./loader-metadata";
|
||||||
|
import { LoaderMetadataReader } from "./loader-metadata-reader";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a base for reading metadata from zipped files for various loaders.
|
||||||
|
*
|
||||||
|
* @template TMetadata - Represents the processed metadata object.
|
||||||
|
* @template TRawMetadata - Represents the raw metadata object to be transformed.
|
||||||
|
*/
|
||||||
|
export abstract class ZippedLoaderMetadataReader<TMetadata extends LoaderMetadata, TRawMetadata> implements LoaderMetadataReader<TMetadata> {
|
||||||
|
/**
|
||||||
|
* The name of the entry inside the zipped file to read.
|
||||||
|
*/
|
||||||
|
private readonly _entry: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link ZippedLoaderMetadataReader} instance.
|
||||||
|
*
|
||||||
|
* @param entry - The name of the entry inside the zipped file to read.
|
||||||
|
*/
|
||||||
|
protected constructor(entry: string) {
|
||||||
|
this._entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the metadata file from a zipped file at the given path.
|
||||||
|
*
|
||||||
|
* @param path - The path to the zipped file.
|
||||||
|
*
|
||||||
|
* @returns The metadata object, or `undefined` if the zipped file cannot be read.
|
||||||
|
*/
|
||||||
|
async readMetadataFile(path: PathLike): Promise<TMetadata | undefined> {
|
||||||
|
let zip = undefined as StreamZipAsync;
|
||||||
|
try {
|
||||||
|
zip = new ZipArchive({ file: path as string });
|
||||||
|
const buffer = await zip.entryData(this._entry);
|
||||||
|
if (!buffer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMetadata = await this.readRawMetadata(buffer);
|
||||||
|
return await this.createMetadata(rawMetadata);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
await zip?.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the raw metadata from a buffer.
|
||||||
|
*
|
||||||
|
* @param buffer - The buffer containing the raw metadata.
|
||||||
|
*
|
||||||
|
* @returns The raw metadata object.
|
||||||
|
*/
|
||||||
|
protected abstract readRawMetadata(buffer: Buffer): Promise<TRawMetadata>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a metadata object from the raw metadata.
|
||||||
|
*
|
||||||
|
* @param config - The raw metadata object.
|
||||||
|
*
|
||||||
|
* @returns The metadata object.
|
||||||
|
*/
|
||||||
|
protected abstract createMetadata(config: TRawMetadata): Promise<TMetadata>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a base for reading metadata from text-based files within zipped files.
|
||||||
|
*
|
||||||
|
* @template TMetadata - Represents the processed metadata object.
|
||||||
|
* @template TRawMetadata - Represents the raw metadata object to be transformed.
|
||||||
|
*/
|
||||||
|
export abstract class ZippedTextLoaderMetadataReader<TMetadata extends LoaderMetadata, TRawMetadata> extends ZippedLoaderMetadataReader<TMetadata, TRawMetadata> {
|
||||||
|
/**
|
||||||
|
* A function to transform the raw metadata into a processed metadata object.
|
||||||
|
*/
|
||||||
|
private readonly _factory: (raw: TRawMetadata) => Awaitable<TMetadata>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function to parse the text content into a raw metadata object.
|
||||||
|
*/
|
||||||
|
private readonly _parser: (text: string) => Awaitable<TRawMetadata>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link ZippedTextLoaderMetadataReader} instance.
|
||||||
|
*
|
||||||
|
* @param entry - The name of the entry inside the zipped file to read.
|
||||||
|
* @param factory - A function to transform the raw metadata into a processed metadata object.
|
||||||
|
* @param parser - A function to parse the text content into a raw metadata object.
|
||||||
|
*/
|
||||||
|
protected constructor(entry: string, factory: (raw: TRawMetadata) => Awaitable<TMetadata>, parser: (text: string) => Awaitable<TRawMetadata>) {
|
||||||
|
super(entry);
|
||||||
|
this._factory = factory;
|
||||||
|
this._parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async readRawMetadata(buffer: Buffer): Promise<TRawMetadata> {
|
||||||
|
return await this._parser(buffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async createMetadata(config: TRawMetadata): Promise<TMetadata> {
|
||||||
|
return await this._factory(config);
|
||||||
|
}
|
||||||
|
}
|
100
tests/unit/loaders/zipped-loader-metadata-reader.spec.ts
Normal file
100
tests/unit/loaders/zipped-loader-metadata-reader.spec.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { zipContent } from "@/../tests/utils/zip-utils";
|
||||||
|
import { LoaderMetadata } from "@/loaders/loader-metadata";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
import { ZippedLoaderMetadataReader, ZippedTextLoaderMetadataReader } from "@/loaders/zipped-loader-metadata-reader";
|
||||||
|
|
||||||
|
class MockZippedLoaderMetadataReader extends ZippedLoaderMetadataReader<LoaderMetadata, string> {
|
||||||
|
constructor(entry: string) {
|
||||||
|
super(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readRawMetadata(buffer: Buffer): Promise<string> {
|
||||||
|
return Promise.resolve(buffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createMetadata(config: string): Promise<LoaderMetadata> {
|
||||||
|
return Promise.resolve({ id: config } as LoaderMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockZippedTextLoaderMetadataReader extends ZippedTextLoaderMetadataReader<LoaderMetadata, string> {
|
||||||
|
constructor(entry: string, factory: (raw: string) => LoaderMetadata, parser: (text: string) => string) {
|
||||||
|
super(entry, factory, parser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockFs({
|
||||||
|
"test.zip": await zipContent("Test", "test.txt"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ZippedLoaderMetadataReader", () => {
|
||||||
|
test("reads metadata file from a zipped file at the given path", async () => {
|
||||||
|
const reader = new MockZippedLoaderMetadataReader("test.txt");
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("test.zip");
|
||||||
|
|
||||||
|
expect(metadata).toMatchObject({ id: "Test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined if the given path does not exist", async () => {
|
||||||
|
const reader = new MockZippedLoaderMetadataReader("test.txt");
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("");
|
||||||
|
|
||||||
|
expect(metadata).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined if the zip entry does not exist", async () => {
|
||||||
|
const reader = new MockZippedLoaderMetadataReader("test");
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("test.zip");
|
||||||
|
|
||||||
|
expect(metadata).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ZippedTextLoaderMetadataReader", () => {
|
||||||
|
test("reads metadata file from a zipped file at the given path", async () => {
|
||||||
|
const factory = jest.fn().mockImplementation(x => ({ id: x }));
|
||||||
|
const parse = jest.fn().mockImplementation(x => [...String(x)].reverse().join(""));
|
||||||
|
const reader = new MockZippedTextLoaderMetadataReader("test.txt", factory, parse);
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("test.zip");
|
||||||
|
|
||||||
|
expect(metadata).toMatchObject({ id: "tseT" });
|
||||||
|
expect(factory).toHaveBeenCalledTimes(1);
|
||||||
|
expect(factory).toHaveBeenCalledWith("tseT");
|
||||||
|
expect(parse).toHaveBeenCalledTimes(1);
|
||||||
|
expect(parse).toHaveBeenCalledWith("Test");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined if the given path does not exist", async () => {
|
||||||
|
const factory = jest.fn();
|
||||||
|
const parse = jest.fn();
|
||||||
|
const reader = new MockZippedTextLoaderMetadataReader("test.txt", factory, parse);
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("");
|
||||||
|
|
||||||
|
expect(metadata).toBeUndefined();
|
||||||
|
expect(factory).not.toHaveBeenCalled();
|
||||||
|
expect(parse).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined if the zip entry does not exist", async () => {
|
||||||
|
const factory = jest.fn();
|
||||||
|
const parse = jest.fn();
|
||||||
|
const reader = new MockZippedTextLoaderMetadataReader("test", factory, parse);
|
||||||
|
|
||||||
|
const metadata = await reader.readMetadataFile("test.zip");
|
||||||
|
|
||||||
|
expect(metadata).toBeUndefined();
|
||||||
|
expect(factory).not.toHaveBeenCalled();
|
||||||
|
expect(parse).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue