Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add const typing for Language Names #2554

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
6e557fe
standardise json formatting
inferrinizzard Mar 19, 2024
2648d2d
fix @types in deps
inferrinizzard Mar 30, 2024
a7cec76
add file association for schema files
inferrinizzard Mar 19, 2024
03e2f12
uses const type for All array
inferrinizzard Mar 30, 2024
dcf6d31
move python args to own constructor
inferrinizzard Mar 30, 2024
a834d2c
make constructor first in misc languages
inferrinizzard Mar 30, 2024
7101e24
add generics to TargetLanguage for names and extension
inferrinizzard Mar 30, 2024
b17d3f2
fix generics for JavaScript + subclassing
inferrinizzard Mar 30, 2024
c529c53
add strict types for language names and display names
inferrinizzard Mar 30, 2024
5ca879c
move language displayName, names, extension to class static members
inferrinizzard Apr 6, 2024
0822bab
add strict typing for language name
inferrinizzard Apr 6, 2024
1072369
Merge branch 'master' into feat/core/language-types
inferrinizzard Apr 6, 2024
36e8514
:recycle:
inferrinizzard Apr 6, 2024
3e5f345
Revert "move language displayName, names, extension to class static m…
inferrinizzard Apr 6, 2024
8d7c622
add generics to TargetLanguage for names and extension
inferrinizzard Mar 30, 2024
4acad79
fix generics for JavaScript + subclassing
inferrinizzard Mar 30, 2024
c4ce60f
:recycle:
inferrinizzard Apr 6, 2024
95beb5f
Merge branch 'feat/core/language-names' into feat/core/language-types
inferrinizzard Apr 6, 2024
6140704
Merge branch 'master' into chore/repo/eslint
inferrinizzard Apr 7, 2024
b2a7904
install eslint packages
inferrinizzard Apr 7, 2024
b97c2b2
add eslint config
inferrinizzard Apr 7, 2024
e504a8a
remove tslint
inferrinizzard Apr 7, 2024
92bd563
eslint autofix
inferrinizzard Apr 7, 2024
392838a
update eslint rules, eslintignore
inferrinizzard Apr 8, 2024
d9baf6e
add lint:fix script
inferrinizzard Apr 8, 2024
8bbabf1
update eslint rules, eslintignore
inferrinizzard Apr 8, 2024
f1d19aa
add lint:fix script
inferrinizzard Apr 8, 2024
994567e
add import rules
inferrinizzard Apr 8, 2024
d259874
add import rules
inferrinizzard Apr 8, 2024
291f5c9
update import rules
inferrinizzard Apr 12, 2024
03d8001
reduce excess style rules
inferrinizzard Apr 12, 2024
99dd240
downgrade remaining to warnings
inferrinizzard Apr 13, 2024
7f22254
fix enum values
inferrinizzard Apr 8, 2024
885b35a
add all missing accessibility modifiers
inferrinizzard Apr 11, 2024
f791721
fix nullish errors
inferrinizzard Apr 12, 2024
db9a9cf
update import rules
inferrinizzard Apr 12, 2024
3088be7
fix all require imports
inferrinizzard Apr 12, 2024
c492962
fix all imports
inferrinizzard Apr 12, 2024
a47609a
reduce excess style rules
inferrinizzard Apr 12, 2024
b536136
fix any types
inferrinizzard Apr 13, 2024
f0e7615
fix misc errors
inferrinizzard Apr 13, 2024
c0232f3
downgrade remaining to warnings
inferrinizzard Apr 13, 2024
59e877f
return types
inferrinizzard Apr 13, 2024
e5edac4
fix types errors
inferrinizzard Apr 13, 2024
0267ad0
Merge branch 'chore/repo/eslint' into chore/repo/eslint-fix
inferrinizzard Apr 13, 2024
130fe31
fix json import for test tsconfig
inferrinizzard Apr 13, 2024
37812f3
Merge branch 'master' into chore/repo/eslint
inferrinizzard Apr 14, 2024
e89f453
Merge branch 'chore/repo/eslint' into chore/repo/eslint-fix
inferrinizzard Apr 14, 2024
06002f9
auto lint fix
inferrinizzard Apr 14, 2024
93ab86e
fix lint errors in extension
inferrinizzard Apr 14, 2024
6a81618
Merge branch 'master' into chore/repo/eslint
inferrinizzard Apr 14, 2024
6ca4eaa
Merge branch 'chore/repo/eslint' into chore/repo/eslint-fix
inferrinizzard Apr 14, 2024
1ec5933
fix lint errors in Elixir
inferrinizzard Apr 14, 2024
4620ba5
make ref.pushElement public
inferrinizzard Apr 14, 2024
4fe1649
fix misc
inferrinizzard Apr 14, 2024
eed53c6
fix accidental public in CSharp raw text get
inferrinizzard Apr 14, 2024
10675c9
Merge branch 'master' into feat/core/language-types
inferrinizzard Apr 16, 2024
15fd452
use full generics for all languages to all extensibility
inferrinizzard Apr 16, 2024
6fd903c
add generics for elixir
inferrinizzard Apr 16, 2024
719a028
typescript 4.9 doesn't support const generics yet
inferrinizzard Apr 16, 2024
a0e399a
export LanguageName type and predicates
inferrinizzard Apr 16, 2024
45c06ce
fix type safety for extension
inferrinizzard Apr 16, 2024
beddba2
fix type safety for cli
inferrinizzard Apr 16, 2024
fb25f79
reduce target language generic to singular config object
inferrinizzard Apr 19, 2024
d0b8c15
update all non-inherited target languages to have external language c…
inferrinizzard Apr 20, 2024
410ed42
flatten all languages to extend from TargetLanguage
inferrinizzard Apr 20, 2024
1053a44
Merge branch 'refactor/core/no-language-inheritance' into feat/core/l…
inferrinizzard Apr 20, 2024
e542e87
Merge branch 'master' into feat/core/language-types
inferrinizzard Apr 20, 2024
a4989bf
Merge branch 'master' into chore/repo/eslint-fix
inferrinizzard Apr 30, 2024
8e6fc4f
fix new lint errors
inferrinizzard Apr 30, 2024
58cfc88
Merge branch 'chore/repo/eslint-fix' into base
inferrinizzard May 5, 2024
f16e6ad
Merge branch 'refactor/imports/languages'
inferrinizzard May 5, 2024
cacc55d
Merge branch 'base' into feat/core/language-types
inferrinizzard May 10, 2024
23e74d3
fix package-lock
inferrinizzard May 10, 2024
811ac4a
lint fixed
inferrinizzard May 10, 2024
b61fbe2
Merge branch 'master' into feat/core/language-types
inferrinizzard May 18, 2024
e0f9089
Merge branch 'master' into feat/core/language-types
inferrinizzard May 18, 2024
9b8908a
use LanguageName in cli index
inferrinizzard May 19, 2024
8555b84
fix type errors in test
inferrinizzard May 19, 2024
23c2c5f
add docs for creating custom languages and renderers
inferrinizzard May 31, 2024
aef27c2
update README
inferrinizzard May 31, 2024
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
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
Loading