Skip to content

Commit 6afe579

Browse files
committed
feat: use spread operator instead of mock builder
Mock builder is nice but it adds overhead and it's not naturally typed. We need an interface to support `withXxx` methods. With ES2015, the spread operator with an `overrides` function parameter can achieve the same goal
1 parent 0b1aaec commit 6afe579

File tree

6 files changed

+666
-122
lines changed

6 files changed

+666
-122
lines changed

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: test
2+
3+
on: [push]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
timeout-minutes: 15
9+
10+
steps:
11+
- uses: actions/checkout@v1
12+
- uses: docker://borales/yarn:latest
13+
with:
14+
args: 'install'
15+
- name: test
16+
uses: docker://borales/yarn:latest
17+
with:
18+
cmd: 'test'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@types/jest": "^24.0.19",
3333
"@typescript-eslint/eslint-plugin": "^1.4.2",
3434
"@typescript-eslint/parser": "^1.4.2",
35+
"auto": "^7.12.2",
3536
"eslint": "6.0.0",
3637
"eslint-config-landr": "0.1.0",
3738
"eslint-config-prettier": "^4.1.0",

src/index.ts

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,11 @@ import { printSchema, parse, visit, ASTKindToNode, NamedTypeNode, TypeNode, Visi
22
import faker from 'faker';
33
import { toPascalCase, PluginFunction } from '@graphql-codegen/plugin-helpers';
44

5-
const mockBuilderHelper = `
6-
function lowerFirst(value: string) {
7-
return value.replace(/^\\w/, c => c.toLowerCase());
8-
}
9-
10-
/**
11-
* Creates a new mock object for use on mock providers, or unit tests.
12-
* @param {T} obj Initial object from which mock will be created
13-
* @returns {T} Object with auto-generated methods \`withPropertyName\`
14-
* for each property \`propertyName\` of the initial object.
15-
* @note Will failed to create accessors for optional TypeScript properties
16-
* like \`propertyName?: boolean\` that will result in no property created in JavaScript.
17-
*/
18-
export function mockBuilder<T extends Object>(obj: T): T & any {
19-
let proxy = new Proxy(obj, {
20-
get: (target: T, key: keyof T) => {
21-
if (typeof key === 'string' && key.startsWith('with') && !(key in target)) {
22-
// It's a builder property \`withXxxx\` we dynamically create the setter function
23-
let property = key.substring(4);
24-
property = property in target ? property : lowerFirst(property);
25-
if (property in target) {
26-
return function(value: any) {
27-
// Property is a simple value
28-
// @ts-ignore
29-
target[property] = value;
30-
return proxy;
31-
};
32-
} else {
33-
throw \`Property '\${property}' doesn't exist for object \${target.constructor.name ||
34-
typeof target}\`;
35-
}
36-
}
37-
return target[key];
38-
}
39-
});
40-
41-
return proxy;
42-
}`;
43-
445
const toMockName = (name: string) => {
456
const isVowel = name.match(/^[AEIO]/);
467
return isVowel ? `an${name}` : `a${name}`;
478
};
489

49-
const capitalize = (s: unknown) => {
50-
if (typeof s !== 'string') {
51-
return '';
52-
}
53-
return s.charAt(0).toUpperCase() + s.slice(1);
54-
};
55-
5610
const hashedString = (value: string) => {
5711
let hash = 0;
5812
if (value.length === 0) {
@@ -131,7 +85,7 @@ const generateMockValue = (
13185

13286
// eslint-disable-next-line @typescript-eslint/no-empty-interface
13387
export interface TypescriptMocksPluginConfig {
134-
typesFile: string;
88+
typesFile?: string;
13589
}
13690

13791
interface TypeItem {
@@ -197,20 +151,12 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
197151
typeName,
198152
mockFn: () => {
199153
const mockFields = fields ? fields.map(({ mockFn }: any) => mockFn(typeName)).join('\n') : '';
200-
const fieldNames = fields ? fields.map(({ name }) => name) : [];
201-
202-
const mockInterface = `interface ${typeName}Mock extends ${typeName} {
203-
${fieldNames
204-
.map(name => `with${capitalize(name)}: (value: ${typeName}['${name}']) => ${typeName}Mock;`)
205-
.join('\n ')}
206-
}`;
207154

208155
return `
209-
${mockInterface}
210-
211-
export const ${toMockName(typeName)} = (): ${typeName}Mock => {
212-
return mockBuilder<${typeName}>({
156+
export const ${toMockName(typeName)} = (overrides?: Partial<${typeName}>): ${typeName} => {
157+
return {
213158
${mockFields}
159+
...overrides
214160
});
215161
};`;
216162
},
@@ -220,7 +166,7 @@ ${mockFields}
220166

221167
const result: any = visit(astNode, { leave: visitor });
222168
const definitions = result.definitions.filter((definition: any) => !!definition);
223-
const typesFile = config.typesFile.replace(/\.[\w]+$/, '');
169+
const typesFile = config.typesFile ? config.typesFile.replace(/\.[\w]+$/, '') : null;
224170
const typeImports = definitions
225171
.map(({ typeName }: { typeName: string }) => typeName)
226172
.filter((typeName: string) => !!typeName);
@@ -233,8 +179,6 @@ ${mockFields}
233179
import { ${typeImports.join(', ')} } from '${typesFile}';\n`
234180
: '';
235181

236-
return `${typesFileImport}
237-
${mockBuilderHelper}
238-
${mockFns.map((mockFn: Function) => mockFn()).join('\n')}
182+
return `${typesFileImport}${mockFns.map((mockFn: Function) => mockFn()).join('\n')}
239183
`;
240184
};

tests/__snapshots__/typescript-mock-data.spec.ts.snap

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,44 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`should generate mock data functions 1`] = `
4-
"/* eslint-disable @typescript-eslint/no-use-before-define,@typescript-eslint/no-unused-vars */
5-
import { Avatar, User } from './types/graphql';
6-
7-
8-
function lowerFirst(value: string) {
9-
return value.replace(/^\\\\w/, c => c.toLowerCase());
10-
}
11-
12-
/**
13-
* Creates a new mock object for use on mock providers, or unit tests.
14-
* @param {T} obj Initial object from which mock will be created
15-
* @returns {T} Object with auto-generated methods \`withPropertyName\`
16-
* for each property \`propertyName\` of the initial object.
17-
* @note Will failed to create accessors for optional TypeScript properties
18-
* like \`propertyName?: boolean\` that will result in no property created in JavaScript.
19-
*/
20-
export function mockBuilder<T extends Object>(obj: T): T & any {
21-
let proxy = new Proxy(obj, {
22-
get: (target: T, key: keyof T) => {
23-
if (typeof key === 'string' && key.startsWith('with') && !(key in target)) {
24-
// It's a builder property \`withXxxx\` we dynamically create the setter function
25-
let property = key.substring(4);
26-
property = property in target ? property : lowerFirst(property);
27-
if (property in target) {
28-
return function(value: any) {
29-
// Property is a simple value
30-
// @ts-ignore
31-
target[property] = value;
32-
return proxy;
33-
};
34-
} else {
35-
throw \`Property '\${property}' doesn't exist for object \${target.constructor.name ||
36-
typeof target}\`;
37-
}
38-
}
39-
return target[key];
40-
}
4+
"
5+
export const anAvatar = (overrides?: Partial<Avatar>): Avatar => {
6+
return {
7+
id: '1550ff93-cd31-49b4-bc38-ef1cb68bdc38',
8+
url: 'aliquid',
9+
...overrides
4110
});
11+
};
4212
43-
return proxy;
44-
}
13+
export const aUser = (overrides?: Partial<User>): User => {
14+
return {
15+
id: 'b5756f00-51a6-422a-9a7d-c13ee6a63750',
16+
login: 'libero',
17+
avatar: anAvatar(),
18+
...overrides
19+
});
20+
};
21+
"
22+
`;
4523
46-
interface AvatarMock extends Avatar {
47-
withId: (value: Avatar['id']) => AvatarMock;
48-
withUrl: (value: Avatar['url']) => AvatarMock;
49-
}
24+
exports[`should generate mock data functions with external types file import 1`] = `
25+
"/* eslint-disable @typescript-eslint/no-use-before-define,@typescript-eslint/no-unused-vars */
26+
import { Avatar, User } from './types/graphql';
5027
51-
export const anAvatar = (): AvatarMock => {
52-
return mockBuilder<Avatar>({
28+
export const anAvatar = (overrides?: Partial<Avatar>): Avatar => {
29+
return {
5330
id: '1550ff93-cd31-49b4-bc38-ef1cb68bdc38',
5431
url: 'aliquid',
32+
...overrides
5533
});
5634
};
5735
58-
interface UserMock extends User {
59-
withId: (value: User['id']) => UserMock;
60-
withLogin: (value: User['login']) => UserMock;
61-
withAvatar: (value: User['avatar']) => UserMock;
62-
}
63-
64-
export const aUser = (): UserMock => {
65-
return mockBuilder<User>({
36+
export const aUser = (overrides?: Partial<User>): User => {
37+
return {
6638
id: 'b5756f00-51a6-422a-9a7d-c13ee6a63750',
6739
login: 'libero',
6840
avatar: anAvatar(),
41+
...overrides
6942
});
7043
};
7144
"

tests/typescript-mock-data.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,16 @@ it('can be called', async () => {
2525
});
2626

2727
it('should generate mock data functions', async () => {
28+
const result = await plugin(testSchema, [], {});
29+
30+
expect(result).toBeDefined();
31+
expect(result).toMatchSnapshot();
32+
});
33+
34+
it('should generate mock data functions with external types file import', async () => {
2835
const result = await plugin(testSchema, [], { typesFile: './types/graphql.ts' });
2936

3037
expect(result).toBeDefined();
31-
// @ts-ignore
38+
expect(result).toContain("import { Avatar, User } from './types/graphql';");
3239
expect(result).toMatchSnapshot();
3340
});

0 commit comments

Comments
 (0)