Skip to content

Commit

Permalink
feat(fixtures): add basic fixtures (and snapshots) functionality (#105)
Browse files Browse the repository at this point in the history
* feat(fixtures): add basic fixtures module, skeleton and basic logic

* refactor(fixtures): improve logic, change types/interfaces, add ioc typedi

* refactor(fixtures): redesign classes and rearrange files, improve error texts

* style(fixtures): functions name changes + adding deps
  • Loading branch information
omermorad committed Sep 9, 2021
1 parent 6b6e69d commit 3bcde46
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = {
'<rootDir>/packages/reflect',
'<rootDir>/packages/parser',
'<rootDir>/packages/logger',
'<rootDir>/packages/fixtures',
],
};
2 changes: 2 additions & 0 deletions packages/fixtures/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
jest.config.js
3 changes: 3 additions & 0 deletions packages/fixtures/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc"
}
Empty file added packages/fixtures/CHANGELOG.md
Empty file.
Empty file added packages/fixtures/README.md
Empty file.
1 change: 1 addition & 0 deletions packages/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
9 changes: 9 additions & 0 deletions packages/fixtures/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const base = require('../../jest.config.base');
const packageJson = require('./package');

module.exports = {
...base,
name: packageJson.name,
displayName: packageJson.name,
collectCoverageFrom: ['src/**/*.ts', 'test/**/*.test.js']
};
64 changes: 64 additions & 0 deletions packages/fixtures/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "@mockinbird/fixtures",
"version": "0.0.0",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"description": "Mockingbird Fixtures Generator Package",
"contributors": [
{
"name": "Omer Morad",
"email": "omer.moradd@gmail.com"
},
{
"name": "Idan Ptichi",
"email": "idanpt@gmail.com"
}
],
"repository": {
"type": "git",
"url": "https://github.com/omermorad/mockingbird.git"
},
"bugs": {
"url": "https://github.com/omermorad/mockingbird/issues"
},
"readme": "https://github.com/omermorad/mockingbird/tree/refactor/master/packages/fixtures/README.md",
"scripts": {
"prebuild": "yarn rimraf dist",
"build": "tsc",
"watch": "tsc --watch",
"test": "jest --runInBand --verbose",
"lint": "eslint '{src,test}/**/*.ts'",
"lint:fix": "eslint '{src,test}/**/*.ts' --fix"
},
"files": [
"dist",
"index.js",
"index.d.ts",
"README.md",
"CHANGELOG.md"
],
"dependencies": {
"@mockinbird/logger": "^0.0.0",
"@mockinbird/reflect": "^3.0.1",
"@plumier/reflect": "^1.0.5",
"lodash.get": "^4.4.2",
"readdir": "^1.0.2",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@mockinbird/common": "^2.0.2",
"@types/lodash.get": "^4.4.6",
"jest": "27.0.6",
"rimraf": "^3.0.2",
"ts-jest": "^27.0.3",
"ts-loader": "^6.2.2",
"ts-node": "8.10.2",
"tsconfig-paths": "^3.9.0",
"typedi": "^0.10.0",
"typescript": "^3.9.7"
},
"publishConfig": {
"access": "public"
}
}
28 changes: 28 additions & 0 deletions packages/fixtures/src/decorators/fixture.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { decorateClass } from '@plumier/reflect';
import { Class } from '@mockinbird/common';

export const FIXTURE_DECORATOR_NAME = 'Fixture';

interface FixtureDecoratorOptions {
class: Class;
}

/**
*
* @param name {string}
*/
export function Fixture(name: string): ClassDecorator;

/**
*
* @param name {string}
* @param origin { class: Class }
*/
export function Fixture(name: string, origin: { class: Class }): ClassDecorator;

export function Fixture(name: string, options?: FixtureDecoratorOptions): ClassDecorator {
return decorateClass({
type: FIXTURE_DECORATOR_NAME,
value: { name, options },
});
}
3 changes: 3 additions & 0 deletions packages/fixtures/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './decorators/fixture.decorator';
export * from './lib/fixture-loader';
export * from './lib/fixture-scanner';
15 changes: 15 additions & 0 deletions packages/fixtures/src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ClassLiteral } from '@mockinbird/common';

export type MockSnapshot<TClass> = {
fixtureName: string;
originFile: string;
originClass: string;
baseClass: string | undefined;
values: Partial<ClassLiteral<TClass>>;
variants?: { [key: string]: Omit<MockSnapshot<unknown>, 'originFile'> };
};

export interface SnapshotFile {
readonly name: string;
readonly path: string;
}
13 changes: 13 additions & 0 deletions packages/fixtures/src/lib/fixture-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ObjectLiteral } from '@mockinbird/common';
import get from 'lodash.get';

export class FixtureEngine {
public static getFixturesDirectory(): string {
const cwd = process.cwd();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require(`${cwd}/package.json`) as ObjectLiteral;
const { fixturesDir = '/fixtures' } = get(pkg, 'mockingbird');

return `${cwd}/${fixturesDir}`;
}
}
44 changes: 44 additions & 0 deletions packages/fixtures/src/lib/fixture-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as fs from 'fs';
import { Container } from 'typedi';
import { FixtureEngine } from './fixture-engine';
import { SnapshotParser } from './snapshot-parser';
import { Snapshot } from './snapshot';

export interface FixtureLoader<TClass = any> {
/**
*
* @param name {string} the name of the fixture variant
*/
variant(name: string): Omit<FixtureLoader<TClass>, 'variant'>;

/**
*
* @param fixtureName {string} the name of the fixture
*/
load(fixtureName: string): Promise<TClass>;
}

export class FixtureLoader<TClass = any> {
private fixtureVariantName: string;

public constructor(private readonly fixtureName: string) {}

public variant(name: string): Omit<this, 'variant'> {
this.fixtureVariantName = name;
return this;
}

public async load(): Promise<TClass> {
const fixturesDir = FixtureEngine.getFixturesDirectory();
const snapshotPath = `${fixturesDir}/snapshots`;

if (!fs.existsSync(snapshotPath)) {
throw new Error(`Can not find directory 'snapshots' under directory '${fixturesDir}'`);
}

const snapshotParser = Container.get<SnapshotParser>(SnapshotParser);
const snapshot = Snapshot.create<TClass>({ name: this.fixtureName, path: snapshotPath });

return snapshotParser.parse<TClass>(snapshot, this.fixtureVariantName);
}
}
44 changes: 44 additions & 0 deletions packages/fixtures/src/lib/fixture-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'fs';
import path from 'path';
import { Class } from '@mockinbird/common';
import { Logger } from '@mockinbird/logger';
import { ClassReflection, reflect } from '@plumier/reflect';

const AAAAA = '/Users/omermorad/projects/mockingbird/sample/entities-monorepo/mocks';

function isMockSnapshotFileExists(name: string) {
return fs.existsSync(`${AAAAA}/snapshots/${name}.mock`);
}

export class FixtureScanner {
public async scan() {
const files = fs.readdirSync(AAAAA);
const paths = [];

const promises = files
.filter((file) => path.extname(file) === '.ts')
.map((file) => {
const fullPath = `${AAAAA}/${file}`;
paths.push(fullPath);

return import(fullPath).catch((e) => {
Logger.error(`Error importing file ${file}`, e);
});
});

const reflections: { [key: string]: { file: string; reflection: ClassReflection } } = {};
const functions = await Promise.all<Class[]>(promises);

functions.forEach((constructors, index) => {
for (const [key, value] of Object.entries(constructors)) {
reflections[key] = { file: paths[index], reflection: reflect(value) };
}
});

for (const [key, value] of Object.entries<{ file: string; reflection: ClassReflection }>(reflections)) {
const fixtureDecorators = value.reflection.decorators.find((decorator) => decorator.type === 'Fixture');

const b = 1;
}
}
}
92 changes: 92 additions & 0 deletions packages/fixtures/src/lib/snapshot-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import get from 'lodash.get';
import { Service } from 'typedi';
import { reflect } from '@plumier/reflect';
import { Class } from '@mockinbird/common';
import { Logger } from '@mockinbird/logger';
import { FixtureEngine } from './fixture-engine';
import { Snapshot } from './snapshot';
import { FIXTURE_DECORATOR_NAME } from '../decorators/fixture.decorator';

@Service()
export class SnapshotParser {
public static isVariantExists(snapshot: Snapshot, variant: string): boolean {
return snapshot.contents.variants.hasOwnProperty(variant);
}

public static async importOriginClass<TClass = unknown>(
originFile: string,
originClass: string
): Promise<Class<TClass>> {
const importedClasses: { [key: string]: Class<TClass> } = await import(
`${FixtureEngine.getFixturesDirectory()}/${originFile}`
);

if (!importedClasses.hasOwnProperty(originClass)) {
throw new Error(
`
Mockingbird was trying to import the class '${originClass}' from file '${originFile}'
but only the class(es) '${Object.keys(importedClasses).join("', ")}' were found.
The origin file does not contain any class named '${originClass}'. \n
It might be that you have changed the name of the origin class or moved it to another
file.
Possible solution: hit "mockingbird regen" in you cli
`
);
}

return importedClasses[originClass];
}

public static fetchDecoratorValues<TClass>(actualClass: Class<TClass>): {
fixtureName: string;
sourceClass: Class<TClass>;
} {
const { decorators = [] } = reflect(actualClass);
const found = decorators.find((decorator) => decorator.type === FIXTURE_DECORATOR_NAME);

return {
fixtureName: get(found, 'value.name'),
sourceClass: get(found, 'value.options.class') as Class<TClass>,
};
}

public async parse<TClass = any>(snapshot: Snapshot<TClass>, variant?: string): Promise<TClass> {
const { originFile, originClass, fixtureName, variants = {}, values = {} } = snapshot.contents;

const actualClass = await SnapshotParser.importOriginClass<TClass>(originFile, originClass);
const { fixtureName: decoratorFixtureName, sourceClass } = SnapshotParser.fetchDecoratorValues(actualClass);

let instance: TClass | any;

if (decoratorFixtureName !== fixtureName) {
throw new Error(`Mockingbird is able to find the file '${fixtureName}', but it does not contain any snapshot named '${fixtureName}'.
'${decoratorFixtureName}' has been found associated to the same class '${originClass}'
`);
}

if (sourceClass) {
instance = new sourceClass();
} else {
Logger.warn(
`Fixture '${decoratorFixtureName}' does not contain any source class in the @Fixture decorator options,\nMockingbird will rely on the mock class '${originClass}' instead`
);
Logger.info(`If you want to instantiate it from a different class please add { src: <Class> } to your fixture`);

instance = new actualClass();
}

const fixture = Object.assign(instance, values);

if (variant) {
if (!SnapshotParser.isVariantExists(snapshot, variant)) {
throw new Error(`Mockingbird can not find variant of fixture '${decoratorFixtureName}' named '${variant}'.
Did you create a variant for your base fixture '${decoratorFixtureName}' named '${variant}'?
Note: check the file '${FixtureEngine.getFixturesDirectory()}/${originFile}'`);
}

return Object.assign(fixture, variants[variant].values);
}

return fixture;
}
}
41 changes: 41 additions & 0 deletions packages/fixtures/src/lib/snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as fs from 'fs';
import { MockSnapshot, SnapshotFile } from '../interfaces/interfaces';

export class Snapshot<TClass = any> {
private readonly snapshotContents: MockSnapshot<TClass>;

public constructor(public readonly name: string, public readonly path: string) {
this.snapshotContents = this.parse();
}

private parse(): MockSnapshot<TClass> {
const { name, path } = this;

try {
if (!fs.existsSync(`${path}/${name}.fixture.json`)) {
throw new Error(`${path}/${name}.fixture.json not found`);
}

const snapshotContents = fs.readFileSync(`${path}/${name}.fixture.json`, 'utf-8');
return JSON.parse(snapshotContents) as MockSnapshot<TClass>;
} catch (error) {
throw new Error(
`
Mockingbird can not find fixture named '${name}'. \n
Maybe you are trying to load a variant of another fixture?
Possible solution: MockFactory(<base-fixture-name>).variant(<fixture-variant-name>)
Looked for file '${name}.fixture.json' under ${path}
`
);
}
}

public static create<TClass = any>(snapshot: SnapshotFile): Snapshot<TClass> {
return new Snapshot<TClass>(snapshot.name, snapshot.path);
}

public get contents(): MockSnapshot<TClass> {
return this.snapshotContents;
}
}
15 changes: 15 additions & 0 deletions packages/fixtures/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"target": "es6"
},
"exclude": [
"node_modules",
"test",
"src/**/*.test.ts",
"index.ts"
],
"include": ["src/"]
}
Loading

0 comments on commit 3bcde46

Please sign in to comment.