Skip to content

Commit

Permalink
feat: add response type renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
marcolink committed Feb 3, 2024
1 parent c70c2a6 commit 06d0f8a
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 0 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [Localized Renderer](#LocalizedContentTypeRenderer)
- [JSDoc Renderer](#JSDocRenderer)
- [Type Guard Renderer](#TypeGuardRenderer)
- [Response Type Renderer](#ResponseTypeRenderer)
- [Direct Usage](#direct-usage)
- [Browser Usage](#browser-usage)

Expand Down Expand Up @@ -454,6 +455,42 @@ export function isTypeAnimal<Modifiers extends ChainModifiers, Locales extends L
}
```

## ResponseTypeRenderer

Adds response types for every content type which are compatible with contentful.js v10.

#### Example Usage

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

const builder = new CFDefinitionsBuilder([
new V10ContentTypeRenderer(),
new ResponseTypeRenderer(),
]);
```

#### Example output

```typescript
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from "contentful";
import type { WithAllLocalesAndWithoutLinkResolutionResponse, WithAllLocalesAndWithoutUnresolvableLinksResponse, WithAllLocalesResponse, WithUnresolvableLinksResponse, WithoutLinkResolutionResponse, WithoutUnresolvableLinksResponse } from "./ResponseType";

export interface TypeAnimalFields {
bread?: EntryFieldTypes.Symbol;
}

export type TypeAnimalSkeleton = EntrySkeletonType<TypeAnimalFields, "animal">;
export type TypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<TypeAnimalSkeleton, Modifiers, Locales>;

export type TypeAnimalWithUnresolvableLinksResponse = WithUnresolvableLinksResponse<TypeTestSkeleton>;
export type TypeAnimalWithoutLinkResolutionResponse = WithoutLinkResolutionResponse<TypeTestSkeleton>;
export type TypeAnimalWithoutUnresolvableLinksResponse = WithoutUnresolvableLinksResponse<TypeTestSkeleton>;
export type TypeAnimalWithAllLocalesResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesResponse<TypeTestSkeleton, Locales>;
export type TypeAnimalWithAllLocalesAndWithoutLinkResolutionResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesAndWithoutLinkResolutionResponse<TypeTestSkeleton, Locales>;
export type TypeAnimalWithAllLocalesAndWithoutUnresolvableLinksResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesAndWithoutUnresolvableLinksResponse<TypeTestSkeleton, Locales>;
```

# 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
10 changes: 10 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
V10ContentTypeRenderer,
V10TypeGuardRenderer,
} from '../renderer';
import { ResponseTypeRenderer } from '../renderer/type/response-type-renderer';
import { CFEditorInterface } from '../types';

class ContentfulMdg extends Command {
Expand All @@ -29,6 +30,7 @@ class ContentfulMdg extends Command {
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' }),
response: Flags.boolean({ char: 'r', description: 'add response types' }),

// remote access
spaceId: Flags.string({ char: 's', description: 'space id' }),
Expand Down Expand Up @@ -94,6 +96,14 @@ class ContentfulMdg extends Command {
renderers.push(flags.v10 ? new V10TypeGuardRenderer() : new TypeGuardRenderer());
}

if (flags.response) {
if (!flags.v10) {
this.error('"--response" option is only available for contentful.js v10 types.');
}

renderers.push(new ResponseTypeRenderer());
}

const editorInterfaces = content.editorInterfaces as CFEditorInterface[] | undefined;

const builder = new CFDefinitionsBuilder(renderers);
Expand Down
168 changes: 168 additions & 0 deletions src/renderer/type/response-type-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Project, SourceFile } from 'ts-morph';
import { CFContentType } from '../../types';
import { renderTypeGeneric } from '../generic';
import { BaseContentTypeRenderer } from './base-content-type-renderer';

/*
* Renders the response types for the contentful content types
* Based on https://github.com/contentful/contentful.js/issues/2138#issuecomment-1921923508
*/

const ChainModifiers = {
WITH_UNRESOLVABLE_LINKS: 'WithUnresolvableLinksResponse',
WITHOUT_UNRESOLVABLE_LINKS: 'WithoutUnresolvableLinksResponse',
WITHOUT_LINK_RESOLUTION: 'WithoutLinkResolutionResponse',
WITH_ALL_LOCALES: 'WithAllLocalesResponse',
WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION: 'WithAllLocalesAndWithoutLinkResolutionResponse',
WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK:
'WithAllLocalesAndWithoutUnresolvableLinksResponse',
};

const LocaleWithDefaultTypeString = 'Locales extends LocaleCode = LocaleCode';

export class ResponseTypeRenderer extends BaseContentTypeRenderer {
public static readonly FILE_BASE_NAME = 'ResponseType';
protected readonly files: SourceFile[];

constructor() {
super();
this.files = [];
}

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

file.addImportDeclaration({
moduleSpecifier: 'contentful',
namedImports: ['Entry', 'EntrySkeletonType', 'FieldsType', 'LocaleCode'],
isTypeOnly: true,
});

file.addStatements('/* Utility types for response types */');

file.addTypeAlias({
name: `${ChainModifiers.WITH_UNRESOLVABLE_LINKS}<T extends EntrySkeletonType<FieldsType, string>>`,
type: `Entry<T>`,
isExported: true,
});

file.addTypeAlias({
name: `${ChainModifiers.WITH_ALL_LOCALES}<T extends EntrySkeletonType<FieldsType, string>, ${LocaleWithDefaultTypeString}>`,
type: `Entry<T, 'WITH_ALL_LOCALES', Locales>`,
isExported: true,
});

file.addTypeAlias({
name: `${ChainModifiers.WITHOUT_LINK_RESOLUTION}<T extends EntrySkeletonType<FieldsType>>`,
type: `Entry<T, 'WITHOUT_LINK_RESOLUTION'>`,
isExported: true,
});

file.addTypeAlias({
name: `${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}<T extends EntrySkeletonType<FieldsType>>`,
type: `Entry<T, 'WITHOUT_UNRESOLVABLE_LINKS'>`,
isExported: true,
});

file.addTypeAlias({
name: `${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}<T extends EntrySkeletonType<FieldsType, string>, ${LocaleWithDefaultTypeString}>`,
type: `Entry<T, 'WITH_ALL_LOCALES' | 'WITHOUT_UNRESOLVABLE_LINKS', Locales>`,
isExported: true,
});

file.addTypeAlias({
name: `${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}<T extends EntrySkeletonType<FieldsType>, ${LocaleWithDefaultTypeString}>`,
type: `Entry<T, 'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION', Locales>`,
isExported: true,
});

file.formatText();
this.files.push(file);
}

public render = (contentType: CFContentType, file: SourceFile): void => {
const context = this.createContext();

const entitySkeletonName = context.moduleSkeletonName(contentType.sys.id);
const entityName = context.moduleName(contentType.sys.id);

// file.addImportDeclaration({
// moduleSpecifier: `./${ResponseTypeRenderer.FILE_BASE_NAME}`,
// namedImports: Object.values(ChainModifiers),
// isTypeOnly: true,
// });

context.imports.add({
moduleSpecifier: `./${ResponseTypeRenderer.FILE_BASE_NAME}`,
namedImports: Object.values(ChainModifiers),
isTypeOnly: true,
});

// file.addImportDeclaration({
// moduleSpecifier: `./${context.moduleName(contentType.sys.id)}`,
// namedImports: [entitySkeletonName],
// isTypeOnly: true,
// });

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_UNRESOLVABLE_LINKS}`,
isExported: true,
type: renderTypeGeneric(ChainModifiers.WITH_UNRESOLVABLE_LINKS, entitySkeletonName),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITHOUT_LINK_RESOLUTION}`,
isExported: true,
type: renderTypeGeneric(ChainModifiers.WITHOUT_LINK_RESOLUTION, entitySkeletonName),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}`,
isExported: true,
type: renderTypeGeneric(ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS, entitySkeletonName),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES}<${LocaleWithDefaultTypeString}>`,
isExported: true,
type: renderTypeGeneric(ChainModifiers.WITH_ALL_LOCALES, entitySkeletonName, 'Locales'),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}<${LocaleWithDefaultTypeString}>`,
isExported: true,
type: renderTypeGeneric(
ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION,
entitySkeletonName,
'Locales',
),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}<${LocaleWithDefaultTypeString}>`,
isExported: true,
type: renderTypeGeneric(
ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK,
entitySkeletonName,
'Locales',
),
});

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

for (const structure of context.imports) {
file.addImportDeclaration(structure);
}

file.formatText();
};

public additionalFiles(): SourceFile[] {
return this.files;
}
}
91 changes: 91 additions & 0 deletions test/renderer/type/response-type-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Project, ScriptTarget, SourceFile } from 'ts-morph';
import { CFContentType } from '../../../src';
import { ResponseTypeRenderer } from '../../../src/renderer/type/response-type-renderer';
import stripIndent = require('strip-indent');

describe('A response type renderer class', () => {
let project: Project;
let testFile: SourceFile;

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

it('adds ResponseType.ts on setup', () => {
const renderer = new ResponseTypeRenderer();
renderer.setup(project);

const file = project.getSourceFile('ResponseType.ts');

// console.log(file?.getFullText());

expect(file?.getFullText()).toEqual(
stripIndent(
`
import type { Entry, EntrySkeletonType, FieldsType, LocaleCode } from "contentful";
/* Utility types for response types */
export type WithUnresolvableLinksResponse<T extends EntrySkeletonType<FieldsType, string>> = Entry<T>;
export type WithAllLocalesResponse<T extends EntrySkeletonType<FieldsType, string>, Locales extends LocaleCode = LocaleCode> = Entry<T, 'WITH_ALL_LOCALES', Locales>;
export type WithoutLinkResolutionResponse<T extends EntrySkeletonType<FieldsType>> = Entry<T, 'WITHOUT_LINK_RESOLUTION'>;
export type WithoutUnresolvableLinksResponse<T extends EntrySkeletonType<FieldsType>> = Entry<T, 'WITHOUT_UNRESOLVABLE_LINKS'>;
export type WithAllLocalesAndWithoutUnresolvableLinksResponse<T extends EntrySkeletonType<FieldsType, string>, Locales extends LocaleCode = LocaleCode> = Entry<T, 'WITH_ALL_LOCALES' | 'WITHOUT_UNRESOLVABLE_LINKS', Locales>;
export type WithAllLocalesAndWithoutLinkResolutionResponse<T extends EntrySkeletonType<FieldsType>, Locales extends LocaleCode = LocaleCode> = Entry<T, 'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION', Locales>;
`
.replace(/.*/, '')
.slice(1),
),
);
});

it('adds localized-entry.ts on setup', () => {
const renderer = new ResponseTypeRenderer();
renderer.setup(project);

const contentType: CFContentType = {
name: 'display name',
sys: {
id: 'test',
type: 'Symbol',
},
fields: [
{
id: 'field_id',
name: 'field_name',
disabled: false,
localized: false,
required: true,
type: 'Symbol',
omitted: false,
validations: [],
},
],
};

renderer.render(contentType, testFile);

expect(testFile.getFullText()).toEqual(
stripIndent(
`
import type { WithAllLocalesAndWithoutLinkResolutionResponse, WithAllLocalesAndWithoutUnresolvableLinksResponse, WithAllLocalesResponse, WithUnresolvableLinksResponse, WithoutLinkResolutionResponse, WithoutUnresolvableLinksResponse } from "./ResponseType";
import type { TypeTestSkeleton } from "./TypeTest";
export type TypeTestWithUnresolvableLinksResponse = WithUnresolvableLinksResponse<TypeTestSkeleton>;
export type TypeTestWithoutLinkResolutionResponse = WithoutLinkResolutionResponse<TypeTestSkeleton>;
export type TypeTestWithoutUnresolvableLinksResponse = WithoutUnresolvableLinksResponse<TypeTestSkeleton>;
export type TypeTestWithAllLocalesResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesResponse<TypeTestSkeleton, Locales>;
export type TypeTestWithAllLocalesAndWithoutLinkResolutionResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesAndWithoutLinkResolutionResponse<TypeTestSkeleton, Locales>;
export type TypeTestWithAllLocalesAndWithoutUnresolvableLinksResponse<Locales extends LocaleCode = LocaleCode> = WithAllLocalesAndWithoutUnresolvableLinksResponse<TypeTestSkeleton, Locales>;
`,
)
.replace(/.*/, '')
.slice(1),
);
});
});

0 comments on commit 06d0f8a

Please sign in to comment.