Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
pull_request:
branches:
- master
- 'release/**'
- "release/**"
jobs:
build:
runs-on: ubuntu-22.04
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "tabWidth": 4, "printWidth": 120, "trailingComma": "none", "arrowParens": "avoid" }
{ "tabWidth": 4, "printWidth": 120, "trailingComma": "none", "arrowParens": "avoid", "quoteProps": "consistent" }
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ main();

The argument to `quicktype` is a complex object with many optional properties. [Explore its definition](https://github.com/quicktype/quicktype/blob/master/packages/quicktype-core/src/Run.ts#L637) to understand what options are allowed.

### Adding Custom logic or Rendering:

Quicktype supports creating your own custom languages and rendering output, you can extend existing classes or create your own to be using by the `quicktype function`.<br/>
Check out [this guide](./doc/CustomRenderer.md) for more info.

## Contributing

`quicktype` is [Open Source](LICENSE) and we love contributors! In fact, we have a [list of issues](https://github.com/quicktype/quicktype/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Ahelp-wanted) that are low-priority for us, but for which we'd happily accept contributions. Support for new target languages is also strongly desired. If you'd like to contribute, need help with anything at all, or would just like to talk things over, come [join us on Slack](http://slack.quicktype.io/).
Expand Down
145 changes: 145 additions & 0 deletions doc/CustomRenderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Extending quicktype functionality with a Custom Renderer

## quicktype Interface

To customise your rendering output, you can extend existing quicktype classes and override existing methods to achieve the behaviour you want.

This process requires 3 main steps:

1. [Extending a `Renderer` Class](#creating-a-custom-renderer)
2. [Wrapping your `Renderer` in a `TargetLanguage` Class](#creating-a-targetlanguage)
3. [Using your new classes in the `quicktype` function](#using-your-custom-language)
4. [Advanced Usage: Creating an entirely new Language](#creating-a-new-language)

## Creating a custom `Renderer`

Adding custom render logic for an existing language often involves extending a Renderer class and simply overriding or amending one of the `emit` methods:

```ts
// MyCustomRenderer.ts
import { CSharpRenderer } from "quicktype-core";

export class MyCustomRenderer extends CSharpRenderer {
// Add your custom logic here, feel free to reference the source code for how existing methods work
//
// ex.
protected superclassForType(t: Type): Sourcelike | undefined {
// if the type is a class, it should extend `GameObject` when rendered in C#
if (t instanceof ClassType) {
return "GameObject";
}
return undefined;
}
// See: http://blog.quicktype.io/customizing-quicktype/ for more context
}
```

## Creating a `TargetLanguage`

If you just want to change the rendering logic for an existing language, you can just extend an exported Language class (`CSharpTargetLanguage` in this example) and override the `makeRenderer` method:

```ts
// MyCustomLanguage.ts
import { CSharpTargetLanguage } from "quicktype-core";

import { MyCustomRenderer } from "./MyCustomRenderer";

export class MyCustomLanguage extends CSharpTargetLanguage {
// `makeRenderer` instantiates the Renderer class for the TargetLanguage
protected makeRenderer(
renderContext: RenderContext,
untypedOptionValues: Record<string, unknown>
): MyCustomRenderer {
// use your new custom renderer class here
return new MyCustomRenderer(this, renderContext, getOptionValues(cSharpOptions, untypedOptionValues));
}
}
```

## Using your custom Language

```ts
import { quicktype } from "quicktype-core";

import { MyCustomLanguage } from './MyCustomLanguage';

const lang = new MyCustomLanguage();

const lines = await quicktype({
lang: lang, // use your new TargetLanguage in the `lang` field here
...
});

console.log(lines);
```

## Creating a new Language

If none of the existing `quicktype` Language classes suit your needs, you can creating your own `TargetLanguge` and `Renderer` classes from scratch. If this satisfies your use cases for a language we don't currently support, please consider opening a PR with your new language and we'd love to take a look.

If you run into any issues, you can open a GitHub issue and we'll help you take a look.

### Creating a `TargetLanguage` from scratch

Instead of just extending an existing language, a new Language requires two additional steps:

- Defining the language config
- Adding any language-specific options

```ts
import { TargetLanguage, BooleanOption } from "quicktype-core";

// language config
const brandNewLanguageConfig = {
displayName: "Scratch", // these can be the same
names: ["scratch"], // these can be the same
extension: "sb" // the file extension that this language commonly has
} as const;

// language options
const brandNewLanguageOptions = {
allowFoo: new BooleanOption(
"allow-foo", // option name
"Allows Foo", // description
true // default value
)
// The default available Option classes are: StringOption, BooleanOption, EnumOption
// Please visit the source code for more examples and usage
};

class BrandNewLanguage extends TargetLanguage<typeof brandNewLanguageConfig> {
public constructor() {
super(brandNewLanguageConfig);
}

public getOptions(): typeof brandNewLanguageOptions {
return brandNewLanguageOptions;
}

protected makeRenderer(
renderContext: RenderContext,
untypedOptionValues: Record<string, unknown>
): BrandNewRenderer {
return new BrandNewRenderer(this, renderContext, getOptionValues(brandNewLanguageOptions, untypedOptionValues));
}
}
```

### Creating a `Renderer` from scratch

Creating a brand new `Renderer` class is very similar to extending an existing class:

```ts
export class BrandNewRenderer extends ConvenienceRenderer {
public constructor(targetLanguage: TargetLanguage, renderContext: RenderContext) {
super(targetLanguage, renderContext);
}

// Additional render methods go here
// Please reference existing Renderer classes and open a GitHub issue if you need help
}
```

## Links

Blog post with an older example: http://blog.quicktype.io/customizing-quicktype/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quicktype",
"version": "23.0.0",
"version": "23.1.0",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
33 changes: 16 additions & 17 deletions packages/quicktype-core/src/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export type ErrorProperties =
// TypeScript input
| { kind: "TypeScriptCompilerError"; properties: { message: string } };

export type ErrorKinds = ErrorProperties extends { kind: infer K } ? K : never;
export type ErrorKinds = ErrorProperties["kind"];

type ErrorMessages = { readonly [K in ErrorKinds]: string };

Expand Down Expand Up @@ -165,7 +165,7 @@ const errorMessages: ErrorMessages = {
TypeScriptCompilerError: "TypeScript error: ${message}"
};

export type ErrorPropertiesForName<K> =
export type ErrorPropertiesForKind<K extends ErrorKinds = ErrorKinds> =
Extract<ErrorProperties, { kind: K }> extends { properties: infer P } ? P : never;

export class QuickTypeError extends Error {
Expand All @@ -179,31 +179,30 @@ export class QuickTypeError extends Error {
}
}

export function messageError<N extends ErrorKinds>(kind: N, properties: ErrorPropertiesForName<N>): never {
export function messageError<Kind extends ErrorKinds>(kind: Kind, properties: ErrorPropertiesForKind<Kind>): never {
const message = errorMessages[kind];
let userMessage: string = message;
const propertiesMap = properties as StringMap;

for (const name of Object.getOwnPropertyNames(propertiesMap)) {
let value = propertiesMap[name];
if (typeof value === "object" && typeof value.toString === "function") {
value = value.toString();
} else if (typeof value.message === "string") {
value = value.message;

for (const [name, value] of Object.entries(properties as StringMap)) {
let valueString = "";
if (typeof value === "object" && typeof value?.toString === "function") {
valueString = value.toString();
} else if (typeof value?.message === "string") {
valueString = value.message;
} else if (typeof value !== "string") {
value = JSON.stringify(value);
valueString = JSON.stringify(value);
}

userMessage = userMessage.replace("${" + name + "}", value);
userMessage = userMessage.replace("${" + name + "}", valueString);
}

throw new QuickTypeError(message, kind, userMessage, propertiesMap);
throw new QuickTypeError(message, kind, userMessage, properties as StringMap);
}

export function messageAssert<N extends ErrorKinds>(
export function messageAssert<Kind extends ErrorKinds>(
assertion: boolean,
kind: N,
properties: ErrorPropertiesForName<N>
kind: Kind,
properties: ErrorPropertiesForKind<Kind>
): void {
if (assertion) return;
return messageError(kind, properties);
Expand Down
Loading
Loading