Skip to content

Commit

Permalink
feat: Add const typing for Language Names (#2554)
Browse files Browse the repository at this point in the history
* standardise json formatting

* fix @types in deps

* add file association for schema files

* uses const type for All array

* move python args to own constructor

* make constructor first in misc languages

* add generics to TargetLanguage for names and extension

* fix generics for JavaScript + subclassing

* add strict types for language names and display names

* move language displayName, names, extension to class static members

* add strict typing for language name

* ♻️

* Revert "move language displayName, names, extension to class static members"

This reverts commit 5ca879c.

* add generics to TargetLanguage for names and extension

* fix generics for JavaScript + subclassing

* ♻️

* install eslint packages

* add eslint config

* remove tslint

* eslint autofix

* update eslint rules, eslintignore

* add lint:fix script

* update eslint rules, eslintignore

* add lint:fix script

* add import rules

* add import rules

* update import rules

* reduce excess style rules

* downgrade remaining to warnings

* fix enum values

fixup! fix enum values

* add all missing accessibility modifiers

fixup! add all missing accessibility modifiers

fixup! add all missing accessibility modifiers

* fix nullish errors

* update import rules

* fix all require imports

* fix all imports

* reduce excess style rules

* fix any types

fixup! fix any types

fixup! fix any types

* fix misc errors

* downgrade remaining to warnings

* return types

* fix types errors

* fix json import for test tsconfig

* auto lint fix

* fix lint errors in extension

* fix lint errors in Elixir

* make ref.pushElement public

* fix misc

* fix accidental public in CSharp raw text get

* use full generics for all languages to all extensibility

* add generics for elixir

* typescript 4.9 doesn't support const generics yet

* export LanguageName type and predicates

* fix type safety for extension

* fix type safety for cli

* reduce target language generic to singular config object

* update all non-inherited target languages to have external language config

* flatten all languages to extend from TargetLanguage

* fix new lint errors

* Merge branch 'refactor/imports/languages'

* fix package-lock

* lint fixed

* use LanguageName in cli index

* fix type errors in test

* add docs for creating custom languages and renderers

* update README
  • Loading branch information
inferrinizzard authored Jun 1, 2024
1 parent 0a34f5b commit 15b652d
Show file tree
Hide file tree
Showing 40 changed files with 537 additions and 1,675 deletions.
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
147 changes: 147 additions & 0 deletions doc/CustomRenderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# 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);
}

protected getOptions(): Array<Option<any>> {
return [
brandNewLanguageOptions.allowFoo // list all options from the options config
];
}

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/
6 changes: 3 additions & 3 deletions packages/quicktype-core/src/Run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import { type MultiFileRenderResult, type TargetLanguage } from "./TargetLanguag
import { type TransformedStringTypeKind } from "./Type";
import { type StringTypeMapping, TypeBuilder } from "./TypeBuilder";
import { type TypeGraph, noneToAny, optionalToNullable, removeIndirectionIntersections } from "./TypeGraph";
import { type FixMeOptionsType } from "./types";
import { type FixMeOptionsType, type LanguageName } from "./types";

export function getTargetLanguage(nameOrInstance: string | TargetLanguage): TargetLanguage {
export function getTargetLanguage(nameOrInstance: LanguageName | TargetLanguage): TargetLanguage {
if (typeof nameOrInstance === "object") {
return nameOrInstance;
}
Expand Down Expand Up @@ -161,7 +161,7 @@ export interface NonInferenceOptions {
* or a string specifying one of the names for quicktype's built-in target languages. For example,
* both `cs` and `csharp` will generate C#.
*/
lang: string | TargetLanguage;
lang: LanguageName | TargetLanguage;
/** If given, output these comments at the beginning of the main output file */
leadingComments?: Comment[];
/** Don't render output. This is mainly useful for benchmarking. */
Expand Down
26 changes: 19 additions & 7 deletions packages/quicktype-core/src/TargetLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,24 @@ import { type FixMeOptionsAnyType, type FixMeOptionsType } from "./types";

export type MultiFileRenderResult = ReadonlyMap<string, SerializedRenderResult>;

export abstract class TargetLanguage {
public constructor(
public readonly displayName: string,
public readonly names: string[],
public readonly extension: string
) {}
export interface LanguageConfig {
readonly displayName: string;
readonly extension: string;
readonly names: readonly string[];
}

export abstract class TargetLanguage<Config extends LanguageConfig = LanguageConfig> {
public readonly displayName: Config["displayName"];

public readonly names: Config["names"];

public readonly extension: Config["extension"];

public constructor({ displayName, names, extension }: Config) {
this.displayName = displayName;
this.names = names;
this.extension = extension;
}

protected abstract getOptions(): Array<Option<FixMeOptionsAnyType>>;

Expand All @@ -38,7 +50,7 @@ export abstract class TargetLanguage {
return { actual, display };
}

public get name(): string {
public get name(): (typeof this.names)[0] {
return defined(this.names[0]);
}

Expand Down
6 changes: 2 additions & 4 deletions packages/quicktype-core/src/TypeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { EqualityMap, iterableFirst, setFilter, setSortBy, setUnion } from "collection-utils";

// eslint-disable-next-line import/no-cycle
import { type StringTypes, stringTypesTypeAttributeKind } from "./attributes/StringTypes";
import {
type CombinationKind,
type TypeAttributes,
combineTypeAttributes,
emptyTypeAttributes
} from "./attributes/TypeAttributes";
import { assert, assertNever, defined, panic } from "./support/Support";
// eslint-disable-next-line import/no-cycle
import {
ArrayType,
type ClassProperty,
Expand All @@ -21,9 +22,6 @@ import {
UnionType,
isPrimitiveStringTypeKind
} from "./Type";
// String types should be imported last to avoid circular dependency issues.
// eslint-disable-next-line import/order
import { type StringTypes, stringTypesTypeAttributeKind } from "./attributes/StringTypes";

export function assertIsObject(t: Type): ObjectType {
if (t instanceof ObjectType) {
Expand Down
3 changes: 2 additions & 1 deletion packages/quicktype-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export { Ref, type JSONSchemaType, type JSONSchemaAttributes } from "./input/JSO
export type { RenderContext } from "./Renderer";
export { Option, type OptionDefinition, getOptionValues, type OptionValues } from "./RendererOptions";
export { TargetLanguage, type MultiFileRenderResult } from "./TargetLanguage";
export { all as defaultTargetLanguages, languageNamed } from "./language/All";
export { all as defaultTargetLanguages, languageNamed, isLanguageName } from "./language/All";
export {
type MultiWord,
type Sourcelike,
Expand Down Expand Up @@ -83,5 +83,6 @@ export { StringTypes } from "./attributes/StringTypes";
export { removeNullFromUnion, matchType, nullableFromUnion } from "./TypeUtils";
export { ConvenienceRenderer } from "./ConvenienceRenderer";
export { uriTypeAttributeKind } from "./attributes/URIAttributes";
export { type LanguageName, type LanguageDisplayName } from "./types";

export * from "./language";
3 changes: 2 additions & 1 deletion packages/quicktype-core/src/input/Inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type RunContext } from "../Run";
import { defined, errorMessage, panic } from "../support/Support";
import { type TargetLanguage } from "../TargetLanguage";
import { type TypeBuilder } from "../TypeBuilder";
import { type LanguageName } from "../types";

import { type CompressedJSON, CompressedJSONFromString, type Value } from "./CompressedJSON";
import { TypeInference } from "./Inference";
Expand Down Expand Up @@ -152,7 +153,7 @@ export class JSONInput<T> implements Input<JSONSourceData<T>> {
}

export function jsonInputForTargetLanguage(
targetLanguage: string | TargetLanguage,
targetLanguage: LanguageName | TargetLanguage,
languages?: TargetLanguage[],
handleJSONRefs = false
): JSONInput<string> {
Expand Down
41 changes: 30 additions & 11 deletions packages/quicktype-core/src/language/All.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { iterableFind } from "collection-utils";

import { type TargetLanguage } from "../TargetLanguage";
import { type LanguageDisplayName, type LanguageName, type LanguageNameMap } from "../types";

import { CJSONTargetLanguage } from "./CJSON";
import { CPlusPlusTargetLanguage } from "./CPlusPlus";
Expand Down Expand Up @@ -30,7 +29,7 @@ import { TypeScriptEffectSchemaTargetLanguage } from "./TypeScriptEffectSchema";
import { FlowTargetLanguage, TypeScriptTargetLanguage } from "./TypeScriptFlow";
import { TypeScriptZodTargetLanguage } from "./TypeScriptZod";

export const all: TargetLanguage[] = [
export const all = [
new CJSONTargetLanguage(),
new CPlusPlusTargetLanguage(),
new CrystalTargetLanguage(),
Expand All @@ -49,7 +48,7 @@ export const all: TargetLanguage[] = [
new ObjectiveCTargetLanguage(),
new PhpTargetLanguage(),
new PikeTargetLanguage(),
new PythonTargetLanguage("Python", ["python", "py"], "py"),
new PythonTargetLanguage(),
new RubyTargetLanguage(),
new RustTargetLanguage(),
new Scala3TargetLanguage(),
Expand All @@ -58,14 +57,34 @@ export const all: TargetLanguage[] = [
new TypeScriptTargetLanguage(),
new TypeScriptEffectSchemaTargetLanguage(),
new TypeScriptZodTargetLanguage()
];
] as const;

all satisfies readonly TargetLanguage[];

export function languageNamed<Name extends LanguageName>(
name: Name,
targetLanguages: readonly TargetLanguage[] = all
): LanguageNameMap[Name] {
const foundLanguage = targetLanguages.find(language => language.names.includes(name));
if (!foundLanguage) {
throw new Error(`Unknown language name: ${name}`);
}

return foundLanguage as LanguageNameMap[Name];
}

export function isLanguageName(maybeName: string): maybeName is LanguageName {
if (all.some(lang => (lang.names as readonly string[]).includes(maybeName))) {
return true;
}

return false;
}

export function languageNamed(name: string, targetLanguages?: TargetLanguage[]): TargetLanguage | undefined {
if (targetLanguages === undefined) {
targetLanguages = all;
export function isLanguageDisplayName(maybeName: string): maybeName is LanguageDisplayName {
if (all.some(lang => lang.displayName === maybeName)) {
return true;
}

const maybeTargetLanguage = iterableFind(targetLanguages, l => l.names.includes(name) || l.displayName === name);
if (maybeTargetLanguage !== undefined) return maybeTargetLanguage;
return iterableFind(targetLanguages, l => l.extension === name);
return false;
}
18 changes: 9 additions & 9 deletions packages/quicktype-core/src/language/CJSON/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ export const cJSONOptions = {
};

/* cJSON generator target language */
export class CJSONTargetLanguage extends TargetLanguage {
/**
* Constructor
* @param displayName: display name
* @params names: names
* @param extension: extension of files
*/
public constructor(displayName = "C (cJSON)", names: string[] = ["cjson", "cJSON"], extension = "h") {
super(displayName, names, extension);
export const cJSONLanguageConfig = {
displayName: "C (cJSON)",
names: ["cjson", "cJSON"],
extension: "h"
} as const;

export class CJSONTargetLanguage extends TargetLanguage<typeof cJSONLanguageConfig> {
public constructor() {
super(cJSONLanguageConfig);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions packages/quicktype-core/src/language/CPlusPlus/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,15 @@ export const cPlusPlusOptions = {
hideNullOptional: new BooleanOption("hide-null-optional", "Hide null value for optional field", false)
};

export class CPlusPlusTargetLanguage extends TargetLanguage {
public constructor(displayName = "C++", names: string[] = ["c++", "cpp", "cplusplus"], extension = "cpp") {
super(displayName, names, extension);
export const cPlusPlusLanguageConfig = {
displayName: "C++",
names: ["c++", "cpp", "cplusplus"],
extension: "cpp"
} as const;

export class CPlusPlusTargetLanguage extends TargetLanguage<typeof cPlusPlusLanguageConfig> {
public constructor() {
super(cPlusPlusLanguageConfig);
}

protected getOptions(): Array<Option<FixMeOptionsAnyType>> {
Expand Down
Loading

0 comments on commit 15b652d

Please sign in to comment.