Skip to content

Commit

Permalink
feat: type guard renderer (#226)
Browse files Browse the repository at this point in the history
Create a type-guard function for every content type.
  • Loading branch information
marcolink committed Mar 3, 2023
1 parent aaa8ff4 commit 4bc6577
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 2 deletions.
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
- [Renderer](#renderer)
- [Default Renderer](#DefaultContentTypeRenderer)
- [Localized Renderer](#LocalizedContentTypeRenderer)
- [JSDoc Renderer](#JSDocContentTypeRenderer)
- [JSDoc Renderer](#JSDocRenderer)
- [Type Guard Renderer](#TypeGuardRenderer)
- [Direct Usage](#direct-usage)
- [Browser Usage](#browser-usage)

Expand Down Expand Up @@ -48,6 +49,7 @@ OPTIONS
-p, --preserve preserve output folder
-l, --localized add localized types
-d, --jsdoc add JSDoc comments
-g, --typeguard add type guards
-s, --spaceId=spaceId space id
-t, --token=token management token
-v, --version show CLI version
Expand Down Expand Up @@ -237,7 +239,7 @@ Extend the default `BaseContentTypeRenderer` class, or implement the `ContentTyp
Relevant methods to override:

| Methods | Description | Override |
| ------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|---------------------|----------------------------------------------------------------------------|-----------------------------------------------------------------------|
| `render` | Enriches a `SourceFile` with all relevant nodes | To control content type rendering (you should know what you're doing) |
| `getContext` | Returns new render context object | To define custom type renderer and custom module name function |
| `addDefaultImports` | Define set of default imports added to every file | To control default imported modules |
Expand Down Expand Up @@ -373,6 +375,35 @@ export interface TypeAnimalFields {
export type TypeAnimal = Contentful.Entry<TypeAnimalFields>;
```

## TypeGuardRenderer

Adds type guard functions for every content type

#### Example Usage

```typescript
import { CFDefinitionsBuilder, TypeGuardRenderer } from 'cf-content-types-generator';

const builder = new CFDefinitionsBuilder([new DefaultContentTypeRenderer(), new TypeGuardRenderer()]);
```

#### Example output

```typescript
import { Entry, EntryFields } from 'contentful';
import type { WithContentTypeLink } from "TypeGuardTypes";

export interface TypeAnimalFields {
bread: EntryFields.Symbol;
}

export type TypeAnimal = Entry<TypeAnimalFields>;

export function isTypeAnimal(entry: WithContentTypeLink): entry is TypeAnimal {
return entry.sys.contentType.sys.id === 'animal'
}
```

# Direct Usage

If you're not a CLI person, or you want to integrate it with your tooling workflow, you can also directly use the `CFDefinitionsBuilder` from `cf-definitions-builder.ts`
Expand Down
6 changes: 6 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DefaultContentTypeRenderer,
JsDocRenderer,
LocalizedContentTypeRenderer,
TypeGuardRenderer,
} from './renderer';

// eslint-disable-next-line unicorn/prefer-module
Expand All @@ -22,6 +23,7 @@ class ContentfulMdg extends Command {
preserve: flags.boolean({ char: 'p', description: 'preserve output folder' }),
localized: flags.boolean({ char: 'l', description: 'add localized types' }),
jsdoc: flags.boolean({ char: 'd', description: 'add JSDoc comments' }),
typeguard: flags.boolean({ char: 'g', description: 'add type guards' }),

// remote access
spaceId: flags.string({ char: 's', description: 'space id' }),
Expand Down Expand Up @@ -72,6 +74,10 @@ class ContentfulMdg extends Command {
renderers.push(new JsDocRenderer());
}

if (flags.typeguard) {
renderers.push(new TypeGuardRenderer());
}

const builder = new CFDefinitionsBuilder(renderers);
for (const model of content.contentTypes) {
builder.appendType(model);
Expand Down
1 change: 1 addition & 0 deletions src/renderer/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { ContentTypeRenderer } from './content-type-renderer';
export { DefaultContentTypeRenderer } from './default-content-type-renderer';
export { LocalizedContentTypeRenderer } from './localized-content-type-renderer';
export { JsDocRenderer } from './js-doc-renderer';
export { TypeGuardRenderer } from './type-guard-renderer';
export { createDefaultContext } from './create-default-context';
export type { RenderContext } from './create-default-context';
58 changes: 58 additions & 0 deletions src/renderer/type/type-guard-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Project, SourceFile } from 'ts-morph';
import { moduleName } from '../../module-name';
import { CFContentType } from '../../types';
import { BaseContentTypeRenderer } from './base-content-type-renderer';

export class TypeGuardRenderer extends BaseContentTypeRenderer {
private readonly files: SourceFile[];

private static readonly WithContentTypeLink = 'WithContentTypeLink';
constructor() {
super();
this.files = [];
}

public override setup(project: Project): void {
super.setup(project);
const file = project.createSourceFile(`TypeGuardTypes.ts`, undefined, {
overwrite: true,
});

file.addTypeAlias({
name: TypeGuardRenderer.WithContentTypeLink,
isExported: true,
type: `{ sys: { contentType: { sys: { id: string } } } }`,
});
file.formatText();
this.files.push(file);
}

public render = (contentType: CFContentType, file: SourceFile): void => {
const entryInterfaceName = moduleName(contentType.sys.id);

file.addImportDeclaration({
moduleSpecifier: 'TypeGuardTypes',
namedImports: [TypeGuardRenderer.WithContentTypeLink],
isTypeOnly: true,
});

file.addFunction({
isExported: true,
name: `is${entryInterfaceName}`,
returnType: `entry is ${entryInterfaceName}`,
parameters: [
{
name: 'entry',
type: TypeGuardRenderer.WithContentTypeLink,
},
],
statements: `return entry.sys.contentType.sys.id === '${contentType.sys.id}'`,
});

file.organizeImports({
ensureNewLineAtEndOfFile: true,
});

file.formatText();
};
}
82 changes: 82 additions & 0 deletions test/renderer/type/type-guard-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Project, ScriptTarget, SourceFile } from 'ts-morph';
import { CFContentType, DefaultContentTypeRenderer, TypeGuardRenderer } from '../../../src';
import stripIndent = require('strip-indent');

describe('A content type type guard renderer class', () => {
let project: Project;
let testFile: SourceFile;
let mockContentType: CFContentType;

beforeEach(() => {
project = new Project({
useInMemoryFileSystem: true,
compilerOptions: {
target: ScriptTarget.ES5,
declaration: true,
},
});

mockContentType = {
name: 'Animal',
sys: {
id: 'animal',
type: 'Symbol',
},
fields: [
{
id: 'bread',
name: 'Bread',
disabled: false,
localized: false,
required: true,
type: 'Symbol',
omitted: false,
validations: [],
},
],
};

testFile = project.createSourceFile('test.ts');
});

describe('with default content type declarations', () => {
it('renders entry type guard', () => {
const defaultRenderer = new DefaultContentTypeRenderer();
defaultRenderer.setup(project);
defaultRenderer.render(mockContentType, testFile);

const typeGuardRenderer = new TypeGuardRenderer();
typeGuardRenderer.render(mockContentType, testFile);

expect('\n' + testFile.getFullText()).toEqual(
stripIndent(`
import type { Entry, EntryFields } from "contentful";
import type { WithContentTypeLink } from "TypeGuardTypes";
export interface TypeAnimalFields {
bread: EntryFields.Symbol;
}
export type TypeAnimal = Entry<TypeAnimalFields>;
export function isTypeAnimal(entry: WithContentTypeLink): entry is TypeAnimal {
return entry.sys.contentType.sys.id === 'animal'
}
`),
);
});

it('creates type guard helper types', () => {
const typeGuardRenderer = new TypeGuardRenderer();
typeGuardRenderer.setup(project);
typeGuardRenderer.render(mockContentType, testFile);
const typeGuardFile = project.getSourceFiles()[1];

expect('\n' + typeGuardFile.getFullText()).toEqual(
stripIndent(`
export type WithContentTypeLink = { sys: { contentType: { sys: { id: string } } } };
`),
);
});
});
});

0 comments on commit 4bc6577

Please sign in to comment.