├── typescript-service-generator-examples ├── .gitignore ├── build.gradle ├── src │ └── main │ │ └── java │ │ └── com │ │ └── palantir │ │ └── code │ │ └── ts │ │ └── generator │ │ └── examples │ │ ├── MyObject.java │ │ ├── Planet.java │ │ ├── MyService.java │ │ └── MyServiceGenerator.java └── exampleTypescript │ ├── generated │ ├── httpApiBridge.ts │ └── myService.ts │ └── angularHttpApiBridge.ts ├── typescript-service-generator-core ├── .gitignore ├── src │ ├── main │ │ ├── resources │ │ │ └── httpApiBridge.ts │ │ └── java │ │ │ └── com │ │ │ └── palantir │ │ │ └── code │ │ │ └── ts │ │ │ └── generator │ │ │ ├── utils │ │ │ └── PathUtils.java │ │ │ ├── model │ │ │ ├── InnerServiceModel.java │ │ │ ├── ServiceModel.java │ │ │ ├── ServiceEndpointModel.java │ │ │ └── ServiceEndpointParameterModel.java │ │ │ ├── IndentedOutputWriter.java │ │ │ ├── ServiceGenerator.java │ │ │ ├── TypescriptServiceGeneratorConfiguration.java │ │ │ ├── ServiceClassParser.java │ │ │ └── ServiceEmitter.java │ └── test │ │ ├── resources │ │ └── eteTestData │ │ │ ├── simpleServiceTestOutput │ │ │ ├── httpApiBridge.ts │ │ │ └── simpleService1.ts │ │ │ └── complexServiceTestOutput │ │ │ ├── httpApiBridge.ts │ │ │ └── testComplexServiceClass.ts │ │ └── java │ │ └── com │ │ └── palantir │ │ └── code │ │ └── ts │ │ └── generator │ │ ├── IndentedOutputWriterTest.java │ │ ├── utils │ │ └── TestUtils.java │ │ ├── ServiceGeneratorEteTest.java │ │ ├── ServiceEmitterTest.java │ │ └── ServiceClassParserTest.java └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── Palantir_Corporate_Contributor_License_Agreement.pdf ├── settings.gradle ├── Palantir_Individual_Contributor_License_Agreement.pdf ├── .gitignore ├── versions.props ├── gradle.properties ├── LICENSE ├── gradlew.bat ├── versions.lock ├── README.md ├── gradlew └── .circleci └── config.yml /typescript-service-generator-examples/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /generated_src/ 3 | -------------------------------------------------------------------------------- /typescript-service-generator-core/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /generated_src/ 3 | /build/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palantir/typescript-service-generator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Palantir_Corporate_Contributor_License_Agreement.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palantir/typescript-service-generator/HEAD/Palantir_Corporate_Contributor_License_Agreement.pdf -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "typescript-service-generator" 2 | 3 | include "typescript-service-generator-core" 4 | include "typescript-service-generator-examples" 5 | -------------------------------------------------------------------------------- /Palantir_Individual_Contributor_License_Agreement.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palantir/typescript-service-generator/HEAD/Palantir_Individual_Contributor_License_Agreement.pdf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(":typescript-service-generator-core") 3 | annotationProcessor "org.immutables:value" 4 | testAnnotationProcessor "org.immutables:value" 5 | 6 | compileOnly 'org.immutables:value::annotations' 7 | testCompileOnly 'org.immutables:value::annotations' 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | .idea/ 15 | 16 | .gradle 17 | **/*.classpath 18 | **/*.project 19 | **/*.settings 20 | build/ 21 | **/.factorypath 22 | -------------------------------------------------------------------------------- /versions.props: -------------------------------------------------------------------------------- 1 | com.google.guava:guava = 32.1.2-jre 2 | cz.habarta.typescript-generator:typescript-generator-core = 2.35.1025 3 | jakarta.ws.rs:jakarta.ws.rs-api = 3.1.0 4 | commons-io:commons-io = 2.4 5 | org.apache.commons:commons-lang3 = 3.4 6 | com.google.code.findbugs:jsr305 = 3.0.2 7 | 8 | junit:junit = 4.12 9 | org.mockito:mockito-core = 1.10.19 10 | 11 | org.immutables:* = 2.9.3 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching = true 2 | org.gradle.parallel = true 3 | org.gradle.jvmargs = --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 4 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/src/main/java/com/palantir/code/ts/generator/examples/MyObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.examples; 6 | 7 | /** 8 | * Note that MyObject is jackson serializable 9 | */ 10 | public class MyObject { 11 | private String payload = "hello world"; 12 | 13 | public String getPayload() { 14 | return payload; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/resources/httpApiBridge.ts: -------------------------------------------------------------------------------- 1 | export interface %sHttpEndpointOptions { 2 | serviceIdentifier?: string; 3 | endpointPath: string; 4 | endpointName: string; 5 | method: string; 6 | requestMediaType: string; 7 | responseMediaType: string; 8 | requiredHeaders: string[]; 9 | pathArguments: any[]; 10 | queryArguments: any; 11 | data?: any; 12 | } 13 | 14 | export interface %sHttpApiBridge { 15 | callEndpoint(parameters: %sHttpEndpointOptions): %s; 16 | } 17 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/src/main/java/com/palantir/code/ts/generator/examples/Planet.java: -------------------------------------------------------------------------------- 1 | package com.palantir.code.ts.generator.examples; 2 | 3 | import org.immutables.value.Value; 4 | 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 7 | 8 | @JsonDeserialize(as = ImmutablePlanet.class) 9 | @JsonSerialize(as = ImmutablePlanet.class) 10 | @Value.Immutable 11 | public interface Planet { 12 | String name(); 13 | double radiusKm(); 14 | } 15 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/resources/eteTestData/simpleServiceTestOutput/httpApiBridge.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2 | // Generated 3 | export interface HttpEndpointOptions { 4 | serviceIdentifier?: string; 5 | endpointPath: string; 6 | endpointName: string; 7 | method: string; 8 | requestMediaType: string; 9 | responseMediaType: string; 10 | requiredHeaders: string[]; 11 | pathArguments: any[]; 12 | queryArguments: any; 13 | data?: any; 14 | } 15 | 16 | export interface HttpApiBridge { 17 | callEndpoint(parameters: HttpEndpointOptions): FooReturn; 18 | } -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/utils/PathUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.utils; 6 | 7 | public final class PathUtils { 8 | 9 | private PathUtils() { 10 | // no 11 | } 12 | 13 | public static String trimSlashes(String input) { 14 | if (input.startsWith("/")) { 15 | input = input.substring(1); 16 | } 17 | if (input.endsWith("/")) { 18 | input = input.substring(0, input.length() - 1); 19 | } 20 | return input; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/exampleTypescript/generated/httpApiBridge.ts: -------------------------------------------------------------------------------- 1 | // A potential copyright header 2 | // A desired generated message 3 | module MyProject.Http { 4 | 5 | export interface IHttpEndpointOptions { 6 | serviceIdentifier?: string; 7 | endpointPath: string; 8 | endpointName: string; 9 | method: string; 10 | mediaType: string; 11 | requiredHeaders: string[]; 12 | pathArguments: string[]; 13 | queryArguments: any; 14 | data?: any; 15 | } 16 | 17 | export interface IHttpApiBridge { 18 | callEndpoint(parameters: IHttpEndpointOptions): ng.IPromise; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/resources/eteTestData/complexServiceTestOutput/httpApiBridge.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2 | // Generated 3 | module ModuleName { 4 | export interface IHttpEndpointOptions { 5 | serviceIdentifier?: string; 6 | endpointPath: string; 7 | endpointName: string; 8 | method: string; 9 | requestMediaType: string; 10 | responseMediaType: string; 11 | requiredHeaders: string[]; 12 | pathArguments: any[]; 13 | queryArguments: any; 14 | data?: any; 15 | } 16 | 17 | export interface IHttpApiBridge { 18 | callEndpoint(parameters: IHttpEndpointOptions): FooReturn; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /typescript-service-generator-core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.palantir.external-publish-jar' 2 | 3 | dependencies { 4 | api "com.google.guava:guava" 5 | api "cz.habarta.typescript-generator:typescript-generator-core" 6 | api "jakarta.ws.rs:jakarta.ws.rs-api" 7 | api "commons-io:commons-io" 8 | api "org.apache.commons:commons-lang3" 9 | api "com.google.code.findbugs:jsr305" 10 | 11 | annotationProcessor "org.immutables:value" 12 | testAnnotationProcessor "org.immutables:value" 13 | 14 | testImplementation "junit:junit" 15 | testImplementation "org.mockito:mockito-core" 16 | 17 | compileOnly 'org.immutables:value::annotations' 18 | testCompileOnly 'org.immutables:value::annotations' 19 | 20 | test { 21 | jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/src/main/java/com/palantir/code/ts/generator/examples/MyService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.examples; 6 | 7 | import java.util.List; 8 | 9 | import jakarta.ws.rs.Consumes; 10 | import jakarta.ws.rs.GET; 11 | import jakarta.ws.rs.Path; 12 | import jakarta.ws.rs.Produces; 13 | import jakarta.ws.rs.core.MediaType; 14 | 15 | @Path("/myservice") 16 | public interface MyService { 17 | @GET 18 | @Path("/foo_get") 19 | @Consumes(MediaType.APPLICATION_JSON) 20 | @Produces(MediaType.APPLICATION_JSON) 21 | MyObject helloWorld(); 22 | 23 | @GET 24 | @Path("/planets") 25 | @Consumes(MediaType.APPLICATION_JSON) 26 | @Produces(MediaType.APPLICATION_JSON) 27 | List planets(); 28 | } 29 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/model/InnerServiceModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.model; 6 | 7 | import java.util.List; 8 | 9 | import org.immutables.value.Value; 10 | 11 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 12 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 13 | 14 | @Value.Immutable 15 | @Value.Style(visibility = Value.Style.ImplementationVisibility.PUBLIC) 16 | @JsonDeserialize(as = ImmutableInnerServiceModel.class) 17 | @JsonSerialize(as = ImmutableInnerServiceModel.class) 18 | public abstract class InnerServiceModel { 19 | public abstract String name(); 20 | public abstract String servicePath(); 21 | public abstract List endpointModels(); 22 | } 23 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/model/ServiceModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.model; 6 | 7 | import java.lang.reflect.Type; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import org.immutables.value.Value; 12 | 13 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 14 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 15 | 16 | @Value.Immutable 17 | @Value.Style(visibility = Value.Style.ImplementationVisibility.PUBLIC) 18 | @JsonDeserialize(as = ImmutableServiceModel.class) 19 | @JsonSerialize(as = ImmutableServiceModel.class) 20 | public abstract class ServiceModel { 21 | public abstract Set referencedTypes(); 22 | public abstract String name(); 23 | public abstract List innerServiceModels(); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Palantir Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/resources/eteTestData/simpleServiceTestOutput/simpleService1.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2 | // Generated 3 | import { HttpEndpointOptions, HttpApiBridge } from "./httpApiBridge"; 4 | /* tslint:disable */ 5 | /* eslint-disable */ 6 | 7 | export interface SimpleService1 { 8 | method1(): FooReturn; 9 | } 10 | 11 | export class SimpleService1Impl implements SimpleService1 { 12 | 13 | private httpApiBridge: HttpApiBridge; 14 | constructor(httpApiBridge: HttpApiBridge) { 15 | this.httpApiBridge = httpApiBridge; 16 | } 17 | 18 | public method1() { 19 | var httpCallData = { 20 | serviceIdentifier: "simpleService1", 21 | endpointPath: "simple1/method1", 22 | endpointName: "method1", 23 | method: "GET", 24 | requestMediaType: "application/json", 25 | responseMediaType: "", 26 | requiredHeaders: [], 27 | pathArguments: [], 28 | queryArguments: { 29 | }, 30 | data: null 31 | }; 32 | return this.httpApiBridge.callEndpoint(httpCallData); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/src/main/java/com/palantir/code/ts/generator/examples/MyServiceGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.examples; 6 | 7 | import java.io.File; 8 | 9 | import com.palantir.code.ts.generator.ImmutableTypescriptServiceGeneratorConfiguration; 10 | import com.palantir.code.ts.generator.ServiceGenerator; 11 | 12 | public class MyServiceGenerator { 13 | public static void main(String[] args) { 14 | String generatedFolderPath = "exampleTypescript/generated"; 15 | 16 | String copyrightHeader = "// A potential copyright header"; 17 | String generatedMessage = "// A desired generated message"; 18 | 19 | ImmutableTypescriptServiceGeneratorConfiguration.Builder builder = ImmutableTypescriptServiceGeneratorConfiguration.builder(); 20 | builder.copyrightHeader(copyrightHeader); 21 | builder.typescriptModule("MyProject.Http"); 22 | builder.generatedMessage(generatedMessage); 23 | builder.generatedFolderLocation(new File(generatedFolderPath)); 24 | // This example targets angular, angular $http returns ng.IPromise<%s> so we target that return type here 25 | builder.genericEndpointReturnType("ng.IPromise<%s>"); 26 | builder.generatedInterfacePrefix("I"); 27 | 28 | ServiceGenerator generator = new ServiceGenerator(builder.build()); 29 | generator.generateTypescriptService(MyService.class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/IndentedOutputWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import java.io.OutputStream; 8 | import java.io.PrintWriter; 9 | 10 | import cz.habarta.typescript.generator.Settings; 11 | 12 | public final class IndentedOutputWriter { 13 | 14 | private int indent; 15 | private final PrintWriter writer; 16 | private final TypescriptServiceGeneratorConfiguration settings; 17 | private final Settings typeSettings; 18 | 19 | public IndentedOutputWriter(OutputStream stream, TypescriptServiceGeneratorConfiguration settings) { 20 | this.writer = new PrintWriter(stream); 21 | this.indent = 0; 22 | this.settings = settings; 23 | this.typeSettings = this.settings.getSettings(); 24 | } 25 | 26 | public void close() { 27 | this.writer.close(); 28 | } 29 | 30 | public void decreaseIndent() { 31 | indent--; 32 | } 33 | 34 | public void increaseIndent() { 35 | indent++; 36 | } 37 | 38 | public void write(String line) { 39 | writer.write(line); 40 | } 41 | 42 | public void writeLine(String line) { 43 | String indentString = ""; 44 | for (int i = 0; !line.isEmpty() && i < indent; i++) { 45 | indentString += this.typeSettings.indentString; 46 | } 47 | write(indentString + line + this.typeSettings.newline); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/model/ServiceEndpointModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.model; 6 | 7 | import java.lang.reflect.Type; 8 | import java.util.List; 9 | 10 | import jakarta.ws.rs.core.MediaType; 11 | 12 | import org.immutables.value.Value; 13 | 14 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 15 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 16 | import com.google.common.base.Optional; 17 | import com.palantir.code.ts.generator.ImmutableTypescriptServiceGeneratorConfiguration; 18 | 19 | import cz.habarta.typescript.generator.TsType; 20 | 21 | @Value.Immutable 22 | @Value.Style(visibility = Value.Style.ImplementationVisibility.PUBLIC) 23 | @JsonDeserialize(as = ImmutableTypescriptServiceGeneratorConfiguration.class) 24 | @JsonSerialize(as = ImmutableTypescriptServiceGeneratorConfiguration.class) 25 | public abstract class ServiceEndpointModel implements Comparable { 26 | public abstract Type javaReturnType(); 27 | public abstract TsType tsReturnType(); 28 | public abstract List parameters(); 29 | public abstract String endpointName(); 30 | public abstract String endpointPath(); 31 | public abstract String endpointMethodType(); 32 | 33 | @Value.Default 34 | public String endpointRequestMediaType() { 35 | return MediaType.APPLICATION_JSON; 36 | } 37 | 38 | public abstract Optional endpointResponseMediaType(); 39 | 40 | @Override 41 | public int compareTo(ServiceEndpointModel o) { 42 | return this.endpointName().compareTo(o.endpointName()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/model/ServiceEndpointParameterModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.model; 6 | 7 | import java.lang.reflect.ParameterizedType; 8 | import java.lang.reflect.Type; 9 | 10 | import javax.annotation.Nullable; 11 | 12 | import org.immutables.value.Value; 13 | 14 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 15 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 16 | 17 | import cz.habarta.typescript.generator.TsType; 18 | 19 | @Value.Immutable 20 | @Value.Style(visibility = Value.Style.ImplementationVisibility.PUBLIC) 21 | @JsonDeserialize(as = ImmutableServiceEndpointParameterModel.class) 22 | @JsonSerialize(as = ImmutableServiceEndpointParameterModel.class) 23 | public abstract class ServiceEndpointParameterModel { 24 | @Nullable public abstract String pathParam(); 25 | @Nullable public abstract String headerParam(); 26 | @Nullable public abstract String queryParam(); 27 | public abstract Type javaType(); 28 | public abstract TsType tsType(); 29 | 30 | public String getParameterName() { 31 | if (pathParam() != null) { 32 | return pathParam(); 33 | } else if (queryParam() != null) { 34 | return queryParam(); 35 | } else { 36 | Class nameClass = null; 37 | if (javaType() instanceof Class) { 38 | nameClass = (Class) javaType(); 39 | } else if (javaType() instanceof ParameterizedType) { 40 | nameClass = (Class) ((ParameterizedType) javaType()).getRawType(); 41 | } 42 | return Character.toLowerCase(nameClass.getSimpleName().charAt(0)) + nameClass.getSimpleName().substring(1); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/exampleTypescript/generated/myService.ts: -------------------------------------------------------------------------------- 1 | // A potential copyright header 2 | // A desired generated message 3 | module MyProject.Http.MyService { 4 | 5 | export interface IMyObject { 6 | payload: string; 7 | } 8 | 9 | export interface IPlanet { 10 | name: string; 11 | radiusKm: number; 12 | } 13 | 14 | export interface IMyService { 15 | helloWorld(): ng.IPromise; 16 | planets(): ng.IPromise; 17 | } 18 | 19 | export class MyServiceImpl implements IMyService { 20 | 21 | private httpApiBridge: IHttpApiBridge; 22 | constructor(httpApiBridge: IHttpApiBridge) { 23 | this.httpApiBridge = httpApiBridge; 24 | } 25 | 26 | public helloWorld() { 27 | var httpCallData = { 28 | serviceIdentifier: "myService", 29 | endpointPath: "myservice/foo_get", 30 | endpointName: "helloWorld", 31 | method: "GET", 32 | mediaType: "application/json", 33 | requiredHeaders: [], 34 | pathArguments: [], 35 | queryArguments: { 36 | }, 37 | data: null 38 | }; 39 | return this.httpApiBridge.callEndpoint(httpCallData); 40 | } 41 | 42 | public planets() { 43 | var httpCallData = { 44 | serviceIdentifier: "myService", 45 | endpointPath: "myservice/planets", 46 | endpointName: "planets", 47 | method: "GET", 48 | mediaType: "application/json", 49 | requiredHeaders: [], 50 | pathArguments: [], 51 | queryArguments: { 52 | }, 53 | data: null 54 | }; 55 | return this.httpApiBridge.callEndpoint(httpCallData); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/java/com/palantir/code/ts/generator/IndentedOutputWriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.mockito.Mockito; 14 | 15 | import cz.habarta.typescript.generator.Settings; 16 | 17 | public class IndentedOutputWriterTest { 18 | 19 | private TypescriptServiceGeneratorConfiguration settings; 20 | private ByteArrayOutputStream stream; 21 | private Settings typeSettings; 22 | private IndentedOutputWriter writer; 23 | 24 | @Before 25 | public void before() { 26 | stream = new ByteArrayOutputStream(); 27 | settings = Mockito.mock(TypescriptServiceGeneratorConfiguration.class); 28 | typeSettings = new Settings(); 29 | typeSettings.indentString = " "; 30 | Mockito.when(settings.getSettings()).thenReturn(typeSettings); 31 | 32 | writer = new IndentedOutputWriter(stream, settings); 33 | } 34 | 35 | @Test 36 | public void testWriteLine() { 37 | writer.writeLine("asdf"); 38 | writer.close(); 39 | assertEquals("asdf\n", new String(stream.toByteArray())); 40 | } 41 | 42 | @Test 43 | public void testIndent() { 44 | writer.writeLine("foo"); 45 | writer.increaseIndent(); 46 | writer.writeLine("bar"); 47 | writer.writeLine(""); 48 | writer.decreaseIndent(); 49 | writer.writeLine("baz"); 50 | writer.close(); 51 | assertEquals("foo\n bar\n\nbaz\n", new String(stream.toByteArray())); 52 | } 53 | 54 | @Test 55 | public void testWrite() { 56 | writer.write("fo"); 57 | writer.write("o\n"); 58 | writer.increaseIndent(); 59 | writer.write("bar"); 60 | writer.close(); 61 | assertEquals("foo\nbar", new String(stream.toByteArray())); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /typescript-service-generator-examples/exampleTypescript/angularHttpApiBridge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | // An example http api bridge, intended to illustrate, not be used directly. 6 | module MyProject.Http { 7 | 8 | export class AngularHttpApiBridge implements IHttpApiBridge { 9 | private $http: ng.IHttpService; 10 | 11 | public static $inject = ["$http"] 12 | constructor($http: ng.IHttpService) { 13 | this.$http = $http; 14 | } 15 | 16 | public callEndpoint(parameters: IHttpEndpointOptions) { 17 | var url = this.createUrlPath(parameters); 18 | var requestConfig: ng.IRequestShortcutConfig = {}; 19 | if (parameters.requiredHeaders.length > 0) { 20 | throw new Error("Required headers not supported for local http api bridge."); 21 | } 22 | requestConfig.params = parameters.queryArguments; 23 | return this.callHttpService(url, requestConfig, parameters, this.$http); 24 | } 25 | 26 | private createUrlPath(parameters: IHttpEndpointOptions) { 27 | var urlParameterRegex = /\{[^\}]+\}/; 28 | var path = parameters.endpointPath; 29 | parameters.pathArguments.forEach((pathArgument) => { 30 | path = path.replace(urlParameterRegex, pathArgument); 31 | }); 32 | Object.keys(parameters.queryArguments).forEach((key) => { 33 | if (parameters.queryArguments[key] == null) { 34 | delete parameters.queryArguments[key]; 35 | } 36 | }); 37 | return path; 38 | } 39 | 40 | private callHttpService(url: string, 41 | requestConfig: ng.IRequestShortcutConfig, 42 | parameters: IHttpEndpointOptions, 43 | $http: ng.IHttpService) { 44 | switch (parameters.method) { 45 | case "GET": 46 | return $http.get(url, requestConfig); 47 | case "DELETE": 48 | return $http.delete(url, requestConfig); 49 | case "POST": 50 | return $http.post(url, parameters.data, requestConfig); 51 | case "PUT": 52 | return $http.put(url, parameters.data, requestConfig); 53 | default: 54 | throw new Error(`Unrecognized http method ${parameters.method}`); 55 | } 56 | } 57 | } 58 | // Register AngularHttpApiBridge as a service 59 | // Inject AngularHttpApiBridge as httpBridge 60 | var myService = new MyService(httpBridge); 61 | // myService can now be used 62 | } 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS="-Xmx1024m" 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /versions.lock: -------------------------------------------------------------------------------- 1 | # Run ./gradlew --write-locks to regenerate this file 2 | com.fasterxml.jackson.core:jackson-annotations:2.13.1 (3 constraints: 9949d7f4) 3 | com.fasterxml.jackson.core:jackson-core:2.13.1 (3 constraints: 9949d7f4) 4 | com.fasterxml.jackson.core:jackson-databind:2.13.1 (3 constraints: 934ffbcb) 5 | com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.13.1 (1 constraints: 8118727b) 6 | com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.13.1 (1 constraints: 8118727b) 7 | com.google.code.findbugs:jsr305:3.0.2 (2 constraints: 1d0fb186) 8 | com.google.code.gson:gson:2.8.9 (1 constraints: 5d180763) 9 | com.google.errorprone:error_prone_annotations:2.18.0 (1 constraints: 4d0a47bf) 10 | com.google.guava:failureaccess:1.0.1 (1 constraints: 140ae1b4) 11 | com.google.guava:guava:32.1.2-jre (1 constraints: a8066553) 12 | com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava (1 constraints: bd17c918) 13 | com.google.j2objc:j2objc-annotations:2.8 (1 constraints: be09f5a0) 14 | com.sun.activation:jakarta.activation:2.0.1 (3 constraints: 9f298668) 15 | com.sun.istack:istack-commons-runtime:4.0.1 (1 constraints: 5f0c0906) 16 | commons-io:commons-io:2.4 (1 constraints: aa04212c) 17 | cz.habarta.typescript-generator:typescript-generator-core:2.35.1025 (1 constraints: d405274c) 18 | io.github.classgraph:classgraph:4.8.138 (1 constraints: c2185594) 19 | jakarta.json.bind:jakarta.json.bind-api:2.0.0 (1 constraints: 4c18e662) 20 | jakarta.ws.rs:jakarta.ws.rs-api:3.1.0 (2 constraints: 521d41e4) 21 | jakarta.xml.bind:jakarta.xml.bind-api:3.0.1 (2 constraints: de29a432) 22 | javax.activation:javax.activation-api:1.2.0 (3 constraints: f1407a18) 23 | javax.json.bind:javax.json.bind-api:1.0 (1 constraints: ed177b32) 24 | javax.ws.rs:javax.ws.rs-api:2.1.1 (1 constraints: 4e18ea62) 25 | javax.xml.bind:jaxb-api:2.3.1 (2 constraints: 80314588) 26 | org.apache.commons:commons-lang3:3.4 (1 constraints: ab04242c) 27 | org.checkerframework:checker-qual:3.33.0 (1 constraints: 4b0a46bf) 28 | org.codehaus.jackson:jackson-core-asl:1.9.13 (1 constraints: f11001cf) 29 | org.codehaus.jackson:jackson-mapper-asl:1.9.13 (1 constraints: 8818857b) 30 | org.glassfish.jaxb:jaxb-core:3.0.2 (1 constraints: ba0db035) 31 | org.glassfish.jaxb:jaxb-runtime:3.0.2 (1 constraints: 4f18ed62) 32 | org.glassfish.jaxb:txw2:3.0.2 (1 constraints: 5f0c0506) 33 | org.graalvm.js:js:22.0.0.2 (1 constraints: de18faac) 34 | org.graalvm.js:js-scriptengine:22.0.0.2 (1 constraints: de18faac) 35 | org.graalvm.regex:regex:22.0.0.2 (1 constraints: ae08ea96) 36 | org.graalvm.sdk:graal-sdk:22.0.0.2 (3 constraints: dd24c37d) 37 | org.graalvm.truffle:truffle-api:22.0.0.2 (2 constraints: d71378ab) 38 | org.immutables:value:2.9.3 (1 constraints: 10051336) 39 | org.jetbrains:annotations:13.0 (1 constraints: df0e795c) 40 | org.jetbrains.kotlin:kotlin-reflect:1.6.10 (1 constraints: 8218767b) 41 | org.jetbrains.kotlin:kotlin-stdlib:1.6.10 (2 constraints: 25289b56) 42 | org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10 (1 constraints: 410fca7a) 43 | 44 | [Test dependencies] 45 | junit:junit:4.12 (1 constraints: db04ff30) 46 | org.hamcrest:hamcrest-core:1.3 (2 constraints: 7910aeb0) 47 | org.mockito:mockito-core:1.10.19 (1 constraints: 6e059840) 48 | org.objenesis:objenesis:2.1 (1 constraints: af0a0fbd) 49 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/resources/eteTestData/complexServiceTestOutput/testComplexServiceClass.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2 | // Generated 3 | module ModuleName.TestComplexServiceClass { 4 | /* tslint:disable */ 5 | /* eslint-disable */ 6 | 7 | export interface IDataObject { 8 | y: IMyObject; 9 | } 10 | 11 | export interface IGenericObject { 12 | y: T; 13 | } 14 | 15 | export interface IImmutablesObject { 16 | y: string; 17 | } 18 | 19 | export interface IMyObject { 20 | y: IMyObject; 21 | } 22 | 23 | export interface ITestComplexServiceClass { 24 | allOptionsPost(a: string, dataObject: IDataObject, b?: number): FooReturn>; 25 | queryGetter(x?: boolean): FooReturn; 26 | simplePut(dataObject: IDataObject): FooReturn; 27 | } 28 | 29 | export class TestComplexServiceClassImpl implements ITestComplexServiceClass { 30 | 31 | private httpApiBridge: IHttpApiBridge; 32 | constructor(httpApiBridge: IHttpApiBridge) { 33 | this.httpApiBridge = httpApiBridge; 34 | } 35 | 36 | public allOptionsPost(a: string, dataObject: IDataObject, b?: number) { 37 | var httpCallData = { 38 | serviceIdentifier: "testComplexServiceClass", 39 | endpointPath: "testComplexService/allOptionsPost/{a}", 40 | endpointName: "allOptionsPost", 41 | method: "POST", 42 | requestMediaType: "application/json", 43 | responseMediaType: "", 44 | requiredHeaders: [], 45 | pathArguments: [a], 46 | queryArguments: { 47 | b: b, 48 | }, 49 | data: dataObject 50 | }; 51 | return this.httpApiBridge.callEndpoint>(httpCallData); 52 | } 53 | 54 | public queryGetter(x?: boolean) { 55 | var httpCallData = { 56 | serviceIdentifier: "testComplexServiceClass", 57 | endpointPath: "testComplexService/queryGetter", 58 | endpointName: "queryGetter", 59 | method: "GET", 60 | requestMediaType: "application/json", 61 | responseMediaType: "", 62 | requiredHeaders: [], 63 | pathArguments: [], 64 | queryArguments: { 65 | x: x, 66 | }, 67 | data: null 68 | }; 69 | return this.httpApiBridge.callEndpoint(httpCallData); 70 | } 71 | 72 | public simplePut(dataObject: IDataObject) { 73 | var httpCallData = { 74 | serviceIdentifier: "testComplexServiceClass", 75 | endpointPath: "testComplexService/simplePut", 76 | endpointName: "simplePut", 77 | method: "PUT", 78 | requestMediaType: "application/json", 79 | responseMediaType: "", 80 | requiredHeaders: [], 81 | pathArguments: [], 82 | queryArguments: { 83 | }, 84 | data: dataObject 85 | }; 86 | return this.httpApiBridge.callEndpoint(httpCallData); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/java/com/palantir/code/ts/generator/utils/TestUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator.utils; 6 | 7 | import javax.annotation.CheckForNull; 8 | import jakarta.ws.rs.Consumes; 9 | import jakarta.ws.rs.GET; 10 | import jakarta.ws.rs.POST; 11 | import jakarta.ws.rs.PUT; 12 | import jakarta.ws.rs.Path; 13 | import jakarta.ws.rs.PathParam; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.QueryParam; 16 | import jakarta.ws.rs.core.MediaType; 17 | 18 | import org.immutables.value.Value; 19 | 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 22 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 23 | 24 | public class TestUtils { 25 | 26 | @Path("/testService") 27 | public interface TestServiceClass { 28 | 29 | @GET 30 | @Path("/stringGetter/{a}/{b}") 31 | String stringGetter(@PathParam("a") String a, @PathParam("b") String b); 32 | } 33 | 34 | @Path("/testComplexService") 35 | public interface TestComplexServiceClass { 36 | 37 | @GET 38 | @Path("/queryGetter/") 39 | MyObject queryGetter(@QueryParam("x") Boolean x); 40 | 41 | @PUT 42 | @Path("simplePut") 43 | ImmutablesObject simplePut(DataObject dataObject); 44 | 45 | @POST 46 | @Path("/allOptionsPost/{a}") 47 | @Consumes(MediaType.APPLICATION_JSON) 48 | GenericObject allOptionsPost(@PathParam("a") String a, @QueryParam("b") Integer x, DataObject dataObject); 49 | } 50 | 51 | @Path("/ignoredParameters") 52 | public interface IgnoredParametersClass { 53 | 54 | @GET 55 | @Path("/stringGetter/{a}/{b}") 56 | String stringGetter(@CheckForNull Integer y, @PathParam("a") String a, @PathParam("b") String b); 57 | } 58 | 59 | @Path("/enumParametersClass") 60 | public interface EnumClass { 61 | 62 | @GET 63 | @Path("/enumGetter") 64 | MyEnum enumGetter(); 65 | 66 | @GET 67 | @Path("/enumPost") 68 | void enumPost(MyEnum dataParam); 69 | } 70 | 71 | @Path("/duplicateMethods") 72 | public interface DuplicateMethodNamesService { 73 | 74 | @GET 75 | @Path("/duplicate") 76 | String duplicate(); 77 | 78 | @GET 79 | @Path("/duplicate/{a}") 80 | String duplicate(@PathParam("a") String a); 81 | } 82 | 83 | @Path("/simple1") 84 | public interface SimpleService1 { 85 | 86 | @GET 87 | @Path("/method1") 88 | String method1(); 89 | } 90 | 91 | @Path("/simple2") 92 | public interface SimpleService2 { 93 | 94 | @GET 95 | @Path("/method2") 96 | String method2(); 97 | } 98 | 99 | @Path("/concreteObject") 100 | public class ConcreteObjectService { 101 | 102 | @GET 103 | public String noPathGetter() { 104 | return ""; 105 | }; 106 | } 107 | 108 | @Path("/plainTextService") 109 | public interface PlainTextService { 110 | 111 | @GET 112 | @Consumes(MediaType.TEXT_PLAIN) 113 | @Produces(MediaType.TEXT_PLAIN) 114 | @Path("/plainText") 115 | public String plainText(String dataBody); 116 | } 117 | 118 | public interface NoPathService { 119 | 120 | @GET 121 | @Path("foo") 122 | public String foo(); 123 | } 124 | 125 | public enum MyEnum { 126 | VALUE1, VALUE2 127 | } 128 | 129 | public static class MyObject { 130 | // Ensure json property overrides 131 | @JsonProperty("y") 132 | public MyObject getZ() { 133 | return null; 134 | } 135 | } 136 | 137 | public static class DataObject { 138 | public MyObject getY() { 139 | return null; 140 | } 141 | } 142 | 143 | public static class GenericObject { 144 | public T getY() { 145 | return null; 146 | } 147 | } 148 | 149 | @JsonDeserialize(as = ImmutableImmutablesObject.class) 150 | @JsonSerialize(as = ImmutableImmutablesObject.class) 151 | @Value.Immutable 152 | public interface ImmutablesObject { 153 | String y(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/java/com/palantir/code/ts/generator/ServiceGeneratorEteTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.fail; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.URISyntaxException; 13 | import java.util.Arrays; 14 | 15 | import org.apache.commons.io.FileUtils; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.rules.TemporaryFolder; 19 | 20 | import com.palantir.code.ts.generator.utils.TestUtils.SimpleService1; 21 | import com.palantir.code.ts.generator.utils.TestUtils.TestComplexServiceClass; 22 | 23 | public class ServiceGeneratorEteTest { 24 | 25 | @Rule 26 | public TemporaryFolder outputFolder = new TemporaryFolder(); 27 | 28 | @Test 29 | public void complexServiceTest() throws URISyntaxException, IOException { 30 | File actualDirectory = outputFolder.getRoot(); 31 | ImmutableTypescriptServiceGeneratorConfiguration config = ImmutableTypescriptServiceGeneratorConfiguration.builder() 32 | .copyrightHeader("// Copyright") 33 | .generatedMessage("// Generated") 34 | .generatedFolderLocation(actualDirectory) 35 | .genericEndpointReturnType("FooReturn<%s>") 36 | .generatedInterfacePrefix("I") 37 | .typescriptModule("ModuleName") 38 | .build(); 39 | ServiceGenerator serviceGenerator = new ServiceGenerator(config); 40 | serviceGenerator.generateTypescriptService(TestComplexServiceClass.class); 41 | 42 | File expectedDirectory = new File(this.getClass().getResource("/eteTestData/complexServiceTestOutput/").toURI()); 43 | assertDirectoriesEqual(expectedDirectory, actualDirectory); 44 | } 45 | 46 | @Test 47 | public void simpleServiceTest() throws URISyntaxException, IOException { 48 | File actualDirectory = outputFolder.getRoot(); 49 | ImmutableTypescriptServiceGeneratorConfiguration config = ImmutableTypescriptServiceGeneratorConfiguration.builder() 50 | .copyrightHeader("// Copyright") 51 | .generatedMessage("// Generated") 52 | .generatedFolderLocation(actualDirectory) 53 | .genericEndpointReturnType("FooReturn<%s>") 54 | .emitES6(true) 55 | .build(); 56 | ServiceGenerator serviceGenerator = new ServiceGenerator(config); 57 | serviceGenerator.generateTypescriptService(SimpleService1.class); 58 | 59 | File expectedDirectory = new File(this.getClass().getResource("/eteTestData/simpleServiceTestOutput/").toURI()); 60 | assertDirectoriesEqual(expectedDirectory, actualDirectory); 61 | } 62 | 63 | private void assertDirectoriesEqual(File a, File b) throws IOException { 64 | if (a.isDirectory() != b.isDirectory()) fail(); 65 | if (a.isDirectory()) { 66 | File[] aFiles = a.listFiles(); 67 | File[] bFiles = b.listFiles(); 68 | Arrays.sort(aFiles); 69 | Arrays.sort(bFiles); 70 | if (aFiles.length != bFiles.length) fail(); 71 | for (int i = 0; i < aFiles.length; i++) { 72 | assertDirectoriesEqual(aFiles[i], bFiles[i]); 73 | } 74 | } else { 75 | assertEquals(FileUtils.readLines(a), FileUtils.readLines(b)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | typescript-service-generator 2 | ==================== 3 | typescript-service-generator is a tool for creating strongly typed typescript http interfaces from jxrs annotated java interfaces. 4 | 5 | For each such java interface, a corresponding .ts class is generated that contains 6 | - Interfaces for all DTO objects used by the java interface, created using the typescript-generator project (https://github.com/vojtechhabarta/typescript-generator/) 7 | - A class, ready to be instantiated with a simple "bridge" object (defined later) that has a callable method for each endpoint in the java class 8 | 9 | For example for these Java files: 10 | 11 | ``` java 12 | package com.palantir.code.ts.generator.examples; 13 | 14 | import jakarta.ws.rs.Consumes; 15 | import jakarta.ws.rs.GET; 16 | import jakarta.ws.rs.Path; 17 | import jakarta.ws.rs.Produces; 18 | import jakarta.ws.rs.core.MediaType; 19 | 20 | @Path("/myservice") 21 | public interface MyService { 22 | @GET 23 | @Path("/foo_get") 24 | @Consumes(MediaType.APPLICATION_JSON) 25 | @Produces(MediaType.APPLICATION_JSON) 26 | MyObject helloWorld(); 27 | } 28 | ``` 29 | ``` java 30 | package com.palantir.code.ts.generator.examples; 31 | 32 | /** 33 | * Note that MyObject is jackson serializable 34 | */ 35 | public class MyObject { 36 | private String payload = "hello world"; 37 | 38 | public String getPayload() { 39 | return payload; 40 | } 41 | } 42 | ``` 43 | 44 | typescript-service-generator outputs this TypeScript file: 45 | ``` typescript 46 | // A potential copyright header 47 | // A desired generated message 48 | module Foundry.Http.MyService { 49 | 50 | export interface IMyObject { 51 | payload: string; 52 | } 53 | 54 | export interface IMyService { 55 | helloWorld(): HttpTypeWrapper; 56 | } 57 | 58 | export class MyService implements IMyService { 59 | 60 | private httpApiBridge: IHttpApiBridge; 61 | constructor(restApiBridge: IHttpApiBridge) { 62 | this.httpApiBridge = restApiBridge; 63 | } 64 | 65 | public helloWorld() { 66 | var httpCallData = { 67 | serviceIdentifier: "myService", 68 | endpointPath: "myservice/foo_get", 69 | method: "GET", 70 | mediaType: "application/json", 71 | requiredHeaders: [], 72 | pathArguments: [], 73 | queryArguments: { 74 | }, 75 | data: null 76 | }; 77 | return this.httpApiBridge.callEndpoint(httpCallData); 78 | } 79 | } 80 | } 81 | ``` 82 | See MyServiceGenerator.java for all details on this example 83 | 84 | Instantiating the generated class requires an implementation of IHttpApiBridge 85 | 86 | IHttpApiBridge 87 | ----- 88 | This is an interface that serves as a "bridge" between the generated typescript service classes. The contract of this interface is that it should know how to issue http calls given the inputs, and returns an object of a configurable type (see TypescriptServiceGeneratorConfiguration.genericEndpointReturnType). Any generated service class can be instantiated by constructing it with an implementation of the httpApiBridge. For an example, see the end of output/angularHttpApiBridge.ts 89 | 90 | Assumptions 91 | ----- 92 | The typescript-service-generator generates 1.8.x+ typescript code. This restriction is inherited from one of its dependencies, [typescript-generator](https://github.com/vojtechhabarta/typescript-generator). 93 | 94 | Contributing 95 | ----- 96 | - Write your code 97 | - Add tests for new functionality 98 | - Fill out the [Individual](https://github.com/palantir/typescript-service-generator/blob/master/Palantir_Individual_Contributor_License_Agreement.pdf?raw=true) or [Corporate](https://github.com/palantir/typescript-service-generator/blob/master/Palantir_Corporate_Contributor_License_Agreement.pdf?raw=true) Contributor License Agreement and send it to [opensource@palantir.com](mailto:opensource@palantir.com) 99 | - You can do this easily on a Mac by using the Tools - Annotate - Signature feature in Preview. 100 | - Submit a pull request 101 | 102 | Depending on Published Artifacts 103 | ----- 104 | typescript-service-generator is hosted on [bintray](https://bintray.com/palantir/releases/typescript-service-generator/view). To include in a gradle project: 105 | 106 | Add bintray to your repository list: 107 | 108 | ``` 109 | repositories { maven { url 'https://dl.bintray.com/palantir/releases/' } } 110 | ``` 111 | 112 | Add the typescript-service-generator-core dependency to projects: 113 | 114 | ``` 115 | dependencies { compile "com.palantir.ts:typescript-service-generator-core:x.x.x" } 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/ServiceGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import java.io.File; 8 | import java.io.FileNotFoundException; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.OutputStream; 12 | import java.lang.reflect.Type; 13 | import java.util.List; 14 | 15 | import org.apache.commons.io.IOUtils; 16 | 17 | import com.google.common.base.Joiner; 18 | import com.google.common.collect.Lists; 19 | import com.palantir.code.ts.generator.model.ServiceModel; 20 | 21 | public final class ServiceGenerator { 22 | 23 | private final TypescriptServiceGeneratorConfiguration settings; 24 | 25 | public ServiceGenerator(TypescriptServiceGeneratorConfiguration settings) { 26 | this.settings = settings; 27 | 28 | // Write out httpApiBridge file 29 | String bridgeFile = "httpApiBridge.ts"; 30 | IndentedOutputWriter writer; 31 | try { 32 | writer = new IndentedOutputWriter(new FileOutputStream(new File(settings.generatedFolderLocation(), bridgeFile)), settings); 33 | } catch (FileNotFoundException e) { 34 | throw new RuntimeException(e); 35 | } 36 | beginService(writer, null); 37 | 38 | List bridgeFileLines = null; 39 | try { 40 | bridgeFileLines = IOUtils.readLines(this.getClass().getClassLoader().getResourceAsStream(bridgeFile)); 41 | } catch (IOException e) { 42 | throw new RuntimeException(e); 43 | } 44 | String generatedInterfacePrefix = settings.generatedInterfacePrefix(); 45 | bridgeFileLines = Lists.newArrayList(String.format(Joiner.on(settings.getSettings().newline).join(bridgeFileLines), generatedInterfacePrefix, generatedInterfacePrefix, generatedInterfacePrefix, String.format(settings.genericEndpointReturnType(), "T")).split("\n")); 46 | for (String line : bridgeFileLines) { 47 | writer.writeLine(line); 48 | } 49 | 50 | endService(writer); 51 | } 52 | 53 | public void generateTypescriptService(Class clazz) { 54 | this.generateTypescriptService(clazz, Lists.newArrayList()); 55 | } 56 | 57 | public void generateTypescriptService(Class clazz, List additionalClassesToOutput) { 58 | this.generateTypescriptService(clazz, additionalClassesToOutput, new Class[0]); 59 | } 60 | 61 | public void generateTypescriptService(Class serviceClass, List additionalClassesToOutput, Class... serviceClassesToMerge) { 62 | OutputStream output = null; 63 | String firstSimpleName = serviceClass.getSimpleName(); 64 | try { 65 | output = new FileOutputStream(new File(settings.generatedFolderLocation(), Character.toLowerCase(firstSimpleName.charAt(0)) + firstSimpleName.substring(1) + ".ts")); 66 | } catch (FileNotFoundException e) { 67 | throw new RuntimeException(e); 68 | } 69 | IndentedOutputWriter writer = new IndentedOutputWriter(output, settings); 70 | beginService(writer, firstSimpleName); 71 | 72 | ServiceModel serviceModel = new ServiceClassParser().parseServiceClass(serviceClass, settings, serviceClassesToMerge); 73 | ServiceEmitter serviceEndpointEmitter = new ServiceEmitter(serviceModel, settings, writer); 74 | 75 | if (settings.emitES6()) { 76 | String generatedInterfacePrefix = settings.generatedInterfacePrefix(); 77 | String endpointOptionsName = generatedInterfacePrefix + "HttpEndpointOptions"; 78 | String apiBridgeName = generatedInterfacePrefix + "HttpApiBridge"; 79 | writer.writeLine("import { " + endpointOptionsName + ", " + apiBridgeName + " } from \"./httpApiBridge\";"); 80 | } 81 | 82 | serviceEndpointEmitter.emitTypescriptTypes(settings, additionalClassesToOutput); 83 | serviceEndpointEmitter.emitTypescriptInterface(); 84 | serviceEndpointEmitter.emitTypescriptClass(); 85 | 86 | endService(writer); 87 | } 88 | 89 | private void beginService(IndentedOutputWriter writer, String subModuleName) { 90 | writer.writeLine(settings.copyrightHeader()); 91 | writer.writeLine(settings.generatedMessage()); 92 | if (settings.typescriptModule().isPresent()) { 93 | String moduleName = settings.typescriptModule().get(); 94 | if (subModuleName != null) { 95 | moduleName += "." + subModuleName; 96 | } 97 | writer.writeLine("module " + moduleName + " {"); 98 | writer.increaseIndent(); 99 | } 100 | } 101 | 102 | private void endService(IndentedOutputWriter writer) { 103 | if (settings.typescriptModule().isPresent()) { 104 | writer.decreaseIndent(); 105 | writer.writeLine("}"); 106 | } 107 | writer.close(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="-Xmx1024m" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # This file was generated by the excavator check 'excavator/manage-circleci' as specified in .circleci/template.sh. 2 | # To request a modification to the general template, file an issue on Excavator. 3 | # To manually manage the CircleCI configuration for this project, remove the .circleci/template.sh file. 4 | 5 | version: 2.1 6 | jobs: 7 | 8 | check: 9 | docker: [{ image: 'cimg/openjdk:11.0.10-node' }] 10 | resource_class: large 11 | environment: 12 | CIRCLE_TEST_REPORTS: /home/circleci/junit 13 | CIRCLE_ARTIFACTS: /home/circleci/artifacts 14 | GRADLE_OPTS: -Dorg.gradle.workers.max=2 -Dorg.gradle.jvmargs='-Xmx2g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' 15 | _JAVA_OPTIONS: -XX:ActiveProcessorCount=4 -XX:MaxRAM=8g -XX:+CrashOnOutOfMemoryError -XX:ErrorFile=/home/circleci/artifacts/hs_err_pid%p.log -XX:HeapDumpPath=/home/circleci/artifacts 16 | steps: 17 | - checkout 18 | - run: 19 | name: delete_unrelated_tags 20 | command: | 21 | ALL_TAGS=$(git tag --points-at HEAD) 22 | 23 | if [ -z "$ALL_TAGS" ]; then 24 | echo "No-op as there are no tags on the current commit ($(git rev-parse HEAD))" 25 | exit 0 26 | fi 27 | 28 | if [ -z "${CIRCLE_TAG:+x}" ]; then 29 | echo "Non-tag build, deleting all tags which point to HEAD: [${ALL_TAGS/$'\n'/,}]" 30 | echo "$ALL_TAGS" | while read -r TAG; do git tag -d "$TAG" 1>/dev/null; done 31 | exit 0 32 | fi 33 | 34 | TAGS_TO_DELETE=$(echo "$ALL_TAGS" | grep -v "^$CIRCLE_TAG$" || :) 35 | if [ -z "$TAGS_TO_DELETE" ]; then 36 | echo "No-op as exactly one tag ($CIRCLE_TAG) points to HEAD" 37 | exit 0 38 | fi 39 | 40 | echo "Detected tag build, deleting all tags except '$CIRCLE_TAG' which point to HEAD: [${TAGS_TO_DELETE/$'\n'/,}]" 41 | echo "$TAGS_TO_DELETE" | while read -r TAG; do git tag -d "$TAG" 1>/dev/null; done 42 | - restore_cache: { key: 'gradle-wrapper-v1-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}' } 43 | - restore_cache: { key: 'check-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' } 44 | - run: 45 | name: check-setup 46 | command: | 47 | if [ -x .circleci/check-setup.sh ]; then 48 | echo "Running check-setup" && .circleci/check-setup.sh && echo "check-setup complete" 49 | fi 50 | - run: ./gradlew --parallel --stacktrace --continue --max-workers=2 check -Porg.gradle.java.installations.fromEnv=JAVA_8_HOME,JAVA_11_HOME,JAVA_15_HOME,JAVA_17_HOME,JAVA_HOME 51 | - persist_to_workspace: 52 | root: /home/circleci 53 | paths: [ project ] 54 | - save_cache: 55 | key: 'gradle-wrapper-v1-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}' 56 | paths: [ ~/.gradle/wrapper ] 57 | - save_cache: 58 | key: 'check-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' 59 | paths: [ ~/.gradle/caches ] 60 | - run: 61 | command: mkdir -p ~/junit && find . -type f -regex ".*/build/.*TEST.*xml" -exec cp --parents {} ~/junit/ \; 62 | when: always 63 | - store_test_results: { path: ~/junit } 64 | - store_artifacts: { path: ~/artifacts } 65 | 66 | trial-publish: 67 | docker: [{ image: 'cimg/openjdk:11.0.10-node' }] 68 | resource_class: medium 69 | environment: 70 | CIRCLE_TEST_REPORTS: /home/circleci/junit 71 | CIRCLE_ARTIFACTS: /home/circleci/artifacts 72 | GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.jvmargs='-Xmx2g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' 73 | _JAVA_OPTIONS: -XX:ActiveProcessorCount=2 -XX:MaxRAM=4g -XX:+CrashOnOutOfMemoryError -XX:ErrorFile=/home/circleci/artifacts/hs_err_pid%p.log -XX:HeapDumpPath=/home/circleci/artifacts 74 | steps: 75 | - checkout 76 | - run: 77 | name: delete_unrelated_tags 78 | command: | 79 | ALL_TAGS=$(git tag --points-at HEAD) 80 | 81 | if [ -z "$ALL_TAGS" ]; then 82 | echo "No-op as there are no tags on the current commit ($(git rev-parse HEAD))" 83 | exit 0 84 | fi 85 | 86 | if [ -z "${CIRCLE_TAG:+x}" ]; then 87 | echo "Non-tag build, deleting all tags which point to HEAD: [${ALL_TAGS/$'\n'/,}]" 88 | echo "$ALL_TAGS" | while read -r TAG; do git tag -d "$TAG" 1>/dev/null; done 89 | exit 0 90 | fi 91 | 92 | TAGS_TO_DELETE=$(echo "$ALL_TAGS" | grep -v "^$CIRCLE_TAG$" || :) 93 | if [ -z "$TAGS_TO_DELETE" ]; then 94 | echo "No-op as exactly one tag ($CIRCLE_TAG) points to HEAD" 95 | exit 0 96 | fi 97 | 98 | echo "Detected tag build, deleting all tags except '$CIRCLE_TAG' which point to HEAD: [${TAGS_TO_DELETE/$'\n'/,}]" 99 | echo "$TAGS_TO_DELETE" | while read -r TAG; do git tag -d "$TAG" 1>/dev/null; done 100 | - restore_cache: { key: 'gradle-wrapper-v1-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}' } 101 | - restore_cache: { key: 'trial-publish-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' } 102 | - run: ./gradlew --stacktrace publishToMavenLocal -Porg.gradle.java.installations.fromEnv=JAVA_8_HOME,JAVA_11_HOME,JAVA_15_HOME,JAVA_17_HOME,JAVA_HOME 103 | - run: 104 | command: git status --porcelain 105 | when: always 106 | - save_cache: 107 | key: 'trial-publish-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' 108 | paths: [ ~/.gradle/caches ] 109 | - store_test_results: { path: ~/junit } 110 | - store_artifacts: { path: ~/artifacts } 111 | 112 | publish: 113 | docker: [{ image: 'cimg/openjdk:11.0.10-node' }] 114 | resource_class: medium 115 | environment: 116 | CIRCLE_TEST_REPORTS: /home/circleci/junit 117 | CIRCLE_ARTIFACTS: /home/circleci/artifacts 118 | GRADLE_OPTS: -Dorg.gradle.workers.max=1 -Dorg.gradle.jvmargs='-Xmx2g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' 119 | _JAVA_OPTIONS: -XX:ActiveProcessorCount=2 -XX:MaxRAM=4g -XX:+CrashOnOutOfMemoryError -XX:ErrorFile=/home/circleci/artifacts/hs_err_pid%p.log -XX:HeapDumpPath=/home/circleci/artifacts 120 | steps: 121 | - attach_workspace: { at: /home/circleci } 122 | - restore_cache: { key: 'gradle-wrapper-v1-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}' } 123 | - restore_cache: { key: 'publish-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' } 124 | - deploy: 125 | command: ./gradlew --parallel --stacktrace --continue publish -Porg.gradle.java.installations.fromEnv=JAVA_8_HOME,JAVA_11_HOME,JAVA_15_HOME,JAVA_17_HOME,JAVA_HOME 126 | - run: 127 | command: git status --porcelain 128 | when: always 129 | - save_cache: 130 | key: 'publish-gradle-cache-v1-{{ checksum "versions.props" }}-{{ checksum "build.gradle" }}' 131 | paths: [ ~/.gradle/caches ] 132 | - store_test_results: { path: ~/junit } 133 | - store_artifacts: { path: ~/artifacts } 134 | 135 | 136 | workflows: 137 | version: 2 138 | build: 139 | jobs: 140 | - check: 141 | filters: { tags: { only: /.*/ } } 142 | 143 | - trial-publish: 144 | filters: { branches: { ignore: develop } } 145 | 146 | - publish: 147 | requires: [ check, trial-publish ] 148 | filters: { tags: { only: /.*/ }, branches: { only: develop } } -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/TypescriptServiceGeneratorConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import java.io.File; 8 | import java.lang.annotation.Annotation; 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.ParameterizedType; 11 | import java.lang.reflect.Type; 12 | import java.net.URI; 13 | import java.util.ArrayList; 14 | import java.util.HashSet; 15 | import java.util.LinkedHashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | 20 | import javax.annotation.CheckForNull; 21 | import javax.annotation.Nullable; 22 | 23 | import org.immutables.value.Value; 24 | 25 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 26 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 27 | import com.google.common.base.Optional; 28 | import com.google.common.collect.Lists; 29 | import com.google.common.collect.Maps; 30 | import com.google.common.collect.Sets; 31 | 32 | import cz.habarta.typescript.generator.GenericsTypeProcessor; 33 | import cz.habarta.typescript.generator.JsonLibrary; 34 | import cz.habarta.typescript.generator.Settings; 35 | import cz.habarta.typescript.generator.TsType; 36 | import cz.habarta.typescript.generator.TypeProcessor; 37 | import cz.habarta.typescript.generator.TypeScriptFileType; 38 | import cz.habarta.typescript.generator.TypeScriptOutputKind; 39 | import cz.habarta.typescript.generator.ext.EnumConstantsExtension; 40 | 41 | @Value.Immutable 42 | @Value.Style(visibility = Value.Style.ImplementationVisibility.PUBLIC) 43 | @JsonDeserialize(as = ImmutableTypescriptServiceGeneratorConfiguration.class) 44 | @JsonSerialize(as = ImmutableTypescriptServiceGeneratorConfiguration.class) 45 | public abstract class TypescriptServiceGeneratorConfiguration { 46 | 47 | /** 48 | * A copyright header. 49 | */ 50 | @Value.Default 51 | public String copyrightHeader() { 52 | return ""; 53 | } 54 | 55 | /** 56 | * See {@link TypeProcessor}, Enables custom parsing of Java types into Typescript types. 57 | * A user might want some type that their api references to parse to Typescript in a special way. 58 | */ 59 | @Value.Default 60 | public TypeProcessor customTypeProcessor() { 61 | return new TypeProcessor() { 62 | @Override 63 | public Result processType(Type javaType, Context context) { 64 | return null; 65 | } 66 | }; 67 | } 68 | 69 | @Value.Default 70 | public Map customTypeNaming() { 71 | return new LinkedHashMap<>(); 72 | } 73 | 74 | public abstract Optional customTypeNamingFunction(); 75 | 76 | /** 77 | * Provides a strategy for resolving duplicate method names 78 | * @return 79 | */ 80 | @Value.Default 81 | public DuplicateMethodNameResolver duplicateEndpointNameResolver() { 82 | return new DuplicateMethodNameResolver() { 83 | @Override 84 | public Map resolveDuplicateNames(List methodsWithSameName) { 85 | return Maps.newHashMap(); 86 | } 87 | }; 88 | } 89 | 90 | /** 91 | * If true, emit method signatures even when it would create duplicate java names (results in typescript that does not compile). 92 | * If false, emit a warning instead. 93 | */ 94 | @Value.Default 95 | public boolean emitDuplicateJavaMethodNames() { 96 | return false; 97 | } 98 | 99 | /** 100 | * If true, emit typings that can be used with module loading systems (no Typescript module). 101 | * If false, emit typings that correspond to the Typescript module to prefix code under. 102 | */ 103 | @Value.Default 104 | public boolean emitES6() { 105 | return false; 106 | } 107 | 108 | /** 109 | * A Java format string, expected to have exactly one %s where a generic should be placed. 110 | * Specifies what return types should look like. 111 | * For example, suppose a Java endpoint returned a string, then for a value of "Foo<Bar<%s>>" for this property, 112 | * the generated Typescript endpoint would return type Foo<string>. 113 | */ 114 | @Value.Parameter 115 | public abstract String genericEndpointReturnType(); 116 | 117 | /** 118 | * A set of annotations that should be ignored on Java method declarations. 119 | * Ignored here means that the Typescript method is generated as if the annotated Java argument did not exist. 120 | */ 121 | @Value.Default 122 | public Set> ignoredAnnotations() { 123 | return new HashSet<>(); 124 | } 125 | 126 | /** 127 | * A list of annotations that will generate fields as optional in the typescript definitions. 128 | */ 129 | @Value.Default 130 | @SuppressWarnings("unchecked") 131 | public List> optionalAnnotations() { 132 | return Lists.newArrayList(CheckForNull.class, Nullable.class); 133 | } 134 | 135 | /** 136 | * The Typescript module to prefix all generated code under, for example: "MyProject.GeneratedCode" 137 | */ 138 | @Value.Parameter 139 | public abstract Optional typescriptModule(); 140 | 141 | /** 142 | * A message to be displayed at the top of every generated file, perhaps informing users that the file is autogenerated. 143 | */ 144 | @Value.Default 145 | public String generatedMessage() { 146 | return ""; 147 | } 148 | 149 | /** 150 | * A Set of classes that Typescript types should not be generated for. 151 | */ 152 | @Value.Default 153 | public Set ignoredClasses() { 154 | return Sets.newHashSet(); 155 | } 156 | 157 | /** 158 | * The pre-existing folder location that all generated files should be placed into. 159 | */ 160 | @Value.Parameter 161 | public abstract File generatedFolderLocation(); 162 | 163 | /** 164 | * The prefix to put before all generated interfaces. 165 | */ 166 | @Value.Default 167 | public String generatedInterfacePrefix() { 168 | return ""; 169 | } 170 | 171 | /** 172 | * A filter for whether or not a method should be parsed from a class. 173 | */ 174 | @Value.Default 175 | public MethodFilter methodFilter() { 176 | return new MethodFilter() { 177 | @Override 178 | public boolean shouldGenerateMethod(Class parentClass, Method method) { 179 | return true; 180 | } 181 | }; 182 | } 183 | 184 | public TypeProcessor getOverridingTypeParser() { 185 | TypeProcessor defaultTypeProcessor = new TypeProcessor() { 186 | @Override 187 | public Result processType(Type javaType, Context context) { 188 | TsType ret = null; 189 | if (javaType instanceof ParameterizedType) { 190 | ParameterizedType param = (ParameterizedType) javaType; 191 | if (param.getRawType() == Optional.class) { 192 | Type arg = param.getActualTypeArguments()[0]; 193 | Result contextResponse = context.processType(arg); 194 | if (contextResponse != null) { 195 | return new Result(contextResponse.getTsType().optional(), contextResponse.getDiscoveredClasses()); 196 | } else { 197 | return null; 198 | } 199 | } 200 | } else if (javaType == URI.class) { 201 | ret = TsType.String; 202 | } if (ret == null) { 203 | return null; 204 | } else { 205 | return new Result(ret, new ArrayList>()); 206 | } 207 | } 208 | }; 209 | return new TypeProcessor.Chain(Lists.newArrayList(customTypeProcessor(), defaultTypeProcessor)); 210 | } 211 | 212 | public Settings getSettings() { 213 | Settings settings = new Settings(); 214 | 215 | TypeProcessor genericTypeProcessor = new GenericsTypeProcessor(); 216 | List typeProcessors = new ArrayList<>(); 217 | typeProcessors.add(customTypeProcessor()); 218 | typeProcessors.add(getOverridingTypeParser()); 219 | typeProcessors.add(genericTypeProcessor); 220 | settings.customTypeProcessor = new TypeProcessor.Chain(typeProcessors); 221 | settings.customTypeNaming = customTypeNaming(); 222 | if (customTypeNamingFunction().isPresent()) { 223 | settings.customTypeNamingFunction = customTypeNamingFunction().get(); 224 | } 225 | settings.addTypeNamePrefix = generatedInterfacePrefix(); 226 | settings.sortDeclarations = true; 227 | settings.noFileComment = true; 228 | // behave like 0.9.0; also fixes when subtypes use generics 229 | settings.disableTaggedUnions = true; 230 | settings.jsonLibrary = JsonLibrary.jackson2; 231 | settings.optionalAnnotations = optionalAnnotations(); 232 | settings.outputKind = TypeScriptOutputKind.global; 233 | settings.outputFileType = TypeScriptFileType.implementationFile; 234 | settings.extensions = Lists.newArrayList(new EnumConstantsExtension()); 235 | 236 | return settings; 237 | } 238 | 239 | public interface DuplicateMethodNameResolver { 240 | /** 241 | * Takes a list of methods that all have the same name, returns a map of resolved names, or 242 | * null if the names can't be resolved 243 | */ 244 | Map resolveDuplicateNames(List methodsWithSameName); 245 | } 246 | 247 | public interface MethodFilter { 248 | /** 249 | * Return true if method should be generated, false otherwise 250 | */ 251 | boolean shouldGenerateMethod(Class parentClass, Method method); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/ServiceClassParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.Parameter; 10 | import java.lang.reflect.Type; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | 16 | import jakarta.ws.rs.Consumes; 17 | import jakarta.ws.rs.DELETE; 18 | import jakarta.ws.rs.GET; 19 | import jakarta.ws.rs.HeaderParam; 20 | import jakarta.ws.rs.OPTIONS; 21 | import jakarta.ws.rs.POST; 22 | import jakarta.ws.rs.PUT; 23 | import jakarta.ws.rs.Path; 24 | import jakarta.ws.rs.PathParam; 25 | import jakarta.ws.rs.Produces; 26 | import jakarta.ws.rs.QueryParam; 27 | 28 | import org.apache.commons.lang3.reflect.MethodUtils; 29 | 30 | import com.google.common.collect.ArrayListMultimap; 31 | import com.google.common.collect.ImmutableList; 32 | import com.google.common.collect.Lists; 33 | import com.google.common.collect.Maps; 34 | import com.google.common.collect.Multimap; 35 | import com.google.common.collect.Sets; 36 | import com.palantir.code.ts.generator.TypescriptServiceGeneratorConfiguration.MethodFilter; 37 | import com.palantir.code.ts.generator.model.ImmutableInnerServiceModel; 38 | import com.palantir.code.ts.generator.model.ImmutableServiceEndpointModel; 39 | import com.palantir.code.ts.generator.model.ImmutableServiceEndpointParameterModel; 40 | import com.palantir.code.ts.generator.model.ImmutableServiceModel; 41 | import com.palantir.code.ts.generator.model.ServiceEndpointModel; 42 | import com.palantir.code.ts.generator.model.ServiceEndpointParameterModel; 43 | import com.palantir.code.ts.generator.model.ServiceModel; 44 | import com.palantir.code.ts.generator.utils.PathUtils; 45 | 46 | import cz.habarta.typescript.generator.TsType; 47 | import cz.habarta.typescript.generator.TypeScriptGenerator; 48 | import cz.habarta.typescript.generator.compiler.ModelCompiler; 49 | 50 | 51 | public final class ServiceClassParser { 52 | @SuppressWarnings("unchecked") 53 | private final static List> ANNOTATION_CLASSES = Lists.newArrayList(POST.class, GET.class, DELETE.class, PUT.class, OPTIONS.class); 54 | 55 | public Set getAllServiceMethods(Class serviceClass, MethodFilter methodFilter) { 56 | Set serviceMethods = Sets.newHashSet(); 57 | for (Class annotation : ANNOTATION_CLASSES) { 58 | for (Method method : MethodUtils.getMethodsListWithAnnotation(serviceClass, annotation)) { 59 | if (methodFilter.shouldGenerateMethod(serviceClass, method)) { 60 | serviceMethods.add(method); 61 | } 62 | } 63 | } 64 | return serviceMethods; 65 | } 66 | 67 | public ServiceModel parseServiceClass(Class mainServiceClass, TypescriptServiceGeneratorConfiguration settings, Class... serviceClassesToMerge) { 68 | List> serviceClazzes = Lists.newArrayList(mainServiceClass); 69 | serviceClazzes.addAll(Lists.newArrayList(serviceClassesToMerge)); 70 | ImmutableServiceModel.Builder serviceModel = ImmutableServiceModel.builder(); 71 | serviceModel.name(mainServiceClass.getSimpleName()); 72 | for (Class serviceClass : serviceClazzes) { 73 | ImmutableInnerServiceModel.Builder innerServiceModel = ImmutableInnerServiceModel.builder(); 74 | Path servicePathAnnotation = serviceClass.getAnnotation(Path.class); 75 | innerServiceModel.servicePath(servicePathAnnotation == null ? "" : PathUtils.trimSlashes(servicePathAnnotation.value())); 76 | innerServiceModel.name(serviceClass.getSimpleName()); 77 | 78 | Set serviceMethods = getAllServiceMethods(serviceClass, settings.methodFilter()); 79 | // find and stores all types that are referenced by this service 80 | Set referencedTypes = Sets.newHashSet(); 81 | for (Method method : serviceMethods) { 82 | referencedTypes.addAll(getTypesFromEndpoint(method, settings)); 83 | } 84 | serviceModel.addAllReferencedTypes(referencedTypes); 85 | 86 | ModelCompiler compiler = new TypeScriptGenerator(settings.getSettings()).getModelCompiler(); 87 | 88 | List endpointModels = Lists.newArrayList(); 89 | endpointModels = computeEndpointModels(serviceMethods, compiler, settings); 90 | 91 | Collections.sort(endpointModels); 92 | innerServiceModel.endpointModels(endpointModels); 93 | serviceModel.addInnerServiceModels(innerServiceModel.build()); 94 | } 95 | return serviceModel.build(); 96 | } 97 | 98 | private static List computeEndpointModels(Set endpoints, ModelCompiler compiler, TypescriptServiceGeneratorConfiguration settings) { 99 | Multimap endpointNameMap = ArrayListMultimap.create(); 100 | Map endpointNameGetter = Maps.newHashMap(); 101 | for (Method endpoint : endpoints) { 102 | endpointNameMap.put(endpoint.getName(), endpoint); 103 | endpointNameGetter.put(endpoint, endpoint.getName()); 104 | } 105 | for (String endpointName : endpointNameMap.keySet()) { 106 | List maybeDuplicates = Lists.newArrayList(endpointNameMap.get(endpointName).iterator()); 107 | if (maybeDuplicates != null && maybeDuplicates.size() > 1) { 108 | endpointNameGetter.putAll(settings.duplicateEndpointNameResolver().resolveDuplicateNames(maybeDuplicates)); 109 | } 110 | } 111 | List result = Lists.newArrayList(); 112 | for (Method endpoint : endpoints) { 113 | ImmutableServiceEndpointModel.Builder ret = ImmutableServiceEndpointModel.builder(); 114 | ret.endpointName(endpointNameGetter.get(endpoint)); 115 | ret.javaReturnType(endpoint.getGenericReturnType()); 116 | ret.tsReturnType(compiler.javaToTypeScript(endpoint.getGenericReturnType())); 117 | ret.endpointMethodType(getMethodType(endpoint)); 118 | 119 | String annotationValue = ""; 120 | if (endpoint.getAnnotation(Path.class) != null) { 121 | annotationValue = endpoint.getAnnotation(Path.class).value(); 122 | } 123 | ret.endpointPath(PathUtils.trimSlashes(annotationValue)); 124 | Consumes consumes = endpoint.getAnnotation(Consumes.class); 125 | if (consumes != null) { 126 | if (consumes.value().length > 1) { 127 | throw new IllegalArgumentException("Don't know how to handle an endpoint with multiple consume types"); 128 | } 129 | if (consumes.value().length == 1) { 130 | ret.endpointRequestMediaType(consumes.value()[0]); 131 | } 132 | } 133 | Produces produces = endpoint.getAnnotation(Produces.class); 134 | if (produces != null) { 135 | if (produces.value().length > 1) { 136 | throw new IllegalArgumentException("Don't know how to handle an endpoint with multiple produce types"); 137 | } 138 | if (produces.value().length == 1) { 139 | ret.endpointResponseMediaType(produces.value()[0]); 140 | } 141 | } 142 | 143 | List, Annotation>> annotationList = getParamterAnnotationMaps(endpoint); 144 | List mandatoryParameters = Lists.newArrayList(); 145 | List optionalParameters = Lists.newArrayList(); 146 | int annotationListIndex = 0; 147 | for (Type javaParameterType : endpoint.getGenericParameterTypes()) { 148 | Map, Annotation> annotations = annotationList.get(annotationListIndex); 149 | ImmutableServiceEndpointParameterModel.Builder parameterModel = ImmutableServiceEndpointParameterModel.builder(); 150 | 151 | // if parameter is annotated with any ignored annotations, skip it entirely 152 | if (!Collections.disjoint(annotations.keySet(), settings.ignoredAnnotations())) { 153 | annotationListIndex++; 154 | continue; 155 | } 156 | 157 | PathParam path = (PathParam) annotations.get(PathParam.class); 158 | if (path != null) { 159 | parameterModel.pathParam(path.value()); 160 | } 161 | HeaderParam header = (HeaderParam) annotations.get(HeaderParam.class); 162 | if (header != null) { 163 | parameterModel.headerParam(header.value()); 164 | } 165 | QueryParam query = (QueryParam) annotations.get(QueryParam.class); 166 | if (query != null) { 167 | parameterModel.queryParam(query.value()); 168 | } 169 | 170 | parameterModel.javaType(javaParameterType); 171 | TsType tsType = compiler.javaToTypeScript(javaParameterType); 172 | parameterModel.tsType(tsType); 173 | if (tsType instanceof TsType.OptionalType || query != null) { 174 | optionalParameters.add(parameterModel.build()); 175 | } else { 176 | mandatoryParameters.add(parameterModel.build()); 177 | } 178 | annotationListIndex++; 179 | } 180 | 181 | ret.parameters(ImmutableList. builder().addAll(mandatoryParameters).addAll(optionalParameters).build()); 182 | result.add(ret.build()); 183 | } 184 | return result; 185 | } 186 | 187 | private static String getMethodType(Method endpoint) { 188 | for (Class annotationClass : ANNOTATION_CLASSES) { 189 | if (endpoint.getAnnotation(annotationClass) != null) { 190 | return annotationClass.getSimpleName(); 191 | } 192 | } 193 | throw new IllegalArgumentException("All endpoints should specify their method type, but one didn't: " + endpoint); 194 | } 195 | 196 | private static List, Annotation>> getParamterAnnotationMaps(Method endpoint) { 197 | List, Annotation>> ret = Lists.newArrayList(); 198 | Annotation[][] array = endpoint.getParameterAnnotations(); 199 | for (int i = 0; i < array.length; i++) { 200 | Annotation[] annotations = array[i]; 201 | Map, Annotation> map = Maps.newHashMap(); 202 | for (Annotation a : annotations) { 203 | map.put(a.annotationType(), a); 204 | } 205 | ret.add(map); 206 | } 207 | return ret; 208 | } 209 | 210 | private static Set getTypesFromEndpoint(Method endpoint, TypescriptServiceGeneratorConfiguration settings) { 211 | Set ret = Sets.newHashSet(); 212 | ret.add(endpoint.getReturnType()); 213 | ret.add(endpoint.getGenericReturnType()); 214 | 215 | List, Annotation>> parameterAnnotationMaps = getParamterAnnotationMaps(endpoint); 216 | Parameter[] parameters = endpoint.getParameters(); 217 | for (int i = 0; i < parameterAnnotationMaps.size(); i++) { 218 | Parameter parameter = parameters[i]; 219 | Map, Annotation> parameterAnnotationMap = parameterAnnotationMaps.get(i); 220 | // if parameter is annotated with any ignored annotations, skip it entirely 221 | if (Collections.disjoint(parameterAnnotationMap.keySet(), settings.ignoredAnnotations())) { 222 | ret.add(parameter.getParameterizedType()); 223 | } 224 | } 225 | return ret; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/java/com/palantir/code/ts/generator/ServiceEmitterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.File; 12 | 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | 16 | import com.google.common.collect.Lists; 17 | import com.palantir.code.ts.generator.model.ServiceModel; 18 | import com.palantir.code.ts.generator.utils.TestUtils.ConcreteObjectService; 19 | import com.palantir.code.ts.generator.utils.TestUtils.DuplicateMethodNamesService; 20 | import com.palantir.code.ts.generator.utils.TestUtils.EnumClass; 21 | import com.palantir.code.ts.generator.utils.TestUtils.MyObject; 22 | import com.palantir.code.ts.generator.utils.TestUtils.SimpleService1; 23 | import com.palantir.code.ts.generator.utils.TestUtils.SimpleService2; 24 | import com.palantir.code.ts.generator.utils.TestUtils.TestComplexServiceClass; 25 | import com.palantir.code.ts.generator.utils.TestUtils.TestServiceClass; 26 | 27 | public class ServiceEmitterTest { 28 | 29 | private TypescriptServiceGeneratorConfiguration settings; 30 | private IndentedOutputWriter writer; 31 | private ByteArrayOutputStream stream; 32 | private ServiceClassParser serviceClassParser; 33 | 34 | @Before 35 | public void before() { 36 | this.settings = ImmutableTypescriptServiceGeneratorConfiguration.builder() 37 | .copyrightHeader("") 38 | .emitDuplicateJavaMethodNames(false) 39 | .generatedFolderLocation(new File("")) 40 | .generatedMessage("") 41 | .genericEndpointReturnType("FooType<%s>") 42 | .typescriptModule("") 43 | .build(); 44 | this.stream = new ByteArrayOutputStream(); 45 | this.writer = new IndentedOutputWriter(stream, settings); 46 | this.serviceClassParser = new ServiceClassParser(); 47 | } 48 | 49 | @Test 50 | public void testComplexServiceClassEmitTypes() { 51 | ServiceModel model = serviceClassParser.parseServiceClass(TestComplexServiceClass.class, settings); 52 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 53 | serviceEmitter.emitTypescriptTypes(settings, Lists.newArrayList()); 54 | writer.close(); 55 | String expectedOutput = "" + 56 | " /* tslint:disable */\n" + 57 | " /* eslint-disable */\n" + 58 | "\n" + 59 | " export interface DataObject {\n" + 60 | " y: MyObject;\n" + 61 | " }\n" + 62 | "\n" + 63 | " export interface GenericObject {\n" + 64 | " y: T;\n" + 65 | " }\n" + 66 | "\n" + 67 | " export interface ImmutablesObject {\n" + 68 | " y: string;\n" + 69 | " }\n" + 70 | "\n" + 71 | " export interface MyObject {\n" + 72 | " y: MyObject;\n" + 73 | " }\n" + 74 | ""; 75 | assertEquals(expectedOutput, new String(stream.toByteArray())); 76 | } 77 | 78 | @Test 79 | public void testComplexServiceClassEmitInterface() { 80 | ServiceModel model = serviceClassParser.parseServiceClass(TestComplexServiceClass.class, settings); 81 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 82 | serviceEmitter.emitTypescriptInterface(); 83 | writer.close(); 84 | String expectedOutput = "\n" + 85 | "export interface TestComplexServiceClass {\n" + 86 | " allOptionsPost(a: string, dataObject: DataObject, b?: number): FooType>;\n" + 87 | " queryGetter(x?: boolean): FooType;\n" + 88 | " simplePut(dataObject: DataObject): FooType;\n" + 89 | "}\n"; 90 | assertEquals(expectedOutput, new String(stream.toByteArray())); 91 | } 92 | 93 | @Test 94 | public void testComplexServiceClassEmitClass() { 95 | ServiceModel model = serviceClassParser.parseServiceClass(TestComplexServiceClass.class, settings); 96 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 97 | serviceEmitter.emitTypescriptClass(); 98 | writer.close(); 99 | String expectedOutput = "\n" + 100 | "export class TestComplexServiceClassImpl implements TestComplexServiceClass {\n" + 101 | "\n" + 102 | " private httpApiBridge: HttpApiBridge;\n" + 103 | " constructor(httpApiBridge: HttpApiBridge) {\n" + 104 | " this.httpApiBridge = httpApiBridge;\n" + 105 | " }\n" + 106 | "\n" + 107 | " public allOptionsPost(a: string, dataObject: DataObject, b?: number) {\n" + 108 | " var httpCallData = {\n" + 109 | " serviceIdentifier: \"testComplexServiceClass\",\n" + 110 | " endpointPath: \"testComplexService/allOptionsPost/{a}\",\n" + 111 | " endpointName: \"allOptionsPost\",\n" + 112 | " method: \"POST\",\n" + 113 | " requestMediaType: \"application/json\",\n" + 114 | " responseMediaType: \"\",\n" + 115 | " requiredHeaders: [],\n" + 116 | " pathArguments: [a],\n" + 117 | " queryArguments: {\n" + 118 | " b: b,\n" + 119 | " },\n" + 120 | " data: dataObject\n" + 121 | " };\n" + 122 | " return this.httpApiBridge.callEndpoint>(httpCallData);\n" + 123 | " }\n" + 124 | "\n" + 125 | " public queryGetter(x?: boolean) {\n" + 126 | " var httpCallData = {\n" + 127 | " serviceIdentifier: \"testComplexServiceClass\",\n" + 128 | " endpointPath: \"testComplexService/queryGetter\",\n" + 129 | " endpointName: \"queryGetter\",\n" + 130 | " method: \"GET\",\n" + 131 | " requestMediaType: \"application/json\",\n" + 132 | " responseMediaType: \"\",\n" + 133 | " requiredHeaders: [],\n" + 134 | " pathArguments: [],\n" + 135 | " queryArguments: {\n" + 136 | " x: x,\n" + 137 | " },\n" + 138 | " data: null\n" + 139 | " };\n" + 140 | " return this.httpApiBridge.callEndpoint(httpCallData);\n" + 141 | " }\n" + 142 | "\n" + 143 | " public simplePut(dataObject: DataObject) {\n" + 144 | " var httpCallData = {\n" + 145 | " serviceIdentifier: \"testComplexServiceClass\",\n" + 146 | " endpointPath: \"testComplexService/simplePut\",\n" + 147 | " endpointName: \"simplePut\",\n" + 148 | " method: \"PUT\",\n" + 149 | " requestMediaType: \"application/json\",\n" + 150 | " responseMediaType: \"\",\n" + 151 | " requiredHeaders: [],\n" + 152 | " pathArguments: [],\n" + 153 | " queryArguments: {\n" + 154 | " },\n" + 155 | " data: dataObject\n" + 156 | " };\n" + 157 | " return this.httpApiBridge.callEndpoint(httpCallData);\n" + 158 | " }\n" + 159 | "}\n"; 160 | assertEquals(expectedOutput, new String(stream.toByteArray())); 161 | } 162 | 163 | @Test 164 | public void testDuplicateMethodInterface() { 165 | ServiceModel model = serviceClassParser.parseServiceClass(DuplicateMethodNamesService.class, settings); 166 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 167 | serviceEmitter.emitTypescriptInterface(); 168 | writer.close(); 169 | String expectedOutput = "\n" + 170 | "export interface DuplicateMethodNamesService {\n" + 171 | "\n" + 172 | " // WARNING: not creating method declaration, java service has multiple methods with the name duplicate\n" + 173 | "}\n"; 174 | assertEquals(expectedOutput, new String(stream.toByteArray())); 175 | } 176 | 177 | @Test 178 | public void testDuplicateMethodClass() { 179 | ServiceModel model = serviceClassParser.parseServiceClass(DuplicateMethodNamesService.class, settings); 180 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 181 | serviceEmitter.emitTypescriptClass(); 182 | writer.close(); 183 | String expectedOutput = "\n" + 184 | "export class DuplicateMethodNamesServiceImpl implements DuplicateMethodNamesService {\n" + 185 | "\n" + 186 | " private httpApiBridge: HttpApiBridge;\n" + 187 | " constructor(httpApiBridge: HttpApiBridge) {\n" + 188 | " this.httpApiBridge = httpApiBridge;\n" + 189 | " }\n" + 190 | "}\n"; 191 | assertEquals(expectedOutput, new String(stream.toByteArray())); 192 | } 193 | 194 | @Test 195 | public void testConcreteObjectService() { 196 | ServiceModel model = serviceClassParser.parseServiceClass(ConcreteObjectService.class, settings); 197 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 198 | serviceEmitter.emitTypescriptClass(); 199 | writer.close(); 200 | String expectedOutput = "\n" + 201 | "export class ConcreteObjectServiceImpl implements ConcreteObjectService {\n" + 202 | "\n" + 203 | " private httpApiBridge: HttpApiBridge;\n" + 204 | " constructor(httpApiBridge: HttpApiBridge) {\n" + 205 | " this.httpApiBridge = httpApiBridge;\n" + 206 | " }\n" + 207 | "\n" + 208 | " public noPathGetter() {\n" + 209 | " var httpCallData = {\n" + 210 | " serviceIdentifier: \"concreteObjectService\",\n" + 211 | " endpointPath: \"concreteObject\",\n" + 212 | " endpointName: \"noPathGetter\",\n" + 213 | " method: \"GET\",\n" + 214 | " requestMediaType: \"application/json\",\n" + 215 | " responseMediaType: \"\",\n" + 216 | " requiredHeaders: [],\n" + 217 | " pathArguments: [],\n" + 218 | " queryArguments: {\n" + 219 | " },\n" + 220 | " data: null\n" + 221 | " };\n" + 222 | " return this.httpApiBridge.callEndpoint(httpCallData);\n" + 223 | " }\n" + 224 | "}\n"; 225 | assertEquals(expectedOutput, new String(stream.toByteArray())); 226 | } 227 | 228 | @Test 229 | public void testAdditionalClassesToOutput() { 230 | ServiceModel model = serviceClassParser.parseServiceClass(TestServiceClass.class, settings); 231 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 232 | serviceEmitter.emitTypescriptTypes(settings, Lists.newArrayList(MyObject.class)); 233 | writer.close(); 234 | String expectedOutput = 235 | " /* tslint:disable */\n" + 236 | " /* eslint-disable */\n" + 237 | "\n" + 238 | " export interface MyObject {\n" + 239 | " y: MyObject;\n" + 240 | " }\n"; 241 | assertEquals(expectedOutput, new String(stream.toByteArray())); 242 | } 243 | 244 | @Test 245 | public void testEnumClass() { 246 | ServiceModel model = serviceClassParser.parseServiceClass(EnumClass.class, settings); 247 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 248 | serviceEmitter.emitTypescriptTypes(settings, Lists.newArrayList()); 249 | writer.close(); 250 | String expectedOutput = "" + 251 | " /* tslint:disable */\n" + 252 | " /* eslint-disable */\n" + 253 | "\n" + 254 | " export type MyEnum = \"VALUE1\" | \"VALUE2\";\n" + 255 | "\n\n // Added by 'EnumConstantsExtension' extension\n\n" + 256 | " export const MyEnum = {\n" + 257 | " VALUE1: \"VALUE1\",\n" + 258 | " VALUE2: \"VALUE2\",\n" + 259 | " }\n"; 260 | assertEquals(expectedOutput, new String(stream.toByteArray())); 261 | } 262 | 263 | @Test 264 | public void testEnumDataParameter() { 265 | ServiceModel model = serviceClassParser.parseServiceClass(EnumClass.class, settings); 266 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 267 | serviceEmitter.emitTypescriptClass(); 268 | writer.close(); 269 | String expectedToContain = "data: `\"${myEnum}\"`"; 270 | assertTrue(new String(stream.toByteArray()).contains(expectedToContain)); 271 | } 272 | 273 | @Test 274 | public void testMultipleClasses() { 275 | ServiceModel model = serviceClassParser.parseServiceClass(SimpleService1.class, settings, SimpleService2.class); 276 | ServiceEmitter serviceEmitter = new ServiceEmitter(model, settings, writer); 277 | serviceEmitter.emitTypescriptInterface(); 278 | writer.close(); 279 | String expectedOutput = "\n" + 280 | "export interface SimpleService1 {\n" + 281 | "\n" + 282 | " // endpoints for service class: SimpleService1\n" + 283 | " method1(): FooType;\n" + 284 | "\n" + 285 | " // endpoints for service class: SimpleService2\n" + 286 | " method2(): FooType;\n" + 287 | "}\n"; 288 | assertEquals(expectedOutput, new String(stream.toByteArray())); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/main/java/com/palantir/code/ts/generator/ServiceEmitter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | import java.net.URI; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | import jakarta.ws.rs.core.MediaType; 16 | 17 | import org.codehaus.jackson.map.JsonSerializer; 18 | import org.codehaus.jackson.map.ObjectMapper; 19 | import org.codehaus.jackson.map.SerializationConfig; 20 | import org.codehaus.jackson.map.ser.BeanSerializer; 21 | import org.codehaus.jackson.map.ser.BeanSerializerFactory; 22 | import org.codehaus.jackson.type.JavaType; 23 | 24 | import com.google.common.base.Joiner; 25 | import com.google.common.base.Optional; 26 | import com.google.common.collect.Lists; 27 | import com.google.common.collect.Sets; 28 | import com.palantir.code.ts.generator.model.InnerServiceModel; 29 | import com.palantir.code.ts.generator.model.ServiceEndpointModel; 30 | import com.palantir.code.ts.generator.model.ServiceEndpointParameterModel; 31 | import com.palantir.code.ts.generator.model.ServiceModel; 32 | import com.palantir.code.ts.generator.utils.PathUtils; 33 | 34 | import cz.habarta.typescript.generator.Input; 35 | import cz.habarta.typescript.generator.Output; 36 | import cz.habarta.typescript.generator.Settings; 37 | import cz.habarta.typescript.generator.TypeProcessor; 38 | import cz.habarta.typescript.generator.TypeProcessor.Context; 39 | import cz.habarta.typescript.generator.TypeScriptGenerator; 40 | import cz.habarta.typescript.generator.compiler.SymbolTable; 41 | 42 | public final class ServiceEmitter { 43 | 44 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 45 | 46 | private final ServiceModel model; 47 | private final TypescriptServiceGeneratorConfiguration settings; 48 | private final IndentedOutputWriter writer; 49 | 50 | public ServiceEmitter(ServiceModel model, TypescriptServiceGeneratorConfiguration settings, IndentedOutputWriter writer) { 51 | this.model = model; 52 | this.settings = settings; 53 | this.writer = writer; 54 | } 55 | 56 | public void emitTypescriptTypes(TypescriptServiceGeneratorConfiguration settings, List additionalTypesToOutput) { 57 | Settings settingsToUse = settings.getSettings(); 58 | TypeProcessor baseTypeProcessor = settingsToUse.customTypeProcessor; 59 | 60 | Set referencedTypes = Sets.newHashSet(model.referencedTypes().iterator()); 61 | referencedTypes.addAll(additionalTypesToOutput); 62 | Set> referencedClasses = getReferencedClasses(referencedTypes, settings); 63 | final Set discoveredTypes = Sets.newHashSet(referencedClasses.iterator()); 64 | referencedClasses = filterInputClasses(referencedClasses); 65 | TypeProcessor discoveringProcessor = new TypeProcessor() { 66 | @Override 67 | public Result processType(Type javaType, Context context) { 68 | discoveredTypes.add(javaType); 69 | return null; 70 | } 71 | }; 72 | 73 | settingsToUse.customTypeProcessor = discoveringProcessor; 74 | if (baseTypeProcessor != null) { 75 | settingsToUse.customTypeProcessor = new TypeProcessor.Chain(discoveringProcessor, baseTypeProcessor); 76 | } 77 | 78 | TypeScriptGenerator typescriptGenerator = new TypeScriptGenerator(settingsToUse); 79 | ByteArrayOutputStream typeDeclarations = new ByteArrayOutputStream(); 80 | Type[] types = new Type[referencedClasses.size()]; 81 | referencedClasses.toArray(types); 82 | int intendationLevel = 1; 83 | if (!settings.typescriptModule().isPresent()) { 84 | intendationLevel = 0; 85 | } 86 | typescriptGenerator.generateEmbeddableTypeScript(Input.from(types), Output.to(typeDeclarations), true, intendationLevel); 87 | writer.write(new String(typeDeclarations.toByteArray())); 88 | } 89 | 90 | public void emitTypescriptClass() { 91 | Set endpointsToWarnAboutDuplicateNames = Sets.newHashSet(); 92 | if (!this.settings.emitDuplicateJavaMethodNames()) { 93 | endpointsToWarnAboutDuplicateNames = getDuplicateEndpointNames(); 94 | } 95 | writer.writeLine(""); 96 | // Adding "Impl" ensures the class name is different from the impl name, which is a compilation requirement. 97 | writer.writeLine("export class " + model.name() + "Impl" + " implements " + settings.getSettings().addTypeNamePrefix + model.name() + " {"); 98 | writer.increaseIndent(); 99 | 100 | writer.writeLine(""); 101 | writer.writeLine(String.format("private httpApiBridge: %sHttpApiBridge;", settings.generatedInterfacePrefix())); 102 | writer.writeLine(String.format("constructor(httpApiBridge: %sHttpApiBridge) {", settings.generatedInterfacePrefix())); 103 | writer.increaseIndent(); 104 | writer.writeLine("this.httpApiBridge = httpApiBridge;"); 105 | writer.decreaseIndent(); 106 | writer.writeLine("}"); 107 | 108 | for (InnerServiceModel innerServiceModel : model.innerServiceModels()) { 109 | if (model.innerServiceModels().size() > 1) { 110 | writer.writeLine(""); 111 | writer.writeLine("// endpoints for service class: " + innerServiceModel.name()); 112 | } 113 | for (ServiceEndpointModel endpointModel: innerServiceModel.endpointModels()) { 114 | if (endpointsToWarnAboutDuplicateNames.contains(endpointModel.endpointName())) { 115 | // don't output any duplicates 116 | continue; 117 | } 118 | writer.writeLine(""); 119 | String line = "public " + endpointModel.endpointName() + "("; 120 | line += getEndpointParametersString(endpointModel); 121 | line += ") {"; 122 | writer.writeLine(line); 123 | writer.increaseIndent(); 124 | writer.writeLine(String.format("var httpCallData = <%sHttpEndpointOptions> {", settings.generatedInterfacePrefix())); 125 | writer.increaseIndent(); 126 | writer.writeLine("serviceIdentifier: \"" + Character.toLowerCase(model.name().charAt(0)) + model.name().substring(1) + "\","); 127 | writer.writeLine("endpointPath: \"" + getEndpointPathString(innerServiceModel, endpointModel) + "\","); 128 | writer.writeLine("endpointName: \"" + endpointModel.endpointName() + "\","); 129 | writer.writeLine("method: \"" + endpointModel.endpointMethodType() + "\","); 130 | writer.writeLine("requestMediaType: \"" + endpointModel.endpointRequestMediaType() + "\","); 131 | writer.writeLine("responseMediaType: \"" + optionalToString(endpointModel.endpointResponseMediaType()) + "\","); 132 | List requiredHeaders = Lists.newArrayList(); 133 | List pathArguments = Lists.newArrayList(); 134 | List queryArguments = Lists.newArrayList(); 135 | String dataArgument = null; 136 | for (ServiceEndpointParameterModel parameterModel : endpointModel.parameters()) { 137 | if (parameterModel.headerParam() != null) { 138 | requiredHeaders.add("\"" + parameterModel.headerParam() + "\""); 139 | } else if (parameterModel.pathParam() != null) { 140 | pathArguments.add(parameterModel.getParameterName()); 141 | } else if (parameterModel.queryParam() != null) { 142 | queryArguments.add(parameterModel.queryParam()); 143 | } else { 144 | if (dataArgument != null) { 145 | throw new IllegalStateException("There should only be one data argument per endpoint. Found both" + dataArgument + " and " + parameterModel.getParameterName()); 146 | } 147 | dataArgument = parameterModel.getParameterName(); 148 | boolean isEnum = false; 149 | if (parameterModel.javaType() instanceof Class) { 150 | isEnum = ((Class) parameterModel.javaType()).isEnum(); 151 | } 152 | if (endpointModel.endpointRequestMediaType().equals(MediaType.APPLICATION_JSON) && (parameterModel.tsType().toString().equals("string") || isEnum)) { 153 | // strings (and enums, the wire format of an enum is a string) have to be wrapped in quotes in order to be valid json 154 | dataArgument = "`\"${" + parameterModel.getParameterName() + "}\"`"; 155 | } 156 | } 157 | } 158 | writer.writeLine("requiredHeaders: [" + Joiner.on(", ").join(requiredHeaders) + "],"); 159 | writer.writeLine("pathArguments: [" + Joiner.on(", ").join(pathArguments) + "],"); 160 | writer.writeLine("queryArguments: {"); 161 | writer.increaseIndent(); 162 | for (String queryArgument: queryArguments) { 163 | writer.writeLine(queryArgument + ": " + queryArgument + ","); 164 | } 165 | writer.decreaseIndent(); 166 | writer.writeLine("},"); 167 | writer.writeLine("data: " + dataArgument); 168 | writer.decreaseIndent(); 169 | writer.writeLine("};"); 170 | writer.writeLine("return this.httpApiBridge.callEndpoint<" + endpointModel.tsReturnType().toString() + ">(httpCallData);"); 171 | writer.decreaseIndent(); 172 | writer.writeLine("}"); 173 | } 174 | } 175 | writer.decreaseIndent(); 176 | writer.writeLine("}"); 177 | } 178 | 179 | public void emitTypescriptInterface() { 180 | Set endpointsToWarnAboutDuplicateNames = Sets.newHashSet(); 181 | if (!this.settings.emitDuplicateJavaMethodNames()) { 182 | endpointsToWarnAboutDuplicateNames = getDuplicateEndpointNames(); 183 | } 184 | 185 | writer.writeLine(""); 186 | writer.writeLine("export interface " + settings.getSettings().addTypeNamePrefix + model.name() + " {"); 187 | writer.increaseIndent(); 188 | 189 | for (InnerServiceModel innerServiceModel : model.innerServiceModels()) { 190 | if (model.innerServiceModels().size() > 1) { 191 | writer.writeLine(""); 192 | writer.writeLine("// endpoints for service class: " + innerServiceModel.name()); 193 | } 194 | 195 | for (ServiceEndpointModel endpointModel: innerServiceModel.endpointModels()) { 196 | if (!endpointsToWarnAboutDuplicateNames.contains(endpointModel.endpointName())) { 197 | String line = endpointModel.endpointName() + "("; 198 | line += getEndpointParametersString(endpointModel); 199 | line += String.format("): " + settings.genericEndpointReturnType(), endpointModel.tsReturnType().toString()) + ";"; 200 | writer.writeLine(line); 201 | } 202 | } 203 | } 204 | if (!endpointsToWarnAboutDuplicateNames.isEmpty()) { 205 | writer.writeLine(""); 206 | } 207 | for (String endpointName : endpointsToWarnAboutDuplicateNames) { 208 | writer.writeLine(String.format("// WARNING: not creating method declaration, java service has multiple methods with the name %s", endpointName)); 209 | } 210 | 211 | writer.decreaseIndent(); 212 | writer.writeLine("}"); 213 | } 214 | 215 | private Set getDuplicateEndpointNames() { 216 | Set seenEndpointNames = Sets.newHashSet(); 217 | Set duplicateEndpointNames = Sets.newHashSet(); 218 | model.innerServiceModels().stream().flatMap(innerServiceModel -> innerServiceModel.endpointModels().stream()).forEach(model -> { 219 | String endpointName = model.endpointName(); 220 | if (seenEndpointNames.contains(endpointName)) { 221 | duplicateEndpointNames.add(endpointName); 222 | } 223 | seenEndpointNames.add(endpointName); 224 | }); 225 | return duplicateEndpointNames; 226 | } 227 | 228 | private String getEndpointPathString(InnerServiceModel model, ServiceEndpointModel endpointModel) { 229 | String endpointPath = model.servicePath() + "/" + endpointModel.endpointPath(); 230 | return PathUtils.trimSlashes(endpointPath); 231 | } 232 | 233 | private String getEndpointParametersString(ServiceEndpointModel endpointModel) { 234 | List parameterStrings = Lists.newArrayList(); 235 | for (ServiceEndpointParameterModel parameterModel : endpointModel.parameters()) { 236 | if (parameterModel.headerParam() != null) { 237 | //continue, header params are implicit 238 | continue; 239 | } 240 | String optionalString = parameterModel.queryParam() != null ? "?" : ""; 241 | parameterStrings.add(parameterModel.getParameterName() + optionalString + ": " + parameterModel.tsType().toString()); 242 | } 243 | 244 | return Joiner.on(", ").join(parameterStrings); 245 | } 246 | 247 | private Set> filterInputClasses(Set> referencedClasses) { 248 | Set> typesToUse = Sets.newHashSet(); 249 | for (Class beanClass : referencedClasses) { 250 | if (beanClass.isEnum()) { 251 | typesToUse.add(beanClass); 252 | continue; 253 | } 254 | if (beanClass.equals(void.class)) { 255 | continue; 256 | } 257 | if (beanClass instanceof Class && beanClass.isEnum()) { 258 | typesToUse.add(beanClass); 259 | continue; 260 | } 261 | if (beanClass == URI.class) { 262 | continue; 263 | } 264 | 265 | // Classes directly passed in to typescript-generator need to be directly serializable, so filter out the ones that serializers 266 | // exist for. 267 | SerializationConfig serializationConfig = OBJECT_MAPPER.getSerializationConfig(); 268 | final JavaType simpleType = OBJECT_MAPPER.constructType(beanClass); 269 | try { 270 | final JsonSerializer jsonSerializer = BeanSerializerFactory.instance.createSerializer(serializationConfig, simpleType, null); 271 | if (jsonSerializer == null || jsonSerializer instanceof BeanSerializer) { 272 | typesToUse.add(beanClass); 273 | } 274 | } catch(Exception e) { 275 | 276 | } 277 | } 278 | return typesToUse; 279 | } 280 | 281 | public static Set> getReferencedClasses(Set referencedTypes, TypescriptServiceGeneratorConfiguration settings) { 282 | Set> ret = Sets.newHashSet(); 283 | for (Type t : referencedTypes) { 284 | if (settings.ignoredClasses().contains(t)) { 285 | continue; 286 | } 287 | 288 | if (t instanceof Class && ((Class) t).isEnum()) { 289 | ret.add((Class) t); 290 | continue; 291 | } 292 | 293 | // dummy context used for below check 294 | Context nullContext = new Context(new SymbolTable(settings.getSettings()), settings.customTypeProcessor(), String.class); 295 | // Don't add any classes that the user has made an exception for 296 | if (settings.customTypeProcessor().processType(t, nullContext) == null) { 297 | if (t instanceof Class) { 298 | ret.add((Class) t); 299 | } else if(t instanceof ParameterizedType) { 300 | ParameterizedType parameterized = (ParameterizedType) t; 301 | ret.addAll(getReferencedClasses(Sets.newHashSet(parameterized.getRawType()), settings)); 302 | ret.addAll(getReferencedClasses(Sets.newHashSet(Arrays.asList(parameterized.getActualTypeArguments()).iterator()), settings)); 303 | } 304 | } 305 | } 306 | return ret; 307 | } 308 | 309 | private static String optionalToString(Optional payload) { 310 | if (payload.isPresent()) { 311 | return payload.get().toString(); 312 | } 313 | return ""; 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /typescript-service-generator-core/src/test/java/com/palantir/code/ts/generator/ServiceClassParserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016 Palantir Technologies Inc. 3 | */ 4 | 5 | package com.palantir.code.ts.generator; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Type; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | import javax.annotation.CheckForNull; 18 | import jakarta.ws.rs.core.MediaType; 19 | 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.mockito.Mockito; 23 | 24 | import com.google.common.collect.ImmutableSet; 25 | import com.google.common.collect.Lists; 26 | import com.google.common.collect.Maps; 27 | import com.google.common.collect.Sets; 28 | import com.palantir.code.ts.generator.TypescriptServiceGeneratorConfiguration.DuplicateMethodNameResolver; 29 | import com.palantir.code.ts.generator.TypescriptServiceGeneratorConfiguration.MethodFilter; 30 | import com.palantir.code.ts.generator.model.ImmutableInnerServiceModel; 31 | import com.palantir.code.ts.generator.model.ImmutableServiceEndpointModel; 32 | import com.palantir.code.ts.generator.model.ImmutableServiceEndpointParameterModel; 33 | import com.palantir.code.ts.generator.model.ImmutableServiceModel; 34 | import com.palantir.code.ts.generator.model.InnerServiceModel; 35 | import com.palantir.code.ts.generator.model.ServiceEndpointModel; 36 | import com.palantir.code.ts.generator.model.ServiceEndpointParameterModel; 37 | import com.palantir.code.ts.generator.model.ServiceModel; 38 | import com.palantir.code.ts.generator.utils.TestUtils; 39 | import com.palantir.code.ts.generator.utils.TestUtils.DataObject; 40 | import com.palantir.code.ts.generator.utils.TestUtils.DuplicateMethodNamesService; 41 | import com.palantir.code.ts.generator.utils.TestUtils.GenericObject; 42 | import com.palantir.code.ts.generator.utils.TestUtils.IgnoredParametersClass; 43 | import com.palantir.code.ts.generator.utils.TestUtils.ImmutablesObject; 44 | import com.palantir.code.ts.generator.utils.TestUtils.MyObject; 45 | import com.palantir.code.ts.generator.utils.TestUtils.PlainTextService; 46 | import com.palantir.code.ts.generator.utils.TestUtils.SimpleService1; 47 | import com.palantir.code.ts.generator.utils.TestUtils.SimpleService2; 48 | import com.palantir.code.ts.generator.utils.TestUtils.TestComplexServiceClass; 49 | import com.palantir.code.ts.generator.utils.TestUtils.TestServiceClass; 50 | 51 | import cz.habarta.typescript.generator.JsonLibrary; 52 | import cz.habarta.typescript.generator.Settings; 53 | import cz.habarta.typescript.generator.TsType; 54 | import cz.habarta.typescript.generator.TypeScriptOutputKind; 55 | import cz.habarta.typescript.generator.compiler.Symbol; 56 | 57 | public class ServiceClassParserTest { 58 | 59 | private ServiceClassParser serviceClassParser; 60 | private TypescriptServiceGeneratorConfiguration settings; 61 | 62 | @Before 63 | public void before() { 64 | this.serviceClassParser = new ServiceClassParser(); 65 | this.settings = Mockito.mock(TypescriptServiceGeneratorConfiguration.class); 66 | 67 | Settings settings = new Settings(); 68 | settings.outputKind = TypeScriptOutputKind.global; 69 | settings.jsonLibrary = JsonLibrary.jackson2; 70 | Mockito.when(this.settings.getSettings()).thenReturn(settings); 71 | Mockito.when(this.settings.methodFilter()).thenReturn(new MethodFilter() { 72 | @Override 73 | public boolean shouldGenerateMethod(Class parentClass, Method method) { 74 | return true; 75 | } 76 | }); 77 | } 78 | 79 | @Test 80 | public void parseSimpleClassTest() { 81 | ServiceModel model = serviceClassParser.parseServiceClass(TestServiceClass.class, settings); 82 | assertEquals(1, model.innerServiceModels().size()); 83 | assertEquals(ImmutableSet.of(String.class), model.referencedTypes()); 84 | assertEquals("TestServiceClass", model.name()); 85 | assertEquals("testService", model.innerServiceModels().get(0).servicePath()); 86 | ServiceEndpointParameterModel aParam = ImmutableServiceEndpointParameterModel.builder().pathParam("a").javaType(String.class).tsType(TsType.String).build(); 87 | ServiceEndpointParameterModel bParam = ImmutableServiceEndpointParameterModel.builder().pathParam("b").javaType(String.class).tsType(TsType.String).build(); 88 | ImmutableServiceEndpointModel stringGetterEndpointModel = ImmutableServiceEndpointModel.builder() 89 | .javaReturnType(String.class) 90 | .tsReturnType(TsType.String) 91 | .parameters(Lists.newArrayList(aParam, bParam)) 92 | .endpointName("stringGetter") 93 | .endpointPath("stringGetter/{a}/{b}") 94 | .endpointMethodType("GET") 95 | .build(); 96 | assertEquals(model.innerServiceModels().get(0).endpointModels(), Lists.newArrayList(stringGetterEndpointModel)); 97 | } 98 | 99 | @Test 100 | public void parseComplexClassTest() throws NoSuchMethodException, SecurityException { 101 | ServiceModel model = serviceClassParser.parseServiceClass(TestComplexServiceClass.class, settings); 102 | assertEquals(1, model.innerServiceModels().size()); 103 | Type genericReturnType = TestComplexServiceClass.class.getMethod("allOptionsPost", String.class, Integer.class, DataObject.class).getGenericReturnType(); 104 | assertEquals(ImmutableSet.of(Boolean.class, ImmutablesObject.class, MyObject.class, GenericObject.class, Integer.class, String.class, DataObject.class, genericReturnType), model.referencedTypes()); 105 | assertEquals("TestComplexServiceClass", model.name()); 106 | assertEquals("testComplexService", model.innerServiceModels().get(0).servicePath()); 107 | List endpoints = Lists.newArrayList(); 108 | { 109 | ServiceEndpointParameterModel aParam = ImmutableServiceEndpointParameterModel.builder().pathParam("a").javaType(String.class).tsType(TsType.String).build(); 110 | ServiceEndpointParameterModel bParam = ImmutableServiceEndpointParameterModel.builder().queryParam("b").javaType(Integer.class).tsType(TsType.Number).build(); 111 | ServiceEndpointParameterModel dataParam = ImmutableServiceEndpointParameterModel.builder().javaType(DataObject.class).tsType(new TsType.ReferenceType(new Symbol("DataObject"))).build(); 112 | endpoints.add(ImmutableServiceEndpointModel.builder().javaReturnType(genericReturnType) 113 | .tsReturnType(new TsType.ReferenceType(new Symbol("GenericObject"))) 114 | .parameters(Lists.newArrayList(aParam, dataParam, bParam)) 115 | .endpointName("allOptionsPost") 116 | .endpointPath("allOptionsPost/{a}") 117 | .endpointMethodType("POST") 118 | .endpointRequestMediaType(MediaType.APPLICATION_JSON) 119 | .build()); 120 | } 121 | { 122 | ServiceEndpointParameterModel xParam = ImmutableServiceEndpointParameterModel.builder().queryParam("x").javaType(Boolean.class).tsType(TsType.Boolean).build(); 123 | endpoints.add(ImmutableServiceEndpointModel.builder().javaReturnType(MyObject.class) 124 | .tsReturnType(new TsType.ReferenceType(new Symbol("MyObject"))) 125 | .parameters(Lists.newArrayList(xParam)) 126 | .endpointName("queryGetter") 127 | .endpointPath("queryGetter") 128 | .endpointMethodType("GET") 129 | .build()); 130 | } 131 | { 132 | ServiceEndpointParameterModel dataParam = ImmutableServiceEndpointParameterModel.builder().javaType(DataObject.class).tsType(new TsType.ReferenceType(new Symbol("DataObject"))).build(); 133 | endpoints.add(ImmutableServiceEndpointModel.builder().javaReturnType(ImmutablesObject.class) 134 | .tsReturnType(new TsType.ReferenceType(new Symbol("ImmutablesObject"))) 135 | .parameters(Lists.newArrayList(dataParam)) 136 | .endpointName("simplePut") 137 | .endpointPath("simplePut") 138 | .endpointMethodType("PUT") 139 | .build()); 140 | } 141 | // To string because TsType has no equals method 142 | assertEquals(endpoints.toString(), model.innerServiceModels().get(0).endpointModels().toString()); 143 | } 144 | 145 | @Test 146 | public void parseIgnoredTest() { 147 | Mockito.when(settings.ignoredAnnotations()).thenReturn(Sets.newHashSet(CheckForNull.class)); 148 | ServiceModel model = serviceClassParser.parseServiceClass(IgnoredParametersClass.class, settings); 149 | assertEquals(1, model.innerServiceModels().size()); 150 | assertEquals(ImmutableSet.of(String.class), model.referencedTypes()); 151 | assertEquals("IgnoredParametersClass", model.name()); 152 | assertEquals("ignoredParameters", model.innerServiceModels().get(0).servicePath()); 153 | ServiceEndpointParameterModel aParam = ImmutableServiceEndpointParameterModel.builder().pathParam("a").javaType(String.class).tsType(TsType.String).build(); 154 | ServiceEndpointParameterModel bParam = ImmutableServiceEndpointParameterModel.builder().pathParam("b").javaType(String.class).tsType(TsType.String).build(); 155 | ImmutableServiceEndpointModel stringGetterEndpointModel = ImmutableServiceEndpointModel.builder() 156 | .javaReturnType(String.class) 157 | .tsReturnType(TsType.String) 158 | .parameters(Lists.newArrayList(aParam, bParam)) 159 | .endpointName("stringGetter") 160 | .endpointPath("stringGetter/{a}/{b}") 161 | .endpointMethodType("GET") 162 | .build(); 163 | assertEquals(model.innerServiceModels().get(0).endpointModels(), Lists.newArrayList(stringGetterEndpointModel)); 164 | } 165 | 166 | @Test 167 | public void duplicateNameResolutionTest() { 168 | // Mock out a simple resolver that does the resolution based on number of parameters 169 | Mockito.when(settings.duplicateEndpointNameResolver()).thenReturn(new DuplicateMethodNameResolver() { 170 | @Override 171 | public Map resolveDuplicateNames(List methodsWithSameName) { 172 | Map result = Maps.newHashMap(); 173 | for (Method method : methodsWithSameName) { 174 | if (method.getParameterTypes().length > 0) { 175 | result.put(method, "nonZeroParameters"); 176 | } else { 177 | result.put(method, "zeroParameters"); 178 | } 179 | } 180 | return result; 181 | } 182 | }); 183 | ServiceModel model = serviceClassParser.parseServiceClass(DuplicateMethodNamesService.class, settings); 184 | assertEquals(1, model.innerServiceModels().size()); 185 | List endpointModels = Lists.newArrayList(model.innerServiceModels().get(0).endpointModels().iterator()); 186 | Collections.sort(endpointModels); 187 | assertEquals(Lists.newArrayList("nonZeroParameters", "zeroParameters"), 188 | endpointModels.stream().map(endpoint -> endpoint.endpointName()).collect(Collectors.toList())); 189 | assertTrue(endpointModels.get(0).parameters().size() > 0); 190 | assertTrue(endpointModels.get(1).parameters().size() == 0); 191 | } 192 | 193 | @Test 194 | public void filterMethodTest() { 195 | Mockito.when(settings.methodFilter()).thenReturn(new MethodFilter() { 196 | @Override 197 | public boolean shouldGenerateMethod(Class parentClass, Method method) { 198 | return false; 199 | } 200 | }); 201 | ServiceModel model = serviceClassParser.parseServiceClass(SimpleService1.class, settings); 202 | assertEquals("SimpleService1", model.name()); 203 | InnerServiceModel innerService1 = ImmutableInnerServiceModel.builder() 204 | .servicePath("simple1") 205 | .name("SimpleService1") 206 | .build(); 207 | 208 | ServiceModel expectedServiceModel = ImmutableServiceModel.builder() 209 | .addInnerServiceModels(innerService1) 210 | .name("SimpleService1") 211 | .build(); 212 | 213 | assertEquals(expectedServiceModel, model); 214 | } 215 | 216 | @Test 217 | public void multipleServiceClassParseTest() { 218 | ServiceModel model = serviceClassParser.parseServiceClass(SimpleService1.class, settings, SimpleService2.class); 219 | assertEquals(2, model.innerServiceModels().size()); 220 | assertEquals(ImmutableSet.of(String.class), model.referencedTypes()); 221 | assertEquals("SimpleService1", model.name()); 222 | ImmutableServiceEndpointModel method1Model = ImmutableServiceEndpointModel.builder() 223 | .javaReturnType(String.class) 224 | .tsReturnType(TsType.String) 225 | .parameters(Lists.newArrayList()) 226 | .endpointName("method1") 227 | .endpointPath("method1") 228 | .endpointMethodType("GET") 229 | .build(); 230 | 231 | ImmutableServiceEndpointModel method2Model = ImmutableServiceEndpointModel.builder() 232 | .javaReturnType(String.class) 233 | .tsReturnType(TsType.String) 234 | .parameters(Lists.newArrayList()) 235 | .endpointName("method2") 236 | .endpointPath("method2") 237 | .endpointMethodType("GET") 238 | .build(); 239 | InnerServiceModel innerService1 = ImmutableInnerServiceModel.builder() 240 | .addEndpointModels(method1Model) 241 | .servicePath("simple1") 242 | .name("SimpleService1") 243 | .build(); 244 | 245 | InnerServiceModel innerService2 = ImmutableInnerServiceModel.builder() 246 | .addEndpointModels(method2Model) 247 | .servicePath("simple2") 248 | .name("SimpleService2") 249 | .build(); 250 | 251 | ServiceModel expectedServiceModel = ImmutableServiceModel.builder() 252 | .addInnerServiceModels(innerService1, innerService2) 253 | .name("SimpleService1") 254 | .addReferencedTypes(String.class) 255 | .build(); 256 | 257 | assertEquals(expectedServiceModel, model); 258 | } 259 | 260 | @Test 261 | public void plainTextTest() { 262 | ServiceModel model = serviceClassParser.parseServiceClass(PlainTextService.class, settings); 263 | 264 | ImmutableServiceEndpointParameterModel expectedParameterModel = ImmutableServiceEndpointParameterModel.builder() 265 | .javaType(String.class) 266 | .tsType(TsType.String) 267 | .build(); 268 | 269 | ImmutableServiceEndpointModel expectedEndpointModel = ImmutableServiceEndpointModel.builder() 270 | .javaReturnType(String.class) 271 | .tsReturnType(TsType.String) 272 | .addParameters(expectedParameterModel) 273 | .endpointName("plainText") 274 | .endpointPath("plainText") 275 | .endpointMethodType("GET") 276 | .endpointRequestMediaType(MediaType.TEXT_PLAIN) 277 | .endpointResponseMediaType(MediaType.TEXT_PLAIN) 278 | .build(); 279 | InnerServiceModel innerServiceModel = ImmutableInnerServiceModel.builder() 280 | .addEndpointModels(expectedEndpointModel) 281 | .servicePath("plainTextService") 282 | .name("PlainTextService") 283 | .build(); 284 | 285 | ServiceModel expectedServiceModel = ImmutableServiceModel.builder() 286 | .addInnerServiceModels(innerServiceModel) 287 | .name("PlainTextService") 288 | .addReferencedTypes(String.class) 289 | .build(); 290 | 291 | assertEquals(expectedServiceModel, model); 292 | } 293 | 294 | @Test 295 | public void noServiceClassPathTest() { 296 | ServiceModel model = serviceClassParser.parseServiceClass(TestUtils.NoPathService.class, settings); 297 | 298 | assertEquals(1, model.innerServiceModels().size()); 299 | assertEquals(1, model.innerServiceModels().get(0).endpointModels().size()); 300 | assertEquals("", model.innerServiceModels().get(0).servicePath()); 301 | } 302 | } 303 | --------------------------------------------------------------------------------