diff --git a/README.md b/README.md index 4cb32f8e..85f686e0 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Options: --help Show help [boolean] --packageVersion The version of the generated package [string] --packageName The name of the generated package [string] + --brandedAliases Generates strongly branded types for compatible aliases. [boolean] [default: false] --flavorizedAliases Generates flavoured types for compatible aliases. [boolean] [default: false] --nodeCompatibleModules Generate node compatible javascript [boolean] [default: false] --rawSource Generate raw source without any package metadata [boolean] [default: false] @@ -109,7 +110,61 @@ We also consider the command line interface and feature flags to be public API. - **Conjure alias** - TypeScript uses structural (duck-typing) so aliases are currently elided. + By default alias types are not generated and the underlying data type is use instead. + However typed Aliases can be generated with either branded or flavored nominal types, with a tradeoff between the two. + + Branded aliases (via `--brandedAliases`) are stronger typed and require type assertions for more intentional usage, similar to the conjure-java generator. + + ```typescript + // generated from conjure: + // FooId: + // alias: string + export type IFooId = string & { + __conjure_type: "FooId", + __conjure_package: "com.palantir.product", + }; + // generated from conjure: + // BarId: + // alias: string + export type IBarId = string & { + __conjure_type: "BarId", + __conjure_package: "com.palantir.product", + }; + + let foo: IFooId; + foo = "foo"; // compile error + foo = "foo" as IFooId; // explicit type assertion ok + + const bar: IBarId = value; // compile error, different alias types do not mix + + const id: string = foo; // ok, can set string value from alias + const bar2: IBarId = id; // compile error, cannot set alias type from string without type assertion. + ``` + + Flavored types (via `--flavorizedAliases`) protect only between aliases but still allow implicit casting to & from the base type. + + ```typescript + // generated from conjure: + // FooId: + // alias: string + export type IFooId = string & { + __conjure_type?: "FooId", + __conjure_package?: "com.palantir.product", + }; + // generated from conjure: + // BarId: + // alias: string + export type IBarId = string & { + __conjure_type?: "BarId", + __conjure_package?: "com.palantir.product", + }; + + const foo: IFooId = "foo"; // ok to set from string + const bar: IBarId = foo; // compile error, different alias types do not mix + + const id: string = foo; // ok, can set string value from alias + const bar2: IBarId = id; // ok, but no protection from string back to alias type this may mask an unintentional bug depending on usage. + ``` ## Example Client interfaces diff --git a/changelog/@unreleased/pr-225.v2.yml b/changelog/@unreleased/pr-225.v2.yml new file mode 100644 index 00000000..3e09eaf3 --- /dev/null +++ b/changelog/@unreleased/pr-225.v2.yml @@ -0,0 +1,5 @@ +type: feature +feature: + description: Add brandedAliases option for strongly branded aliases + links: + - https://github.com/palantir/conjure-typescript/pull/225 diff --git a/src/commands/generate/__tests__/generatorTest.ts b/src/commands/generate/__tests__/generatorTest.ts index 2cb559af..754e9dd3 100644 --- a/src/commands/generate/__tests__/generatorTest.ts +++ b/src/commands/generate/__tests__/generatorTest.ts @@ -33,7 +33,7 @@ import { generate } from "../generator"; import { typeNameToFilePath } from "../simpleAst"; import { ITypeGenerationFlags } from "../typeGenerationFlags"; import { isFlavorizable } from "../utils"; -import { DEFAULT_TYPE_GENERATION_FLAGS, FLAVORED_TYPE_GENERATION_FLAGS, READONLY_TYPE_GENERATION_FLAGS } from "./resources/constants"; +import { BRANDED_TYPE_GENERATION_FLAGS, DEFAULT_TYPE_GENERATION_FLAGS, FLAVORED_TYPE_GENERATION_FLAGS, READONLY_TYPE_GENERATION_FLAGS } from "./resources/constants"; import { assertOutputAndExpectedAreEqual } from "./testTypesGeneratorTest"; describe("generator", () => { @@ -106,6 +106,7 @@ export { integrationSecond }; const irDir = path.join(__dirname, "../../../../build/ir-test-cases"); const testCaseDir = path.join(__dirname, "resources/test-cases"); +const brandedTestCaseDir = path.join(__dirname, "resources/branded-test-cases"); const flavoredTestCaseDir = path.join(__dirname, "resources/flavored-test-cases"); const readonlyTestCaseDir = path.join(__dirname, "resources/readonly-test-cases"); @@ -114,11 +115,17 @@ describe("definitionTests", () => { const definitionFilePath = path.join(irDir, fileName); const paths = fileName.substring(0, fileName.lastIndexOf(".")); const actualTestCaseDir = path.join(testCaseDir, paths); + const actualBrandedTestCaseDir = path.join(brandedTestCaseDir, paths); const actualFlavoredTestCaseDir = path.join(flavoredTestCaseDir, paths); const actualReadonlyTestCaseDir = path.join(readonlyTestCaseDir, paths); it(`${fileName} produces equivalent TypeScript`, testGenerateAllFilesAreTheSame(definitionFilePath, paths, actualTestCaseDir, DEFAULT_TYPE_GENERATION_FLAGS)); + // Not every test has a branded version + if (fs.existsSync(actualBrandedTestCaseDir)) { + it(`${fileName} produces equivalent branded TypeScript`, testGenerateAllFilesAreTheSame(definitionFilePath, paths, actualBrandedTestCaseDir, BRANDED_TYPE_GENERATION_FLAGS)); + } + // Not every test has a flavored version if (fs.existsSync(actualFlavoredTestCaseDir)) { it(`${fileName} produces equivalent flavored TypeScript`, testGenerateAllFilesAreTheSame(definitionFilePath, paths, actualFlavoredTestCaseDir, FLAVORED_TYPE_GENERATION_FLAGS)); @@ -152,7 +159,7 @@ function expectAllFilesAreTheSame( ) { for (const type of definition.types) { // We do not generate flavoured types for all aliases - if (type.type === "alias" && !isFlavorizable(type.alias.alias, typeGenerationFlags.flavorizedAliases)) { + if (type.type === "alias" && !isFlavorizable(type.alias.alias, typeGenerationFlags.aliases)) { continue; } const relativeFilePath = typeNameToFilePath(ITypeDefinition.visit(type, typeNameVisitor)); diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-service/another/testService.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/another/testService.ts new file mode 100644 index 00000000..625b2e1b --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/another/testService.ts @@ -0,0 +1,406 @@ +import { IBackingFileSystem } from "../product-datasets/backingFileSystem"; +import { IDataset } from "../product-datasets/dataset"; +import { IAliasedString } from "../product/aliasedString"; +import { ICreateDatasetRequest } from "../product/createDatasetRequest"; +import { IHttpApiBridge } from "conjure-client"; + +/** + * Constant reference to `undefined` that we expect to get minified and therefore reduce total code size + */ +const __undefined: undefined = undefined; + +/** + * A Markdown description of the service. + * + */ +export interface ITestService { + /** + * Returns a mapping from file system id to backing file system configuration. + * + */ + getFileSystems(): Promise<{ [key: string]: IBackingFileSystem }>; + createDataset(request: ICreateDatasetRequest, testHeaderArg: string): Promise; + getDataset(datasetRid: string): Promise; + getRawData(datasetRid: string): Promise>; + getAliasedRawData(datasetRid: string): Promise>; + maybeGetRawData(datasetRid: string): Promise | null>; + getAliasedString(datasetRid: string): Promise; + uploadRawData(input: ReadableStream | BufferSource | Blob): Promise; + uploadAliasedRawData(input: ReadableStream | BufferSource | Blob): Promise; + getBranches(datasetRid: string): Promise>; + /** + * Gets all branches of this dataset. + * + * @deprecated use getBranches instead + */ + getBranchesDeprecated(datasetRid: string): Promise>; + resolveBranch(datasetRid: string, branch: string): Promise; + testParam(datasetRid: string): Promise; + testQueryParams(query: string, something: string, implicit: string, setEnd: Array, optionalMiddle?: string | null, optionalEnd?: string | null): Promise; + testNoResponseQueryParams(query: string, something: string, implicit: string, setEnd: Array, optionalMiddle?: string | null, optionalEnd?: string | null): Promise; + testBoolean(): Promise; + testDouble(): Promise; + testInteger(): Promise; + testPostOptional(maybeString?: string | null): Promise; + testOptionalIntegerAndDouble(maybeInteger?: number | null, maybeDouble?: number | "NaN" | null): Promise; +} + +export class TestService { + constructor(private bridge: IHttpApiBridge) { + } + + /** + * Returns a mapping from file system id to backing file system configuration. + * + */ + public getFileSystems(): Promise<{ [key: string]: IBackingFileSystem }> { + return this.bridge.call<{ [key: string]: IBackingFileSystem }>( + "TestService", + "getFileSystems", + "GET", + "/catalog/fileSystems", + __undefined, + __undefined, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public createDataset(request: ICreateDatasetRequest, testHeaderArg: string): Promise { + return this.bridge.call( + "TestService", + "createDataset", + "POST", + "/catalog/datasets", + request, + { + "Test-Header": testHeaderArg, + }, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public getDataset(datasetRid: string): Promise { + return this.bridge.call( + "TestService", + "getDataset", + "GET", + "/catalog/datasets/{datasetRid}", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + __undefined + ); + } + + public getRawData(datasetRid: string): Promise> { + return this.bridge.call>( + "TestService", + "getRawData", + "GET", + "/catalog/datasets/{datasetRid}/raw", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + "application/octet-stream" + ); + } + + public getAliasedRawData(datasetRid: string): Promise> { + return this.bridge.call>( + "TestService", + "getAliasedRawData", + "GET", + "/catalog/datasets/{datasetRid}/raw-aliased", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + "application/octet-stream" + ); + } + + public maybeGetRawData(datasetRid: string): Promise | null> { + return this.bridge.call | null>( + "TestService", + "maybeGetRawData", + "GET", + "/catalog/datasets/{datasetRid}/raw-maybe", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + "application/octet-stream" + ); + } + + public getAliasedString(datasetRid: string): Promise { + return this.bridge.call( + "TestService", + "getAliasedString", + "GET", + "/catalog/datasets/{datasetRid}/string-aliased", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + __undefined + ); + } + + public uploadRawData(input: ReadableStream | BufferSource | Blob): Promise { + return this.bridge.call( + "TestService", + "uploadRawData", + "POST", + "/catalog/datasets/upload-raw", + input, + __undefined, + __undefined, + __undefined, + "application/octet-stream", + __undefined + ); + } + + public uploadAliasedRawData(input: ReadableStream | BufferSource | Blob): Promise { + return this.bridge.call( + "TestService", + "uploadAliasedRawData", + "POST", + "/catalog/datasets/upload-raw-aliased", + input, + __undefined, + __undefined, + __undefined, + "application/octet-stream", + __undefined + ); + } + + public getBranches(datasetRid: string): Promise> { + return this.bridge.call>( + "TestService", + "getBranches", + "GET", + "/catalog/datasets/{datasetRid}/branches", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + __undefined + ); + } + + /** + * Gets all branches of this dataset. + * + * @deprecated use getBranches instead + */ + public getBranchesDeprecated(datasetRid: string): Promise> { + return this.bridge.call>( + "TestService", + "getBranchesDeprecated", + "GET", + "/catalog/datasets/{datasetRid}/branchesDeprecated", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + __undefined + ); + } + + public resolveBranch(datasetRid: string, branch: string): Promise { + return this.bridge.call( + "TestService", + "resolveBranch", + "GET", + "/catalog/datasets/{datasetRid}/branches/{branch:.+}/resolve", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + + branch, + ], + __undefined, + __undefined + ); + } + + public testParam(datasetRid: string): Promise { + return this.bridge.call( + "TestService", + "testParam", + "GET", + "/catalog/datasets/{datasetRid}/testParam", + __undefined, + __undefined, + __undefined, + [ + datasetRid, + ], + __undefined, + __undefined + ); + } + + public testQueryParams(query: string, something: string, implicit: string, setEnd: Array, optionalMiddle?: string | null, optionalEnd?: string | null): Promise { + return this.bridge.call( + "TestService", + "testQueryParams", + "POST", + "/catalog/test-query-params", + query, + __undefined, + { + "different": something, + + "implicit": implicit, + + "setEnd": setEnd, + + "optionalMiddle": optionalMiddle, + + "optionalEnd": optionalEnd, + }, + __undefined, + __undefined, + __undefined + ); + } + + public testNoResponseQueryParams(query: string, something: string, implicit: string, setEnd: Array, optionalMiddle?: string | null, optionalEnd?: string | null): Promise { + return this.bridge.call( + "TestService", + "testNoResponseQueryParams", + "POST", + "/catalog/test-no-response-query-params", + query, + __undefined, + { + "different": something, + + "implicit": implicit, + + "setEnd": setEnd, + + "optionalMiddle": optionalMiddle, + + "optionalEnd": optionalEnd, + }, + __undefined, + __undefined, + __undefined + ); + } + + public testBoolean(): Promise { + return this.bridge.call( + "TestService", + "testBoolean", + "GET", + "/catalog/boolean", + __undefined, + __undefined, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public testDouble(): Promise { + return this.bridge.call( + "TestService", + "testDouble", + "GET", + "/catalog/double", + __undefined, + __undefined, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public testInteger(): Promise { + return this.bridge.call( + "TestService", + "testInteger", + "GET", + "/catalog/integer", + __undefined, + __undefined, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public testPostOptional(maybeString?: string | null): Promise { + return this.bridge.call( + "TestService", + "testPostOptional", + "POST", + "/catalog/optional", + maybeString, + __undefined, + __undefined, + __undefined, + __undefined, + __undefined + ); + } + + public testOptionalIntegerAndDouble(maybeInteger?: number | null, maybeDouble?: number | "NaN" | null): Promise { + return this.bridge.call( + "TestService", + "testOptionalIntegerAndDouble", + "GET", + "/catalog/optional-integer-double", + __undefined, + __undefined, + { + "maybeInteger": maybeInteger, + + "maybeDouble": maybeDouble, + }, + __undefined, + __undefined, + __undefined + ); + } +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/backingFileSystem.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/backingFileSystem.ts new file mode 100644 index 00000000..21e74ea9 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/backingFileSystem.ts @@ -0,0 +1,8 @@ +export interface IBackingFileSystem { + /** + * The name by which this file system is identified. + */ + 'fileSystemId': string; + 'baseUri': string; + 'configuration': { [key: string]: string }; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/dataset.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/dataset.ts new file mode 100644 index 00000000..e034e968 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product-datasets/dataset.ts @@ -0,0 +1,7 @@ +export interface IDataset { + 'fileSystemId': string; + /** + * Uniquely identifies this dataset. + */ + 'rid': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/aliasedString.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/aliasedString.ts new file mode 100644 index 00000000..1c70d376 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/aliasedString.ts @@ -0,0 +1,4 @@ +export type IAliasedString = string & { + __conjure_type: "AliasedString", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/createDatasetRequest.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/createDatasetRequest.ts new file mode 100644 index 00000000..0e619a80 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-service/product/createDatasetRequest.ts @@ -0,0 +1,4 @@ +export interface ICreateDatasetRequest { + 'fileSystemId': string; + 'path': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/aliasAsMapKeyExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/aliasAsMapKeyExample.ts new file mode 100644 index 00000000..65508591 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/aliasAsMapKeyExample.ts @@ -0,0 +1,17 @@ +import { IBearerTokenAliasExample } from "./bearerTokenAliasExample"; +import { IIntegerAliasExample } from "./integerAliasExample"; +import { IManyFieldExample } from "./manyFieldExample"; +import { IRidAliasExample } from "./ridAliasExample"; +import { ISafeLongAliasExample } from "./safeLongAliasExample"; +import { IStringAliasExample } from "./stringAliasExample"; +import { IUuidAliasExample } from "./uuidAliasExample"; + +export interface IAliasAsMapKeyExample { + 'strings': { [key: IStringAliasExample]: IManyFieldExample }; + 'rids': { [key: IRidAliasExample]: IManyFieldExample }; + 'bearertokens': { [key: IBearerTokenAliasExample]: IManyFieldExample }; + 'integers': { [key: IIntegerAliasExample]: IManyFieldExample }; + 'safelongs': { [key: ISafeLongAliasExample]: IManyFieldExample }; + 'datetimes': { [key: string]: IManyFieldExample }; + 'uuids': { [key: IUuidAliasExample]: IManyFieldExample }; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyExample.ts new file mode 100644 index 00000000..23b20795 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyExample.ts @@ -0,0 +1,3 @@ +export interface IAnyExample { + 'any': any; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyMapExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyMapExample.ts new file mode 100644 index 00000000..70bc2be7 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/anyMapExample.ts @@ -0,0 +1,3 @@ +export interface IAnyMapExample { + 'items': { [key: string]: any }; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerAliasExample.ts new file mode 100644 index 00000000..320e7dec --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerAliasExample.ts @@ -0,0 +1,4 @@ +export type IBearerAliasExample = string & { + __conjure_type: "BearerAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenAliasExample.ts new file mode 100644 index 00000000..526fb3a1 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenAliasExample.ts @@ -0,0 +1,4 @@ +export type IBearerTokenAliasExample = string & { + __conjure_type: "BearerTokenAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenExample.ts new file mode 100644 index 00000000..59dd2368 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/bearerTokenExample.ts @@ -0,0 +1,3 @@ +export interface IBearerTokenExample { + 'bearerTokenValue': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/binaryExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/binaryExample.ts new file mode 100644 index 00000000..0c7b09a6 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/binaryExample.ts @@ -0,0 +1,3 @@ +export interface IBinaryExample { + 'binary': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanAliasExample.ts new file mode 100644 index 00000000..725808a8 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanAliasExample.ts @@ -0,0 +1,4 @@ +export type IBooleanAliasExample = boolean & { + __conjure_type: "BooleanAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanExample.ts new file mode 100644 index 00000000..93692402 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/booleanExample.ts @@ -0,0 +1,3 @@ +export interface IBooleanExample { + 'coin': boolean; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantListExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantListExample.ts new file mode 100644 index 00000000..575429ed --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantListExample.ts @@ -0,0 +1,4 @@ +export interface ICovariantListExample { + 'items': Array; + 'externalItems': Array; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantOptionalExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantOptionalExample.ts new file mode 100644 index 00000000..324d1fe0 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/covariantOptionalExample.ts @@ -0,0 +1,3 @@ +export interface ICovariantOptionalExample { + 'item'?: any | null; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/dateTimeExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/dateTimeExample.ts new file mode 100644 index 00000000..472c9a4a --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/dateTimeExample.ts @@ -0,0 +1,3 @@ +export interface IDateTimeExample { + 'datetime': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedEnumExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedEnumExample.ts new file mode 100644 index 00000000..e98dfee7 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedEnumExample.ts @@ -0,0 +1,27 @@ +export namespace DeprecatedEnumExample { + export type ONE = "ONE"; + /** + * @deprecated use ONE + */ + export type OLD_ONE = "OLD_ONE"; + /** + * You should no longer use this + * + * @deprecated use ONE + */ + export type OLD_DEPRECATED_ONE = "OLD_DEPRECATED_ONE"; + /** + * You should no longer use this + * + * @deprecated should use ONE + * + */ + export type OLD_DOCUMENTED_ONE = "OLD_DOCUMENTED_ONE"; + + export const ONE = "ONE" as "ONE"; + export const OLD_ONE = "OLD_ONE" as "OLD_ONE"; + export const OLD_DEPRECATED_ONE = "OLD_DEPRECATED_ONE" as "OLD_DEPRECATED_ONE"; + export const OLD_DOCUMENTED_ONE = "OLD_DOCUMENTED_ONE" as "OLD_DOCUMENTED_ONE"; +} + +export type DeprecatedEnumExample = keyof typeof DeprecatedEnumExample; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedFieldExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedFieldExample.ts new file mode 100644 index 00000000..78c7c774 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedFieldExample.ts @@ -0,0 +1,20 @@ +export interface IDeprecatedFieldExample { + 'one': string; + /** + * @deprecated use ONE + */ + 'deprecatedOne': string; + /** + * You should no longer use this + * + * @deprecated use ONE + */ + 'documentedDeprecatedOne': string; + /** + * You should no longer use this + * + * @deprecated should use ONE + * + */ + 'deprecatedWithinDocumentOne': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedUnion.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedUnion.ts new file mode 100644 index 00000000..77151310 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/deprecatedUnion.ts @@ -0,0 +1,92 @@ +export interface IDeprecatedUnion_Good { + 'good': string; + 'type': "good"; +} + +/** + * @deprecated use good + */ +export interface IDeprecatedUnion_NoGood { + 'noGood': string; + 'type': "noGood"; +} + +/** + * this is no good + * @deprecated use good + */ +export interface IDeprecatedUnion_NoGoodDoc { + 'noGoodDoc': string; + 'type': "noGoodDoc"; +} + +function isGood(obj: IDeprecatedUnion): obj is IDeprecatedUnion_Good { + return (obj.type === "good"); +} + +function good(obj: string): IDeprecatedUnion_Good { + return { + good: obj, + type: "good", + }; +} + +function isNoGood(obj: IDeprecatedUnion): obj is IDeprecatedUnion_NoGood { + return (obj.type === "noGood"); +} + +/** + * @deprecated use good + */ +function noGood(obj: string): IDeprecatedUnion_NoGood { + return { + noGood: obj, + type: "noGood", + }; +} + +function isNoGoodDoc(obj: IDeprecatedUnion): obj is IDeprecatedUnion_NoGoodDoc { + return (obj.type === "noGoodDoc"); +} + +/** + * @deprecated use good + */ +function noGoodDoc(obj: string): IDeprecatedUnion_NoGoodDoc { + return { + noGoodDoc: obj, + type: "noGoodDoc", + }; +} + +export type IDeprecatedUnion = IDeprecatedUnion_Good | IDeprecatedUnion_NoGood | IDeprecatedUnion_NoGoodDoc; + +export interface IDeprecatedUnionVisitor { + 'good': (obj: string) => T; + 'noGood': (obj: string) => T; + 'noGoodDoc': (obj: string) => T; + 'unknown': (obj: IDeprecatedUnion) => T; +} + +function visit(obj: IDeprecatedUnion, visitor: IDeprecatedUnionVisitor): T { + if (isGood(obj)) { + return visitor.good(obj.good); + } + if (isNoGood(obj)) { + return visitor.noGood(obj.noGood); + } + if (isNoGoodDoc(obj)) { + return visitor.noGoodDoc(obj.noGoodDoc); + } + return visitor.unknown(obj); +} + +export const IDeprecatedUnion = { + isGood: isGood, + good: good, + isNoGood: isNoGood, + noGood: noGood, + isNoGoodDoc: isNoGoodDoc, + noGoodDoc: noGoodDoc, + visit: visit +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleAliasExample.ts new file mode 100644 index 00000000..896b98f3 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleAliasExample.ts @@ -0,0 +1,4 @@ +export type IDoubleAliasExample = number | "NaN" & { + __conjure_type: "DoubleAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleExample.ts new file mode 100644 index 00000000..e0ca0c57 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/doubleExample.ts @@ -0,0 +1,3 @@ +export interface IDoubleExample { + 'doubleValue': number | "NaN"; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyEnum.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyEnum.ts new file mode 100644 index 00000000..a9ccba77 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyEnum.ts @@ -0,0 +1,3 @@ +export const EmptyEnum = {}; + +export type EmptyEnum = void; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyObjectExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyObjectExample.ts new file mode 100644 index 00000000..46155e1c --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/emptyObjectExample.ts @@ -0,0 +1,2 @@ +export interface IEmptyObjectExample { +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumExample.ts new file mode 100644 index 00000000..542e2aa8 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumExample.ts @@ -0,0 +1,18 @@ +/** + * This enumerates the numbers 1:2 also 100. + * + */ +export namespace EnumExample { + export type ONE = "ONE"; + export type TWO = "TWO"; + /** + * Value of 100. + */ + export type ONE_HUNDRED = "ONE_HUNDRED"; + + export const ONE = "ONE" as "ONE"; + export const TWO = "TWO" as "TWO"; + export const ONE_HUNDRED = "ONE_HUNDRED" as "ONE_HUNDRED"; +} + +export type EnumExample = keyof typeof EnumExample; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumFieldExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumFieldExample.ts new file mode 100644 index 00000000..7f4f691b --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/enumFieldExample.ts @@ -0,0 +1,5 @@ +import { EnumExample } from "./enumExample"; + +export interface IEnumFieldExample { + 'enum': EnumExample; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/externalLongExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/externalLongExample.ts new file mode 100644 index 00000000..9e744b35 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/externalLongExample.ts @@ -0,0 +1,5 @@ +export interface IExternalLongExample { + 'externalLong': number; + 'optionalExternalLong'?: number | null; + 'listExternalLong': Array; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerAliasExample.ts new file mode 100644 index 00000000..1197a6ac --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerAliasExample.ts @@ -0,0 +1,4 @@ +export type IIntegerAliasExample = number & { + __conjure_type: "IntegerAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerExample.ts new file mode 100644 index 00000000..2baf4799 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/integerExample.ts @@ -0,0 +1,3 @@ +export interface IIntegerExample { + 'integer': number; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/listExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/listExample.ts new file mode 100644 index 00000000..64fb056d --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/listExample.ts @@ -0,0 +1,5 @@ +export interface IListExample { + 'items': Array; + 'primitiveItems': Array; + 'doubleItems': Array; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/manyFieldExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/manyFieldExample.ts new file mode 100644 index 00000000..c675b04a --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/manyFieldExample.ts @@ -0,0 +1,36 @@ +import { IStringAliasExample } from "./stringAliasExample"; + +export interface IManyFieldExample { + /** + * docs for string field + */ + 'string': string; + /** + * docs for integer field + */ + 'integer': number; + /** + * docs for doubleValue field + */ + 'doubleValue': number | "NaN"; + /** + * docs for optionalItem field + */ + 'optionalItem'?: string | null; + /** + * docs for items field + */ + 'items': Array; + /** + * docs for set field + */ + 'set': Array; + /** + * docs for map field + */ + 'map': { [key: string]: string }; + /** + * docs for alias field + */ + 'alias': IStringAliasExample; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/mapExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/mapExample.ts new file mode 100644 index 00000000..261df991 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/mapExample.ts @@ -0,0 +1,3 @@ +export interface IMapExample { + 'items': { [key: string]: string }; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/optionalExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/optionalExample.ts new file mode 100644 index 00000000..69a9592a --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/optionalExample.ts @@ -0,0 +1,3 @@ +export interface IOptionalExample { + 'item'?: string | null; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/primitiveOptionalsExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/primitiveOptionalsExample.ts new file mode 100644 index 00000000..d181e9dd --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/primitiveOptionalsExample.ts @@ -0,0 +1,9 @@ +export interface IPrimitiveOptionalsExample { + 'num'?: number | "NaN" | null; + 'bool'?: boolean | null; + 'integer'?: number | null; + 'safelong'?: number | null; + 'rid'?: string | null; + 'bearertoken'?: string | null; + 'uuid'?: string | null; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/reservedKeyExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/reservedKeyExample.ts new file mode 100644 index 00000000..34d24639 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/reservedKeyExample.ts @@ -0,0 +1,7 @@ +export interface IReservedKeyExample { + 'package': string; + 'interface': string; + 'field-name-with-dashes': string; + 'primitve-field-name-with-dashes': number; + 'memoizedHashCode': number; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridAliasExample.ts new file mode 100644 index 00000000..22a94677 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridAliasExample.ts @@ -0,0 +1,4 @@ +export type IRidAliasExample = string & { + __conjure_type: "RidAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridExample.ts new file mode 100644 index 00000000..01628bd2 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/ridExample.ts @@ -0,0 +1,3 @@ +export interface IRidExample { + 'ridValue': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongAliasExample.ts new file mode 100644 index 00000000..07c010cc --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongAliasExample.ts @@ -0,0 +1,4 @@ +export type ISafeLongAliasExample = number & { + __conjure_type: "SafeLongAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongExample.ts new file mode 100644 index 00000000..b4022431 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/safeLongExample.ts @@ -0,0 +1,3 @@ +export interface ISafeLongExample { + 'safeLongValue': number; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/setExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/setExample.ts new file mode 100644 index 00000000..9568386a --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/setExample.ts @@ -0,0 +1,4 @@ +export interface ISetExample { + 'items': Array; + 'doubleItems': Array; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/simpleEnum.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/simpleEnum.ts new file mode 100644 index 00000000..0c56f742 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/simpleEnum.ts @@ -0,0 +1,7 @@ +export namespace SimpleEnum { + export type VALUE = "VALUE"; + + export const VALUE = "VALUE" as "VALUE"; +} + +export type SimpleEnum = keyof typeof SimpleEnum; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/singleUnion.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/singleUnion.ts new file mode 100644 index 00000000..b4974e6a --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/singleUnion.ts @@ -0,0 +1,35 @@ +export interface ISingleUnion_Foo { + 'foo': string; + 'type': "foo"; +} + +function isFoo(obj: ISingleUnion): obj is ISingleUnion_Foo { + return (obj.type === "foo"); +} + +function foo(obj: string): ISingleUnion_Foo { + return { + foo: obj, + type: "foo", + }; +} + +export type ISingleUnion = ISingleUnion_Foo; + +export interface ISingleUnionVisitor { + 'foo': (obj: string) => T; + 'unknown': (obj: ISingleUnion) => T; +} + +function visit(obj: ISingleUnion, visitor: ISingleUnionVisitor): T { + if (isFoo(obj)) { + return visitor.foo(obj.foo); + } + return visitor.unknown(obj); +} + +export const ISingleUnion = { + isFoo: isFoo, + foo: foo, + visit: visit +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasExample.ts new file mode 100644 index 00000000..6c873155 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasExample.ts @@ -0,0 +1,4 @@ +export type IStringAliasExample = string & { + __conjure_type: "StringAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasOne.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasOne.ts new file mode 100644 index 00000000..6b7dd7ca --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringAliasOne.ts @@ -0,0 +1,4 @@ +export type IStringAliasOne = string & { + __conjure_type: "StringAliasOne", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringExample.ts new file mode 100644 index 00000000..72955bf0 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/stringExample.ts @@ -0,0 +1,3 @@ +export interface IStringExample { + 'string': string; +} diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/union.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/union.ts new file mode 100644 index 00000000..01215620 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/union.ts @@ -0,0 +1,79 @@ +export interface IUnion_Foo { + 'foo': string; + 'type': "foo"; +} + +export interface IUnion_Bar { + 'bar': number; + 'type': "bar"; +} + +export interface IUnion_Baz { + 'baz': number; + 'type': "baz"; +} + +function isFoo(obj: IUnion): obj is IUnion_Foo { + return (obj.type === "foo"); +} + +function foo(obj: string): IUnion_Foo { + return { + foo: obj, + type: "foo", + }; +} + +function isBar(obj: IUnion): obj is IUnion_Bar { + return (obj.type === "bar"); +} + +function bar(obj: number): IUnion_Bar { + return { + bar: obj, + type: "bar", + }; +} + +function isBaz(obj: IUnion): obj is IUnion_Baz { + return (obj.type === "baz"); +} + +function baz(obj: number): IUnion_Baz { + return { + baz: obj, + type: "baz", + }; +} + +export type IUnion = IUnion_Foo | IUnion_Bar | IUnion_Baz; + +export interface IUnionVisitor { + 'foo': (obj: string) => T; + 'bar': (obj: number) => T; + 'baz': (obj: number) => T; + 'unknown': (obj: IUnion) => T; +} + +function visit(obj: IUnion, visitor: IUnionVisitor): T { + if (isFoo(obj)) { + return visitor.foo(obj.foo); + } + if (isBar(obj)) { + return visitor.bar(obj.bar); + } + if (isBaz(obj)) { + return visitor.baz(obj.baz); + } + return visitor.unknown(obj); +} + +export const IUnion = { + isFoo: isFoo, + foo: foo, + isBar: isBar, + bar: bar, + isBaz: isBaz, + baz: baz, + visit: visit +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/unionTypeExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/unionTypeExample.ts new file mode 100644 index 00000000..ec551749 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/unionTypeExample.ts @@ -0,0 +1,175 @@ +import { IStringExample } from "./stringExample"; + +/** + * Docs for when UnionTypeExample is of type StringExample. + */ +export interface IUnionTypeExample_StringExample { + 'stringExample': IStringExample; + 'type': "stringExample"; +} + +export interface IUnionTypeExample_Set { + 'set': Array; + 'type': "set"; +} + +export interface IUnionTypeExample_ThisFieldIsAnInteger { + 'thisFieldIsAnInteger': number; + 'type': "thisFieldIsAnInteger"; +} + +export interface IUnionTypeExample_AlsoAnInteger { + 'alsoAnInteger': number; + 'type': "alsoAnInteger"; +} + +export interface IUnionTypeExample_If { + 'if': number; + 'type': "if"; +} + +export interface IUnionTypeExample_New { + 'new': number; + 'type': "new"; +} + +export interface IUnionTypeExample_Interface { + 'interface': number; + 'type': "interface"; +} + +function isStringExample(obj: IUnionTypeExample): obj is IUnionTypeExample_StringExample { + return (obj.type === "stringExample"); +} + +function stringExample(obj: IStringExample): IUnionTypeExample_StringExample { + return { + stringExample: obj, + type: "stringExample", + }; +} + +function isSet(obj: IUnionTypeExample): obj is IUnionTypeExample_Set { + return (obj.type === "set"); +} + +function set(obj: Array): IUnionTypeExample_Set { + return { + set: obj, + type: "set", + }; +} + +function isThisFieldIsAnInteger(obj: IUnionTypeExample): obj is IUnionTypeExample_ThisFieldIsAnInteger { + return (obj.type === "thisFieldIsAnInteger"); +} + +function thisFieldIsAnInteger(obj: number): IUnionTypeExample_ThisFieldIsAnInteger { + return { + thisFieldIsAnInteger: obj, + type: "thisFieldIsAnInteger", + }; +} + +function isAlsoAnInteger(obj: IUnionTypeExample): obj is IUnionTypeExample_AlsoAnInteger { + return (obj.type === "alsoAnInteger"); +} + +function alsoAnInteger(obj: number): IUnionTypeExample_AlsoAnInteger { + return { + alsoAnInteger: obj, + type: "alsoAnInteger", + }; +} + +function isIf(obj: IUnionTypeExample): obj is IUnionTypeExample_If { + return (obj.type === "if"); +} + +function if_(obj: number): IUnionTypeExample_If { + return { + if: obj, + type: "if", + }; +} + +function isNew(obj: IUnionTypeExample): obj is IUnionTypeExample_New { + return (obj.type === "new"); +} + +function new_(obj: number): IUnionTypeExample_New { + return { + new: obj, + type: "new", + }; +} + +function isInterface(obj: IUnionTypeExample): obj is IUnionTypeExample_Interface { + return (obj.type === "interface"); +} + +function interface_(obj: number): IUnionTypeExample_Interface { + return { + interface: obj, + type: "interface", + }; +} + +/** + * A type which can either be a StringExample, a set of strings, or an integer. + */ +export type IUnionTypeExample = IUnionTypeExample_StringExample | IUnionTypeExample_Set | IUnionTypeExample_ThisFieldIsAnInteger | IUnionTypeExample_AlsoAnInteger | IUnionTypeExample_If | IUnionTypeExample_New | IUnionTypeExample_Interface; + +export interface IUnionTypeExampleVisitor { + 'stringExample': (obj: IStringExample) => T; + 'set': (obj: Array) => T; + 'thisFieldIsAnInteger': (obj: number) => T; + 'alsoAnInteger': (obj: number) => T; + 'if': (obj: number) => T; + 'new': (obj: number) => T; + 'interface': (obj: number) => T; + 'unknown': (obj: IUnionTypeExample) => T; +} + +function visit(obj: IUnionTypeExample, visitor: IUnionTypeExampleVisitor): T { + if (isStringExample(obj)) { + return visitor.stringExample(obj.stringExample); + } + if (isSet(obj)) { + return visitor.set(obj.set); + } + if (isThisFieldIsAnInteger(obj)) { + return visitor.thisFieldIsAnInteger(obj.thisFieldIsAnInteger); + } + if (isAlsoAnInteger(obj)) { + return visitor.alsoAnInteger(obj.alsoAnInteger); + } + if (isIf(obj)) { + return visitor.if(obj.if); + } + if (isNew(obj)) { + return visitor.new(obj.new); + } + if (isInterface(obj)) { + return visitor.interface(obj.interface); + } + return visitor.unknown(obj); +} + +export const IUnionTypeExample = { + isStringExample: isStringExample, + stringExample: stringExample, + isSet: isSet, + set: set, + isThisFieldIsAnInteger: isThisFieldIsAnInteger, + thisFieldIsAnInteger: thisFieldIsAnInteger, + isAlsoAnInteger: isAlsoAnInteger, + alsoAnInteger: alsoAnInteger, + isIf: isIf, + if_: if_, + isNew: isNew, + new_: new_, + isInterface: isInterface, + interface_: interface_, + visit: visit +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidAliasExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidAliasExample.ts new file mode 100644 index 00000000..18739ce4 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidAliasExample.ts @@ -0,0 +1,4 @@ +export type IUuidAliasExample = string & { + __conjure_type: "UuidAliasExample", + __conjure_package: "com.palantir.product", +}; diff --git a/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidExample.ts b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidExample.ts new file mode 100644 index 00000000..15e3a845 --- /dev/null +++ b/src/commands/generate/__tests__/resources/branded-test-cases/example-types/product/uuidExample.ts @@ -0,0 +1,3 @@ +export interface IUuidExample { + 'uuid': string; +} diff --git a/src/commands/generate/__tests__/resources/constants.ts b/src/commands/generate/__tests__/resources/constants.ts index 56d2830b..7b412c38 100644 --- a/src/commands/generate/__tests__/resources/constants.ts +++ b/src/commands/generate/__tests__/resources/constants.ts @@ -1,7 +1,9 @@ -import { ITypeGenerationFlags } from "../../typeGenerationFlags"; +import { AliasGenerationType, ITypeGenerationFlags } from "../../typeGenerationFlags"; -export const DEFAULT_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { flavorizedAliases: false, readonlyInterfaces: false }; +export const DEFAULT_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { aliases: AliasGenerationType.DEFAULT, readonlyInterfaces: false }; -export const FLAVORED_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { ...DEFAULT_TYPE_GENERATION_FLAGS, flavorizedAliases: true }; +export const BRANDED_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { ...DEFAULT_TYPE_GENERATION_FLAGS, aliases: AliasGenerationType.BRANDED }; + +export const FLAVORED_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { ...DEFAULT_TYPE_GENERATION_FLAGS, aliases: AliasGenerationType.FLAVORED }; export const READONLY_TYPE_GENERATION_FLAGS: ITypeGenerationFlags = { ...DEFAULT_TYPE_GENERATION_FLAGS, readonlyInterfaces: true }; diff --git a/src/commands/generate/generateCommand.ts b/src/commands/generate/generateCommand.ts index 0c58106b..190c1aec 100644 --- a/src/commands/generate/generateCommand.ts +++ b/src/commands/generate/generateCommand.ts @@ -22,6 +22,7 @@ import { SlsVersion, SlsVersionMatcher } from "sls-version"; import { Argv, CommandModule } from "yargs"; import { IPackageJson, IProductDependency, ISlsManifestDependency, writeJson } from "../../utils"; import { generate } from "./generator"; +import { AliasGenerationType } from "./typeGenerationFlags"; export interface IGenerateCommandArgs { /* @@ -54,6 +55,11 @@ export interface IGenerateCommandArgs { */ productDependencies?: string; + /** + * Generates strongly branded types for compatible aliases (string, rids...) + */ + brandedAliases?: boolean; + /** * Generates flavoured types for compatible aliases (string, rids...) */ @@ -103,6 +109,11 @@ export class GenerateCommand implements CommandModule { describe: "The name of the generated package", type: "string", }) + .option("brandedAliases", { + default: false, + describe: "Generates strongly branded types for compatible aliases.", + type: "boolean", + }) .option("flavorizedAliases", { default: false, describe: "Generates flavoured types for compatible aliases.", @@ -136,7 +147,9 @@ export class GenerateCommand implements CommandModule { const { rawSource } = args; const { conjureDefinition, packageJson, tsConfig, gitIgnore } = await this.parseCommandLineArguments(args); const generatePromise = generate(conjureDefinition, output, { - flavorizedAliases: args.flavorizedAliases ?? false, + aliases: args.brandedAliases ? AliasGenerationType.BRANDED + : args.flavorizedAliases ? AliasGenerationType.FLAVORED + : AliasGenerationType.DEFAULT, readonlyInterfaces: args.readonlyInterfaces ?? false, }); if (rawSource) { diff --git a/src/commands/generate/imports.ts b/src/commands/generate/imports.ts index 562a3e7f..b17e9e39 100644 --- a/src/commands/generate/imports.ts +++ b/src/commands/generate/imports.ts @@ -64,7 +64,7 @@ export class ImportsVisitor implements ITypeVisitor { return `{ ${maybeReadonly}[key in ${obj.keyType.reference.name}]?: ${valueTsType} }`; } else if ( ITypeDefinition.isAlias(keyTypeDefinition) && - isFlavorizable(keyTypeDefinition.alias.alias, this.typeGenerationFlags.flavorizedAliases) + isFlavorizable(keyTypeDefinition.alias.alias, this.typeGenerationFlags.aliases) ) { return `{ ${maybeReadonly}[key: I${obj.keyType.reference.name}]: ${valueTsType} }`; } @@ -99,7 +99,7 @@ export class TsReturnTypeVisitor implements ITypeVisitor { throw new Error(`unknown reference type. package: '${obj.package}', name: '${obj.name}'`); } else if ( ITypeDefinition.isAlias(typeDefinition) && - !isFlavorizable(typeDefinition.alias.alias, this.typeGenerationFlags.flavorizedAliases) + !isFlavorizable(typeDefinition.alias.alias, this.typeGenerationFlags.aliases) ) { return IType.visit(typeDefinition.alias.alias, this); } else if (ITypeDefinition.isEnum(typeDefinition)) { diff --git a/src/commands/generate/typeGenerationFlags.ts b/src/commands/generate/typeGenerationFlags.ts index 4a544ea8..b47b886e 100644 --- a/src/commands/generate/typeGenerationFlags.ts +++ b/src/commands/generate/typeGenerationFlags.ts @@ -15,14 +15,22 @@ * limitations under the License. */ +export const AliasGenerationType = { + DEFAULT: "DEFAULT", + BRANDED: "BRANDED", + FLAVORED: "FLAVORED", +} as const; + +export type AliasGenerationType = keyof typeof AliasGenerationType; + /** * Simple and convenient interface allowing for passing flags through the "generation" code. */ export interface ITypeGenerationFlags { /** - * When set to true compatible alias types will be converted as flavoured strings. + * When set compatible alias types will be converted as strict branded or flavoured strings. */ - readonly flavorizedAliases: boolean; + readonly aliases: AliasGenerationType; /** * Generated interfaces have readonly properties and use ReadonlyArray instead of Array. diff --git a/src/commands/generate/typeGenerator.ts b/src/commands/generate/typeGenerator.ts index 2f1aaf7a..79366139 100644 --- a/src/commands/generate/typeGenerator.ts +++ b/src/commands/generate/typeGenerator.ts @@ -33,7 +33,7 @@ import { import { ImportsVisitor, sortImports } from "./imports"; import { SimpleAst } from "./simpleAst"; import { TsReturnTypeVisitor } from "./tsReturnTypeVisitor"; -import { ITypeGenerationFlags } from "./typeGenerationFlags"; +import { AliasGenerationType, ITypeGenerationFlags } from "./typeGenerationFlags"; import { addDeprecatedToDocs, doubleQuote, isFlavorizable, isValidFunctionName, singleQuote } from "./utils"; export function generateType( @@ -59,13 +59,23 @@ const FLAVOR_TYPE_FIELD = "__conjure_type"; const FLAVOR_PACKAGE_FIELD = "__conjure_package"; /** - * Generates a file of the following format: + * Generates a file of the following formats depending on alias generation flags + * + * For Flavored Aliases, the fields are optional allow implicit conversion to and from string, but providing type safety between aliases: * ``` * export type ExampleAlias = string & { * __conjure_type?: "ExampleAlias"; * __conjure_package?: "com.palantir.product"; * }; * ``` + * + * For Branded Aliases, the fields are not optional, providing type safety between aliases and disallowing implicit type satisfaction from string: + * ``` + * export type ExampleAlias = string & { + * __conjure_type: "ExampleAlias"; + * __conjure_package: "com.palantir.product"; + * }; + * ``` */ export async function generateAlias( definition: IAliasDefinition, @@ -73,17 +83,20 @@ export async function generateAlias( simpleAst: SimpleAst, typeGenerationFlags: ITypeGenerationFlags, ): Promise { - if (isFlavorizable(definition.alias, typeGenerationFlags.flavorizedAliases)) { + if (isFlavorizable(definition.alias, typeGenerationFlags.aliases)) { const tsTypeVisitor = new TsReturnTypeVisitor(knownTypes, definition.typeName, false, typeGenerationFlags); const fieldType = IType.visit(definition.alias, tsTypeVisitor); const sourceFile = simpleAst.createSourceFile(definition.typeName); + + const maybeOptional = typeGenerationFlags.aliases === AliasGenerationType.FLAVORED ? "?" : ""; + const typeAlias = sourceFile.addTypeAlias({ isExported: true, name: "I" + definition.typeName.name, type: [ `${fieldType} & {`, - `\t${FLAVOR_TYPE_FIELD}?: "${definition.typeName.name}",`, - `\t${FLAVOR_PACKAGE_FIELD}?: "${definition.typeName.package}",`, + `\t${FLAVOR_TYPE_FIELD}${maybeOptional}: "${definition.typeName.name}",`, + `\t${FLAVOR_PACKAGE_FIELD}${maybeOptional}: "${definition.typeName.package}",`, "}", ].join("\n"), }); diff --git a/src/commands/generate/utils.ts b/src/commands/generate/utils.ts index 4df3bdcb..cb033d8b 100644 --- a/src/commands/generate/utils.ts +++ b/src/commands/generate/utils.ts @@ -22,6 +22,7 @@ import { ITypeName, PrimitiveType, } from "conjure-api"; +import { AliasGenerationType } from "./typeGenerationFlags"; export const CONJURE_CLIENT = "conjure-client"; @@ -136,6 +137,7 @@ const NON_FLAVORIZABLE_TYPES = new Set([ PrimitiveType.DATETIME, ]); -export function isFlavorizable(type: IType, flavorizedAliases: boolean): boolean { - return flavorizedAliases && IType.isPrimitive(type) && !NON_FLAVORIZABLE_TYPES.has(type.primitive); +export function isFlavorizable(type: IType, aliasesGenerationType: AliasGenerationType): boolean { + return (aliasesGenerationType === AliasGenerationType.FLAVORED || aliasesGenerationType === AliasGenerationType.BRANDED) + && IType.isPrimitive(type) && !NON_FLAVORIZABLE_TYPES.has(type.primitive); }