Skip to content

Commit

Permalink
feat: add response type renderer (#326)
Browse files Browse the repository at this point in the history
* feat: add response type renderer

* Update README.md

Co-authored-by: Rebecca König <[email protected]>

---------

Co-authored-by: Rebecca König <[email protected]>
  • Loading branch information
marcolink and veu authored Feb 13, 2024
1 parent c70c2a6 commit 983b552
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 14 deletions.
88 changes: 81 additions & 7 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 @@ -397,7 +398,11 @@ Adds type guard functions for every content type
#### Example Usage

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

const builder = new CFDefinitionsBuilder([
new DefaultContentTypeRenderer(),
Expand Down Expand Up @@ -429,7 +434,11 @@ Adds type guard functions for every content type which are compatible with conte
#### Example Usage

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

const builder = new CFDefinitionsBuilder([
new V10ContentTypeRenderer(),
Expand All @@ -440,20 +449,85 @@ const builder = new CFDefinitionsBuilder([
#### Example output

```typescript
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from "contentful";
import type {
ChainModifiers,
Entry,
EntryFieldTypes,
EntrySkeletonType,
LocaleCode,
} from 'contentful';

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 TypeAnimalSkeleton = EntrySkeletonType<TypeAnimalFields, 'animal'>;
export type TypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<
TypeAnimalSkeleton,
Modifiers,
Locales
>;

export function isTypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode>(entry: Entry<EntrySkeletonType, Modifiers, Locales>): entry is TypeAnimal<Modifiers, Locales> {
return entry.sys.contentType.sys.id === 'animal'
export function isTypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode>(
entry: Entry<EntrySkeletonType, Modifiers, Locales>,
): entry is TypeAnimal<Modifiers, Locales> {
return entry.sys.contentType.sys.id === 'animal';
}
```

## 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';

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 TypeAnimalWithoutLinkResolutionResponse = TypeAnimal<'WITHOUT_LINK_RESOLUTION'>;
export type TypeAnimalWithoutUnresolvableLinksResponse = TypeAnimal<'WITHOUT_UNRESOLVABLE_LINKS'>;
export type TypeAnimalWithAllLocalesResponse<Locales extends LocaleCode = LocaleCode> =
TypeAnimal<'WITH_ALL_LOCALES'>;
export type TypeAnimalWithAllLocalesAndWithoutLinkResolutionResponse<
Locales extends LocaleCode = LocaleCode,
> = TypeAnimal<'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION', Locales>;
export type TypeAnimalWithAllLocalesAndWithoutUnresolvableLinksResponse<
Locales extends LocaleCode = LocaleCode,
> = TypeAnimal<'WITH_ALL_LOCALES' | 'WITHOUT_UNRESOLVABLE_LINKS', 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
6 changes: 3 additions & 3 deletions bin/dev
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
(async () => {
const oclif = await import('@oclif/core')
await oclif.execute({type: 'cjs', development: true, dir: __dirname})
})()
const oclif = await import('@oclif/core');
await oclif.execute({ type: 'cjs', development: true, dir: __dirname });
})();
4 changes: 2 additions & 2 deletions bin/run
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node

const oclif = require('@oclif/core')
const oclif = require('@oclif/core');

oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'));
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
87 changes: 87 additions & 0 deletions src/renderer/type/response-type-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { SourceFile } from 'ts-morph';
import { CFContentType } from '../../types';
import { renderTypeGeneric, renderTypeLiteral, renderTypeUnion } 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 = {
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 render = (contentType: CFContentType, file: SourceFile): void => {
const context = this.createContext();

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

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITHOUT_LINK_RESOLUTION}`,
isExported: true,
type: renderTypeGeneric(
entityName,
renderTypeUnion([renderTypeLiteral('WITHOUT_LINK_RESOLUTION')]),
),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}`,
isExported: true,
type: renderTypeGeneric(
entityName,
renderTypeUnion([renderTypeLiteral('WITHOUT_UNRESOLVABLE_LINKS')]),
),
});

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

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}<${LocaleWithDefaultTypeString}>`,
isExported: true,
type: renderTypeGeneric(
entityName,
renderTypeUnion([
renderTypeLiteral('WITH_ALL_LOCALES'),
renderTypeLiteral('WITHOUT_LINK_RESOLUTION'),
]),
'Locales',
),
});

file.addTypeAlias({
name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}<${LocaleWithDefaultTypeString}>`,
isExported: true,
type: renderTypeGeneric(
entityName,
renderTypeUnion([
renderTypeLiteral('WITH_ALL_LOCALES'),
renderTypeLiteral('WITHOUT_UNRESOLVABLE_LINKS'),
]),
'Locales',
),
});

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

file.formatText();
};
}
3 changes: 2 additions & 1 deletion test/renderer/type/localized-content-type-renfderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ describe('A localized content type renderer class', () => {
: EntryType[Key]
};
`
.replace(/.*/, '').slice(1),
.replace(/.*/, '')
.slice(1),
),
);
});
Expand Down
61 changes: 61 additions & 0 deletions test/renderer/type/response-type-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 response types', () => {
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(
`
export type TypeTestWithoutLinkResolutionResponse = TypeTest<"WITHOUT_LINK_RESOLUTION">;
export type TypeTestWithoutUnresolvableLinksResponse = TypeTest<"WITHOUT_UNRESOLVABLE_LINKS">;
export type TypeTestWithAllLocalesResponse<Locales extends LocaleCode = LocaleCode> = TypeTest<"WITH_ALL_LOCALES", Locales>;
export type TypeTestWithAllLocalesAndWithoutLinkResolutionResponse<Locales extends LocaleCode = LocaleCode> = TypeTest<"WITHOUT_LINK_RESOLUTION" | "WITH_ALL_LOCALES", Locales>;
export type TypeTestWithAllLocalesAndWithoutUnresolvableLinksResponse<Locales extends LocaleCode = LocaleCode> = TypeTest<"WITHOUT_UNRESOLVABLE_LINKS" | "WITH_ALL_LOCALES", Locales>;
`,
)
.replace(/.*/, '')
.slice(1),
);
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"outDir": "lib",
"strict": true,
"target": "esnext",
"allowSyntheticDefaultImports": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["**/*.test.ts", "lib", "test"]
Expand Down

0 comments on commit 983b552

Please sign in to comment.