├── settings.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .codebeatsettings ├── HEADER.txt ├── src ├── main │ └── java │ │ └── io │ │ └── outfoxx │ │ └── typescriptpoet │ │ ├── Modifier.kt │ │ ├── TypeSpec.kt │ │ ├── FileModules.kt │ │ ├── DecoratorSpec.kt │ │ ├── LineWrapper.kt │ │ ├── EnumSpec.kt │ │ ├── TypeAliasSpec.kt │ │ ├── PropertySpec.kt │ │ ├── NameAllocator.kt │ │ ├── ParameterSpec.kt │ │ ├── Taggable.kt │ │ ├── Utils.kt │ │ ├── ModuleSpec.kt │ │ ├── InterfaceSpec.kt │ │ ├── CodeWriter.kt │ │ ├── SymbolSpec.kt │ │ ├── FunctionSpec.kt │ │ ├── ClassSpec.kt │ │ ├── FileSpec.kt │ │ └── TypeName.kt └── test │ └── java │ └── io │ └── outfoxx │ └── typescriptpoet │ └── test │ ├── UtilTests.kt │ ├── CodeWriterTests.kt │ ├── FileModuleTests.kt │ ├── TypeNameTests.kt │ ├── EnumSpecTests.kt │ ├── TypeAliasSpecTests.kt │ ├── DecoratorSpecTests.kt │ ├── SymbolSpecTests.kt │ ├── InterfaceSpecTests.kt │ └── FunctionSpecTests.kt ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── gradlew.bat ├── .gitignore ├── README.md ├── gradlew └── LICENSE.txt /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "typescriptpoet" 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m 2 | 3 | releaseVersion=1.2.0-SNAPSHOT 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outfoxx/typescriptpoet/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.codebeatsettings: -------------------------------------------------------------------------------- 1 | { 2 | "KOTLIN": { 3 | "ARITY": [6, 7, 8, 9], 4 | "ABC": [20, 30, 50, 60], 5 | "TOO_MANY_METHODS": [25, 30, 40, 60] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Sep 17 18:47:31 MST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | distributionSha256Sum=8de6efc274ab52332a9c820366dd5cf5fc9d35ec7078fd70c8ec6913431ee610 8 | -------------------------------------------------------------------------------- /HEADER.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Outfox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/Modifier.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** Available declaration modifiers. */ 20 | enum class Modifier { 21 | 22 | EXPORT, 23 | PUBLIC, 24 | PROTECTED, 25 | PRIVATE, 26 | READONLY, 27 | GET, 28 | SET, 29 | STATIC, 30 | ABSTRACT, 31 | DECLARE, 32 | CONST, 33 | LET, 34 | VAR; 35 | 36 | val keyword: String 37 | get() = name.toLowerCase() 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/TypeSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | abstract class TypeSpec, B : TypeSpec.Builder> 20 | protected constructor( 21 | builder: Builder 22 | ) : Taggable(builder.tags.toImmutableMap()) { 23 | 24 | abstract val name: String 25 | 26 | internal abstract fun emit(codeWriter: CodeWriter) 27 | 28 | override fun toString() = buildCodeString { emit(this) } 29 | 30 | abstract class Builder, B : Builder>( 31 | internal val name: String 32 | ) : Taggable.Builder() { 33 | 34 | abstract fun build(): T 35 | } 36 | } 37 | 38 | typealias AnyTypeSpec = TypeSpec<*, *> 39 | typealias AnyTypeSpecBuilder = TypeSpec.Builder<*, *> 40 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/FileModules.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import java.nio.file.Path 20 | 21 | object FileModules { 22 | 23 | fun importPath(directory: Path, importer: String, import: String): String { 24 | return if (import.startsWith("!")) { 25 | // Ensure two generated files use proper relative import path 26 | val importerPath = directory.resolve(importer).toAbsolutePath().normalize() 27 | val importerDir = importerPath.parent ?: importerPath 28 | val importPath = directory.resolve(import.drop(1)).toAbsolutePath().normalize() 29 | val importedPath = importerDir.relativize(importPath).normalize().toString() 30 | if (importedPath.startsWith(".")) 31 | importedPath 32 | else 33 | "./$importedPath" 34 | } else { 35 | import 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: ["v[0-9]+.[0-9]+.[0-9]+**"] 6 | 7 | jobs: 8 | 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: '11' 19 | 20 | - uses: olegtarasov/get-tag@v2.1 21 | id: tagName 22 | with: 23 | tagRegex: "v(.*)" 24 | 25 | - name: Publish Maven 26 | uses: burrunan/gradle-cache-action@v1 27 | env: 28 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} 29 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 30 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 31 | with: 32 | job-id: jdk8-build-test 33 | arguments: >- 34 | build 35 | publishRelease 36 | -x test 37 | properties: | 38 | releaseVersion=${{ steps.tagName.outputs.tag }} 39 | ossrhUsername=${{ secrets.OSSRH_USER }} 40 | ossrhPassword=${{ secrets.OSSRH_PASS }} 41 | github.token=${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Build Docs 44 | uses: burrunan/gradle-cache-action@v1 45 | with: 46 | job-id: jdk8-build-test 47 | arguments: dokkaHtml 48 | 49 | - name: Publish Documentation 50 | uses: JamesIves/github-pages-deploy-action@4.0.0 51 | with: 52 | branch: gh-pages 53 | clean: false 54 | folder: build/javadoc 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | types: [ opened, synchronize, reopened, closed ] 8 | branches: [ main ] 9 | 10 | jobs: 11 | build-test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | 16 | - uses: actions/checkout@v2 17 | 18 | - uses: actions/setup-java@v1 19 | with: 20 | java-version: '11' 21 | 22 | - name: Build & Test 23 | uses: burrunan/gradle-cache-action@v1 24 | with: 25 | job-id: jdk8-build-test 26 | arguments: build 27 | 28 | - name: Updload Reports 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: reports 32 | path: build/reports 33 | 34 | 35 | publish: 36 | runs-on: ubuntu-latest 37 | 38 | needs: [build-test] 39 | 40 | if: github.event.pull_request.merged || github.event_name == 'push' 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Build 46 | uses: burrunan/gradle-cache-action@v1 47 | with: 48 | job-id: jdk8-build-test 49 | arguments: build -x test 50 | 51 | - name: Publish Maven (Snapshot) 52 | uses: burrunan/gradle-cache-action@v1 53 | with: 54 | job-id: jdk8-build-test 55 | arguments: publishMavenRelease 56 | properties: | 57 | ossrhUsername=${{ secrets.OSSRH_USER }} 58 | ossrhPassword=${{ secrets.OSSRH_PASS }} 59 | 60 | - name: Publish Documentation 61 | uses: JamesIves/github-pages-deploy-action@4.0.0 62 | with: 63 | branch: gh-pages 64 | clean: false 65 | folder: build/javadoc 66 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/UtilTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.dropCommon 20 | import org.hamcrest.MatcherAssert.assertThat 21 | import org.hamcrest.Matchers.equalTo 22 | import org.junit.jupiter.api.Test 23 | 24 | class UtilTests { 25 | 26 | @Test 27 | fun testDropCommon() { 28 | 29 | val full = listOf("a", "b", "c", "d") 30 | 31 | assertThat( 32 | full.dropCommon(listOf("a", "b")), 33 | equalTo(listOf("c", "d")) 34 | ) 35 | 36 | assertThat( 37 | full.dropCommon(listOf("a")), 38 | equalTo(listOf("b", "c", "d")) 39 | ) 40 | 41 | assertThat( 42 | full.dropCommon(listOf("a", "b", "c", "d")), 43 | equalTo(listOf()) 44 | ) 45 | 46 | assertThat( 47 | full.dropCommon(listOf("A", "b")), 48 | equalTo(listOf("a", "b", "c", "d")) 49 | ) 50 | 51 | assertThat( 52 | full.dropCommon(listOf("a", "b", "c", "d", "e")), 53 | equalTo(listOf("a", "b", "c", "d")) 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/CodeWriterTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.FunctionSpec 20 | import io.outfoxx.typescriptpoet.TypeName.Companion.STRING 21 | import org.hamcrest.CoreMatchers 22 | import org.hamcrest.MatcherAssert 23 | import org.junit.jupiter.api.DisplayName 24 | import org.junit.jupiter.api.Test 25 | 26 | @DisplayName("CodeWriter Tests") 27 | class CodeWriterTests { 28 | 29 | @Test 30 | fun `test long line wrapping`() { 31 | val testFunc = FunctionSpec.builder("test") 32 | .returns(STRING) 33 | .addStatement("return X(aaaaa,%Wbbbbb,%Wccccc,%Wddddd,%Weeeee,%Wfffff,%Wggggg,%Whhhhh,%Wiiiii,%Wjjjjj,%Wkkkkk,%Wlllll,%Wmmmmm,%Wnnnnn,%Wooooo,%Wppppp,%Wqqqqq)") 34 | .build() 35 | 36 | MatcherAssert.assertThat( 37 | testFunc.toString(), 38 | CoreMatchers.equalTo( 39 | """ 40 | function test(): string { 41 | return X(aaaaa, bbbbb, ccccc, ddddd, eeeee, fffff, ggggg, hhhhh, iiiii, jjjjj, kkkkk, lllll, 42 | mmmmm, nnnnn, ooooo, ppppp, qqqqq); 43 | } 44 | 45 | """.trimIndent() 46 | ) 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/FileModuleTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.FileModules.importPath 20 | import org.hamcrest.CoreMatchers.equalTo 21 | import org.hamcrest.MatcherAssert.assertThat 22 | import org.junit.jupiter.api.DisplayName 23 | import org.junit.jupiter.api.Test 24 | import java.nio.file.Paths 25 | 26 | @DisplayName("FileModule Tests") 27 | class FileModuleTests { 28 | 29 | @Test 30 | @DisplayName("Generates correct import path for relative paths") 31 | fun testRelativeImportPathGeneration() { 32 | 33 | val import = "!generated/src/main/api/Api" 34 | val importer = "generated/src/main/impl/Impl" 35 | 36 | val path = importPath(Paths.get("/"), importer, import) 37 | assertThat(path, equalTo("../api/Api")) 38 | } 39 | 40 | @Test 41 | @DisplayName("Generates correct import path for relative paths referencing the same dir") 42 | fun testRelativeImportPathGenerationSameDir() { 43 | 44 | val import = "!generated/src/main/api/Api" 45 | val importer = "generated/src/main/api/Api2" 46 | 47 | val path = importPath(Paths.get("/"), importer, import) 48 | assertThat(path, equalTo("./Api")) 49 | } 50 | 51 | @Test 52 | @DisplayName("Generates correct import path for sibling paths with no parent") 53 | fun testRelativeImportPathGenerationSiblingsNoParent() { 54 | 55 | val import = "!Api" 56 | val importer = "Api2" 57 | 58 | val path = importPath(Paths.get("/"), importer, import) 59 | assertThat(path, equalTo("./Api")) 60 | } 61 | 62 | @Test 63 | @DisplayName("Generates correct import path for implied modules") 64 | fun testImpliedImportPathGeneration() { 65 | 66 | val import = "rxjs/Observable" 67 | val importer = "generated/src/main/impl/Impl" 68 | 69 | val path = importPath(Paths.get("/"), importer, import) 70 | assertThat(path, equalTo("rxjs/Observable")) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/TypeNameTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.SymbolSpec 20 | import io.outfoxx.typescriptpoet.TypeName 21 | import io.outfoxx.typescriptpoet.TypeName.Anonymous.Member 22 | import io.outfoxx.typescriptpoet.TypeName.Companion.BOOLEAN 23 | import io.outfoxx.typescriptpoet.TypeName.Companion.DATE 24 | import io.outfoxx.typescriptpoet.TypeName.Companion.NUMBER 25 | import io.outfoxx.typescriptpoet.TypeName.Companion.STRING 26 | import org.hamcrest.CoreMatchers.equalTo 27 | import org.hamcrest.CoreMatchers.hasItems 28 | import org.hamcrest.MatcherAssert.assertThat 29 | import org.junit.jupiter.api.DisplayName 30 | import org.junit.jupiter.api.Test 31 | import org.junit.jupiter.api.fail 32 | 33 | @DisplayName("TypeName Tests") 34 | class TypeNameTests { 35 | 36 | @Test 37 | @DisplayName("Parsing nested type import only imports root symbol while referencing fully nested import") 38 | fun testParsingNestedImport() { 39 | 40 | val typeName = TypeName.namedImport("This", "!Api").nested("Is.Nested") 41 | 42 | val symbol = typeName.base as? SymbolSpec.ImportsName ?: fail("ImportsName symbol expected") 43 | assertThat(symbol.value, equalTo("This.Is.Nested")) 44 | assertThat(symbol.source, equalTo("!Api")) 45 | } 46 | 47 | @Test 48 | @DisplayName("Anonymous type names produce valid syntax") 49 | fun testAnonymousNameGen() { 50 | 51 | val typeName = TypeName.anonymousType("a" to STRING, "b" to NUMBER, "C" to BOOLEAN) 52 | 53 | assertThat( 54 | typeName.members, 55 | hasItems( 56 | Member("a", STRING, false), 57 | Member("b", NUMBER, false), 58 | Member("C", BOOLEAN, false) 59 | ) 60 | ) 61 | assertThat(typeName.toString(), equalTo("{ a: string, b: number, C: boolean }")) 62 | 63 | val typeName2 = TypeName.anonymousType( 64 | arrayListOf( 65 | Member("a", NUMBER, true), 66 | Member("B", STRING, false), 67 | Member("c", DATE, true) 68 | ) 69 | ) 70 | 71 | assertThat( 72 | typeName2.members, 73 | hasItems( 74 | Member("a", NUMBER, true), 75 | Member("B", STRING, false), 76 | Member("c", DATE, true) 77 | ) 78 | ) 79 | assertThat(typeName2.toString(), equalTo("{ a?: number, B: string, c?: Date }")) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/DecoratorSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated function or class decorator declaration. */ 20 | class DecoratorSpec 21 | internal constructor( 22 | builder: Builder 23 | ) : Taggable(builder.tags.toImmutableMap()) { 24 | 25 | val name = builder.name 26 | val parameters = builder.parameters 27 | val factory = builder.factory 28 | 29 | internal fun emit(codeWriter: CodeWriter) { 30 | 31 | codeWriter.emitCode(CodeBlock.of("@%Q", name)) 32 | 33 | if (parameters.isNotEmpty()) { 34 | 35 | codeWriter.emitCode("(") 36 | 37 | parameters.forEachIndexed { index, (first, second) -> 38 | if (index > 0 && index < parameters.size) { 39 | codeWriter.emitCode(",%W") 40 | } 41 | if (first != null) { 42 | codeWriter.emit("/* $first */ ") 43 | } 44 | codeWriter.emitCode(second) 45 | } 46 | 47 | codeWriter.emitCode(")") 48 | } else if (factory ?: parameters.isNotEmpty()) { 49 | codeWriter.emit("()") 50 | } 51 | } 52 | 53 | override fun toString() = buildCodeString { emit(this) } 54 | 55 | fun toBuilder(): Builder { 56 | val builder = Builder(name) 57 | builder.parameters += parameters 58 | builder.factory = factory 59 | return builder 60 | } 61 | 62 | class Builder 63 | internal constructor( 64 | val name: SymbolSpec 65 | ) : Taggable.Builder() { 66 | 67 | internal val parameters = mutableListOf>() 68 | internal var factory: Boolean? = null 69 | 70 | fun asFactory() = apply { 71 | this.factory = true 72 | } 73 | 74 | fun addParameter(name: String? = null, format: String, vararg args: Any?) = apply { 75 | parameters += name to CodeBlock.of(format, *args) 76 | } 77 | 78 | fun addParameter(name: String? = null, codeBlock: CodeBlock) = apply { 79 | parameters += name to codeBlock 80 | } 81 | 82 | fun build(): DecoratorSpec { 83 | return DecoratorSpec(this) 84 | } 85 | } 86 | 87 | companion object { 88 | 89 | @JvmStatic 90 | fun builder(name: String): Builder { 91 | return Builder(SymbolSpec.from(name)) 92 | } 93 | 94 | @JvmStatic 95 | fun builder(name: SymbolSpec): Builder { 96 | return Builder(name) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,macos,gradle,intellij 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | 13 | # Sensitive or high-churn files: 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Cursive Clojure plugin 47 | .idea/replstate.xml 48 | 49 | # Ruby plugin and RubyMine 50 | /.rakeTasks 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | 58 | ### Intellij Patch ### 59 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 60 | 61 | # *.iml 62 | # modules.xml 63 | # .idea/misc.xml 64 | # *.ipr 65 | 66 | # Sonarlint plugin 67 | .idea/sonarlint 68 | 69 | ### Java ### 70 | # Compiled class file 71 | *.class 72 | 73 | # Log file 74 | *.log 75 | 76 | # BlueJ files 77 | *.ctxt 78 | 79 | # Mobile Tools for Java (J2ME) 80 | .mtj.tmp/ 81 | 82 | # Package Files # 83 | *.jar 84 | *.war 85 | *.ear 86 | *.zip 87 | *.tar.gz 88 | *.rar 89 | 90 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 91 | hs_err_pid* 92 | 93 | ### macOS ### 94 | *.DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Icon must end with two \r 99 | Icon 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | .com.apple.timemachine.donotpresent 112 | 113 | # Directories potentially created on remote AFP share 114 | .AppleDB 115 | .AppleDesktop 116 | Network Trash Folder 117 | Temporary Items 118 | .apdisk 119 | 120 | ### Gradle ### 121 | .gradle 122 | **/build/ 123 | 124 | # Ignore Gradle GUI config 125 | gradle-app.setting 126 | 127 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 128 | !gradle-wrapper.jar 129 | 130 | # Cache of project 131 | .gradletasknamecache 132 | 133 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 134 | # gradle/wrapper/gradle-wrapper.properties 135 | 136 | # End of https://www.gitignore.io/api/java,macos,gradle,intellij 137 | 138 | run 139 | /.idea 140 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/LineWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** 20 | * Implements soft line wrapping on an appendable. To use, append characters using 21 | * [LineWrapper.append] or soft-wrapping spaces using [LineWrapper.wrappingSpace]. 22 | */ 23 | internal class LineWrapper( 24 | private val out: Appendable, 25 | private val indent: String, 26 | private val columnLimit: Int 27 | ) { 28 | 29 | private var closed = false 30 | 31 | /** Characters written since the last wrapping space that haven't yet been flushed. */ 32 | private val buffer = StringBuilder() 33 | 34 | /** The number of characters since the most recent newline. Includes both out and the buffer. */ 35 | private var column = 0 36 | 37 | /** -1 if we have no buffering; otherwise the number of spaces to write after wrapping. */ 38 | private var indentLevel = -1 39 | 40 | /** Emit `s`. This may be buffered to permit line wraps to be inserted. */ 41 | fun append(s: String) { 42 | check(!closed) { "closed" } 43 | 44 | if (indentLevel != -1) { 45 | val nextNewline = s.indexOf('\n') 46 | 47 | // If s doesn't cause the current line to cross the limit, buffer it and return. We'll decide 48 | // whether or not we have to wrap it later. 49 | if (nextNewline == -1 && column + s.length <= columnLimit) { 50 | buffer.append(s) 51 | column += s.length 52 | return 53 | } 54 | 55 | // Wrap if appending s would overflow the current line. 56 | val wrap = nextNewline == -1 || column + nextNewline > columnLimit 57 | flush(wrap) 58 | } 59 | 60 | out.append(s) 61 | val lastNewline = s.lastIndexOf('\n') 62 | column = if (lastNewline != -1) 63 | s.length - lastNewline - 1 64 | else 65 | column + s.length 66 | } 67 | 68 | /** Emit either a space or a newline character. */ 69 | fun wrappingSpace(indentLevel: Int) { 70 | check(!closed) { "closed" } 71 | 72 | if (this.indentLevel != -1) flush(false) 73 | this.column++ 74 | this.indentLevel = indentLevel 75 | } 76 | 77 | /** Flush any outstanding text and forbid future writes to this line wrapper. */ 78 | fun close() { 79 | if (indentLevel != -1) flush(false) 80 | closed = true 81 | } 82 | 83 | /** Write the space followed by any buffered text that follows it. */ 84 | private fun flush(wrap: Boolean) { 85 | if (wrap) { 86 | out.append('\n') 87 | for (i in 0 until indentLevel) { 88 | out.append(indent) 89 | } 90 | column = indentLevel * indent.length 91 | column += buffer.length 92 | } else { 93 | out.append(' ') 94 | } 95 | out.append(buffer) 96 | buffer.delete(0, buffer.length) 97 | indentLevel = -1 98 | } 99 | 100 | override fun toString(): String { 101 | return out.toString() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/EnumSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated `enum` declaration. */ 20 | class EnumSpec 21 | private constructor( 22 | builder: Builder 23 | ) : TypeSpec(builder) { 24 | 25 | override val name = builder.name 26 | val tsDoc = builder.tsDoc.build() 27 | val modifiers = builder.modifiers.toImmutableSet() 28 | val constants = builder.constants.toImmutableMap() 29 | 30 | override fun emit(codeWriter: CodeWriter) { 31 | 32 | codeWriter.emitTSDoc(tsDoc) 33 | codeWriter.emitModifiers(modifiers, setOf()) 34 | codeWriter.emitCode(CodeBlock.of("enum %L {\n", name)) 35 | 36 | codeWriter.indent() 37 | val i = constants.entries.iterator() 38 | while (i.hasNext()) { 39 | val constant = i.next() 40 | codeWriter.emitCode(CodeBlock.of("%L", constant.key)) 41 | constant.value?.let { 42 | codeWriter.emitCode(" = ") 43 | codeWriter.emitCode(it) 44 | } 45 | if (i.hasNext()) { 46 | codeWriter.emit(",\n") 47 | } else { 48 | codeWriter.emit("\n") 49 | } 50 | } 51 | 52 | codeWriter.unindent() 53 | codeWriter.emit("}\n") 54 | } 55 | 56 | private val hasNoBody: Boolean 57 | get() = constants.isEmpty() 58 | 59 | fun toBuilder(): Builder { 60 | val builder = Builder(name) 61 | builder.tsDoc.add(tsDoc) 62 | builder.modifiers += modifiers 63 | builder.constants += constants 64 | return builder 65 | } 66 | 67 | class Builder 68 | internal constructor( 69 | name: String, 70 | val constants: MutableMap = mutableMapOf() 71 | ) : TypeSpec.Builder(name) { 72 | 73 | internal val tsDoc = CodeBlock.builder() 74 | internal val modifiers = mutableListOf() 75 | 76 | fun addTSDoc(format: String, vararg args: Any) = apply { 77 | tsDoc.add(format, *args) 78 | } 79 | 80 | fun addTSDoc(block: CodeBlock) = apply { 81 | tsDoc.add(block) 82 | } 83 | 84 | fun addModifiers(vararg modifiers: Modifier) = apply { 85 | modifiers.forEach { require(it == Modifier.EXPORT || it == Modifier.DECLARE) } 86 | this.modifiers += modifiers 87 | } 88 | 89 | fun addConstant(name: String, initializer: String? = null) = 90 | addConstant(name, initializer?.let { CodeBlock.of(it) }) 91 | 92 | fun addConstant(name: String, initializer: CodeBlock?) = apply { 93 | require(name.isName) { "not a valid enum constant: $name" } 94 | constants.put(name, initializer) 95 | } 96 | 97 | override fun build(): EnumSpec { 98 | return EnumSpec(this) 99 | } 100 | } 101 | 102 | companion object { 103 | 104 | @JvmStatic 105 | fun builder(name: String) = Builder(name) 106 | 107 | @JvmStatic 108 | fun builder(name: TypeName) = Builder("$name") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/TypeAliasSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated typealias declaration */ 20 | class TypeAliasSpec 21 | private constructor( 22 | builder: Builder 23 | ) : TypeSpec(builder) { 24 | 25 | override val name = builder.name 26 | val type = builder.type 27 | val tsDoc = builder.tsDoc.build() 28 | val modifiers = builder.modifiers.toImmutableSet() 29 | val typeVariables = builder.typeVariables.toImmutableList() 30 | 31 | override fun emit(codeWriter: CodeWriter) { 32 | codeWriter.emitTSDoc(tsDoc) 33 | codeWriter.emitModifiers(modifiers) 34 | codeWriter.emitCode(CodeBlock.of("type %L", name)) 35 | codeWriter.emitTypeVariables(typeVariables) 36 | codeWriter.emitCode(CodeBlock.of(" = %T", type)) 37 | codeWriter.emit(";\n") 38 | } 39 | 40 | override fun equals(other: Any?): Boolean { 41 | if (this === other) return true 42 | if (other == null) return false 43 | if (javaClass != other.javaClass) return false 44 | return toString() == other.toString() 45 | } 46 | 47 | override fun hashCode() = toString().hashCode() 48 | 49 | override fun toString() = buildCodeString { emit(this) } 50 | 51 | fun toBuilder(): Builder { 52 | val builder = Builder(name, type) 53 | builder.tsDoc.add(tsDoc) 54 | builder.modifiers += modifiers 55 | builder.typeVariables += typeVariables 56 | return builder 57 | } 58 | 59 | class Builder internal constructor( 60 | name: String, 61 | internal val type: TypeName 62 | ) : TypeSpec.Builder(name) { 63 | 64 | internal val tsDoc = CodeBlock.builder() 65 | internal val modifiers = mutableSetOf() 66 | internal val typeVariables = mutableSetOf() 67 | 68 | init { 69 | require(name.isName) { "not a valid name: $name" } 70 | } 71 | 72 | fun addTSDoc(format: String, vararg args: Any) = apply { 73 | tsDoc.add(format, *args) 74 | } 75 | 76 | fun addTSDoc(block: CodeBlock) = apply { 77 | tsDoc.add(block) 78 | } 79 | 80 | fun addModifiers(vararg modifiers: Modifier) = apply { 81 | modifiers.forEach(this::addModifier) 82 | } 83 | 84 | fun addModifier(modifier: Modifier) { 85 | require(modifier in setOf(Modifier.EXPORT)) { 86 | "unexpected typealias modifier $modifier" 87 | } 88 | this.modifiers.add(modifier) 89 | } 90 | 91 | fun addTypeVariables(typeVariables: Iterable) = apply { 92 | this.typeVariables += typeVariables 93 | } 94 | 95 | fun addTypeVariable(typeVariable: TypeName.TypeVariable) = apply { 96 | typeVariables += typeVariable 97 | } 98 | 99 | override fun build() = TypeAliasSpec(this) 100 | } 101 | 102 | companion object { 103 | 104 | @JvmStatic 105 | fun builder(name: String, type: TypeName) = Builder( 106 | name, type 107 | ) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | TypeScriptPoet 3 | ========== 4 | 5 | [![CI](https://github.com/outfoxx/typescriptpoet/actions/workflows/ci.yml/badge.svg)](https://github.com/outfoxx/typescriptpoet/actions/workflows/ci.yml) 6 | [![Maven Central](https://img.shields.io/maven-central/v/io.outfoxx/typescriptpoet.svg)][dl] 7 | [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.outfoxx/typescriptpoet.svg)][snap] 8 | [![codebeat badge](https://codebeat.co/badges/70f7939d-185e-42d7-b7a8-ea240840a121)](https://codebeat.co/projects/github-com-outfoxx-typescriptpoet-master) 9 | 10 | `TypeScriptPoet` is a Kotlin and Java API for generating `.ts` source files. 11 | 12 | Source file generation can be useful when doing things such as annotation processing or interacting 13 | with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate 14 | the need to write boilerplate while also keeping a single source of truth for the metadata. 15 | 16 | 17 | ### Example 18 | 19 | Here's a `HelloWorld` file: 20 | 21 | ```typescript 22 | import {Observable} from 'rxjs/Observable'; 23 | import 'rxjs/add/observable/from'; 24 | 25 | 26 | class Greeter { 27 | 28 | private name: string; 29 | 30 | constructor(private name: string) { 31 | } 32 | 33 | greet(): Observable { 34 | return Observable.from(`Hello $name`)} 35 | 36 | } 37 | ``` 38 | 39 | And this is the code to generate it with TypeScriptPoet: 40 | 41 | ```kotlin 42 | val observableTypeName = TypeName.importedType("@rxjs/Observable") 43 | 44 | val testClass = ClassSpec.builder("Greeter") 45 | .addProperty("name", TypeName.STRING, false, Modifier.PRIVATE) 46 | .constructor( 47 | FunctionSpec.constructorBuilder() 48 | .addParameter("name", TypeName.STRING, false, Modifier.PRIVATE) 49 | .build() 50 | ) 51 | .addFunction( 52 | FunctionSpec.builder("greet") 53 | .returns(TypeName.parameterizedType(observableTypeName, TypeName.STRING)) 54 | .addCode("return %T.%N(`Hello \$name`)", observableTypeName, SymbolSpec.from("+rxjs/add/observable/from#Observable")) 55 | .build() 56 | ) 57 | .build() 58 | 59 | val file = FileSpec.builder("Greeter") 60 | .addClass(testClass) 61 | .build() 62 | 63 | val out = StringWriter() 64 | file.writeTo(out) 65 | ``` 66 | 67 | The [KDoc][kdoc] catalogs the complete TypeScriptPoet API, which is inspired by [JavaPoet][javapoet]. 68 | 69 | 70 | Download 71 | -------- 72 | 73 | Download [the latest .jar][dl] or depend via Maven: 74 | 75 | ```xml 76 | 77 | io.outfoxx 78 | typescriptpoet 79 | 1.1.2 80 | 81 | ``` 82 | 83 | or Gradle: 84 | 85 | ```groovy 86 | compile 'io.outfoxx:typescriptpoet:1.1.2' 87 | ``` 88 | 89 | Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. 90 | 91 | 92 | License 93 | ------- 94 | 95 | Copyright 2017 Outfox, Inc. 96 | 97 | Licensed under the Apache License, Version 2.0 (the "License"); 98 | you may not use this file except in compliance with the License. 99 | You may obtain a copy of the License at 100 | 101 | http://www.apache.org/licenses/LICENSE-2.0 102 | 103 | Unless required by applicable law or agreed to in writing, software 104 | distributed under the License is distributed on an "AS IS" BASIS, 105 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 106 | See the License for the specific language governing permissions and 107 | limitations under the License. 108 | 109 | 110 | [dl]: https://search.maven.org/remote_content?g=io.outfoxx&a=typescriptpoet&v=LATEST 111 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/io/outfoxx/typescriptpoet/ 112 | [kdoc]: https://outfoxx.github.io/typescriptpoet/1.1.2/index.html 113 | [javapoet]: https://github.com/square/javapoet/ 114 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/EnumSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.CodeWriter 20 | import io.outfoxx.typescriptpoet.EnumSpec 21 | import io.outfoxx.typescriptpoet.Modifier 22 | import io.outfoxx.typescriptpoet.tag 23 | import org.hamcrest.CoreMatchers.equalTo 24 | import org.hamcrest.CoreMatchers.hasItems 25 | import org.hamcrest.MatcherAssert.assertThat 26 | import org.junit.jupiter.api.DisplayName 27 | import org.junit.jupiter.api.Test 28 | import java.io.StringWriter 29 | 30 | @DisplayName("EnumSpec Tests") 31 | class EnumSpecTests { 32 | 33 | @Test 34 | @DisplayName("Tags on builders can be retrieved on builders and built specs") 35 | fun testTags() { 36 | val testBuilder = EnumSpec.builder("Test") 37 | .tag(5) 38 | val testSpec = testBuilder.build() 39 | 40 | assertThat(testBuilder.tags[Integer::class] as? Int, equalTo(5)) 41 | assertThat(testSpec.tag(), equalTo(5)) 42 | } 43 | 44 | @Test 45 | @DisplayName("Generates TSDoc at before class definition") 46 | fun testGenTSDoc() { 47 | val testClass = EnumSpec.builder("Test") 48 | .addTSDoc("this is a comment\n") 49 | .build() 50 | 51 | val out = StringWriter() 52 | testClass.emit(CodeWriter(out)) 53 | 54 | assertThat( 55 | out.toString(), 56 | equalTo( 57 | """ 58 | /** 59 | * this is a comment 60 | */ 61 | enum Test { 62 | } 63 | 64 | """.trimIndent() 65 | ) 66 | ) 67 | } 68 | 69 | @Test 70 | @DisplayName("Generates modifiers in order") 71 | fun testGenModifiersInOrder() { 72 | val testClass = EnumSpec.builder("Test") 73 | .addModifiers(Modifier.EXPORT) 74 | .build() 75 | 76 | val out = StringWriter() 77 | testClass.emit(CodeWriter(out)) 78 | 79 | assertThat( 80 | out.toString(), 81 | equalTo( 82 | """ 83 | export enum Test { 84 | } 85 | 86 | """.trimIndent() 87 | ) 88 | ) 89 | } 90 | 91 | @Test 92 | @DisplayName("Generates formatted constants") 93 | fun testGenConstants() { 94 | val testClass = EnumSpec.builder("Test") 95 | .addConstant("A", "10") 96 | .addConstant("B", "20") 97 | .addConstant("C", "30") 98 | .build() 99 | 100 | val out = StringWriter() 101 | testClass.emit(CodeWriter(out)) 102 | 103 | assertThat( 104 | out.toString(), 105 | equalTo( 106 | """ 107 | enum Test { 108 | A = 10, 109 | B = 20, 110 | C = 30 111 | } 112 | 113 | """.trimIndent() 114 | ) 115 | ) 116 | } 117 | 118 | @Test 119 | @DisplayName("toBuilder copies all fields") 120 | fun testToBuilder() { 121 | val testEnumBldr = EnumSpec.builder("Test") 122 | .addTSDoc("this is a comment\n") 123 | .addModifiers(Modifier.EXPORT) 124 | .addConstant("A", "10") 125 | .build() 126 | .toBuilder() 127 | 128 | assertThat(testEnumBldr.name, equalTo("Test")) 129 | assertThat(testEnumBldr.tsDoc.formatParts, hasItems("this is a comment\n")) 130 | assertThat(testEnumBldr.modifiers, hasItems(Modifier.EXPORT)) 131 | assertThat(testEnumBldr.constants.keys, hasItems("A")) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/PropertySpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated property declaration. */ 20 | class PropertySpec 21 | private constructor( 22 | builder: Builder 23 | ) : Taggable(builder.tags.toImmutableMap()) { 24 | 25 | val name = builder.name 26 | val type = builder.type 27 | val tsDoc = builder.tsDoc.build() 28 | val decorators = builder.decorators.toImmutableList() 29 | val modifiers = builder.modifiers.toImmutableSet() 30 | val initializer = builder.initializer 31 | val optional = builder.optional 32 | 33 | internal fun emit( 34 | codeWriter: CodeWriter, 35 | implicitModifiers: Set, 36 | asStatement: Boolean = false, 37 | withInitializer: Boolean = true, 38 | compactOptionalAllowed: Boolean = false, 39 | ) { 40 | codeWriter.emitTSDoc(tsDoc) 41 | codeWriter.emitDecorators(decorators, false) 42 | codeWriter.emitModifiers(modifiers, implicitModifiers) 43 | codeWriter.emitCode( 44 | CodeBlock.of( 45 | "%L${if (optional && compactOptionalAllowed) "?" else ""}: %T${if (optional && !compactOptionalAllowed) " | undefined" else ""}", 46 | name, 47 | type 48 | ), 49 | ) 50 | if (withInitializer && initializer != null) { 51 | codeWriter.emit(" = ") 52 | codeWriter.emitCode(CodeBlock.of("%[%L%]", initializer)) 53 | } 54 | if (asStatement) { 55 | codeWriter.emit(";\n") 56 | } 57 | } 58 | 59 | override fun toString() = buildCodeString { emit(this, emptySet()) } 60 | 61 | fun toBuilder(): Builder { 62 | val bldr = Builder(name, type, optional) 63 | .addTSDoc(tsDoc) 64 | .addDecorators(decorators) 65 | .addModifiers(*modifiers.toTypedArray()) 66 | initializer?.let { bldr.initializer(it) } 67 | 68 | return bldr 69 | } 70 | 71 | class Builder internal constructor( 72 | internal val name: String, 73 | internal val type: TypeName, 74 | internal var optional: Boolean 75 | ) : Taggable.Builder() { 76 | 77 | internal val tsDoc = CodeBlock.builder() 78 | internal val decorators = mutableListOf() 79 | internal val modifiers = mutableListOf() 80 | internal var initializer: CodeBlock? = null 81 | 82 | fun addTSDoc(format: String, vararg args: Any) = apply { 83 | tsDoc.add(format, *args) 84 | } 85 | 86 | fun addTSDoc(block: CodeBlock) = apply { 87 | tsDoc.add(block) 88 | } 89 | 90 | fun addDecorators(decoratorSpecs: Iterable) = apply { 91 | decorators += decoratorSpecs 92 | } 93 | 94 | fun addDecorator(decoratorSpec: DecoratorSpec) = apply { 95 | decorators += decoratorSpec 96 | } 97 | 98 | fun addModifiers(vararg modifiers: Modifier) = apply { 99 | this.modifiers += modifiers 100 | } 101 | 102 | fun optional(optional: Boolean) = apply { 103 | this.optional = optional 104 | } 105 | 106 | fun initializer(format: String, vararg args: Any?) = initializer( 107 | CodeBlock.of(format, *args) 108 | ) 109 | 110 | fun initializer(codeBlock: CodeBlock) = apply { 111 | check(this.initializer == null) { "initializer was already set" } 112 | this.initializer = codeBlock 113 | } 114 | 115 | fun build() = PropertySpec(this) 116 | } 117 | 118 | companion object { 119 | 120 | @JvmStatic 121 | fun builder(name: String, type: TypeName, optional: Boolean = false, vararg modifiers: Modifier): Builder { 122 | return Builder(name, type, optional).addModifiers(*modifiers) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/TypeAliasSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.CodeWriter 20 | import io.outfoxx.typescriptpoet.Modifier 21 | import io.outfoxx.typescriptpoet.TypeAliasSpec 22 | import io.outfoxx.typescriptpoet.TypeName 23 | import io.outfoxx.typescriptpoet.tag 24 | import org.hamcrest.CoreMatchers.equalTo 25 | import org.hamcrest.CoreMatchers.hasItems 26 | import org.hamcrest.MatcherAssert.assertThat 27 | import org.junit.jupiter.api.DisplayName 28 | import org.junit.jupiter.api.Test 29 | import java.io.StringWriter 30 | 31 | @DisplayName("TypeAliasSpec Tests") 32 | class TypeAliasSpecTests { 33 | 34 | @Test 35 | @DisplayName("Tags on builders can be retrieved on builders and built specs") 36 | fun testTags() { 37 | val testBuilder = TypeAliasSpec.builder("Test", TypeName.STRING) 38 | .tag(5) 39 | val testSpec = testBuilder.build() 40 | 41 | assertThat(testBuilder.tags[Integer::class] as? Int, equalTo(5)) 42 | assertThat(testSpec.tag(), equalTo(5)) 43 | } 44 | 45 | @Test 46 | @DisplayName("Generates TSDoc at before class definition") 47 | fun testGenTSDoc() { 48 | val testAlias = TypeAliasSpec.builder("Integer", TypeName.NUMBER) 49 | .addTSDoc("this is a comment\n") 50 | .build() 51 | 52 | val out = StringWriter() 53 | testAlias.emit(CodeWriter(out)) 54 | 55 | assertThat( 56 | out.toString(), 57 | equalTo( 58 | """ 59 | /** 60 | * this is a comment 61 | */ 62 | type Integer = number; 63 | 64 | """.trimIndent() 65 | ) 66 | ) 67 | } 68 | 69 | @Test 70 | @DisplayName("Generates modifiers in order") 71 | fun testGenModifiersInOrder() { 72 | val testAlias = TypeAliasSpec.builder("Integer", TypeName.NUMBER) 73 | .addModifiers(Modifier.EXPORT) 74 | .build() 75 | 76 | val out = StringWriter() 77 | testAlias.emit(CodeWriter(out)) 78 | 79 | assertThat( 80 | out.toString(), 81 | equalTo( 82 | """ 83 | export type Integer = number; 84 | 85 | """.trimIndent() 86 | ) 87 | ) 88 | } 89 | 90 | @Test 91 | @DisplayName("Generates simple alias") 92 | fun testSimpleAlias() { 93 | val testAlias = TypeAliasSpec.builder("Integer", TypeName.NUMBER) 94 | .build() 95 | 96 | val out = StringWriter() 97 | testAlias.emit(CodeWriter(out)) 98 | 99 | assertThat( 100 | out.toString(), 101 | equalTo( 102 | """ 103 | type Integer = number; 104 | 105 | """.trimIndent() 106 | ) 107 | ) 108 | } 109 | 110 | @Test 111 | @DisplayName("Generates generic alias") 112 | fun testGenericAlias() { 113 | val typeVar = TypeName.typeVariable( 114 | "A", 115 | TypeName.bound( 116 | TypeName.implicit("Test") 117 | ) 118 | ) 119 | val testAlias = TypeAliasSpec.builder( 120 | "StringMap", 121 | TypeName.mapType( 122 | TypeName.STRING, typeVar 123 | ) 124 | ) 125 | .addTypeVariable(typeVar) 126 | .build() 127 | 128 | val out = StringWriter() 129 | testAlias.emit(CodeWriter(out)) 130 | 131 | assertThat( 132 | out.toString(), 133 | equalTo( 134 | """ 135 | type StringMap = Map; 136 | 137 | """.trimIndent() 138 | ) 139 | ) 140 | } 141 | 142 | @Test 143 | @DisplayName("toBuilder copies all fields") 144 | fun testToBuilder() { 145 | val testAliasBldr = TypeAliasSpec.builder("Test", TypeName.NUMBER) 146 | .addTSDoc("this is a comment\n") 147 | .addModifiers(Modifier.EXPORT) 148 | .addTypeVariable( 149 | TypeName.typeVariable( 150 | "A", 151 | TypeName.bound( 152 | TypeName.implicit("Test") 153 | ) 154 | ) 155 | ) 156 | .build() 157 | .toBuilder() 158 | 159 | assertThat(testAliasBldr.name, equalTo("Test")) 160 | assertThat(testAliasBldr.type, equalTo(TypeName.NUMBER)) 161 | assertThat(testAliasBldr.tsDoc.formatParts, hasItems("this is a comment\n")) 162 | assertThat(testAliasBldr.modifiers, hasItems(Modifier.EXPORT)) 163 | assertThat(testAliasBldr.typeVariables.map { it.name }, hasItems("A")) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/DecoratorSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.CodeBlock 20 | import io.outfoxx.typescriptpoet.CodeWriter 21 | import io.outfoxx.typescriptpoet.DecoratorSpec 22 | import io.outfoxx.typescriptpoet.SymbolSpec 23 | import io.outfoxx.typescriptpoet.tag 24 | import org.hamcrest.CoreMatchers.equalTo 25 | import org.hamcrest.CoreMatchers.hasItems 26 | import org.hamcrest.MatcherAssert.assertThat 27 | import org.junit.jupiter.api.DisplayName 28 | import org.junit.jupiter.api.Test 29 | import java.io.StringWriter 30 | 31 | @DisplayName("DecoratorSpec Tests") 32 | class DecoratorSpecTests { 33 | 34 | @Test 35 | @DisplayName("Tags on builders can be retrieved on builders and built specs") 36 | fun testTags() { 37 | val testBuilder = DecoratorSpec.builder("Test") 38 | .tag(5) 39 | val testSpec = testBuilder.build() 40 | 41 | assertThat(testBuilder.tags[Integer::class] as? Int, equalTo(5)) 42 | assertThat(testSpec.tag(), equalTo(5)) 43 | } 44 | 45 | @Test 46 | fun `Generate with multi-line argument`() { 47 | val testDec = DecoratorSpec.builder("test") 48 | .addParameter(null, "{%>\nvalue: 5%<\n}") 49 | .build() 50 | 51 | assertThat( 52 | testDec.toString(), 53 | equalTo( 54 | """ 55 | @test({ 56 | value: 5 57 | }) 58 | """.trimIndent() 59 | ) 60 | ) 61 | } 62 | 63 | @Test 64 | fun `Generate with named arguments`() { 65 | val testDec = DecoratorSpec.builder("test") 66 | .addParameter("value", "100") 67 | .addParameter("value2", "20") 68 | .build() 69 | 70 | assertThat( 71 | testDec.toString(), 72 | equalTo( 73 | """ 74 | @test(/* value */ 100, /* value2 */ 20) 75 | """.trimIndent() 76 | ) 77 | ) 78 | } 79 | 80 | @Test 81 | fun `Generate with unnamed arguments`() { 82 | val testDec = DecoratorSpec.builder("test") 83 | .addParameter(null, "100") 84 | .addParameter(null, "20") 85 | .build() 86 | 87 | assertThat( 88 | testDec.toString(), 89 | equalTo( 90 | """ 91 | @test(100, 20) 92 | """.trimIndent() 93 | ) 94 | ) 95 | } 96 | 97 | @Test 98 | fun `Generate with mixed named arguments`() { 99 | val testDec = DecoratorSpec.builder("test") 100 | .addParameter(null, "100") 101 | .addParameter("value", "20") 102 | .addParameter(null, "30") 103 | .addParameter("value2", "40") 104 | .build() 105 | 106 | assertThat( 107 | testDec.toString(), 108 | equalTo( 109 | """ 110 | @test(100, /* value */ 20, 30, /* value2 */ 40) 111 | """.trimIndent() 112 | ) 113 | ) 114 | } 115 | 116 | @Test 117 | @DisplayName("Generate with no-arguments") 118 | fun testGenNoArg() { 119 | val testDec = DecoratorSpec.builder("test") 120 | .build() 121 | 122 | val out = StringWriter() 123 | testDec.emit(CodeWriter(out)) 124 | 125 | assertThat( 126 | out.toString(), 127 | equalTo( 128 | """ 129 | @test 130 | """.trimIndent() 131 | ) 132 | ) 133 | } 134 | 135 | @Test 136 | @DisplayName("Generate factory with no-arguments") 137 | fun testGenNoArgFactory() { 138 | val testDec = DecoratorSpec.builder("test") 139 | .asFactory() 140 | .build() 141 | 142 | val out = StringWriter() 143 | testDec.emit(CodeWriter(out)) 144 | 145 | assertThat( 146 | out.toString(), 147 | equalTo( 148 | """ 149 | @test() 150 | """.trimIndent() 151 | ) 152 | ) 153 | } 154 | 155 | @Test 156 | @DisplayName("toBuilder copies all fields") 157 | fun testToBuilder() { 158 | val testDecBldr = DecoratorSpec.builder("test") 159 | .addParameter("value", "100") 160 | .addParameter("value2", "20") 161 | .asFactory() 162 | .build() 163 | .toBuilder() 164 | 165 | assertThat(testDecBldr.name, equalTo(SymbolSpec.from("test"))) 166 | assertThat( 167 | testDecBldr.parameters, 168 | hasItems( 169 | Pair("value", CodeBlock.of("100")), 170 | Pair("value2", CodeBlock.of("20")) 171 | ) 172 | ) 173 | assertThat(testDecBldr.factory, equalTo(true)) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/NameAllocator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import java.util.UUID 20 | 21 | /** 22 | * Assigns TypeScript identifier names to avoid collisions, keywords, and invalid characters. To use, 23 | * first create an instance and allocate all of the names that you need. Typically this is a 24 | * mix of user-supplied names and constants: 25 | * 26 | * ``` 27 | * val nameAllocator = NameAllocator() 28 | * for (property in properties) { 29 | * nameAllocator.newName(property.name, property) 30 | * } 31 | * nameAllocator.newName("sb", "string builder") 32 | * ``` 33 | * 34 | * Pass a unique tag object to each allocation. The tag scopes the name, and can be used to look up 35 | * the allocated name later. Typically the tag is the object that is being named. In the above 36 | * example we use `property` for the user-supplied property names, and `"string builder"` for our 37 | * constant string builder. 38 | * 39 | * Once we've allocated names we can use them when generating code: 40 | * 41 | * ``` 42 | * val builder = FunctionSpec.name("toString") 43 | * .returns(DeclaredTypeName.STRING) 44 | * 45 | * builder.addStatement("var %N = ''", nameAllocator.get("string builder")) 46 | * 47 | * for (property in properties) { 48 | * builder.addStatement("%N += '${%N}'", nameAllocator.get("string builder"), nameAllocator.get(property)) 49 | * } 50 | * builder.addStatement("return %N", nameAllocator.get("string builder")) 51 | * 52 | * return builder.build() 53 | * ``` 54 | * 55 | * The above code generates unique names if presented with conflicts. Given user-supplied properties 56 | * with names `ab` and `sb` this generates the following: 57 | * 58 | * ``` 59 | * toString(): String { 60 | * var sb_ = ''; 61 | * sb_ += '${ab}'; 62 | * sb_ += '${sb}'; 63 | * return sb_; 64 | * } 65 | * ``` 66 | * 67 | * The underscore is appended to `sb` to avoid conflicting with the user-supplied `sb` property. 68 | * Underscores are also prefixed for names that start with a digit, and used to replace name-unsafe 69 | * characters like space or dash. 70 | * 71 | * When dealing with multiple independent inner scopes, use a [copy][NameAllocator.copy] of the 72 | * NameAllocator used for the outer scope to further refine name allocation for a specific inner 73 | * scope. 74 | */ 75 | class NameAllocator private constructor( 76 | private val allocatedNames: MutableSet, 77 | private val tagToName: MutableMap 78 | ) { 79 | constructor() : this(mutableSetOf(), mutableMapOf()) 80 | 81 | /** 82 | * Return a new name using `suggestion` that will not be a TypeScript identifier or clash with other 83 | * names. The returned value can be queried multiple times by passing `tag` to 84 | * [NameAllocator.get]. 85 | */ 86 | @JvmOverloads fun newName( 87 | suggestion: String, 88 | tag: Any = UUID.randomUUID().toString() 89 | ): String { 90 | var result = toTypeScriptIdentifier(suggestion) 91 | while (result.isKeyword || !allocatedNames.add(result)) { 92 | result += "_" 93 | } 94 | 95 | val replaced = tagToName.put(tag, result) 96 | if (replaced != null) { 97 | tagToName[tag] = replaced // Put things back as they were! 98 | throw IllegalArgumentException("tag $tag cannot be used for both '$replaced' and '$result'") 99 | } 100 | 101 | return result 102 | } 103 | 104 | /** Retrieve a name created with [NameAllocator.newName]. */ 105 | operator fun get(tag: Any): String = requireNotNull(tagToName[tag]) { "unknown tag: $tag" } 106 | 107 | /** 108 | * Create a deep copy of this NameAllocator. Useful to create multiple independent refinements 109 | * of a NameAllocator to be used in the respective definition of multiples, independently-scoped, 110 | * inner code blocks. 111 | * 112 | * @return A deep copy of this NameAllocator. 113 | */ 114 | fun copy(): NameAllocator { 115 | return NameAllocator(allocatedNames.toMutableSet(), tagToName.toMutableMap()) 116 | } 117 | 118 | fun tagsToNames() = tagToName.toImmutableMap() 119 | } 120 | 121 | /** 122 | * TODO Replace Java identifier checks with valid TypeScript code point checks 123 | */ 124 | private fun toTypeScriptIdentifier(suggestion: String) = buildString { 125 | var i = 0 126 | while (i < suggestion.length) { 127 | val codePoint = suggestion.codePointAt(i) 128 | if (i == 0 && 129 | !Character.isJavaIdentifierStart(codePoint) && 130 | Character.isJavaIdentifierPart(codePoint) 131 | ) { 132 | append("_") 133 | } 134 | 135 | val validCodePoint: Int = if (Character.isJavaIdentifierPart(codePoint)) { 136 | codePoint 137 | } else { 138 | '_'.toInt() 139 | } 140 | appendCodePoint(validCodePoint) 141 | i += Character.charCount(codePoint) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/ParameterSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import kotlin.math.min 20 | 21 | class ParameterSpec private constructor( 22 | builder: Builder 23 | ) : Taggable(builder.tags.toImmutableMap()) { 24 | 25 | val name = builder.name 26 | val optional = builder.optional 27 | val decorators = builder.decorators.toImmutableList() 28 | val modifiers = builder.modifiers.toImmutableSet() 29 | val type = builder.type 30 | val defaultValue = builder.defaultValue 31 | 32 | internal fun emit( 33 | codeWriter: CodeWriter, 34 | includeType: Boolean = true, 35 | isRest: Boolean = false, 36 | optionalAllowed: Boolean = false, 37 | ) { 38 | codeWriter.emitDecorators(decorators, true) 39 | codeWriter.emitModifiers(modifiers) 40 | if (isRest) { 41 | codeWriter.emitCode("... ") 42 | } 43 | codeWriter.emitCode(CodeBlock.of("%L", name)) 44 | if (includeType) { 45 | if (optional && optionalAllowed) { 46 | codeWriter.emitCode("?") 47 | } 48 | codeWriter.emitCode(CodeBlock.of(": %T", type)) 49 | if (optional && !optionalAllowed) { 50 | codeWriter.emitCode(" | undefined") 51 | } 52 | } 53 | emitDefaultValue(codeWriter) 54 | } 55 | 56 | internal fun emitDefaultValue(codeWriter: CodeWriter) { 57 | if (defaultValue != null) { 58 | codeWriter.emitCode(CodeBlock.of(" = %[%L%]", defaultValue)) 59 | } 60 | } 61 | 62 | override fun equals(other: Any?): Boolean { 63 | if (this === other) return true 64 | if (other == null) return false 65 | if (javaClass != other.javaClass) return false 66 | return toString() == other.toString() 67 | } 68 | 69 | override fun hashCode() = toString().hashCode() 70 | 71 | override fun toString() = buildCodeString { emit(this) } 72 | 73 | fun toBuilder(name: String = this.name, type: TypeName = this.type): Builder { 74 | val builder = Builder(name, type, optional) 75 | builder.decorators += decorators 76 | builder.modifiers += modifiers 77 | builder.defaultValue = defaultValue 78 | return builder 79 | } 80 | 81 | class Builder internal constructor( 82 | internal val name: String, 83 | internal val type: TypeName, 84 | internal var optional: Boolean 85 | ) : Taggable.Builder() { 86 | 87 | internal val decorators = mutableListOf() 88 | internal val modifiers = mutableListOf() 89 | internal var defaultValue: CodeBlock? = null 90 | 91 | fun addDecorators(decoratorSpecs: Iterable) = apply { 92 | decorators += decoratorSpecs 93 | } 94 | 95 | fun addDecorator(decoratorSpec: DecoratorSpec) = apply { 96 | decorators += decoratorSpec 97 | } 98 | 99 | fun addDecorator(decorator: SymbolSpec) = apply { 100 | decorators += DecoratorSpec.builder(decorator).build() 101 | } 102 | 103 | fun addModifiers(vararg modifiers: Modifier) = apply { 104 | this.modifiers += modifiers 105 | } 106 | 107 | fun addModifiers(modifiers: Iterable) = apply { 108 | this.modifiers += modifiers 109 | } 110 | 111 | fun optional(optional: Boolean) = apply { 112 | this.optional = optional 113 | } 114 | 115 | fun defaultValue(format: String, vararg args: Any?) = defaultValue( 116 | CodeBlock.of(format, *args) 117 | ) 118 | 119 | fun defaultValue(codeBlock: CodeBlock) = apply { 120 | check(this.defaultValue == null) { "initializer was already set" } 121 | this.defaultValue = codeBlock 122 | } 123 | 124 | fun build() = ParameterSpec(this) 125 | } 126 | 127 | companion object { 128 | 129 | @JvmStatic 130 | fun builder(name: String, type: TypeName, optional: Boolean = false, vararg modifiers: Modifier): Builder { 131 | require(name.isName) { "not a valid name: $name" } 132 | return Builder(name, type, optional).addModifiers(*modifiers) 133 | } 134 | } 135 | } 136 | 137 | internal fun List.emit( 138 | codeWriter: CodeWriter, 139 | enclosed: Boolean = true, 140 | rest: ParameterSpec? = null, 141 | constructorProperties: Map = emptyMap(), 142 | emitBlock: (ParameterSpec, Boolean, Boolean) -> Unit = 143 | { param, isRest, optionalAllowed -> 144 | param.emit(codeWriter, optionalAllowed = optionalAllowed, isRest = isRest) 145 | } 146 | ) = with(codeWriter) { 147 | val params = this@emit + if (rest != null) listOf(rest) else emptyList() 148 | if (enclosed) emit("(") 149 | if (size < 5 && all { constructorProperties[it.name]?.decorators?.isEmpty() ?: it.decorators.isEmpty() }) { 150 | params.forEachIndexed { index, parameter -> 151 | val optionalAllowed = subList(min(index + 1, size), size).all { it.optional } 152 | if (index > 0) emitCode(",%W") 153 | emitBlock(parameter, rest === parameter, optionalAllowed) 154 | } 155 | } else { 156 | emit("\n") 157 | indent(2) 158 | params.forEachIndexed { index, parameter -> 159 | val optionalAllowed = subList(min(index + 1, size), size).all { it.optional } 160 | if (index > 0) emit(",\n") 161 | emitBlock(parameter, rest === parameter, optionalAllowed) 162 | } 163 | unindent(2) 164 | emit("\n") 165 | } 166 | if (enclosed) emit(")") 167 | } 168 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -symbol 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 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/Taggable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import kotlin.reflect.KClass 20 | 21 | /** A type that can be tagged with extra metadata of the user's choice. */ 22 | abstract class Taggable( 23 | /** all tags. */ 24 | val tags: Map, Any> 25 | ) { 26 | 27 | /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ 28 | inline fun tag(type: KClass): T? = tags[type] as T? 29 | 30 | /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ 31 | inline fun tag(type: Class): T? = tag(type.kotlin) 32 | 33 | /** The builder analogue to [Taggable] types. */ 34 | abstract class Builder> { 35 | 36 | /** Mutable map of the current tags this builder contains. */ 37 | val tags: MutableMap, Any> = mutableMapOf() 38 | 39 | /** 40 | * Attaches [tag] to the request using [type] as a key. Tags can be read from a 41 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 42 | * [type]. 43 | * 44 | * Use this API to attach originating elements, debugging, or other application data to a spec 45 | * so that you may read it in other APIs or callbacks. 46 | */ 47 | fun tag(type: Class, tag: T?): B = tag(type.kotlin, tag) 48 | 49 | /** 50 | * Attaches [tag] to the request using [type] as a key. Tags can be read from a 51 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 52 | * [type]. 53 | * 54 | * Use this API to attach originating elements, debugging, or other application data to a spec 55 | * so that you may read it in other APIs or callbacks. 56 | */ 57 | @Suppress("UNCHECKED_CAST") 58 | fun tag(type: KClass, tag: T?): B = apply { 59 | if (tag == null) { 60 | this.tags.remove(type) 61 | } else { 62 | this.tags[type] = tag 63 | } 64 | } as B 65 | } 66 | } 67 | 68 | /** Returns the tag attached with [T] as a key, or null if no tag is attached with that key. */ 69 | inline fun Taggable.tag(): T? = tag(T::class) 70 | 71 | /** 72 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 73 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 74 | * [T]. 75 | * 76 | * Use this API to attach debugging or other application data to a spec so that you may read it in 77 | * other APIs or callbacks. 78 | */ 79 | 80 | inline fun , B : TypeSpec.Builder> TypeSpec.Builder.tag(tag: TAG?): B = 81 | tag(TAG::class, tag) 82 | 83 | /** 84 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 85 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 86 | * [T]. 87 | * 88 | * Use this API to attach debugging or other application data to a spec so that you may read it in 89 | * other APIs or callbacks. 90 | */ 91 | 92 | inline fun DecoratorSpec.Builder.tag(tag: T?): DecoratorSpec.Builder = 93 | tag(T::class, tag) 94 | 95 | /** 96 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 97 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 98 | * [T]. 99 | * 100 | * Use this API to attach debugging or other application data to a spec so that you may read it in 101 | * other APIs or callbacks. 102 | */ 103 | inline fun FileSpec.Builder.tag(tag: T?): FileSpec.Builder = 104 | tag(T::class, tag) 105 | 106 | /** 107 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 108 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 109 | * [T]. 110 | * 111 | * Use this API to attach debugging or other application data to a spec so that you may read it in 112 | * other APIs or callbacks. 113 | */ 114 | inline fun FunctionSpec.Builder.tag(tag: T?): FunctionSpec.Builder = 115 | tag(T::class, tag) 116 | 117 | /** 118 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 119 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 120 | * [T]. 121 | * 122 | * Use this API to attach debugging or other application data to a spec so that you may read it in 123 | * other APIs or callbacks. 124 | */ 125 | 126 | inline fun ModuleSpec.Builder.tag(tag: T?): ModuleSpec.Builder = 127 | tag(T::class, tag) 128 | 129 | /** 130 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 131 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 132 | * [T]. 133 | * 134 | * Use this API to attach debugging or other application data to a spec so that you may read it in 135 | * other APIs or callbacks. 136 | */ 137 | inline fun ParameterSpec.Builder.tag(tag: T?): ParameterSpec.Builder = 138 | tag(T::class, tag) 139 | 140 | /** 141 | * Attaches [tag] to the request using [T] as a key. Tags can be read from a 142 | * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for 143 | * [T]. 144 | * 145 | * Use this API to attach debugging or other application data to a spec so that you may read it in 146 | * other APIs or callbacks. 147 | */ 148 | inline fun PropertySpec.Builder.tag(tag: T?): PropertySpec.Builder = 149 | tag(T::class, tag) 150 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/SymbolSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.SymbolSpec 20 | import org.hamcrest.CoreMatchers.equalTo 21 | import org.hamcrest.CoreMatchers.instanceOf 22 | import org.hamcrest.MatcherAssert.assertThat 23 | import org.junit.jupiter.api.DisplayName 24 | import org.junit.jupiter.api.Test 25 | 26 | @DisplayName("SymbolSpec Tests") 27 | class SymbolSpecTests { 28 | 29 | @Test 30 | @DisplayName("Parsing implicitly defined (non-imported) symbols") 31 | fun testParsingImplicit() { 32 | 33 | val parsed = SymbolSpec.from("Some.Symbol.Depth") 34 | assertThat(parsed.value, equalTo("Some.Symbol.Depth")) 35 | } 36 | 37 | @Test 38 | @DisplayName("Parsing named import: exported symbol implied by module path") 39 | fun testParsingImplicitImportNamed() { 40 | 41 | val parsed = SymbolSpec.from("@rxjs/Observable") 42 | assertThat(parsed, instanceOf(SymbolSpec.ImportsName::class.java)) 43 | 44 | val sym = parsed as SymbolSpec.ImportsName 45 | assertThat(sym.value, equalTo("Observable")) 46 | assertThat(sym.source, equalTo("rxjs/Observable")) 47 | } 48 | 49 | @Test 50 | @DisplayName("Parsing named import: exported symbol implied by generated module path") 51 | fun testParsingImplicitImportNamedGeneratedModule() { 52 | 53 | val parsed = SymbolSpec.from("@!Api") 54 | assertThat(parsed, instanceOf(SymbolSpec.ImportsName::class.java)) 55 | 56 | val sym = parsed as SymbolSpec.ImportsName 57 | assertThat(sym.value, equalTo("Api")) 58 | assertThat(sym.source, equalTo("!Api")) 59 | } 60 | 61 | @Test 62 | @DisplayName("Parsing named import: exported symbol explicit, source relative to current dir") 63 | fun testParsingExplicitImportNamedSourceCurrentDirectory() { 64 | 65 | val parsed = SymbolSpec.from("BackendService@./some/local/source/file") 66 | assertThat(parsed, instanceOf(SymbolSpec.ImportsName::class.java)) 67 | 68 | val sym = parsed as SymbolSpec.ImportsName 69 | assertThat(sym.value, equalTo("BackendService")) 70 | assertThat(sym.source, equalTo("./some/local/source/file")) 71 | } 72 | 73 | @Test 74 | @DisplayName("Parsing named import: exported symbol explicit, source relative to parent dir") 75 | fun testParsingImplicitImportNamedSourceParentDirectory() { 76 | 77 | val parsed = SymbolSpec.from("BackendService@../some/local/source/file") 78 | assertThat(parsed, instanceOf(SymbolSpec.ImportsName::class.java)) 79 | 80 | val sym = parsed as SymbolSpec.ImportsName 81 | assertThat(sym.value, equalTo("BackendService")) 82 | assertThat(sym.source, equalTo("../some/local/source/file")) 83 | } 84 | 85 | @Test 86 | @DisplayName("Parsing named import: exported symbol explicit, source is implied module") 87 | fun testParsingExplicitImportNamed() { 88 | 89 | val parsed = SymbolSpec.from("SomeOtherSymbolDepth@rxjs/Observable") 90 | assertThat(parsed, instanceOf(SymbolSpec.ImportsName::class.java)) 91 | 92 | val sym = parsed as SymbolSpec.ImportsName 93 | assertThat(sym.value, equalTo("SomeOtherSymbolDepth")) 94 | assertThat(sym.source, equalTo("rxjs/Observable")) 95 | } 96 | 97 | @Test 98 | @DisplayName("Parsing all import: exported symbol implied by module path") 99 | fun testParsingImplicitImportAll() { 100 | 101 | val parsed = SymbolSpec.from("*rxjs/Observable") 102 | assertThat(parsed, instanceOf(SymbolSpec.ImportsAll::class.java)) 103 | 104 | val sym = parsed as SymbolSpec.ImportsAll 105 | assertThat(sym.value, equalTo("Observable")) 106 | assertThat(sym.source, equalTo("rxjs/Observable")) 107 | } 108 | 109 | @Test 110 | @DisplayName("Parsing all import: exported symbol explicit, source is implied module") 111 | fun testParsingExplicitImportAll() { 112 | 113 | val parsed = SymbolSpec.from("SomeOther*rxjs/Observable") 114 | assertThat(parsed, instanceOf(SymbolSpec.ImportsAll::class.java)) 115 | 116 | val sym = parsed as SymbolSpec.ImportsAll 117 | assertThat(sym.value, equalTo("SomeOther")) 118 | assertThat(sym.source, equalTo("rxjs/Observable")) 119 | } 120 | 121 | @Test 122 | @DisplayName("Parsing side effect import: exported symbol made available as side effect of import") 123 | fun testParsingSymbolViaSideEffect() { 124 | 125 | val parsed = SymbolSpec.from("describe+mocha") 126 | assertThat(parsed, instanceOf(SymbolSpec.SideEffect::class.java)) 127 | 128 | val sym = parsed as SymbolSpec.SideEffect 129 | assertThat(sym.value, equalTo("describe")) 130 | assertThat(sym.source, equalTo("mocha")) 131 | } 132 | 133 | @Test 134 | @DisplayName("Parsing augmentation import: exported symbol implied by module path") 135 | fun testParsingImplicitAugmentationWithAssociatedSymbol() { 136 | 137 | val parsed = SymbolSpec.from("+rxjs/add/operator/toPromise#Observable") 138 | assertThat(parsed, instanceOf(SymbolSpec.Augmented::class.java)) 139 | 140 | val sym = parsed as SymbolSpec.Augmented 141 | assertThat(sym.value, equalTo("toPromise")) 142 | assertThat(sym.source, equalTo("rxjs/add/operator/toPromise")) 143 | assertThat(sym.augmented, equalTo("Observable")) 144 | } 145 | 146 | @Test 147 | @DisplayName("Parsing augmentation import: exported symbol explicit") 148 | fun testParsingExplicitAugmentationWithAssociatedSymbol() { 149 | 150 | val parsed = SymbolSpec.from("SomeSymbol+rxjs/add/operator/toPromise#Observable") 151 | assertThat(parsed, instanceOf(SymbolSpec.Augmented::class.java)) 152 | 153 | val sym = parsed as SymbolSpec.Augmented 154 | assertThat(sym.value, equalTo("SomeSymbol")) 155 | assertThat(sym.source, equalTo("rxjs/add/operator/toPromise")) 156 | assertThat(sym.augmented, equalTo("Observable")) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import java.lang.Character.isISOControl 20 | import java.util.ArrayList 21 | import java.util.Collections 22 | import java.util.LinkedHashMap 23 | import java.util.LinkedHashSet 24 | 25 | internal object NullAppendable : Appendable { 26 | 27 | override fun append(charSequence: CharSequence) = this 28 | override fun append(charSequence: CharSequence, start: Int, end: Int) = this 29 | override fun append(c: Char) = this 30 | } 31 | 32 | internal fun String.parentSegment(separator: String = "."): String? { 33 | val parts = split(separator) 34 | val parent = parts.dropLast(1).joinToString(separator) 35 | return if (parent.isEmpty()) null else parent 36 | } 37 | 38 | internal fun String.topLevelSegment(separator: String = "."): String { 39 | return split(separator).first() 40 | } 41 | 42 | internal fun Map.toImmutableMap(): Map = Collections.unmodifiableMap(LinkedHashMap(this)) 43 | 44 | internal fun Collection.toImmutableList(): List = Collections.unmodifiableList(ArrayList(this)) 45 | 46 | internal fun Collection.toImmutableSet(): Set = Collections.unmodifiableSet(LinkedHashSet(this)) 47 | 48 | internal fun List.dropCommon(other: List): List { 49 | if (size < other.size) return this 50 | var lastDiff = 0 51 | for (idx in other.indices) { 52 | if (get(idx) != other[idx]) { 53 | break 54 | } 55 | lastDiff = idx + 1 56 | } 57 | return subList(lastDiff, size) 58 | } 59 | 60 | internal fun requireExactlyOneOf(modifiers: Set, vararg mutuallyExclusive: Modifier) { 61 | val count = mutuallyExclusive.count(modifiers::contains) 62 | require(count == 1) { 63 | "modifiers $modifiers must contain one of ${mutuallyExclusive.contentToString()}" 64 | } 65 | } 66 | 67 | internal fun requireNoneOrOneOf(modifiers: Set, vararg mutuallyExclusive: Modifier) { 68 | val count = mutuallyExclusive.count(modifiers::contains) 69 | require(count <= 1) { 70 | "modifiers $modifiers must contain none or only one of ${mutuallyExclusive.contentToString()}" 71 | } 72 | } 73 | 74 | internal fun requireNoneOf(modifiers: Set, vararg forbidden: Modifier) { 75 | require(forbidden.none(modifiers::contains)) { 76 | "modifiers $modifiers must contain none of ${forbidden.contentToString()}" 77 | } 78 | } 79 | 80 | internal fun T.isOneOf(t1: T, t2: T, t3: T? = null, t4: T? = null, t5: T? = null, t6: T? = null) = 81 | this == t1 || this == t2 || this == t3 || this == t4 || this == t5 || this == t6 82 | 83 | // see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6 84 | internal fun characterLiteralWithoutDoubleQuotes(c: Char) = when { 85 | c == '\b' -> "\\b" // \u0008: backspace (BS) 86 | c == '\t' -> "\\t" // \u0009: horizontal tab (HT) 87 | c == '\n' -> "\\n" // \u000a: linefeed (LF) 88 | c == '\r' -> "\\r" // \u000d: carriage return (CR) 89 | c == '\"' -> "\\\"" // \u0022: double quote (") 90 | c == '\'' -> "'" // \u0027: single quote (') 91 | c == '\\' -> "\\\\" // \u005c: backslash (\) 92 | isISOControl(c) -> String.format("\\u%04x", c.toInt()) 93 | else -> c.toString() 94 | } 95 | 96 | /** Returns the string literal representing `value`, including wrapping double quotes. */ 97 | internal fun stringLiteralWithQuotes(value: String, multiLineIndent: String): String { 98 | return value.split("\n").joinToString(" +\n$multiLineIndent") { line -> 99 | val result = StringBuilder(line.length + 32) 100 | result.append('\'') 101 | for (c in line) { 102 | // Trivial case: single quote must be escaped. 103 | if (c == '\'') { 104 | result.append("\\\'") 105 | continue 106 | } 107 | // Trivial case: dollar sign must be escaped. 108 | if (c == '$') { 109 | result.append("\\$") 110 | continue 111 | } 112 | // Trivial case: double quotes must not be escaped. 113 | if (c == '\"') { 114 | result.append('"') 115 | continue 116 | } 117 | // Default case: just let character literal do its work. 118 | result.append(characterLiteralWithoutDoubleQuotes(c)) 119 | } 120 | result.append('\'') 121 | 122 | result.toString() 123 | } 124 | } 125 | 126 | /** Returns the string template literal representing `value`, including wrapping backticks. */ 127 | internal fun stringTemplateLiteralWithBackticks(value: String, multiLineIndent: String): String { 128 | return value.split("\n").joinToString("\n$multiLineIndent", prefix = "`", postfix = "`") { line -> 129 | val result = StringBuilder(line.length + 32) 130 | for (c in line) { 131 | // Trivial case: single quote must not be escaped. 132 | if (c == '\'') { 133 | result.append("'") 134 | continue 135 | } 136 | // Trivial case: dollar sign must not be escaped. 137 | if (c == '$') { 138 | result.append("$") 139 | continue 140 | } 141 | // Trivial case: double quotes must not be escaped. 142 | if (c == '\"') { 143 | result.append('"') 144 | continue 145 | } 146 | // Default case: just let character literal do its work. 147 | result.append(characterLiteralWithoutDoubleQuotes(c)) 148 | } 149 | 150 | result.toString() 151 | } 152 | } 153 | 154 | internal val String.isKeyword get() = KEYWORDS.contains(this) 155 | 156 | internal val String.isName get() = split("\\.").none { it.isKeyword } 157 | 158 | // https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java 159 | private val KEYWORDS = setOf( 160 | "package", 161 | "as", 162 | "typealias", 163 | "class", 164 | "this", 165 | "super", 166 | "val", 167 | "var", 168 | "fun", 169 | "for", 170 | "null", 171 | "true", 172 | "false", 173 | "is", 174 | "in", 175 | "throw", 176 | "return", 177 | "break", 178 | "continue", 179 | "object", 180 | "if", 181 | "try", 182 | "else", 183 | "while", 184 | "do", 185 | "when", 186 | "interface", 187 | "typeof" 188 | ) 189 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/ModuleSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated `module` declaration. */ 20 | class ModuleSpec 21 | private constructor( 22 | builder: Builder 23 | ) : Taggable(builder.tags.toImmutableMap()) { 24 | 25 | enum class Kind(val keyword: String) { 26 | MODULE("module"), 27 | NAMESPACE("namespace") 28 | } 29 | 30 | val name = builder.name 31 | val tsDoc = builder.tsDoc.build() 32 | val modifiers = builder.modifiers.toImmutableList() 33 | val members = builder.members.toImmutableList() 34 | val kind = builder.kind 35 | 36 | internal fun emit(codeWriter: CodeWriter) { 37 | codeWriter.pushScope(name) 38 | try { 39 | 40 | if (tsDoc.isNotEmpty()) { 41 | codeWriter.emitComment(tsDoc) 42 | } 43 | 44 | if (modifiers.isNotEmpty()) { 45 | codeWriter.emitCode(CodeBlock.of("%L ", modifiers.joinToString(" ") { it.keyword })) 46 | } 47 | codeWriter.emitCode(CodeBlock.of("${kind.keyword} %L {\n", name)) 48 | codeWriter.indent() 49 | 50 | if (members.isNotEmpty()) { 51 | codeWriter.emitCode("\n") 52 | } 53 | 54 | members.forEachIndexed { index, member -> 55 | if (index > 0) codeWriter.emit("\n") 56 | when (member) { 57 | is ModuleSpec -> member.emit(codeWriter) 58 | is InterfaceSpec -> member.emit(codeWriter) 59 | is ClassSpec -> member.emit(codeWriter) 60 | is EnumSpec -> member.emit(codeWriter) 61 | is FunctionSpec -> 62 | member.emit(codeWriter, null, setOf(Modifier.PUBLIC)) 63 | is PropertySpec -> 64 | member.emit(codeWriter, setOf(Modifier.PUBLIC), asStatement = true) 65 | is TypeAliasSpec -> member.emit(codeWriter) 66 | is CodeBlock -> codeWriter.emitCode(member) 67 | else -> throw AssertionError() 68 | } 69 | } 70 | 71 | if (members.isNotEmpty()) { 72 | codeWriter.emitCode("\n") 73 | } 74 | 75 | codeWriter.unindent() 76 | codeWriter.emitCode("}\n") 77 | } finally { 78 | codeWriter.popScope() 79 | } 80 | } 81 | 82 | fun isEmpty(): Boolean { 83 | return members.isEmpty() 84 | } 85 | 86 | fun isNotEmpty(): Boolean { 87 | return !isEmpty() 88 | } 89 | 90 | override fun toString() = buildCodeString { emit(this) } 91 | 92 | fun toBuilder(): Builder { 93 | val builder = Builder(name, kind) 94 | builder.tsDoc.add(tsDoc) 95 | builder.modifiers += modifiers 96 | builder.members.addAll(this.members) 97 | return builder 98 | } 99 | 100 | open class Builder 101 | internal constructor( 102 | internal val name: String, 103 | internal val kind: Kind = Kind.NAMESPACE 104 | ) : Taggable.Builder() { 105 | 106 | internal val tsDoc = CodeBlock.builder() 107 | internal val modifiers = mutableSetOf() 108 | internal val members = mutableListOf() 109 | 110 | private fun checkMemberModifiers(modifiers: Set) { 111 | requireNoneOf( 112 | modifiers, 113 | Modifier.PUBLIC, 114 | Modifier.PROTECTED, 115 | Modifier.PRIVATE, 116 | Modifier.READONLY, 117 | Modifier.GET, 118 | Modifier.SET, 119 | Modifier.STATIC 120 | ) 121 | } 122 | 123 | fun addTSDoc(format: String, vararg args: Any) = apply { 124 | tsDoc.add(format, *args) 125 | } 126 | 127 | fun addTSDoc(block: CodeBlock) = apply { 128 | tsDoc.add(block) 129 | } 130 | 131 | fun addModifier(modifier: Modifier) = apply { 132 | requireNoneOrOneOf( 133 | modifiers + modifier, Modifier.EXPORT, 134 | Modifier.DECLARE 135 | ) 136 | modifiers += modifier 137 | } 138 | 139 | fun addModule(moduleSpec: ModuleSpec) = apply { 140 | members += moduleSpec 141 | } 142 | 143 | fun addClass(classSpec: ClassSpec) = apply { 144 | checkMemberModifiers(classSpec.modifiers) 145 | members += classSpec 146 | } 147 | 148 | fun addInterface(ifaceSpec: InterfaceSpec) = apply { 149 | checkMemberModifiers(ifaceSpec.modifiers) 150 | members += ifaceSpec 151 | } 152 | 153 | fun addEnum(enumSpec: EnumSpec) = apply { 154 | checkMemberModifiers(enumSpec.modifiers) 155 | members += enumSpec 156 | } 157 | 158 | fun addType(typeSpec: AnyTypeSpec) = apply { 159 | when (typeSpec) { 160 | is EnumSpec -> addEnum(typeSpec) 161 | is InterfaceSpec -> addInterface(typeSpec) 162 | is ClassSpec -> addClass(typeSpec) 163 | is TypeAliasSpec -> addTypeAlias(typeSpec) 164 | } 165 | } 166 | 167 | fun addFunction(functionSpec: FunctionSpec) = apply { 168 | require(!functionSpec.isConstructor) { "cannot add ${functionSpec.name} to module $name" } 169 | require(functionSpec.decorators.isEmpty()) { "decorators on module functions are not allowed" } 170 | checkMemberModifiers(functionSpec.modifiers) 171 | members += functionSpec 172 | } 173 | 174 | fun addProperty(propertySpec: PropertySpec) = apply { 175 | requireExactlyOneOf( 176 | propertySpec.modifiers, Modifier.CONST, 177 | Modifier.LET, 178 | Modifier.VAR 179 | ) 180 | require(propertySpec.decorators.isEmpty()) { "decorators on file properties are not allowed" } 181 | checkMemberModifiers(propertySpec.modifiers) 182 | members += propertySpec 183 | } 184 | 185 | fun addTypeAlias(typeAliasSpec: TypeAliasSpec) = apply { 186 | members += typeAliasSpec 187 | } 188 | 189 | fun addCode(codeBlock: CodeBlock) = apply { 190 | members += codeBlock 191 | } 192 | 193 | fun isEmpty(): Boolean { 194 | return members.isEmpty() 195 | } 196 | 197 | fun isNotEmpty(): Boolean { 198 | return !isEmpty() 199 | } 200 | 201 | fun build() = ModuleSpec(this) 202 | } 203 | 204 | companion object { 205 | 206 | @JvmStatic 207 | fun builder(name: String, kind: Kind = Kind.NAMESPACE) = Builder(name, kind) 208 | 209 | @JvmStatic 210 | fun builder(name: TypeName, kind: Kind = Kind.NAMESPACE) = builder("$name", kind) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/InterfaceSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import io.outfoxx.typescriptpoet.CodeBlock.Companion.joinToCode 20 | 21 | /** A generated `interface` declaration. */ 22 | class InterfaceSpec 23 | private constructor( 24 | builder: Builder 25 | ) : TypeSpec(builder) { 26 | 27 | override val name = builder.name 28 | val tsDoc = builder.tsDoc.build() 29 | val modifiers = builder.modifiers.toImmutableSet() 30 | val typeVariables = builder.typeVariables.toImmutableList() 31 | val superInterfaces = builder.superInterfaces.toImmutableList() 32 | val propertySpecs = builder.propertySpecs.toImmutableList() 33 | val functionSpecs = builder.functionSpecs.toImmutableList() 34 | val indexableSpecs = builder.indexableSpecs.toImmutableList() 35 | val callable = builder.callable 36 | 37 | override fun emit(codeWriter: CodeWriter) { 38 | 39 | codeWriter.emitTSDoc(tsDoc) 40 | codeWriter.emitModifiers(modifiers, setOf()) 41 | codeWriter.emit("interface") 42 | codeWriter.emitCode(CodeBlock.of(" %L", name)) 43 | codeWriter.emitTypeVariables(typeVariables) 44 | 45 | val superClasses = superInterfaces.map { CodeBlock.of("%T", it) }.let { 46 | if (it.isNotEmpty()) it.joinToCode(prefix = " extends ") else CodeBlock.empty() 47 | } 48 | codeWriter.emitCode(superClasses) 49 | 50 | codeWriter.emit(" {\n") 51 | codeWriter.indent() 52 | 53 | // Callable 54 | callable?.let { 55 | codeWriter.emitCode("\n") 56 | it.emit(codeWriter, null, setOf(Modifier.ABSTRACT)) 57 | } 58 | 59 | // Properties. 60 | for (propertySpec in propertySpecs) { 61 | codeWriter.emit("\n") 62 | propertySpec.emit( 63 | codeWriter, 64 | setOf(Modifier.PUBLIC), 65 | asStatement = true, 66 | compactOptionalAllowed = true, 67 | ) 68 | } 69 | 70 | // Indexables 71 | for (funSpec in indexableSpecs) { 72 | codeWriter.emit("\n") 73 | funSpec.emit(codeWriter, null, setOf(Modifier.PUBLIC, Modifier.ABSTRACT)) 74 | } 75 | 76 | // Functions. 77 | for (funSpec in functionSpecs) { 78 | if (funSpec.isConstructor) continue 79 | codeWriter.emit("\n") 80 | funSpec.emit(codeWriter, name, setOf(Modifier.PUBLIC, Modifier.ABSTRACT)) 81 | } 82 | 83 | codeWriter.unindent() 84 | 85 | if (!hasNoBody) { 86 | codeWriter.emit("\n") 87 | } 88 | codeWriter.emit("}\n") 89 | } 90 | 91 | private val hasNoBody: Boolean 92 | get() { 93 | return propertySpecs.isEmpty() && functionSpecs.isEmpty() && indexableSpecs.isEmpty() && callable == null 94 | } 95 | 96 | fun toBuilder(): Builder { 97 | val builder = Builder(name) 98 | builder.tsDoc.add(tsDoc) 99 | builder.modifiers += modifiers 100 | builder.typeVariables += typeVariables 101 | builder.superInterfaces += superInterfaces 102 | builder.propertySpecs += propertySpecs 103 | builder.functionSpecs += functionSpecs 104 | builder.indexableSpecs += indexableSpecs 105 | builder.callable = callable 106 | return builder 107 | } 108 | 109 | class Builder( 110 | name: String 111 | ) : TypeSpec.Builder(name) { 112 | 113 | internal val tsDoc = CodeBlock.builder() 114 | internal val modifiers = mutableListOf() 115 | internal val typeVariables = mutableListOf() 116 | internal val superInterfaces = mutableListOf() 117 | internal val propertySpecs = mutableListOf() 118 | internal val functionSpecs = mutableListOf() 119 | internal val indexableSpecs = mutableListOf() 120 | internal var callable: FunctionSpec? = null 121 | 122 | fun addTSDoc(format: String, vararg args: Any) = apply { 123 | tsDoc.add(format, *args) 124 | } 125 | 126 | fun addTSDoc(block: CodeBlock) = apply { 127 | tsDoc.add(block) 128 | } 129 | 130 | fun addModifiers(vararg modifiers: Modifier) = apply { 131 | this.modifiers += modifiers 132 | } 133 | 134 | fun addTypeVariables(typeVariables: Iterable) = apply { 135 | this.typeVariables += typeVariables 136 | } 137 | 138 | fun addTypeVariable(typeVariable: TypeName.TypeVariable) = apply { 139 | typeVariables += typeVariable 140 | } 141 | 142 | fun addSuperInterface(superClass: TypeName) = apply { 143 | this.superInterfaces.add(superClass) 144 | } 145 | 146 | fun addProperties(propertySpecs: Iterable) = apply { 147 | propertySpecs.forEach { addProperty(it) } 148 | } 149 | 150 | fun addProperty(propertySpec: PropertySpec) = apply { 151 | require(propertySpec.decorators.isEmpty()) { "Interface properties cannot have decorators" } 152 | require(propertySpec.initializer == null) { "Interface properties cannot have initializers" } 153 | propertySpecs += propertySpec 154 | } 155 | 156 | fun addProperty(name: String, type: TypeName, optional: Boolean = false, vararg modifiers: Modifier) = 157 | addProperty(PropertySpec.builder(name, type, optional, *modifiers).build()) 158 | 159 | fun addFunctions(functionSpecs: Iterable) = apply { 160 | functionSpecs.forEach { addFunction(it) } 161 | } 162 | 163 | fun addFunction(functionSpec: FunctionSpec) = apply { 164 | require(functionSpec.modifiers.contains(Modifier.ABSTRACT)) { "Interface methods must be abstract" } 165 | require(functionSpec.body.isEmpty()) { "Interface methods cannot have code" } 166 | require(!functionSpec.isConstructor) { "Interfaces cannot have a constructor" } 167 | require(functionSpec.decorators.isEmpty()) { "Interface functions cannot have decorators" } 168 | this.functionSpecs += functionSpec 169 | } 170 | 171 | fun addIndexables(indexableSpecs: Iterable) = apply { 172 | indexableSpecs.forEach { addIndexable(it) } 173 | } 174 | 175 | fun addIndexable(functionSpec: FunctionSpec) = apply { 176 | require(functionSpec.modifiers.contains(Modifier.ABSTRACT)) { "Indexables must be ABSTRACT" } 177 | this.indexableSpecs += functionSpec 178 | } 179 | 180 | fun callable(callable: FunctionSpec?) = apply { 181 | if (callable != null) { 182 | require(callable.isCallable) { 183 | "expected a callable signature but was ${callable.name}; use FunctionSpec.callableBuilder when building" 184 | } 185 | require(callable.modifiers == setOf(Modifier.ABSTRACT)) { "Callable must be ABSTRACT and nothing else" } 186 | } 187 | this.callable = callable 188 | } 189 | 190 | override fun build(): InterfaceSpec { 191 | return InterfaceSpec(this) 192 | } 193 | } 194 | 195 | companion object { 196 | 197 | @JvmStatic 198 | fun builder(name: String) = Builder(name) 199 | 200 | @JvmStatic 201 | fun builder(name: TypeName) = Builder("$name") 202 | 203 | @JvmStatic 204 | fun builder(classSpec: ClassSpec): Builder { 205 | val builder = Builder(classSpec.name) 206 | .addModifiers(*classSpec.modifiers.toTypedArray()) 207 | .addProperties(classSpec.propertySpecs) 208 | builder.functionSpecs.forEach { builder.addFunction(it.abstract()) } 209 | return builder 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/CodeWriter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import java.io.Closeable 20 | import java.util.EnumSet 21 | import java.util.Stack 22 | 23 | /** 24 | * Converts a [FileSpec] to a string suitable to both human- and tsc-consumption. This honors 25 | * imports, indentation, and deferred variable names. 26 | */ 27 | internal class CodeWriter constructor( 28 | out: Appendable, 29 | private val indent: String = " ", 30 | val renamedSymbols: Map = emptyMap() 31 | ) : Closeable { 32 | 33 | private val out = LineWrapper(out, indent, 100) 34 | private var indentLevel = 0 35 | 36 | private var tsDoc = false 37 | private var comment = false 38 | private var trailingNewline = false 39 | private var referencedSymbols = mutableSetOf() 40 | private val scope = Stack() 41 | 42 | inline fun referencedSymbols() = referencedSymbols.filterIsInstance().toImmutableSet() 43 | 44 | fun currentScope(): List = scope.toImmutableList() 45 | 46 | fun pushScope(name: String) { 47 | scope.push(name) 48 | } 49 | 50 | fun popScope() { 51 | scope.pop() 52 | } 53 | 54 | /** 55 | * When emitting a statement, this is the line of the statement currently being written. The first 56 | * line of a statement is indented normally and subsequent wrapped lines are double-indented. This 57 | * is -1 when the currently-written line isn't part of a statement. 58 | */ 59 | private var statementLine = -1 60 | 61 | fun indent(levels: Int = 1) = apply { 62 | indentLevel += levels 63 | } 64 | 65 | fun unindent(levels: Int = 1) = apply { 66 | require(indentLevel - levels >= 0) { "cannot unindent $levels from $indentLevel" } 67 | indentLevel -= levels 68 | } 69 | 70 | fun emitComment(codeBlock: CodeBlock) { 71 | trailingNewline = true // Force the '//' prefix for the comment. 72 | comment = true 73 | try { 74 | emitCode(codeBlock) 75 | emit("\n") 76 | } finally { 77 | comment = false 78 | } 79 | } 80 | 81 | fun emitTSDoc(tsDocCodeBlock: CodeBlock) { 82 | if (tsDocCodeBlock.isEmpty()) return 83 | 84 | emit("/**\n") 85 | tsDoc = true 86 | try { 87 | emitCode(tsDocCodeBlock) 88 | } finally { 89 | tsDoc = false 90 | } 91 | emit(" */\n") 92 | } 93 | 94 | fun emitDecorators(decorators: List, inline: Boolean) { 95 | for (decoratorSpec in decorators) { 96 | decoratorSpec.emit(this) 97 | emitCode(if (inline) "%W" else "\n") 98 | } 99 | } 100 | 101 | /** 102 | * Emits `modifiers` in the standard order. Modifiers in `implicitModifiers` will not 103 | * be emitted. 104 | */ 105 | fun emitModifiers( 106 | modifiers: Set, 107 | implicitModifiers: Set = emptySet() 108 | ) { 109 | if (modifiers.isEmpty()) return 110 | for (modifier in EnumSet.copyOf(modifiers)) { 111 | if (implicitModifiers.contains(modifier)) continue 112 | emit(modifier.keyword) 113 | emit(" ") 114 | } 115 | } 116 | 117 | /** 118 | * Emit type variables with their bounds. 119 | * 120 | * This should only be used when declaring type variables; everywhere else bounds are omitted. 121 | */ 122 | fun emitTypeVariables(typeVariables: List) { 123 | if (typeVariables.isEmpty()) return 124 | 125 | emit("<") 126 | typeVariables.forEachIndexed { index, typeVariable -> 127 | if (index > 0) emit(", ") 128 | emitCode( 129 | CodeBlock.of( 130 | buildString { 131 | append(typeVariable.name) 132 | if (typeVariable.bounds.isNotEmpty()) { 133 | val parts = mutableListOf() 134 | parts.add(" extends") 135 | typeVariable.bounds.forEachIndexed { index, bound -> 136 | if (index > 0) parts.add(bound.combiner.symbol) 137 | bound.modifier?.let { parts.add(it.keyword) } 138 | parts.add("${bound.type}") 139 | } 140 | append(parts.joinToString(" ")) 141 | } 142 | }, 143 | *typeVariable.bounds.map { it.type }.toTypedArray() 144 | ) 145 | ) 146 | } 147 | emit(">") 148 | } 149 | 150 | fun emitSymbol(symbolSpec: SymbolSpec) { 151 | if (symbolSpec.isTopLevelSymbol) { 152 | referencedSymbols.add(symbolSpec) 153 | emit(renamedSymbols[symbolSpec] ?: symbolSpec.value) 154 | } else { 155 | val topLevel = symbolSpec.topLevel() 156 | referencedSymbols.add(topLevel) 157 | emit(renamedSymbols[topLevel] ?: topLevel.value) 158 | emit(".") 159 | emit(symbolSpec.value.split(".").drop(1).joinToString(".")) 160 | } 161 | } 162 | 163 | fun emitCode(s: String) = emitCode(CodeBlock.of(s)) 164 | 165 | fun emitCode(codeBlock: CodeBlock) = apply { 166 | 167 | var a = 0 168 | val partIterator = codeBlock.formatParts.listIterator() 169 | while (partIterator.hasNext()) { 170 | when (val part = partIterator.next()) { 171 | "%L" -> emitLiteral(codeBlock.args[a++]) 172 | 173 | "%N" -> emit(codeBlock.args[a++] as String) 174 | 175 | "%S" -> emitString(codeBlock.args[a++] as String?) 176 | 177 | "%P" -> emitStringTemplate(codeBlock.args[a++] as String?) 178 | 179 | "%T" -> emitTypeName(codeBlock.args[a++] as TypeName) 180 | 181 | "%Q" -> emitSymbol(codeBlock.args[a++] as SymbolSpec) 182 | 183 | "%%" -> emit("%") 184 | 185 | "%>" -> indent() 186 | 187 | "%<" -> unindent() 188 | 189 | "%[" -> beginStatement() 190 | 191 | "%]" -> endStatement() 192 | 193 | "%W" -> emitWrappingSpace() 194 | 195 | else -> { 196 | // Handle deferred type. 197 | emit(part) 198 | } 199 | } 200 | } 201 | } 202 | 203 | private fun beginStatement() { 204 | check(statementLine == -1) { "statement enter %[ followed by statement enter %[" } 205 | 206 | statementLine = 0 207 | } 208 | 209 | private fun endStatement() { 210 | check(statementLine != -1) { "statement exit %] has no matching statement enter %[" } 211 | 212 | if (statementLine > 0) { 213 | unindent(2) // End a multi-line statement. Decrease the indentation level. 214 | } 215 | 216 | statementLine = -1 217 | } 218 | 219 | private fun emitWrappingSpace() = apply { 220 | out.wrappingSpace(indentLevel + 2) 221 | } 222 | 223 | private fun emitTypeName(typeName: TypeName) { 224 | typeName.emit(this) 225 | } 226 | 227 | private fun emitString(string: String?) { 228 | // Emit null as a literal null: no quotes. 229 | emit( 230 | if (string != null) 231 | stringLiteralWithQuotes(string, (0 until (indentLevel + 1)).joinToString("") { indent }) 232 | else 233 | "null" 234 | ) 235 | } 236 | 237 | private fun emitStringTemplate(string: String?) { 238 | // Emit null as a literal null: no quotes. 239 | emit( 240 | if (string != null) 241 | stringTemplateLiteralWithBackticks(string, (0 until (indentLevel + 1)).joinToString("") { indent }) 242 | else 243 | "null" 244 | ) 245 | } 246 | 247 | private fun emitLiteral(o: Any?) { 248 | when (o) { 249 | is ClassSpec -> o.emit(this) 250 | is InterfaceSpec -> o.emit(this) 251 | is EnumSpec -> o.emit(this) 252 | is DecoratorSpec -> o.emit(this) 253 | is CodeBlock -> emitCode(o) 254 | else -> emit(o.toString()) 255 | } 256 | } 257 | 258 | /** 259 | * Emits `s` with indentation as required. It's important that all code that writes to 260 | * [CodeWriter.out] does it through here, since we emit indentation lazily in order to avoid 261 | * unnecessary trailing whitespace. 262 | */ 263 | fun emit(s: String) = apply { 264 | var first = true 265 | for (line in s.split('\n')) { 266 | // Emit a newline character. Make sure blank lines in KDoc & comments look good. 267 | if (!first) { 268 | if ((tsDoc || comment) && trailingNewline) { 269 | emitIndentation() 270 | out.append(if (tsDoc) " *" else "//") 271 | } 272 | out.append("\n") 273 | trailingNewline = true 274 | if (statementLine != -1) { 275 | if (statementLine == 0) { 276 | indent(2) // Begin multiple-line statement. Increase the indentation level. 277 | } 278 | statementLine++ 279 | } 280 | } 281 | 282 | first = false 283 | if (line.isEmpty()) continue // Don't indent empty lines. 284 | 285 | // Emit indentation and comment prefix if necessary. 286 | if (trailingNewline) { 287 | emitIndentation() 288 | if (tsDoc) { 289 | out.append(" * ") 290 | } else if (comment) { 291 | out.append("// ") 292 | } 293 | } 294 | 295 | out.append(line) 296 | trailingNewline = false 297 | } 298 | } 299 | 300 | private fun emitIndentation() { 301 | for (j in 0 until indentLevel) { 302 | out.append(indent) 303 | } 304 | } 305 | 306 | override fun close() { 307 | out.close() 308 | } 309 | } 310 | 311 | internal inline fun buildCodeString(builderAction: CodeWriter.() -> Unit): String { 312 | val stringBuilder = StringBuilder() 313 | CodeWriter(stringBuilder).use { 314 | it.builderAction() 315 | } 316 | return stringBuilder.toString() 317 | } 318 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/SymbolSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** 20 | * Specifies a symbol and its related origin, either via import or implicit/local declaration 21 | * 22 | * @param value Value of the symbol 23 | */ 24 | sealed class SymbolSpec( 25 | open val value: String 26 | ) { 27 | 28 | abstract fun nested(name: String): SymbolSpec 29 | abstract fun enclosing(): SymbolSpec? 30 | abstract fun topLevel(): SymbolSpec 31 | 32 | val isTopLevelSymbol: Boolean 33 | get() = value.count { it == '.' } == 0 34 | 35 | companion object { 36 | 37 | private val fileNamePattern = 38 | """(?:[a-zA-Z0-9._\-]+)""".toRegex() 39 | private val modulePattern = 40 | """@?(?:(?:!?$fileNamePattern)(?:/$fileNamePattern)*)""".toRegex() 41 | private val identPattern = 42 | """(?:(?:[a-zA-Z][_a-zA-Z0-9.]*)|(?:[_a-zA-Z][_a-zA-Z0-9.]+))""".toRegex() 43 | private val importPattern = 44 | """($identPattern)?([*@+])($modulePattern)(?:#($identPattern))?""".toRegex() 45 | 46 | /** 47 | * Parses a symbol reference pattern to create a symbol. The pattern 48 | * allows the simple definition of all symbol types including any possible 49 | * import variation. If the spec to parse does not follow the proper format 50 | * an implicit symbol is created from the unparsed spec. 51 | * 52 | * Pattern: `? (#)?` 53 | * 54 | * * symbol_name = `[a-zA-Z0-9._]+` 55 | * 56 | * Any legal compound JS/TS symbol (e.g. symbol._member.member). If no symbol name is 57 | * specified then the last component of the module path is used as the symbol name; 58 | * allows easy use with libraries that follow normal conventions. 59 | * 60 | * * import_type = `@ | * | +` 61 | * 62 | * `@` = Import named symbol from module (e.g. `import { } from ''`) 63 | * 64 | * `*` = Import all symbols from module (e.g. `import * from ''`) 65 | * 66 | * `+` = Symbol is declared implicitly via import of the module (e.g. `import ''`) 67 | * 68 | * * module_path = `! | (/ importsAll(symbolName, modulePath) 99 | "@" -> importsName(symbolName, modulePath) 100 | "+" -> if (targetName == null) sideEffect(symbolName, modulePath) else augmented( 101 | symbolName, 102 | modulePath, 103 | targetName 104 | ) 105 | else -> throw IllegalArgumentException("Invalid type character") 106 | } 107 | } 108 | 109 | return implicit(spec) 110 | } 111 | 112 | /** 113 | * Creates an import of a single named symbol from the module's exported 114 | * symbols. 115 | * 116 | * e.g. `import { Engine } from 'templates';` 117 | * 118 | * @param exportedName The symbol that is both exported and imported 119 | * @param from The module the symbol is exported from 120 | */ 121 | @JvmStatic 122 | fun importsName(exportedName: String, from: String): SymbolSpec { 123 | return ImportsName(exportedName, from) 124 | } 125 | 126 | /** 127 | * Creates an import of all the modules exported symbols as a single 128 | * local named symbol 129 | * 130 | * e.g. `import * as Engine from 'templates';` 131 | * 132 | * @param localName The local name of the imported symbols 133 | * @param from The module to import the symbols from 134 | */ 135 | @JvmStatic 136 | fun importsAll(localName: String, from: String): SymbolSpec { 137 | return ImportsAll(localName, from) 138 | } 139 | 140 | /** 141 | * Creates a symbol that is brought in by a whole module import 142 | * that "augments" an existing symbol. 143 | * 144 | * e.g. `import 'rxjs/add/operator/flatMap'` 145 | * 146 | * @param symbolName The augmented symbol to be imported 147 | * @param from The entire import that does the augmentation 148 | * @param target The symbol that is augmented 149 | */ 150 | @JvmStatic 151 | fun augmented(symbolName: String, from: String, target: String): SymbolSpec { 152 | return Augmented(symbolName, from, target) 153 | } 154 | 155 | /** 156 | * Creates a symbol that is brought in as a side effect of 157 | * an import. 158 | * 159 | * e.g. `import 'mocha'` 160 | * 161 | * @param symbolName The symbol to be imported 162 | * @param from The entire import that does the augmentation 163 | */ 164 | @JvmStatic 165 | fun sideEffect(symbolName: String, from: String): SymbolSpec { 166 | return SideEffect(symbolName, from) 167 | } 168 | 169 | /** 170 | * An implied symbol that does no tracking of imports 171 | * 172 | * @param name The implicit symbol name 173 | */ 174 | @JvmStatic 175 | fun implicit(name: String): SymbolSpec { 176 | return Implicit(name) 177 | } 178 | } 179 | 180 | /** 181 | * Non-imported symbol 182 | */ 183 | data class Implicit 184 | internal constructor( 185 | override val value: String 186 | ) : SymbolSpec(value) { 187 | 188 | override fun nested(name: String) = Implicit("$value.$name") 189 | override fun enclosing() = value.parentSegment()?.let { Implicit(it) } 190 | override fun topLevel() = Implicit(value.topLevelSegment()) 191 | } 192 | 193 | /** 194 | * Common base class for imported symbols 195 | */ 196 | abstract class Imported( 197 | override val value: String, 198 | open val source: String 199 | ) : SymbolSpec(value) 200 | 201 | /** 202 | * Imports a single named symbol from the module's exported 203 | * symbols. 204 | * 205 | * e.g. `import { Engine } from 'templates';` 206 | */ 207 | data class ImportsName 208 | internal constructor( 209 | override val value: String, 210 | override val source: String 211 | ) : Imported(value, source) { 212 | 213 | override fun nested(name: String) = ImportsName("$value.$name", source) 214 | override fun enclosing() = value.parentSegment()?.let { ImportsName(it, source) } 215 | override fun topLevel() = ImportsName(value.topLevelSegment(), source) 216 | } 217 | 218 | /** 219 | * Imports all of the modules exported symbols as a single 220 | * named symbol 221 | * 222 | * e.g. `import * as Engine from 'templates';` 223 | */ 224 | data class ImportsAll 225 | internal constructor( 226 | override val value: String, 227 | override val source: String 228 | ) : Imported(value, source) { 229 | 230 | override fun nested(name: String) = ImportsAll("$value.$name", source) 231 | override fun enclosing() = value.parentSegment()?.let { ImportsAll(it, source) } 232 | override fun topLevel() = ImportsAll(value.topLevelSegment(), source) 233 | } 234 | 235 | /** 236 | * A symbol that is brought in by a whole module import 237 | * that "augments" an existing symbol. 238 | * 239 | * e.g. `import 'rxjs/add/operator/flatMap'` 240 | */ 241 | data class Augmented 242 | internal constructor( 243 | override val value: String, 244 | override val source: String, 245 | val augmented: String 246 | ) : Imported(value, source) { 247 | 248 | override fun nested(name: String) = Augmented("$value.$name", source, augmented) 249 | override fun enclosing() = value.parentSegment()?.let { Augmented(it, source, augmented) } 250 | override fun topLevel() = Augmented(value.topLevelSegment(), source, augmented) 251 | } 252 | 253 | /** 254 | * A symbol that is brought in as a side effect of an 255 | * import. 256 | * 257 | * e.g. `import 'mocha'` 258 | */ 259 | data class SideEffect 260 | internal constructor( 261 | override val value: String, 262 | override val source: String 263 | ) : Imported(value, source) { 264 | 265 | override fun nested(name: String) = SideEffect("$value.$name", source) 266 | override fun enclosing() = value.parentSegment()?.let { SideEffect(it, source) } 267 | override fun topLevel() = SideEffect(value.topLevelSegment(), source) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/FunctionSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | /** A generated function declaration. */ 20 | class FunctionSpec 21 | private constructor( 22 | builder: Builder 23 | ) : Taggable(builder.tags.toImmutableMap()) { 24 | 25 | val name = builder.name 26 | val tsDoc = builder.tsDoc.build() 27 | val decorators = builder.decorators.toImmutableList() 28 | val modifiers = builder.modifiers.toImmutableSet() 29 | val typeVariables = builder.typeVariables.toImmutableList() 30 | val returnType = builder.returnType 31 | val parameters = builder.parameters.toImmutableList() 32 | val restParameter = builder.restParameter 33 | val body = builder.body.build() 34 | 35 | init { 36 | require(body.isEmpty() || Modifier.ABSTRACT !in builder.modifiers) { 37 | "abstract function ${builder.name} cannot have code" 38 | } 39 | } 40 | 41 | fun abstract(): FunctionSpec { 42 | return builder(name) 43 | .addModifiers(Modifier.ABSTRACT) 44 | .addTypeVariables(typeVariables) 45 | .addParameters(parameters) 46 | .build() 47 | } 48 | 49 | internal fun parameter(name: String) = parameters.firstOrNull { it.name == name } 50 | 51 | internal fun emit( 52 | codeWriter: CodeWriter, 53 | enclosingName: String?, 54 | implicitModifiers: Set, 55 | ) { 56 | codeWriter.emitTSDoc(tsDoc) 57 | codeWriter.emitDecorators(decorators, false) 58 | codeWriter.emitModifiers(modifiers, implicitModifiers) 59 | 60 | emitSignature(codeWriter, enclosingName) 61 | 62 | val isEmptyConstructor = isConstructor && body.isEmpty() 63 | if (Modifier.ABSTRACT in modifiers || isEmptyConstructor) { 64 | codeWriter.emit(";\n") 65 | return 66 | } 67 | 68 | codeWriter.emit(" {\n") 69 | codeWriter.indent() 70 | codeWriter.emitCode(body) 71 | codeWriter.unindent() 72 | codeWriter.emit("}\n") 73 | } 74 | 75 | private fun emitSignature( 76 | codeWriter: CodeWriter, 77 | enclosingName: String?, 78 | ) { 79 | when { 80 | isConstructor -> codeWriter.emitCode("constructor") 81 | isCallable -> codeWriter.emitCode("") 82 | isIndexable -> codeWriter.emitCode("[") 83 | else -> { 84 | if (enclosingName == null) { 85 | codeWriter.emit("function ") 86 | } 87 | codeWriter.emitCode(CodeBlock.of("%L", name)) 88 | } 89 | } 90 | 91 | if (typeVariables.isNotEmpty()) { 92 | codeWriter.emitTypeVariables(typeVariables) 93 | } 94 | 95 | parameters.emit( 96 | codeWriter, 97 | enclosed = !isIndexable, 98 | rest = restParameter, 99 | ) { param, isRest, optionalAllowed -> 100 | param.emit(codeWriter, isRest = isRest, optionalAllowed = optionalAllowed) 101 | } 102 | 103 | if (isIndexable) { 104 | codeWriter.emitCode("]") 105 | } 106 | 107 | if (returnType != null && returnType != TypeName.VOID) { 108 | codeWriter.emitCode(CodeBlock.of(": %T", returnType)) 109 | } 110 | } 111 | 112 | val isConstructor get() = name.isConstructor 113 | val isAccessor get() = modifiers.contains(Modifier.GET) || modifiers.contains(Modifier.SET) 114 | val isCallable get() = name.isCallable 115 | val isIndexable get() = name.isIndexable 116 | 117 | override fun equals(other: Any?): Boolean { 118 | if (this === other) return true 119 | if (other == null) return false 120 | if (javaClass != other.javaClass) return false 121 | return toString() == other.toString() 122 | } 123 | 124 | override fun hashCode() = toString().hashCode() 125 | 126 | override fun toString() = buildCodeString { emit(this, null, emptySet()) } 127 | 128 | fun toBuilder(): Builder { 129 | val builder = Builder(name) 130 | builder.tsDoc.add(tsDoc) 131 | builder.decorators += decorators 132 | builder.modifiers += modifiers 133 | builder.typeVariables += typeVariables 134 | builder.returnType = returnType 135 | builder.parameters += parameters 136 | builder.body.add(body) 137 | return builder 138 | } 139 | 140 | class Builder internal constructor( 141 | internal val name: String 142 | ) : Taggable.Builder() { 143 | 144 | internal val tsDoc = CodeBlock.builder() 145 | internal val decorators = mutableListOf() 146 | internal val modifiers = mutableSetOf() 147 | internal val typeVariables = mutableListOf() 148 | internal var returnType: TypeName? = null 149 | internal val parameters = mutableListOf() 150 | internal var restParameter: ParameterSpec? = null 151 | internal val body = CodeBlock.builder() 152 | 153 | init { 154 | require(name.isConstructor || name.isName) { 155 | "not a valid name: $name" 156 | } 157 | } 158 | 159 | fun addTSDoc(format: String, vararg args: Any) = apply { 160 | tsDoc.add(format, *args) 161 | } 162 | 163 | fun addTSDoc(block: CodeBlock) = apply { 164 | tsDoc.add(block) 165 | } 166 | 167 | fun addDecorators(decoratorSpecs: Iterable) = apply { 168 | this.decorators += decoratorSpecs 169 | } 170 | 171 | fun addDecorator(decoratorSpec: DecoratorSpec) = apply { 172 | decorators += decoratorSpec 173 | } 174 | 175 | fun addModifiers(vararg modifiers: Modifier) = apply { 176 | this.modifiers += modifiers 177 | } 178 | 179 | fun addModifiers(modifiers: Iterable) = apply { 180 | this.modifiers += modifiers 181 | } 182 | 183 | fun addTypeVariables(typeVariables: Iterable) = apply { 184 | this.typeVariables += typeVariables 185 | } 186 | 187 | fun addTypeVariable(typeVariable: TypeName.TypeVariable) = apply { 188 | typeVariables += typeVariable 189 | } 190 | 191 | fun returns(returnType: TypeName) = apply { 192 | check(!name.isConstructor) { "$name cannot have a return type" } 193 | this.returnType = returnType 194 | } 195 | 196 | fun addParameters(parameterSpecs: Iterable) = apply { 197 | for (parameterSpec in parameterSpecs) { 198 | addParameter(parameterSpec) 199 | } 200 | } 201 | 202 | fun addParameter(parameterSpec: ParameterSpec) = apply { 203 | parameters += parameterSpec 204 | } 205 | 206 | fun addParameter( 207 | name: String, 208 | type: TypeName, 209 | optional: Boolean = false, 210 | defaultValue: CodeBlock, 211 | vararg modifiers: Modifier 212 | ) = addParameter( 213 | ParameterSpec.builder( 214 | name, type, optional, 215 | *modifiers 216 | ).defaultValue(defaultValue).build() 217 | ) 218 | 219 | fun addParameter(name: String, type: TypeName, optional: Boolean = false, vararg modifiers: Modifier) = 220 | addParameter(ParameterSpec.builder(name, type, optional, *modifiers).build()) 221 | 222 | fun restParameter(name: String, type: TypeName) = restParameter(ParameterSpec.builder(name, type).build()) 223 | 224 | fun restParameter(parameterSpec: ParameterSpec) = apply { 225 | this.restParameter = parameterSpec 226 | } 227 | 228 | fun addCode(format: String, vararg args: Any) = apply { 229 | modifiers -= Modifier.ABSTRACT 230 | body.add(format, *args) 231 | } 232 | 233 | fun addNamedCode(format: String, args: Map) = apply { 234 | modifiers -= Modifier.ABSTRACT 235 | body.addNamed(format, args) 236 | } 237 | 238 | fun addCode(codeBlock: CodeBlock) = apply { 239 | modifiers -= Modifier.ABSTRACT 240 | body.add(codeBlock) 241 | } 242 | 243 | fun addComment(format: String, vararg args: Any) = apply { 244 | body.add("// " + format + "\n", *args) 245 | } 246 | 247 | /** 248 | * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". 249 | * * Shouldn't contain braces or newline characters. 250 | */ 251 | fun beginControlFlow(controlFlow: String, vararg args: Any) = apply { 252 | modifiers -= Modifier.ABSTRACT 253 | body.beginControlFlow(controlFlow, *args) 254 | } 255 | 256 | /** 257 | * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". 258 | * * Shouldn't contain braces or newline characters. 259 | */ 260 | fun nextControlFlow(controlFlow: String, vararg args: Any) = apply { 261 | modifiers -= Modifier.ABSTRACT 262 | body.nextControlFlow(controlFlow, *args) 263 | } 264 | 265 | fun endControlFlow() = apply { 266 | modifiers -= Modifier.ABSTRACT 267 | body.endControlFlow() 268 | } 269 | 270 | fun addStatement(format: String, vararg args: Any) = apply { 271 | modifiers -= Modifier.ABSTRACT 272 | body.addStatement(format, *args) 273 | } 274 | 275 | fun build() = FunctionSpec(this) 276 | } 277 | 278 | companion object { 279 | 280 | private const val CONSTRUCTOR = "constructor()" 281 | private const val CALLABLE = "callable()" 282 | private const val INDEXABLE = "indexable()" 283 | 284 | private val String.isConstructor get() = this == CONSTRUCTOR 285 | private val String.isCallable get() = this == CALLABLE 286 | private val String.isIndexable get() = this == INDEXABLE 287 | 288 | @JvmStatic 289 | fun builder(name: String) = Builder(name) 290 | 291 | @JvmStatic 292 | fun constructorBuilder() = Builder( 293 | CONSTRUCTOR 294 | ) 295 | 296 | @JvmStatic 297 | fun callableBuilder() = Builder( 298 | CALLABLE 299 | ) 300 | 301 | @JvmStatic 302 | fun indexableBuilder() = Builder( 303 | INDEXABLE 304 | ) 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/InterfaceSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.CodeWriter 20 | import io.outfoxx.typescriptpoet.FunctionSpec 21 | import io.outfoxx.typescriptpoet.InterfaceSpec 22 | import io.outfoxx.typescriptpoet.Modifier 23 | import io.outfoxx.typescriptpoet.TypeName 24 | import io.outfoxx.typescriptpoet.tag 25 | import io.outfoxx.typescriptpoet.toImmutableSet 26 | import org.hamcrest.CoreMatchers.equalTo 27 | import org.hamcrest.CoreMatchers.hasItems 28 | import org.hamcrest.MatcherAssert.assertThat 29 | import org.junit.jupiter.api.DisplayName 30 | import org.junit.jupiter.api.Test 31 | import java.io.StringWriter 32 | 33 | @DisplayName("InterfaceSpec Tests") 34 | class InterfaceSpecTests { 35 | 36 | @Test 37 | @DisplayName("Tags on builders can be retrieved on builders and built specs") 38 | fun testTags() { 39 | val testBuilder = InterfaceSpec.builder("Test") 40 | .tag(5) 41 | val testSpec = testBuilder.build() 42 | 43 | assertThat(testBuilder.tags[Integer::class] as? Int, equalTo(5)) 44 | assertThat(testSpec.tag(), equalTo(5)) 45 | } 46 | 47 | @Test 48 | @DisplayName("Generates TSDoc at before interface definition") 49 | fun testGenTSDoc() { 50 | val testIface = InterfaceSpec.builder("Test") 51 | .addTSDoc("this is a comment\n") 52 | .build() 53 | 54 | val out = StringWriter() 55 | testIface.emit(CodeWriter(out)) 56 | 57 | assertThat( 58 | out.toString(), 59 | equalTo( 60 | """ 61 | /** 62 | * this is a comment 63 | */ 64 | interface Test { 65 | } 66 | 67 | """.trimIndent() 68 | ) 69 | ) 70 | } 71 | 72 | @Test 73 | @DisplayName("Generates modifiers in order") 74 | fun testGenModifiersInOrder() { 75 | val testIface = InterfaceSpec.builder("Test") 76 | .addModifiers(Modifier.EXPORT) 77 | .build() 78 | 79 | val out = StringWriter() 80 | testIface.emit(CodeWriter(out)) 81 | 82 | assertThat( 83 | out.toString(), 84 | equalTo( 85 | """ 86 | export interface Test { 87 | } 88 | 89 | """.trimIndent() 90 | ) 91 | ) 92 | } 93 | 94 | @Test 95 | @DisplayName("Generates type variables") 96 | fun testGenTypeVars() { 97 | val testIface = InterfaceSpec.builder("Test") 98 | .addTypeVariable( 99 | TypeName.typeVariable("X", TypeName.bound(TypeName.implicit("Test2"))) 100 | ) 101 | .addTypeVariable( 102 | TypeName.typeVariable("Y", TypeName.bound(TypeName.implicit("Test3")), TypeName.intersectBound(TypeName.implicit("Test4"))) 103 | ) 104 | .addTypeVariable( 105 | TypeName.typeVariable("Z", TypeName.bound(TypeName.implicit("Test5")), TypeName.unionBound(TypeName.implicit("Test6"), true)) 106 | ) 107 | .build() 108 | 109 | val out = StringWriter() 110 | testIface.emit(CodeWriter(out)) 111 | 112 | assertThat( 113 | out.toString(), 114 | equalTo( 115 | """ 116 | interface Test { 117 | } 118 | 119 | """.trimIndent() 120 | ) 121 | ) 122 | } 123 | 124 | @Test 125 | @DisplayName("Generates super interfaces") 126 | fun testGenMixins() { 127 | val testIface = InterfaceSpec.builder("Test") 128 | .addSuperInterface(TypeName.implicit("Test2")) 129 | .addSuperInterface(TypeName.implicit("Test3")) 130 | .build() 131 | 132 | val out = StringWriter() 133 | testIface.emit(CodeWriter(out)) 134 | 135 | assertThat( 136 | out.toString(), 137 | equalTo( 138 | """ 139 | interface Test extends Test2, Test3 { 140 | } 141 | 142 | """.trimIndent() 143 | ) 144 | ) 145 | } 146 | 147 | @Test 148 | @DisplayName("Generates type vars & super interfaces properly formatted") 149 | fun testGenTypeVarsAndSuperInterfacesFormatted() { 150 | val testIface = InterfaceSpec.builder("Test") 151 | .addTypeVariable( 152 | TypeName.typeVariable("Y", TypeName.bound(TypeName.implicit("Test3")), TypeName.intersectBound(TypeName.implicit("Test4"))) 153 | ) 154 | .addSuperInterface(TypeName.implicit("Test2")) 155 | .addSuperInterface(TypeName.implicit("Test3")) 156 | .addSuperInterface(TypeName.implicit("Test4")) 157 | .build() 158 | 159 | val out = StringWriter() 160 | testIface.emit(CodeWriter(out)) 161 | 162 | assertThat( 163 | out.toString(), 164 | equalTo( 165 | """ 166 | interface Test extends Test2, Test3, Test4 { 167 | } 168 | 169 | """.trimIndent() 170 | ) 171 | ) 172 | } 173 | 174 | @Test 175 | @DisplayName("Generates property declarations") 176 | fun testGenProperties() { 177 | val testIface = InterfaceSpec.builder("Test") 178 | .addProperty("value", TypeName.NUMBER, false, Modifier.PRIVATE) 179 | .addProperty("value2", TypeName.STRING, true, Modifier.PUBLIC) 180 | .build() 181 | 182 | val out = StringWriter() 183 | testIface.emit(CodeWriter(out)) 184 | 185 | assertThat( 186 | out.toString(), 187 | equalTo( 188 | """ 189 | interface Test { 190 | 191 | private value: number; 192 | 193 | value2?: string; 194 | 195 | } 196 | 197 | """.trimIndent() 198 | ) 199 | ) 200 | } 201 | 202 | @Test 203 | @DisplayName("Generates method declarations") 204 | fun testGenMethods() { 205 | val testIface = InterfaceSpec.builder("Test") 206 | .addFunction( 207 | FunctionSpec.builder("test1") 208 | .addModifiers(Modifier.ABSTRACT) 209 | .build() 210 | ) 211 | .addFunction( 212 | FunctionSpec.builder("test2") 213 | .addModifiers(Modifier.ABSTRACT) 214 | .build() 215 | ) 216 | .build() 217 | 218 | val out = StringWriter() 219 | testIface.emit(CodeWriter(out)) 220 | 221 | assertThat( 222 | out.toString(), 223 | equalTo( 224 | """ 225 | interface Test { 226 | 227 | test1(); 228 | 229 | test2(); 230 | 231 | } 232 | 233 | """.trimIndent() 234 | ) 235 | ) 236 | } 237 | 238 | @Test 239 | @DisplayName("Generates indexing declarations") 240 | fun testGenIndexables() { 241 | val testIface = InterfaceSpec.builder("Test") 242 | .addIndexable( 243 | FunctionSpec.indexableBuilder() 244 | .addModifiers(Modifier.ABSTRACT) 245 | .addParameter("idx", TypeName.STRING) 246 | .returns(TypeName.ANY) 247 | .build() 248 | ) 249 | .addIndexable( 250 | FunctionSpec.indexableBuilder() 251 | .addModifiers(Modifier.READONLY, Modifier.ABSTRACT) 252 | .addParameter("idx", TypeName.STRING) 253 | .returns(TypeName.ANY) 254 | .build() 255 | ) 256 | .build() 257 | 258 | val out = StringWriter() 259 | testIface.emit(CodeWriter(out)) 260 | 261 | assertThat( 262 | out.toString(), 263 | equalTo( 264 | """ 265 | interface Test { 266 | 267 | [idx: string]: any; 268 | 269 | readonly [idx: string]: any; 270 | 271 | } 272 | 273 | """.trimIndent() 274 | ) 275 | ) 276 | } 277 | 278 | @Test 279 | @DisplayName("Generates callable declaration") 280 | fun testGenCallable() { 281 | val testIface = InterfaceSpec.builder("Test") 282 | .callable( 283 | FunctionSpec.callableBuilder() 284 | .addModifiers(Modifier.ABSTRACT) 285 | .addParameter("a", TypeName.STRING) 286 | .returns(TypeName.implicit("Test")) 287 | .build() 288 | ) 289 | .build() 290 | 291 | val out = StringWriter() 292 | testIface.emit(CodeWriter(out)) 293 | 294 | assertThat( 295 | out.toString(), 296 | equalTo( 297 | """ 298 | interface Test { 299 | 300 | (a: string): Test; 301 | 302 | } 303 | 304 | """.trimIndent() 305 | ) 306 | ) 307 | } 308 | 309 | @Test 310 | @DisplayName("toBuilder copies all fields") 311 | fun testToBuilder() { 312 | val testIfaceBlder = InterfaceSpec.builder("Test") 313 | .addTSDoc("this is a comment\n") 314 | .addModifiers(Modifier.ABSTRACT, Modifier.EXPORT) 315 | .addTypeVariable( 316 | TypeName.typeVariable("X", TypeName.bound(TypeName.implicit("Test2"))) 317 | ) 318 | .addSuperInterface(TypeName.implicit("Test3")) 319 | .addProperty("value", TypeName.NUMBER, false, Modifier.PRIVATE) 320 | .addProperty("value2", TypeName.STRING, false, Modifier.PUBLIC) 321 | .addFunction( 322 | FunctionSpec.builder("test1") 323 | .addModifiers(Modifier.ABSTRACT) 324 | .build() 325 | ) 326 | .addIndexable( 327 | FunctionSpec.indexableBuilder() 328 | .addModifiers(Modifier.ABSTRACT) 329 | .addParameter("idx", TypeName.STRING) 330 | .returns(TypeName.ANY) 331 | .build() 332 | ) 333 | .callable( 334 | FunctionSpec.callableBuilder() 335 | .addModifiers(Modifier.ABSTRACT) 336 | .build() 337 | ) 338 | .build() 339 | .toBuilder() 340 | 341 | assertThat(testIfaceBlder.tsDoc.formatParts, hasItems("this is a comment\n")) 342 | assertThat(testIfaceBlder.modifiers.toImmutableSet(), equalTo(setOf(Modifier.ABSTRACT, Modifier.EXPORT))) 343 | assertThat(testIfaceBlder.typeVariables.size, equalTo(1)) 344 | assertThat( 345 | testIfaceBlder.superInterfaces, 346 | hasItems( 347 | TypeName.implicit("Test3") 348 | ) 349 | ) 350 | assertThat(testIfaceBlder.propertySpecs.map { it.name }, hasItems("value", "value2")) 351 | assertThat(testIfaceBlder.functionSpecs.map { it.name }, hasItems("test1")) 352 | assertThat(testIfaceBlder.indexableSpecs.map { it.name }, hasItems("indexable()")) 353 | assertThat(testIfaceBlder.callable?.name, equalTo("callable()")) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/ClassSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import io.outfoxx.typescriptpoet.CodeBlock.Companion.joinToCode 20 | 21 | /** A generated `class` declaration. */ 22 | class ClassSpec 23 | private constructor( 24 | builder: Builder 25 | ) : TypeSpec(builder) { 26 | 27 | override val name = builder.name 28 | val tsDoc = builder.tsDoc.build() 29 | val decorators = builder.decorators.toImmutableList() 30 | val modifiers = builder.modifiers.toImmutableSet() 31 | val typeVariables = builder.typeVariables.toImmutableList() 32 | val superClass = builder.superClass 33 | val mixins = builder.mixins.toImmutableList() 34 | val propertySpecs = builder.propertySpecs.toImmutableList() 35 | val constructor = builder.constructor 36 | val functionSpecs = builder.functionSpecs.toImmutableList() 37 | val useConstructorPropertiesAutomatically = builder.useConstructorPropertiesAutomatically 38 | 39 | override fun emit(codeWriter: CodeWriter) { 40 | 41 | val constructorProperties: Map = 42 | if (useConstructorPropertiesAutomatically) 43 | constructorProperties() 44 | else 45 | emptyMap() 46 | 47 | codeWriter.emitTSDoc(tsDoc) 48 | codeWriter.emitDecorators(decorators, false) 49 | codeWriter.emitModifiers(modifiers, setOf(Modifier.PUBLIC)) 50 | codeWriter.emit("class") 51 | codeWriter.emitCode(CodeBlock.of(" %L", name)) 52 | codeWriter.emitTypeVariables(typeVariables) 53 | 54 | val superClass = if (superClass != null) CodeBlock.of("extends %T", superClass) else CodeBlock.empty() 55 | val mixins = mixins.map { CodeBlock.of("%T", it) }.let { 56 | if (it.isNotEmpty()) it.joinToCode(prefix = "implements ") else CodeBlock.empty() 57 | } 58 | 59 | val parents = (listOf(superClass) + mixins).filter { it.isNotEmpty() } 60 | if (parents.any { it.isNotEmpty() }) { 61 | codeWriter.emitCode(parents.joinToCode(separator = " ", prefix = " ")) 62 | } 63 | 64 | codeWriter.emit(" {\n") 65 | codeWriter.indent() 66 | 67 | // Non-static properties. 68 | for (propertySpec in propertySpecs) { 69 | if (constructorProperties.containsKey(propertySpec.name) && !propertySpec.modifiers.contains(Modifier.STATIC)) { 70 | continue 71 | } 72 | codeWriter.emit("\n") 73 | propertySpec.emit( 74 | codeWriter, setOf(Modifier.PUBLIC), asStatement = true, 75 | compactOptionalAllowed = !useConstructorPropertiesAutomatically, 76 | ) 77 | } 78 | 79 | // Write the constructor manually, allowing the replacement 80 | // of property specs with constructor parameters 81 | constructor?.let { 82 | codeWriter.emit("\n") 83 | 84 | if (it.decorators.isNotEmpty()) { 85 | codeWriter.emit(" ") 86 | codeWriter.emitDecorators(it.decorators, false) 87 | codeWriter.emit("\n") 88 | } 89 | 90 | if (it.modifiers.isNotEmpty()) { 91 | codeWriter.emitModifiers(it.modifiers) 92 | } 93 | 94 | codeWriter.emit("constructor") 95 | 96 | val body = constructor.body 97 | 98 | // Emit constructor parameters & property specs that can be replaced with parameters 99 | it.parameters.emit( 100 | codeWriter, rest = it.restParameter, 101 | constructorProperties = constructorProperties 102 | ) { param, isRest, optionalAllowed -> 103 | 104 | var property = constructorProperties[param.name] 105 | if (property != null && !isRest) { 106 | 107 | // Ensure the parameter always has a modifier (that makes it a property in TS) 108 | if ( 109 | property.modifiers.none { mod -> 110 | mod.isOneOf( 111 | Modifier.PUBLIC, 112 | Modifier.PRIVATE, 113 | Modifier.PROTECTED, 114 | Modifier.READONLY 115 | ) 116 | } 117 | ) { 118 | // Add default public modifier 119 | property = property.toBuilder().addModifiers(Modifier.PUBLIC).build() 120 | } 121 | property.emit(codeWriter, setOf(), compactOptionalAllowed = false, withInitializer = false) 122 | param.emitDefaultValue(codeWriter) 123 | } else { 124 | param.emit( 125 | codeWriter, 126 | isRest = isRest, 127 | optionalAllowed = optionalAllowed && !useConstructorPropertiesAutomatically, 128 | ) 129 | } 130 | } 131 | 132 | codeWriter.emit(" {\n") 133 | codeWriter.indent() 134 | codeWriter.emitCode(body) 135 | codeWriter.unindent() 136 | codeWriter.emit("}\n") 137 | } 138 | 139 | // Constructors. 140 | for (funSpec in functionSpecs) { 141 | if (!funSpec.isConstructor) continue 142 | codeWriter.emit("\n") 143 | funSpec.emit(codeWriter, name, setOf(Modifier.PUBLIC)) 144 | } 145 | 146 | // Functions (static and non-static). 147 | for (funSpec in functionSpecs) { 148 | if (funSpec.isConstructor) continue 149 | codeWriter.emit("\n") 150 | funSpec.emit(codeWriter, name, setOf(Modifier.PUBLIC)) 151 | } 152 | 153 | codeWriter.unindent() 154 | 155 | if (!hasNoBody) { 156 | codeWriter.emit("\n") 157 | } 158 | codeWriter.emit("}\n") 159 | } 160 | 161 | /** Returns the properties that can be declared inline as constructor parameters. */ 162 | private fun constructorProperties(): Map = 163 | propertySpecs.filter { it.name == it.initializer?.toString() }.map { it.name to it }.toMap() 164 | 165 | private val hasNoBody: Boolean 166 | get() { 167 | if (propertySpecs.isNotEmpty()) { 168 | val constructorProperties = constructorProperties() 169 | propertySpecs 170 | .filterNot { constructorProperties.containsKey(it.name) } 171 | .forEach { _ -> return false } 172 | } 173 | return constructor == null && functionSpecs.isEmpty() 174 | } 175 | 176 | fun toBuilder(): Builder { 177 | val builder = Builder(name) 178 | builder.tsDoc.add(tsDoc) 179 | builder.decorators += decorators 180 | builder.modifiers += modifiers 181 | builder.typeVariables += typeVariables 182 | builder.superClass = superClass 183 | builder.mixins += mixins 184 | builder.propertySpecs += propertySpecs 185 | builder.constructor = constructor 186 | builder.functionSpecs += functionSpecs 187 | return builder 188 | } 189 | 190 | class Builder( 191 | name: String 192 | ) : TypeSpec.Builder(name) { 193 | 194 | internal val tsDoc = CodeBlock.builder() 195 | internal val decorators = mutableListOf() 196 | internal val modifiers = mutableListOf() 197 | internal val typeVariables = mutableListOf() 198 | internal var superClass: TypeName? = null 199 | internal val mixins = mutableListOf() 200 | internal val propertySpecs = mutableListOf() 201 | internal var constructor: FunctionSpec? = null 202 | internal val functionSpecs = mutableListOf() 203 | internal var useConstructorPropertiesAutomatically = true 204 | 205 | fun addTSDoc(format: String, vararg args: Any) = apply { 206 | tsDoc.add(format, *args) 207 | } 208 | 209 | fun addTSDoc(block: CodeBlock) = apply { 210 | tsDoc.add(block) 211 | } 212 | 213 | fun addDecorators(decoratorSpecs: Iterable) = apply { 214 | decorators += decoratorSpecs 215 | } 216 | 217 | fun addDecorator(decoratorSpec: DecoratorSpec) = apply { 218 | decorators += decoratorSpec 219 | } 220 | 221 | fun addModifiers(vararg modifiers: Modifier) = apply { 222 | this.modifiers += modifiers 223 | } 224 | 225 | fun addTypeVariables(typeVariables: Iterable) = apply { 226 | this.typeVariables += typeVariables 227 | } 228 | 229 | fun addTypeVariable(typeVariable: TypeName.TypeVariable) = apply { 230 | typeVariables += typeVariable 231 | } 232 | 233 | fun superClass(superClass: TypeName) = apply { 234 | check(this.superClass == null) { "superclass already set to ${this.superClass}" } 235 | this.superClass = superClass 236 | } 237 | 238 | fun addMixins(mixins: Iterable) = apply { 239 | this.mixins += mixins 240 | } 241 | 242 | fun addMixin(mixin: TypeName) = apply { 243 | mixins += mixin 244 | } 245 | 246 | fun constructor(constructor: FunctionSpec?) = apply { 247 | if (constructor != null) { 248 | require(constructor.isConstructor) { 249 | "expected a constructor but was ${constructor.name}; use FunctionSpec.constructorBuilder when building" 250 | } 251 | } 252 | this.constructor = constructor 253 | } 254 | 255 | fun addProperties(propertySpecs: Iterable) = apply { 256 | this.propertySpecs += propertySpecs 257 | } 258 | 259 | fun addProperty(propertySpec: PropertySpec) = apply { 260 | propertySpecs += propertySpec 261 | } 262 | 263 | fun addProperty(name: String, type: TypeName, optional: Boolean = false, vararg modifiers: Modifier) = 264 | addProperty(PropertySpec.builder(name, type, optional, *modifiers).build()) 265 | 266 | fun addFunctions(functionSpecs: Iterable) = apply { 267 | functionSpecs.forEach { addFunction(it) } 268 | } 269 | 270 | fun addFunction(functionSpec: FunctionSpec) = apply { 271 | require(!functionSpec.isConstructor) { "Use the 'constructor' method for the constructor" } 272 | this.functionSpecs += functionSpec 273 | } 274 | 275 | fun allowUsingConstructorPropertiesAutomatically(value: Boolean = true) = apply { 276 | this.useConstructorPropertiesAutomatically = value 277 | } 278 | 279 | override fun build(): ClassSpec { 280 | val isAbstract = modifiers.contains(Modifier.ABSTRACT) 281 | for (functionSpec in functionSpecs) { 282 | require(isAbstract || !functionSpec.modifiers.contains(Modifier.ABSTRACT)) { 283 | "non-abstract type $name cannot declare abstract function ${functionSpec.name}" 284 | } 285 | } 286 | 287 | return ClassSpec(this) 288 | } 289 | } 290 | 291 | companion object { 292 | 293 | @JvmStatic 294 | fun builder(name: String) = Builder(name) 295 | 296 | @JvmStatic 297 | fun builder(name: TypeName) = Builder("$name") 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/test/java/io/outfoxx/typescriptpoet/test/FunctionSpecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet.test 18 | 19 | import io.outfoxx.typescriptpoet.CodeBlock 20 | import io.outfoxx.typescriptpoet.CodeWriter 21 | import io.outfoxx.typescriptpoet.DecoratorSpec 22 | import io.outfoxx.typescriptpoet.FunctionSpec 23 | import io.outfoxx.typescriptpoet.Modifier 24 | import io.outfoxx.typescriptpoet.ParameterSpec 25 | import io.outfoxx.typescriptpoet.SymbolSpec 26 | import io.outfoxx.typescriptpoet.TypeName 27 | import io.outfoxx.typescriptpoet.tag 28 | import io.outfoxx.typescriptpoet.toImmutableSet 29 | import org.hamcrest.CoreMatchers.equalTo 30 | import org.hamcrest.CoreMatchers.hasItems 31 | import org.hamcrest.MatcherAssert.assertThat 32 | import org.junit.jupiter.api.DisplayName 33 | import org.junit.jupiter.api.Test 34 | import java.io.StringWriter 35 | 36 | @DisplayName("FunctionSpec Tests") 37 | class FunctionSpecTests { 38 | 39 | @Test 40 | @DisplayName("Tags on builders can be retrieved on builders and built specs") 41 | fun testTags() { 42 | val testBuilder = FunctionSpec.builder("Test") 43 | .tag(5) 44 | val testSpec = testBuilder.build() 45 | 46 | assertThat(testBuilder.tags[Integer::class] as? Int, equalTo(5)) 47 | assertThat(testSpec.tag(), equalTo(5)) 48 | } 49 | 50 | @Test 51 | @DisplayName("Generates TSDoc at before class definition") 52 | fun testGenTSDoc() { 53 | val testFunc = FunctionSpec.builder("test") 54 | .addTSDoc("this is a comment\n") 55 | .build() 56 | 57 | val out = StringWriter() 58 | testFunc.emit(CodeWriter(out), null, setOf()) 59 | 60 | assertThat( 61 | out.toString(), 62 | equalTo( 63 | """ 64 | /** 65 | * this is a comment 66 | */ 67 | function test() { 68 | } 69 | 70 | """.trimIndent() 71 | ) 72 | ) 73 | } 74 | 75 | @Test 76 | @DisplayName("Generates decorators") 77 | fun testGenDecorators() { 78 | val testFunc = FunctionSpec.builder("test") 79 | .addDecorator( 80 | DecoratorSpec.builder("decorate") 81 | .addParameter(null, "true") 82 | .addParameter("targetType", "Test2") 83 | .build() 84 | ) 85 | .build() 86 | 87 | val out = StringWriter() 88 | testFunc.emit(CodeWriter(out), null, setOf()) 89 | 90 | assertThat( 91 | out.toString(), 92 | equalTo( 93 | """ 94 | @decorate(true, /* targetType */ Test2) 95 | function test() { 96 | } 97 | 98 | """.trimIndent() 99 | ) 100 | ) 101 | } 102 | 103 | @Test 104 | @DisplayName("Generates modifiers in order") 105 | fun testGenModifiersInOrder() { 106 | val testClass = FunctionSpec.builder("test") 107 | .addModifiers(Modifier.PRIVATE, Modifier.GET, Modifier.EXPORT) 108 | .addCode("") 109 | .build() 110 | 111 | val out = StringWriter() 112 | testClass.emit(CodeWriter(out), null, setOf()) 113 | 114 | assertThat( 115 | out.toString(), 116 | equalTo( 117 | """ 118 | export private get function test() { 119 | } 120 | 121 | """.trimIndent() 122 | ) 123 | ) 124 | } 125 | 126 | @Test 127 | @DisplayName("Generates no block when abstract") 128 | fun testGenModifiersAbstract() { 129 | val testClass = FunctionSpec.builder("test") 130 | .addModifiers(Modifier.PRIVATE, Modifier.ABSTRACT) 131 | .build() 132 | 133 | val out = StringWriter() 134 | testClass.emit(CodeWriter(out), null, setOf()) 135 | 136 | assertThat( 137 | out.toString(), 138 | equalTo( 139 | """ 140 | private abstract function test(); 141 | 142 | """.trimIndent() 143 | ) 144 | ) 145 | } 146 | 147 | @Test 148 | @DisplayName("Generates type variables") 149 | fun testGenTypeVars() { 150 | val testClass = FunctionSpec.builder("test") 151 | .addTypeVariable( 152 | TypeName.typeVariable("X", TypeName.bound(TypeName.implicit("Test2"))) 153 | ) 154 | .addTypeVariable( 155 | TypeName.typeVariable("Y", TypeName.bound(TypeName.implicit("Test3")), TypeName.intersectBound(TypeName.implicit("Test4"))) 156 | ) 157 | .addTypeVariable( 158 | TypeName.typeVariable("Z", TypeName.bound(TypeName.implicit("Test5")), TypeName.unionBound(TypeName.implicit("Test6"), true)) 159 | ) 160 | .build() 161 | 162 | val out = StringWriter() 163 | testClass.emit(CodeWriter(out), null, setOf()) 164 | 165 | assertThat( 166 | out.toString(), 167 | equalTo( 168 | """ 169 | function test() { 170 | } 171 | 172 | """.trimIndent() 173 | ) 174 | ) 175 | } 176 | 177 | @Test 178 | @DisplayName("Generates return type") 179 | fun testGenReturnType() { 180 | val testClass = FunctionSpec.builder("test") 181 | .returns(TypeName.implicit("Value")) 182 | .build() 183 | 184 | val out = StringWriter() 185 | testClass.emit(CodeWriter(out), null, setOf()) 186 | 187 | assertThat( 188 | out.toString(), 189 | equalTo( 190 | """ 191 | function test(): Value { 192 | } 193 | 194 | """.trimIndent() 195 | ) 196 | ) 197 | } 198 | 199 | @Test 200 | @DisplayName("Generates no return type when void") 201 | fun testGenNoReturnTypeForVoid() { 202 | val testClass = FunctionSpec.builder("test") 203 | .returns(TypeName.VOID) 204 | .build() 205 | 206 | val out = StringWriter() 207 | testClass.emit(CodeWriter(out), null, setOf()) 208 | 209 | assertThat( 210 | out.toString(), 211 | equalTo( 212 | """ 213 | function test() { 214 | } 215 | 216 | """.trimIndent() 217 | ) 218 | ) 219 | } 220 | 221 | @Test 222 | @DisplayName("Generates no return type when not set") 223 | fun testGenNoReturnType() { 224 | val testClass = FunctionSpec.builder("test") 225 | .build() 226 | 227 | val out = StringWriter() 228 | testClass.emit(CodeWriter(out), null, setOf()) 229 | 230 | assertThat( 231 | out.toString(), 232 | equalTo( 233 | """ 234 | function test() { 235 | } 236 | 237 | """.trimIndent() 238 | ) 239 | ) 240 | } 241 | 242 | @Test 243 | @DisplayName("Generates parameters") 244 | fun testGenParameters() { 245 | val testClass = FunctionSpec.builder("test") 246 | .addParameter("b", TypeName.STRING) 247 | .build() 248 | 249 | val out = StringWriter() 250 | testClass.emit(CodeWriter(out), null, setOf()) 251 | 252 | assertThat( 253 | out.toString(), 254 | equalTo( 255 | """ 256 | function test(b: string) { 257 | } 258 | 259 | """.trimIndent() 260 | ) 261 | ) 262 | } 263 | 264 | @Test 265 | @DisplayName("Generates parameters with rest") 266 | fun testGenParametersRest() { 267 | val testClass = FunctionSpec.builder("test") 268 | .addParameter("b", TypeName.STRING) 269 | .restParameter("c", TypeName.arrayType(TypeName.STRING)) 270 | .build() 271 | 272 | val out = StringWriter() 273 | testClass.emit(CodeWriter(out), null, setOf()) 274 | 275 | assertThat( 276 | out.toString(), 277 | equalTo( 278 | """ 279 | function test(b: string, ... c: Array) { 280 | } 281 | 282 | """.trimIndent() 283 | ) 284 | ) 285 | } 286 | 287 | @Test 288 | @DisplayName("Generates parameters with default values") 289 | fun testGenParametersDefaults() { 290 | val testClass = FunctionSpec.builder("test") 291 | .addParameter("a", TypeName.NUMBER, false, CodeBlock.of("10")) 292 | .build() 293 | 294 | val out = StringWriter() 295 | testClass.emit(CodeWriter(out), null, setOf()) 296 | 297 | assertThat( 298 | out.toString(), 299 | equalTo( 300 | """ 301 | function test(a: number = 10) { 302 | } 303 | 304 | """.trimIndent() 305 | ) 306 | ) 307 | } 308 | 309 | @Test 310 | @DisplayName("Generates parameter decorators") 311 | fun testGenParameterDecorators() { 312 | val testClass = FunctionSpec.builder("test") 313 | .addParameter( 314 | ParameterSpec.builder("a", TypeName.NUMBER) 315 | .addDecorator( 316 | DecoratorSpec.builder("required") 317 | .build() 318 | ) 319 | .addDecorator( 320 | DecoratorSpec.builder("size") 321 | .addParameter("min", "10") 322 | .addParameter("max", "100") 323 | .build() 324 | ) 325 | .addDecorator( 326 | DecoratorSpec.builder("logged") 327 | .asFactory() 328 | .build() 329 | ) 330 | .build() 331 | ) 332 | .build() 333 | 334 | val out = StringWriter() 335 | testClass.emit(CodeWriter(out), null, setOf()) 336 | 337 | assertThat( 338 | out.toString(), 339 | equalTo( 340 | """ 341 | function test( 342 | @required @size(/* min */ 10, /* max */ 100) @logged() a: number 343 | ) { 344 | } 345 | 346 | """.trimIndent() 347 | ) 348 | ) 349 | } 350 | 351 | @Test 352 | @DisplayName("toBuilder copies all fields") 353 | fun testToBuilder() { 354 | val testFuncBlder = FunctionSpec.builder("Test") 355 | .addTSDoc("this is a comment\n") 356 | .addDecorator( 357 | DecoratorSpec.builder("decorate") 358 | .addParameter(null, "true") 359 | .addParameter("targetType", "Test2") 360 | .build() 361 | ) 362 | .addModifiers(Modifier.EXPORT) 363 | .addTypeVariable( 364 | TypeName.typeVariable("X", TypeName.bound(TypeName.implicit("Test2"))) 365 | ) 366 | .addCode("val;\n") 367 | .build() 368 | .toBuilder() 369 | 370 | assertThat(testFuncBlder.tsDoc.formatParts, hasItems("this is a comment\n")) 371 | assertThat(testFuncBlder.decorators.size, equalTo(1)) 372 | assertThat(testFuncBlder.decorators[0].name, equalTo(SymbolSpec.from("decorate"))) 373 | assertThat(testFuncBlder.decorators[0].parameters.size, equalTo(2)) 374 | assertThat(testFuncBlder.modifiers.toImmutableSet(), equalTo(setOf(Modifier.EXPORT))) 375 | assertThat(testFuncBlder.typeVariables.size, equalTo(1)) 376 | assertThat(testFuncBlder.body.formatParts, hasItems("val;\n")) 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/FileSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import java.io.File 20 | import java.io.IOException 21 | import java.io.OutputStreamWriter 22 | import java.nio.charset.StandardCharsets.UTF_8 23 | import java.nio.file.Files 24 | import java.nio.file.Path 25 | import java.nio.file.Paths 26 | 27 | /** 28 | * A TypeScript file containing top level objects like classes, objects, functions, properties, and type 29 | * aliases. 30 | * 31 | * Items are output in the following order: 32 | * - Comment 33 | * - Imports 34 | * - Members 35 | */ 36 | class FileSpec 37 | private constructor( 38 | builder: Builder 39 | ) : Taggable(builder.tags.toImmutableMap()) { 40 | 41 | val modulePath = builder.modulePath 42 | val comment = builder.comment.build() 43 | val members = builder.members.toList() 44 | val indent = builder.indent 45 | 46 | @Throws(IOException::class) 47 | fun writeTo(out: Appendable, directory: Path = Paths.get("/")) { 48 | // First pass: emit the entire file, just to collect the symbols we'll need to import. 49 | val importsCollector = CodeWriter(NullAppendable, indent) 50 | importsCollector.use { emit(it, directory) } 51 | 52 | val absPath = directory.resolve(modulePath).toAbsolutePath() 53 | 54 | val importedSymbols = 55 | importsCollector.referencedSymbols() 56 | .filter { // Include only imports from other files 57 | when { 58 | it.source.startsWith("./") -> { 59 | val absImportPath = absPath.resolve(it.source).toAbsolutePath().normalize() 60 | absImportPath != absPath 61 | } 62 | it.source.startsWith("!") -> { 63 | val absImportPath = directory.resolve(it.source.removePrefix("!")).toAbsolutePath().normalize() 64 | absImportPath != absPath 65 | } 66 | else -> true 67 | } 68 | } 69 | .toSet() 70 | 71 | // Pass local type name & imports to name allocator to resolve collisions 72 | val topLevelNameAllocator = NameAllocator() 73 | 74 | // Allocate unique set of top level members 75 | members 76 | .filterIsInstance() 77 | .map { it.name } 78 | .toSet() 79 | .forEach { 80 | topLevelNameAllocator.newName(it) 81 | } 82 | 83 | importedSymbols 84 | .forEach { 85 | topLevelNameAllocator.newName(it.value, it) 86 | } 87 | 88 | val renamedSymbols = 89 | topLevelNameAllocator.tagsToNames() 90 | .filterKeys { it is SymbolSpec } 91 | .mapKeys { it.key as SymbolSpec } 92 | .filter { it.key.value != it.value } 93 | 94 | // Second pass: write the code, taking advantage of the imports. 95 | CodeWriter(out, indent, renamedSymbols).use { 96 | emit(it, directory, importedSymbols) 97 | } 98 | } 99 | 100 | /** Writes this to `directory` as UTF-8 using the standard directory structure. */ 101 | @Throws(IOException::class) 102 | fun writeTo(directory: Path) { 103 | require(Files.notExists(directory) || Files.isDirectory(directory)) { 104 | "path $directory exists but is not a directory." 105 | } 106 | val outputPath = directory.resolve("$modulePath.ts") 107 | 108 | if (outputPath.parent != null) { 109 | Files.createDirectories(outputPath.parent) 110 | } 111 | 112 | OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8).use { writeTo(it, directory) } 113 | } 114 | 115 | /** Writes this to `directory` as UTF-8 using the standard directory structure. */ 116 | @Throws(IOException::class) 117 | fun writeTo(directory: File) = writeTo(directory.toPath()) 118 | 119 | private fun emit(codeWriter: CodeWriter, directory: Path = Paths.get("/"), imports: Set = emptySet()) { 120 | 121 | if (comment.isNotEmpty()) { 122 | codeWriter.emitComment(comment) 123 | } 124 | 125 | if (imports.isNotEmpty()) { 126 | emitImports(codeWriter, directory, imports) 127 | } 128 | 129 | members.filterNot { it is ModuleSpec || it is CodeBlock }.forEach { member -> 130 | codeWriter.emit("\n") 131 | when (member) { 132 | is ModuleSpec -> member.emit(codeWriter) 133 | is InterfaceSpec -> member.emit(codeWriter) 134 | is ClassSpec -> member.emit(codeWriter) 135 | is EnumSpec -> member.emit(codeWriter) 136 | is FunctionSpec -> member.emit(codeWriter, null, setOf(Modifier.PUBLIC)) 137 | is PropertySpec -> member.emit(codeWriter, setOf(Modifier.PUBLIC), asStatement = true) 138 | is TypeAliasSpec -> member.emit(codeWriter) 139 | is CodeBlock -> codeWriter.emitCode(member) 140 | else -> throw AssertionError() 141 | } 142 | } 143 | 144 | members.filterIsInstance().forEach { member -> 145 | codeWriter.emit("\n") 146 | member.emit(codeWriter) 147 | } 148 | 149 | members.filterIsInstance().forEach { member -> 150 | codeWriter.emit("\n") 151 | codeWriter.emitCode(member) 152 | } 153 | } 154 | 155 | private fun emitImports(codeWriter: CodeWriter, directory: Path, imports: Set) { 156 | 157 | val augmentImports = imports 158 | .filterIsInstance() 159 | .groupBy { it.augmented } 160 | 161 | val sideEffectImports = imports 162 | .filterIsInstance() 163 | .groupBy { it.source } 164 | 165 | if (imports.isNotEmpty()) { 166 | imports 167 | .filter { it !is SymbolSpec.Augmented || it !is SymbolSpec.SideEffect } 168 | .groupBy { FileModules.importPath(directory, modulePath, it.source) } 169 | .toSortedMap() 170 | .forEach { (sourceImportPath, imports) -> 171 | 172 | imports.filterIsInstance().forEach { import -> 173 | // Output star imports individually 174 | codeWriter.emitCode(CodeBlock.of("%[import * as %L from '%L';\n%]", import.value, sourceImportPath)) 175 | // Output related augments 176 | augmentImports[import.value]?.forEach { augment -> 177 | codeWriter.emitCode(CodeBlock.of("%[import '%L';\n%]", augment.source)) 178 | } 179 | } 180 | 181 | imports.filterIsInstance() 182 | .map { 183 | val renamed = codeWriter.renamedSymbols[it] ?: return@map it.value 184 | "${it.value} as $renamed" 185 | } 186 | .toSortedSet() 187 | .let { names -> 188 | if (names.isEmpty()) return@let 189 | // Output named imports as a group 190 | codeWriter 191 | .emitCode("import {") 192 | .indent() 193 | .emitCode(names.joinToString(", ")) 194 | .unindent() 195 | .emitCode(CodeBlock.of("} from '%L';\n", sourceImportPath)) 196 | // Output related augments 197 | names.forEach { name -> 198 | augmentImports[name]?.forEach { augment -> 199 | codeWriter.emitCode(CodeBlock.of("%[import '%L';\n%]", augment.source)) 200 | } 201 | } 202 | } 203 | } 204 | 205 | sideEffectImports.forEach { 206 | codeWriter.emitCode(CodeBlock.of("%[import %S;\n%]", it.key)) 207 | } 208 | 209 | codeWriter.emit("\n") 210 | } 211 | } 212 | 213 | fun isEmpty(): Boolean { 214 | return members.isEmpty() 215 | } 216 | 217 | fun isNotEmpty(): Boolean { 218 | return !isEmpty() 219 | } 220 | 221 | override fun equals(other: Any?): Boolean { 222 | if (this === other) return true 223 | if (other == null) return false 224 | if (javaClass != other.javaClass) return false 225 | return toString() == other.toString() 226 | } 227 | 228 | override fun hashCode() = toString().hashCode() 229 | 230 | override fun toString() = buildCodeString { emit(this, Paths.get("/")) } 231 | 232 | fun toBuilder(): Builder { 233 | val builder = Builder(modulePath) 234 | builder.comment.add(comment) 235 | builder.members.addAll(this.members) 236 | builder.indent = indent 237 | return builder 238 | } 239 | 240 | class Builder internal constructor( 241 | internal val modulePath: String 242 | ) : Taggable.Builder() { 243 | 244 | init { 245 | require(!modulePath.endsWith(".ts")) { "File's modulePath should not include typescript extension" } 246 | } 247 | 248 | internal val comment = CodeBlock.builder() 249 | internal var indent = " " 250 | internal val members = mutableListOf() 251 | 252 | private fun checkMemberModifiers(modifiers: Set) { 253 | requireNoneOf( 254 | modifiers, 255 | Modifier.PUBLIC, 256 | Modifier.PROTECTED, 257 | Modifier.PRIVATE, 258 | Modifier.READONLY, 259 | Modifier.GET, 260 | Modifier.SET, 261 | Modifier.STATIC, 262 | Modifier.CONST, 263 | Modifier.LET, 264 | Modifier.VAR 265 | ) 266 | } 267 | 268 | fun addComment(format: String, vararg args: Any) = apply { 269 | comment.add(format, *args) 270 | } 271 | 272 | fun addModule(moduleSpec: ModuleSpec) = apply { 273 | members += moduleSpec 274 | } 275 | 276 | fun addClass(classSpec: ClassSpec) = apply { 277 | checkMemberModifiers(classSpec.modifiers) 278 | members += classSpec 279 | } 280 | 281 | fun addInterface(ifaceSpec: InterfaceSpec) = apply { 282 | checkMemberModifiers(ifaceSpec.modifiers) 283 | members += ifaceSpec 284 | } 285 | 286 | fun addEnum(enumSpec: EnumSpec) = apply { 287 | checkMemberModifiers(enumSpec.modifiers) 288 | members += enumSpec 289 | } 290 | 291 | fun addType(typeSpec: AnyTypeSpec) = apply { 292 | when (typeSpec) { 293 | is EnumSpec -> addEnum(typeSpec) 294 | is InterfaceSpec -> addInterface(typeSpec) 295 | is ClassSpec -> addClass(typeSpec) 296 | is TypeAliasSpec -> addTypeAlias(typeSpec) 297 | } 298 | } 299 | 300 | fun addFunction(functionSpec: FunctionSpec) = apply { 301 | require(!functionSpec.isConstructor) { "cannot add ${functionSpec.name} to file $modulePath" } 302 | require(functionSpec.decorators.isEmpty()) { "decorators on module functions are not allowed" } 303 | checkMemberModifiers(functionSpec.modifiers) 304 | members += functionSpec 305 | } 306 | 307 | fun addProperty(propertySpec: PropertySpec) = apply { 308 | requireExactlyOneOf( 309 | propertySpec.modifiers, Modifier.CONST, 310 | Modifier.LET, 311 | Modifier.VAR 312 | ) 313 | require(propertySpec.decorators.isEmpty()) { "decorators on file properties are not allowed" } 314 | checkMemberModifiers(propertySpec.modifiers) 315 | members += propertySpec 316 | } 317 | 318 | fun addTypeAlias(typeAliasSpec: TypeAliasSpec) = apply { 319 | members += typeAliasSpec 320 | } 321 | 322 | fun addCode(codeBlock: CodeBlock) = apply { 323 | members += codeBlock 324 | } 325 | 326 | fun indent(indent: String) = apply { 327 | this.indent = indent 328 | } 329 | 330 | fun isEmpty(): Boolean { 331 | return members.isEmpty() 332 | } 333 | 334 | fun isNotEmpty(): Boolean { 335 | return !isEmpty() 336 | } 337 | 338 | fun build() = FileSpec(this) 339 | } 340 | 341 | companion object { 342 | 343 | @JvmStatic 344 | fun builder(modulePath: String) = Builder(modulePath) 345 | 346 | @JvmStatic 347 | fun get(moduleSpec: ModuleSpec, modulePath: String = moduleSpec.name.replace('.', '/').toLowerCase()): FileSpec = 348 | builder(modulePath) 349 | .apply { members.addAll(moduleSpec.members.toMutableList()) } 350 | .build() 351 | 352 | @JvmStatic 353 | fun get(typeSpec: AnyTypeSpec, modulePath: String = typeSpec.name.replace('.', '/').toLowerCase()): FileSpec = 354 | builder(modulePath) 355 | .addType(typeSpec) 356 | .build() 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/main/java/io/outfoxx/typescriptpoet/TypeName.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Outfox, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.outfoxx.typescriptpoet 18 | 19 | import io.outfoxx.typescriptpoet.TypeName.TypeVariable.Bound 20 | import io.outfoxx.typescriptpoet.TypeName.TypeVariable.Bound.Combiner 21 | import io.outfoxx.typescriptpoet.TypeName.TypeVariable.Bound.Combiner.UNION 22 | 23 | /** 24 | * Name of any possible type that can be referenced 25 | * 26 | */ 27 | sealed class TypeName { 28 | 29 | internal abstract fun emit(codeWriter: CodeWriter) 30 | 31 | data class Standard 32 | internal constructor( 33 | val base: SymbolSpec, 34 | ) : TypeName() { 35 | 36 | fun nested(name: String) = Standard(base.nested(name)) 37 | 38 | fun enclosingTypeName() = base.enclosing()?.let { Standard(it) } 39 | 40 | fun topLevelTypeName() = Standard(base.topLevel()) 41 | 42 | fun simpleName() = base.value.split(".").last() 43 | 44 | fun simpleNames(): List { 45 | val names = base.value.split(".") 46 | return names.subList(1, names.size) 47 | } 48 | 49 | val isTopLevelTypeName: Boolean get() = base.isTopLevelSymbol 50 | 51 | fun parameterized(vararg typeArgs: TypeName) = parameterizedType(this, *typeArgs) 52 | 53 | override fun emit(codeWriter: CodeWriter) { 54 | val fullPath = base.value.split(".") 55 | val relativePath = fullPath.dropCommon(codeWriter.currentScope()) 56 | val relativeName = 57 | if (relativePath.isNotEmpty()) { 58 | relativePath.joinToString(".") 59 | } else { 60 | fullPath.last() 61 | } 62 | 63 | if (relativeName == base.value) { 64 | codeWriter.emitSymbol(base) 65 | } else { 66 | codeWriter.emitSymbol(SymbolSpec.implicit(relativeName)) 67 | } 68 | } 69 | 70 | override fun toString() = buildCodeString { emit(this) } 71 | } 72 | 73 | data class Parameterized 74 | internal constructor( 75 | val rawType: Standard, 76 | val typeArgs: List 77 | ) : TypeName() { 78 | 79 | override fun emit(codeWriter: CodeWriter) { 80 | rawType.emit(codeWriter) 81 | codeWriter.emit("<") 82 | typeArgs.forEachIndexed { idx, typeArg -> 83 | typeArg.emit(codeWriter) 84 | 85 | if (idx < typeArgs.size - 1) { 86 | codeWriter.emit(", ") 87 | } 88 | } 89 | codeWriter.emit(">") 90 | } 91 | } 92 | 93 | data class TypeVariable 94 | internal constructor( 95 | val name: String, 96 | val bounds: List 97 | ) : TypeName() { 98 | 99 | data class Bound( 100 | val type: TypeName, 101 | val combiner: Combiner = UNION, 102 | val modifier: Modifier? 103 | ) { 104 | 105 | enum class Combiner( 106 | val symbol: String 107 | ) { 108 | 109 | UNION("|"), 110 | INTERSECT("&") 111 | } 112 | 113 | enum class Modifier( 114 | val keyword: String 115 | ) { 116 | 117 | KEY_OF("keyof") 118 | } 119 | } 120 | 121 | override fun emit(codeWriter: CodeWriter) { 122 | codeWriter.emit(name) 123 | } 124 | 125 | override fun toString() = buildCodeString { emit(this) } 126 | } 127 | 128 | data class Anonymous 129 | internal constructor( 130 | val members: List 131 | ) : TypeName() { 132 | 133 | data class Member( 134 | val name: String, 135 | val type: TypeName, 136 | val optional: Boolean 137 | ) 138 | 139 | override fun emit(codeWriter: CodeWriter) { 140 | codeWriter.emit("{ ") 141 | members.forEachIndexed { idx, member -> 142 | codeWriter.emit(member.name) 143 | if (member.optional) { 144 | codeWriter.emit("?") 145 | } 146 | codeWriter.emit(": ") 147 | member.type.emit(codeWriter) 148 | 149 | if (idx != members.size - 1) { 150 | codeWriter.emit(", ") 151 | } 152 | } 153 | codeWriter.emit(" }") 154 | } 155 | 156 | override fun toString() = buildCodeString { emit(this) } 157 | } 158 | 159 | data class Tuple 160 | internal constructor( 161 | val memberTypes: List 162 | ) : TypeName() { 163 | 164 | override fun emit(codeWriter: CodeWriter) { 165 | codeWriter.emit("[") 166 | memberTypes.forEachIndexed { idx, memberType -> 167 | memberType.emit(codeWriter) 168 | 169 | if (idx != memberTypes.size - 1) { 170 | codeWriter.emit(", ") 171 | } 172 | } 173 | codeWriter.emit("]") 174 | } 175 | 176 | override fun toString() = buildCodeString { emit(this) } 177 | } 178 | 179 | data class Intersection 180 | internal constructor( 181 | val typeRequirements: List 182 | ) : TypeName() { 183 | 184 | override fun emit(codeWriter: CodeWriter) { 185 | typeRequirements.forEachIndexed { idx, typeRequirement -> 186 | typeRequirement.emit(codeWriter) 187 | 188 | if (idx != typeRequirements.size - 1) { 189 | codeWriter.emit(" & ") 190 | } 191 | } 192 | } 193 | 194 | override fun toString() = buildCodeString { emit(this) } 195 | } 196 | 197 | data class Union 198 | internal constructor( 199 | val typeChoices: List 200 | ) : TypeName() { 201 | 202 | override fun emit(codeWriter: CodeWriter) { 203 | typeChoices.forEachIndexed { idx, typeChoice -> 204 | typeChoice.emit(codeWriter) 205 | 206 | if (idx != typeChoices.size - 1) { 207 | codeWriter.emit(" | ") 208 | } 209 | } 210 | } 211 | 212 | override fun toString() = buildCodeString { emit(this) } 213 | } 214 | 215 | data class Lambda 216 | internal constructor( 217 | private val parameters: Map = emptyMap(), 218 | private val returnType: TypeName = VOID 219 | ) : TypeName() { 220 | 221 | override fun emit(codeWriter: CodeWriter) { 222 | codeWriter.emit("(") 223 | parameters.entries.forEachIndexed { idx, entry -> 224 | val (name, type) = entry 225 | 226 | codeWriter.emit(name) 227 | codeWriter.emit(": ") 228 | type.emit(codeWriter) 229 | 230 | if (idx != parameters.size - 1) { 231 | codeWriter.emit(", ") 232 | } 233 | } 234 | codeWriter.emit(") => ") 235 | returnType.emit(codeWriter) 236 | } 237 | 238 | override fun toString() = buildCodeString { emit(this) } 239 | } 240 | 241 | companion object { 242 | 243 | val NULL = implicit("null") 244 | val UNDEFINED = implicit("undefined") 245 | val NEVER = implicit("never") 246 | val VOID = implicit("void") 247 | val ANY = implicit("any") 248 | 249 | val BOOLEAN = implicit("boolean") 250 | val NUMBER = implicit("number") 251 | val BIGINT = implicit("bigint") 252 | val STRING = implicit("string") 253 | val OBJECT = implicit("object") 254 | val SYMBOL = implicit("symbol") 255 | 256 | val BOOLEAN_CLASS = implicit("Boolean") 257 | val NUMBER_CLASS = implicit("Number") 258 | val BIGINT_CLASS = implicit("BigInt") 259 | val STRING_CLASS = implicit("String") 260 | val OBJECT_CLASS = implicit("Object") 261 | val SYMBOL_CLASS = implicit("Symbol") 262 | 263 | val FUNCTION = implicit("Function") 264 | val ERROR = implicit("Error") 265 | val REGEXP = implicit("RegExp") 266 | val MATH = implicit("Math") 267 | val DATE = implicit("Date") 268 | 269 | val SET = implicit("Set") 270 | val WEAK_SET = implicit("WeakSet") 271 | val MAP = implicit("Map") 272 | val WEAK_MAP = implicit("WeakMap") 273 | 274 | val ARRAY = implicit("Array") 275 | val INT8_ARRAY = implicit("Int8Array") 276 | val UINT8_ARRAY = implicit("Uint8Array") 277 | val UINT8_CLAMPED_ARRAY = implicit("Uint8ClampedArray") 278 | val INT16_ARRAY = implicit("Int16Array") 279 | val UINT16_ARRAY = implicit("Uint16Array") 280 | val INT32_ARRAY = implicit("Int32Array") 281 | val UINT32_ARRAY = implicit("Uint32Array") 282 | val FLOAT32_ARRAY = implicit("Float32Array") 283 | val FLOAT64_ARRAY = implicit("Float64Array") 284 | val BIG_INT64_ARRAY = implicit("BigInt64Array") 285 | val BIG_UINT64_ARRAY = implicit("BigUint64Array") 286 | 287 | val ARRAY_BUFFER = implicit("ArrayBuffer") 288 | val SHARED_ARRAY_BUFFER = implicit("SharedArrayBuffer") 289 | val ATOMICS = implicit("Atomics") 290 | val DATA_VIEW = implicit("DataView") 291 | val JSON = implicit("JSON") 292 | 293 | val PROMISE = implicit("Promise") 294 | val GENERATOR = implicit("Generator") 295 | 296 | /** 297 | * Any class/enum/primitive/etc type name 298 | * 299 | * @param exportedName The name of the symbol exported from module `from` 300 | * @param from The module the that `exportedName` is exported from 301 | */ 302 | @JvmStatic 303 | fun namedImport(exportedName: String, from: String): Standard { 304 | return Standard(SymbolSpec.importsName(exportedName, from)) 305 | } 306 | 307 | /** 308 | * Any class/enum/primitive/etc type name 309 | * 310 | * @param name Name for the implicit type, will be symbolized 311 | */ 312 | @JvmStatic 313 | fun implicit(name: String): Standard { 314 | return Standard(SymbolSpec.implicit(name)) 315 | } 316 | 317 | /** 318 | * Any class/enum/primitive/etc type name 319 | * 320 | * @param symbolSpec Serialized symbol spec 321 | */ 322 | @JvmStatic 323 | fun standard(symbolSpec: String): Standard { 324 | return Standard(SymbolSpec.from(symbolSpec)) 325 | } 326 | 327 | /** 328 | * Any class/enum/primitive/etc type name 329 | * 330 | * @param symbolSpec Symbol spec 331 | */ 332 | @JvmStatic 333 | fun standard(symbolSpec: SymbolSpec): Standard { 334 | return Standard(symbolSpec) 335 | } 336 | 337 | /** 338 | * Type name for the generic Array type 339 | * 340 | * @param elementType Element type of the array 341 | * @return Type name of the new array type 342 | */ 343 | @JvmStatic 344 | fun arrayType(elementType: TypeName): TypeName { 345 | return parameterizedType( 346 | ARRAY, elementType 347 | ) 348 | } 349 | 350 | /** 351 | * Type name for the generic Set type 352 | * 353 | * @param elementType Element type of the set 354 | * @return Type name of the new set type 355 | */ 356 | @JvmStatic 357 | fun setType(elementType: TypeName): TypeName { 358 | return parameterizedType( 359 | SET, elementType 360 | ) 361 | } 362 | 363 | /** 364 | * Type name for the generic Map type 365 | * 366 | * @param keyType Key type of the map 367 | * @param valueType Value type of the map 368 | * @return Type name of the new map type 369 | */ 370 | @JvmStatic 371 | fun mapType(keyType: TypeName, valueType: TypeName): TypeName { 372 | return parameterizedType( 373 | MAP, keyType, valueType 374 | ) 375 | } 376 | 377 | /** 378 | * Parameterized type that represents a concrete 379 | * usage of a generic type 380 | * 381 | * @param rawType Generic type to invoke with arguments 382 | * @param typeArgs Names of the provided type arguments 383 | * @return Type name of the new parameterized type 384 | */ 385 | @JvmStatic 386 | fun parameterizedType(rawType: Standard, vararg typeArgs: TypeName): Parameterized { 387 | return Parameterized(rawType, typeArgs.toList()) 388 | } 389 | 390 | /** 391 | * Type variable represents a single variable type in a 392 | * generic type or function. 393 | * 394 | * @param name The name of the variable as it will be used in the definition 395 | * @param bounds Bound constraints that will be required during instantiation 396 | * @return Type name of the new type variable 397 | */ 398 | @JvmStatic 399 | fun typeVariable(name: String, vararg bounds: Bound): TypeVariable { 400 | return TypeVariable(name, bounds.toList()) 401 | } 402 | 403 | /** 404 | * Factory for type variable bounds 405 | */ 406 | @JvmStatic 407 | fun bound( 408 | type: TypeName, 409 | combiner: Combiner = UNION, 410 | modifier: Bound.Modifier? = null 411 | ): Bound { 412 | return Bound(type, combiner, modifier) 413 | } 414 | 415 | /** 416 | * Factory for type variable bounds 417 | */ 418 | @JvmStatic 419 | fun unionBound(type: TypeName, keyOf: Boolean = false): Bound { 420 | return bound(type, UNION, if (keyOf) Bound.Modifier.KEY_OF else null) 421 | } 422 | 423 | /** 424 | * Factory for type variable bounds 425 | */ 426 | @JvmStatic 427 | fun intersectBound(type: TypeName, keyOf: Boolean = false): Bound { 428 | return bound(type, Combiner.INTERSECT, if (keyOf) Bound.Modifier.KEY_OF else null) 429 | } 430 | 431 | /** 432 | * Anonymous type name (e.g. `{ length: number, name: string }`) 433 | * 434 | * @param members Member pairs to define the anonymous type 435 | * @return Type name representing the anonymous type 436 | */ 437 | @JvmStatic 438 | fun anonymousType(members: List): Anonymous { 439 | return Anonymous(members) 440 | } 441 | 442 | /** 443 | * Anonymous type name (e.g. `{ length?: number, name: string }`) 444 | * 445 | * @param members Member pairs to define the anonymous type (all properties are required) 446 | * @return Type name representing the anonymous type 447 | */ 448 | @JvmStatic 449 | fun anonymousType(vararg members: Pair): Anonymous { 450 | return anonymousType(members.map { Anonymous.Member(it.first, it.second, false) }) 451 | } 452 | 453 | /** 454 | * Tuple type name (e.g. `[number, boolean, string]`} 455 | * 456 | * @param memberTypes Each argument represents a distinct member type 457 | * @return Type name representing the tuple type 458 | */ 459 | @JvmStatic 460 | fun tupleType(vararg memberTypes: TypeName): Tuple { 461 | return Tuple(memberTypes.toList()) 462 | } 463 | 464 | /** 465 | * Intersection type name (e.g. `Person & Serializable & Loggable`) 466 | * 467 | * @param typeRequirements Requirements of the intersection as individual type names 468 | * @return Type name representing the intersection type 469 | */ 470 | @JvmStatic 471 | fun intersectionType(vararg typeRequirements: TypeName): Intersection { 472 | return Intersection(typeRequirements.toList()) 473 | } 474 | 475 | /** 476 | * Union type name (e.g. `int | number | any`) 477 | * 478 | * @param typeChoices All possible choices allowed in the union 479 | * @return Type name representing the union type 480 | */ 481 | @JvmStatic 482 | fun unionType(vararg typeChoices: TypeName): Union { 483 | return Union(typeChoices.toList()) 484 | } 485 | 486 | /** Returns a lambda type with `returnType` and parameters of listed in `parameters`. */ 487 | @JvmStatic 488 | fun lambda(parameters: Map = emptyMap(), returnType: TypeName) = 489 | Lambda(parameters, returnType) 490 | 491 | /** Returns a lambda type with `returnType` and parameters of listed in `parameters`. */ 492 | @JvmStatic 493 | fun lambda(vararg parameters: Pair = emptyArray(), returnType: TypeName) = 494 | Lambda(parameters.toMap(), returnType) 495 | } 496 | } 497 | --------------------------------------------------------------------------------