diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2f5e91e..87e6ee3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,17 +1,35 @@ -name: Test +name: Build the Distribution -on: [push] +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up JDK - uses: actions/setup-java@v1.3.0 - with: - java-version: 11 - - name: Build with Gradle - run: ./gradlew test + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: gradle clean build + + - name: Dist + run: gradle cli:distZip + + - name: Upload binary + uses: actions/upload-artifact@v2 + with: + name: apifi-codegen + path: cli/build/distributions/apifi-codegen.zip diff --git a/README.md b/README.md index 0f08337..1d7cb07 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,6 @@ Spec driven HTTP APIs ![Test](https://github.com/medly/apifi/workflows/Test/badge.svg) -## Include in gradle project -1\. Add jitpack repository -``` -maven { - url 'https://jitpack.io' - content { - includeGroup "com.github.medly" - } -} -``` -2\. Add dependency -``` -configurations { - apifi -} -dependencies { - apifi 'com.github.medly:apifi:' -} -``` +With CLI -3\. Add gradle task to generate -``` -task generate(type: JavaExec) { - classpath = configurations.apifi - main = "apifi.AppKt" - args "${rootProject.rootDir}/" - args "${rootProject.rootDir}/" - args "" -} -``` \ No newline at end of file +`apifi-codegen -f codegen/petstore.yml -o src/generated/ -p "com.foo.petstore"` diff --git a/build.gradle b/build.gradle index fbbfbb6..f6bc6de 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,11 @@ plugins { id 'org.jetbrains.kotlin.jvm' version '1.3.72' apply false + id "com.kdabir.mksrc" version "1.1.0" } subprojects { apply plugin: 'org.jetbrains.kotlin.jvm' + apply plugin: "com.kdabir.mksrc" group = 'com.medly.apifi' @@ -46,4 +48,16 @@ project(":codegen") { implementation 'org.slf4j:slf4j-simple:1.7.30' implementation 'org.apache.commons:commons-text:1.8' } -} \ No newline at end of file +} + +project(":cli") { + apply plugin: 'application' + + applicationName = 'apifi-codegen' + mainClassName = "apifi.cli.ApifiCliKt" + + dependencies { + implementation project(":codegen") + implementation "com.github.ajalt:clikt:2.7.1" + } +} diff --git a/cli/src/main/kotlin/apifi/cli/ApifiCli.kt b/cli/src/main/kotlin/apifi/cli/ApifiCli.kt new file mode 100644 index 0000000..1e6a179 --- /dev/null +++ b/cli/src/main/kotlin/apifi/cli/ApifiCli.kt @@ -0,0 +1,44 @@ +package apifi.cli + +import apifi.codegen.CodegenIO +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.file + + +/** + * Entry point - The main function + */ +fun main(args: Array) = ApifiCli().main(args) + + +/** + * Implementation of CLI using Apifi API + * + * Can use env variable to pass in sensitive information + */ +class ApifiCli : CliktCommand( // command name is inferred as apifi-cli + name = "apifi-codegen", + help = """ + Generates Kotlin Source files for given Open API Spec file + """ +) { + private val inputFile by option("-f", "--input-file", help = "Open API Spec file that is entry point, this file can refer to another yaml files") + .file(canBeFile = true, canBeDir = false, mustExist = true) + .required() + + private val outDir by option("-o", "--out-dir", help = "Output dir where source should be generated") + .file(canBeFile = false, canBeDir = true, mustExist = true) + .required() + + private val basePackage by option("-p", "--base-package", help = "package name for generated classes") + .required() + + + override fun run() { + CodegenIO().execute(inputFile, outDir, basePackage) + } +} + + diff --git a/codegen/src/main/kotlin/apifi/App.kt b/codegen/src/main/kotlin/apifi/App.kt deleted file mode 100644 index 97493d7..0000000 --- a/codegen/src/main/kotlin/apifi/App.kt +++ /dev/null @@ -1,30 +0,0 @@ -package apifi - -import apifi.codegen.CodeGenerator -import apifi.parser.SpecFileParser -import com.squareup.kotlinpoet.FileSpec -import io.swagger.v3.parser.OpenAPIV3Parser -import java.io.File - -fun main(args: Array) { - val specFile = File(args[0]) - val outputDir = File(args[1]) - outputDir.mkdirs() - val basePackageName = args[2] - - if (!specFile.isFile || !outputDir.isDirectory) { - println("invalid spec file or output directory") - } else { - val openApi = OpenAPIV3Parser().read(specFile.absolutePath) - val spec = SpecFileParser.parse(openApi) - CodeGenerator.generate(spec, "$basePackageName.${specFile.nameWithoutExtension}").forEach { - writeToFile(it, outputDir) - } - } -} - -private fun writeToFile(fileSpec: FileSpec, outputDir: File) { - val outFileParentDir = File(outputDir, fileSpec.packageName.replace(".", File.separator)) - outFileParentDir.mkdirs() - File(outFileParentDir, fileSpec.name).writeText(fileSpec.toString()) -} \ No newline at end of file diff --git a/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt b/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt index 4e520d2..53c80a7 100644 --- a/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt +++ b/codegen/src/main/kotlin/apifi/codegen/CodeGenerator.kt @@ -5,7 +5,7 @@ import apifi.parser.models.Spec import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec -object CodeGenerator { +class CodeGenerator { fun generate(spec: Spec, basePackageName: String): List { val modelFiles: List = if (spec.models.isNotEmpty()) listOf(ModelFileBuilder.build(spec.models, basePackageName)) else emptyList() val modelMapping = modelFiles.flatMap { it.members.mapNotNull { m -> (m as TypeSpec).name }.map { name -> name to "${it.packageName}.$name" } }.toMap() diff --git a/codegen/src/main/kotlin/apifi/codegen/Codegen.kt b/codegen/src/main/kotlin/apifi/codegen/Codegen.kt new file mode 100644 index 0000000..ee32f58 --- /dev/null +++ b/codegen/src/main/kotlin/apifi/codegen/Codegen.kt @@ -0,0 +1,32 @@ +package apifi.codegen + +import apifi.parser.OpenApiSpecReader +import io.swagger.v3.parser.OpenAPIV3Parser +import java.io.File + + +class CodegenIO { + private val openAPIV3Parser = OpenAPIV3Parser() + private val openApiSpecReader = OpenApiSpecReader() + private val codeGenerator = CodeGenerator() + + fun execute(specFile: File, outputDir: File, basePackageName: String) { + outputDir.mkdirs() + + if (!specFile.isFile || !outputDir.isDirectory) { + throw Exception("invalid spec file or output directory") + } + + val openApi = openAPIV3Parser.read(specFile.absolutePath) + val spec = openApiSpecReader.read(openApi) + val fileSpecs = codeGenerator.generate(spec, "$basePackageName.${specFile.nameWithoutExtension}") + + fileSpecs.forEach { fileSpec -> + val outFileParentDir = File(outputDir, fileSpec.packageName.replace(".", File.separator)) + outFileParentDir.mkdirs() + File(outFileParentDir, fileSpec.name).writeText(fileSpec.toString()) + } + } + +} + diff --git a/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt b/codegen/src/main/kotlin/apifi/parser/OpenApiSpecReader.kt similarity index 92% rename from codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt rename to codegen/src/main/kotlin/apifi/parser/OpenApiSpecReader.kt index 73a7847..8a0a659 100644 --- a/codegen/src/main/kotlin/apifi/parser/SpecFileParser.kt +++ b/codegen/src/main/kotlin/apifi/parser/OpenApiSpecReader.kt @@ -5,8 +5,8 @@ import apifi.parser.models.SecurityDefinitionType import apifi.parser.models.Spec import io.swagger.v3.oas.models.OpenAPI -object SpecFileParser { - fun parse(openApiSpec: OpenAPI): Spec { +class OpenApiSpecReader { + fun read(openApiSpec: OpenAPI): Spec { val paths = PathsParser.parse(openApiSpec.paths) val models = (openApiSpec.components?.schemas?.flatMap { (name, schema) -> ModelParser.modelsFromSchema(name, schema) } ?: emptyList()) + paths.models @@ -14,4 +14,4 @@ object SpecFileParser { val securityRequirements = openApiSpec.security?.flatMap { it.keys } ?: emptyList() return Spec(paths.result, models, securityRequirements, securitySchemes) } -} \ No newline at end of file +} diff --git a/codegen/src/test/kotlin/apifi/codegen/CodeGeneratorTest.kt b/codegen/src/test/kotlin/apifi/codegen/CodeGeneratorTest.kt index 975ea33..852256f 100644 --- a/codegen/src/test/kotlin/apifi/codegen/CodeGeneratorTest.kt +++ b/codegen/src/test/kotlin/apifi/codegen/CodeGeneratorTest.kt @@ -1,6 +1,6 @@ package apifi.codegen -import apifi.parser.SpecFileParser +import apifi.parser.OpenApiSpecReader import io.kotest.matchers.collections.shouldNotContainInOrder import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec @@ -13,8 +13,8 @@ class CodeGeneratorTest : DescribeSpec({ it("should generate files according to spec") { val file = FileUtils.getFile("src", "test-res", "codegen", "all-paths.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val spec = SpecFileParser.parse(openApi) - val fileSpecs = CodeGenerator.generate(spec, "com.pets") + val spec = OpenApiSpecReader().read(openApi) + val fileSpecs = CodeGenerator().generate(spec, "com.pets") fileSpecs.size shouldBe 8 val expectedPetApi = FileUtils.getFile("src", "test-res", "codegen", "expected-pet-api").readText() @@ -27,4 +27,4 @@ class CodeGeneratorTest : DescribeSpec({ } } } -) \ No newline at end of file +) diff --git a/codegen/src/test/kotlin/apifi/parser/SpecFileParserTest.kt b/codegen/src/test/kotlin/apifi/parser/SpecFileParserTest.kt index d3fcb6c..754cffd 100644 --- a/codegen/src/test/kotlin/apifi/parser/SpecFileParserTest.kt +++ b/codegen/src/test/kotlin/apifi/parser/SpecFileParserTest.kt @@ -1,7 +1,7 @@ package apifi.parser -import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe import io.swagger.v3.parser.OpenAPIV3Parser import org.apache.commons.io.FileUtils @@ -12,17 +12,17 @@ class SpecFileParserTest : DescribeSpec({ it("should parse models & security requirements") { val file = FileUtils.getFile("src", "test-res", "parser", "securityschemes", "with-basic-auth-security-scheme.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val spec = SpecFileParser.parse(openApi) + val spec = OpenApiSpecReader().read(openApi) spec.securityRequirements shouldBe listOf("httpBasic") } it("should not throw errors when no security scheme present") { val file = FileUtils.getFile("src", "test-res", "parser", "params", "with-query-params.yml").readText().trimIndent() val openApi = OpenAPIV3Parser().readContents(file).openAPI - val spec = SpecFileParser.parse(openApi) + val spec = OpenApiSpecReader().read(openApi) spec.securityRequirements shouldBe emptyList() spec.models shouldBe emptyList() } } -}) \ No newline at end of file +}) diff --git a/settings.gradle b/settings.gradle index 81fc289..a5e51b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,4 @@ rootProject.name = 'apifi' -include 'codegen' \ No newline at end of file +include 'codegen' +include 'cli' +include 'runtime'