diff --git a/telemetry/jetbrains/.editorconfig b/telemetry/jetbrains/.editorconfig new file mode 100644 index 00000000..08885399 --- /dev/null +++ b/telemetry/jetbrains/.editorconfig @@ -0,0 +1,12 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 160 +tab_width = 4 +ij_continuation_indent_size = 4 + +[*.{kt,kts}] +ktlint_code_style = ktlint_official diff --git a/telemetry/jetbrains/build.gradle.kts b/telemetry/jetbrains/build.gradle.kts index 2c81b5ee..e74ef01f 100644 --- a/telemetry/jetbrains/build.gradle.kts +++ b/telemetry/jetbrains/build.gradle.kts @@ -11,6 +11,7 @@ plugins { `maven-publish` signing alias(libs.plugins.nexus.publishing) + alias(libs.plugins.jlleitschuh.ktlint) } java { @@ -146,9 +147,10 @@ publishing { gradlePlugin { setAutomatedPublishing(false) } signing { - if (project.hasProperty("signing.keyId") - && project.hasProperty("signing.password") - && project.hasProperty("signing.secretKeyRingFile")) { + if (project.hasProperty("signing.keyId") && + project.hasProperty("signing.password") && + project.hasProperty("signing.secretKeyRingFile") + ) { sign(publishing.publications["mavenJava"]) } } diff --git a/telemetry/jetbrains/gradle/libs.versions.toml b/telemetry/jetbrains/gradle/libs.versions.toml index 8cb2e3b2..88bac014 100644 --- a/telemetry/jetbrains/gradle/libs.versions.toml +++ b/telemetry/jetbrains/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] assertJ = "3.26.3" jackson = "2.17.2" +jlleitschuh-ktlint = "12.1.1" # deprecated; should move to json-skema jsonSchema = "1.14.4" junit4 = "4.13.2" @@ -16,5 +17,6 @@ json-schema = { module = "com.github.erosb:everit-json-schema", version.ref = "j junit4 = { module = "junit:junit", version.ref = "junit4" } [plugins] +jlleitschuh-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "jlleitschuh-ktlint" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } nexus-publishing = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus" } diff --git a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/ResourceLoader.kt b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/ResourceLoader.kt index 17377820..5105e2a9 100644 --- a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/ResourceLoader.kt +++ b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/ResourceLoader.kt @@ -4,13 +4,15 @@ package software.aws.toolkits.telemetry.generator object ResourceLoader { - private const val schemaPath = "/telemetrySchema.json" + private const val SCHEMA_PATH = "/telemetrySchema.json" + + val SCHEMA_FILE = this.javaClass.getResourceAsStream(SCHEMA_PATH).use { it.bufferedReader().readText() } - val SCHEMA_FILE = this.javaClass.getResourceAsStream(schemaPath).use { it.bufferedReader().readText() } // TODO add a manifest or something - val DEFINITIONS_FILES = listOf("/definitions/commonDefinitions.json").map { - this.javaClass.getResourceAsStream(it).use { - it.bufferedReader().readText() + val DEFINITIONS_FILES = + listOf("/definitions/commonDefinitions.json").map { + this.javaClass.getResourceAsStream(it).use { + it.bufferedReader().readText() + } } - } } diff --git a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryGenerator.kt b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryGenerator.kt index cccecfd8..b657b318 100644 --- a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryGenerator.kt +++ b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryGenerator.kt @@ -30,6 +30,7 @@ const val RESULT = "result" const val SUCCESS = "success" fun String.filterInvalidCharacters() = this.replace(".", "") + fun String.toTypeFormat() = this.filterInvalidCharacters().split("_", "-").joinToString(separator = "") { it.capitalize() } fun String.toArgumentFormat() = this.toTypeFormat().decapitalize() @@ -37,38 +38,41 @@ fun String.toArgumentFormat() = this.toTypeFormat().decapitalize() fun generateTelemetryFromFiles( inputFiles: List, defaultDefinitions: List = ResourceLoader.DEFINITIONS_FILES, - outputFolder: File + outputFolder: File, ) { val telemetry = TelemetryParser.parseFiles(defaultDefinitions, inputFiles) - val commonMetadataTypes = setOf( - "duration", - "httpStatusCode", - "reason", - "reasonDesc", - "requestId", - "requestServiceType", - "result", - "traceId", - "metricId", - "parentId" - ) - - val metricsWithCommonMetadata = telemetry.metrics.map { metric -> - // compute [MetadataSchema] for any common types not already declared in the schema - val commonMetadata = commonMetadataTypes.mapNotNull { commonMetadataType -> - val type = telemetry.types.firstOrNull { it.name == commonMetadataType } + val commonMetadataTypes = + setOf( + "duration", + "httpStatusCode", + "reason", + "reasonDesc", + "requestId", + "requestServiceType", + "result", + "traceId", + "metricId", + "parentId", + ) - if (type != null && metric.metadata.none { it.type.name == commonMetadataType }) { - MetadataSchema(type, false) - } else { - null - } + val metricsWithCommonMetadata = + telemetry.metrics.map { metric -> + // compute [MetadataSchema] for any common types not already declared in the schema + val commonMetadata = + commonMetadataTypes.mapNotNull { commonMetadataType -> + val type = telemetry.types.firstOrNull { it.name == commonMetadataType } + + if (type != null && metric.metadata.none { it.type.name == commonMetadataType }) { + MetadataSchema(type, false) + } else { + null + } + } + + // metadata will be sorted during generation + metric.copy(metadata = metric.metadata + commonMetadata) } - // metadata will be sorted during generation - metric.copy(metadata = metric.metadata + commonMetadata) - } - val indent = " ".repeat(4) // make sure the output directory exists before writing to it outputFolder.mkdirs() @@ -114,21 +118,23 @@ private fun FileSpec.Builder.generateTelemetryEnumTypes(items: List enum.addEnumConstant( - enumValue.toString().replace(Regex("\\s"), "_").toTypeFormat(), TypeSpec.anonymousClassBuilder() + enumValue.toString().replace(Regex("\\s"), "_").toTypeFormat(), + TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter("%S", enumValue.toString()) - .build() + .build(), ) } @@ -139,18 +145,19 @@ private fun FileSpec.Builder.generateTelemetryEnumType(item: TelemetryMetricType unknownType, TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter("%S", "unknown") - .build() + .build(), ).build() - val companion = TypeSpec.companionObjectBuilder() - .addFunction( - FunSpec.builder("from") - .returns(ClassName("", item.name.toTypeFormat())) - .addParameter("type", String::class) - .addStatement("return values().firstOrNull·{·it.value·==·type·} ?:·$unknownType") - .build() - ) - .build() + val companion = + TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("from") + .returns(ClassName("", item.name.toTypeFormat())) + .addParameter("type", String::class) + .addStatement("return values().firstOrNull·{·it.value·==·type·} ?:·$unknownType") + .build(), + ) + .build() enum.addType(companion) @@ -159,7 +166,10 @@ private fun FileSpec.Builder.generateTelemetryEnumType(item: TelemetryMetricType return this } -private fun FileSpec.Builder.generateNamespaces(namespace: String, metrics: List): FileSpec.Builder { +private fun FileSpec.Builder.generateNamespaces( + namespace: String, + metrics: List, +): FileSpec.Builder { val telemetryObject = TypeSpec.objectBuilder("${namespace.toTypeFormat()}Telemetry") metrics.sortedBy { it.name }.forEach { @@ -189,25 +199,38 @@ private fun TypeSpec.Builder.generateRecordFunctions(metric: MetricSchema) { addFunction(buildMetricMetadataOverloadFunction(functionName, metric)) } -fun buildProjectFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildProjectFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("project", PROJECT).build() return buildRecordFunction(metadataProvider, functionName, metric) } -fun buildConnectionSettingsFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildConnectionSettingsFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("connectionSettings", CONNECTION_SETTINGS).defaultValue("null").build() return buildRecordFunction(metadataProvider, functionName, metric) } -fun buildMetricMetadataFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildMetricMetadataFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("metadata", METRIC_METADATA).build() return buildRecordFunction(metadataProvider, functionName, metric) } -private fun buildRecordFunction(metadataProvider: ParameterSpec, functionName: String, metric: MetricSchema): FunSpec { +private fun buildRecordFunction( + metadataProvider: ParameterSpec, + functionName: String, + metric: MetricSchema, +): FunSpec { val functionParameters = mutableListOf() functionParameters.add(metadataProvider) functionParameters.addAll(buildMetricParameters(metric)) @@ -232,11 +255,12 @@ private fun buildMetricParameters(metric: MetricSchema): List { private fun MetadataSchema.metadataToParameter(): ParameterSpec { // Allowed values indicates an enum - val typeName = if (type.allowedValues != null) { - ClassName(PACKAGE_NAME, type.name.toTypeFormat()) - } else { - type.type.kotlinType() - }.copy(nullable = required == false) + val typeName = + if (type.allowedValues != null) { + ClassName(PACKAGE_NAME, type.name.toTypeFormat()) + } else { + type.type.kotlinType() + }.copy(nullable = required == false) val parameterSpec = ParameterSpec.builder(type.name.toArgumentFormat(), typeName) if (required == false) { @@ -245,7 +269,10 @@ private fun MetadataSchema.metadataToParameter(): ParameterSpec { return parameterSpec.build() } -private fun FunSpec.Builder.generateFunctionBody(metadataParameter: ParameterSpec, metric: MetricSchema): FunSpec.Builder { +private fun FunSpec.Builder.generateFunctionBody( + metadataParameter: ParameterSpec, + metric: MetricSchema, +): FunSpec.Builder { val metricUnit = MemberName("software.amazon.awssdk.services.toolkittelemetry.model", "Unit") beginControlFlow("%T.getInstance().record(${metadataParameter.name})", TELEMETRY_SERVICE) beginControlFlow("datum(%S)", metric.name) @@ -262,31 +289,45 @@ private fun FunSpec.Builder.generateFunctionBody(metadataParameter: ParameterSpe return this } -fun buildProjectOverloadFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildProjectOverloadFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("project", PROJECT).defaultValue("null").build() return buildResultOverloadFunction(metadataProvider, functionName, metric) } -fun buildConnectionSettingsOverloadFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildConnectionSettingsOverloadFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("connectionSettings", CONNECTION_SETTINGS).defaultValue("null").build() return buildResultOverloadFunction(metadataProvider, functionName, metric) } -fun buildMetricMetadataOverloadFunction(functionName: String, metric: MetricSchema): FunSpec { +fun buildMetricMetadataOverloadFunction( + functionName: String, + metric: MetricSchema, +): FunSpec { val metadataProvider = ParameterSpec.builder("metadata", METRIC_METADATA).build() return buildResultOverloadFunction(metadataProvider, functionName, metric) } -fun buildResultOverloadFunction(metadataProvider: ParameterSpec, functionName: String, metric: MetricSchema): FunSpec { - val overloadedParameters = buildMetricParameters(metric).map { - if (it.name == RESULT) { - ParameterSpec.builder(SUCCESS, BOOLEAN).build() - } else { - it +fun buildResultOverloadFunction( + metadataProvider: ParameterSpec, + functionName: String, + metric: MetricSchema, +): FunSpec { + val overloadedParameters = + buildMetricParameters(metric).map { + if (it.name == RESULT) { + ParameterSpec.builder(SUCCESS, BOOLEAN).build() + } else { + it + } } - } val functionParameters = mutableListOf() functionParameters.add(metadataProvider) @@ -299,14 +340,21 @@ fun buildResultOverloadFunction(metadataProvider: ParameterSpec, functionName: S .build() } -private fun FunSpec.Builder.generateResultOverloadFunctionBody(functionName: String, parameters: List): FunSpec.Builder { - addStatement("%L(%L)", functionName, parameters.joinToString { - if (it.name != SUCCESS) { - it.name - } else { - "if($SUCCESS) Result.Succeeded else Result.Failed" - } - }) +private fun FunSpec.Builder.generateResultOverloadFunctionBody( + functionName: String, + parameters: List, +): FunSpec.Builder { + addStatement( + "%L(%L)", + functionName, + parameters.joinToString { + if (it.name != SUCCESS) { + it.name + } else { + "if($SUCCESS) Result.Succeeded else Result.Failed" + } + }, + ) return this } @@ -317,11 +365,12 @@ private fun FunSpec.Builder.generateMetadataStatement(data: MetadataSchema): Fun } // If its type is already a string, we dont need to call toString - val setStatement = if (data.type.type.kotlinType() != STRING || data.type.allowedValues?.isNotEmpty() == true) { - "${data.type.name.toArgumentFormat()}.toString()" - } else { - data.type.name.toArgumentFormat() - } + val setStatement = + if (data.type.type.kotlinType() != STRING || data.type.allowedValues?.isNotEmpty() == true) { + "${data.type.name.toArgumentFormat()}.toString()" + } else { + data.type.name.toArgumentFormat() + } addStatement("metadata(%S, %L)", data.type.name.toArgumentFormat(), setStatement) if (data.required == false) { diff --git a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryParser.kt b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryParser.kt index b5775bbc..813bb543 100644 --- a/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryParser.kt +++ b/telemetry/jetbrains/src/main/kotlin/software/aws/toolkits/telemetry/generator/TelemetryParser.kt @@ -13,7 +13,9 @@ import org.json.JSONObject import org.json.JSONTokener import java.io.File -enum class MetricMetadataTypes(@get:JsonValue val type: String) { +enum class MetricMetadataTypes( + @get:JsonValue val type: String, +) { STRING("string") { override fun kotlinType(): TypeName = com.squareup.kotlinpoet.STRING }, @@ -25,7 +27,7 @@ enum class MetricMetadataTypes(@get:JsonValue val type: String) { }, BOOLEAN("boolean") { override fun kotlinType(): TypeName = com.squareup.kotlinpoet.BOOLEAN - }; + }, ; abstract fun kotlinType(): TypeName } @@ -34,20 +36,22 @@ data class TelemetryMetricType( val name: String, val description: String, val type: MetricMetadataTypes = MetricMetadataTypes.STRING, - val allowedValues: List? + val allowedValues: List?, ) -enum class MetricUnit(@get:JsonValue val type: String) { +enum class MetricUnit( + @get:JsonValue val type: String, +) { NONE("None"), MILLISECONDS("Milliseconds"), BYTES("Bytes"), PERCENT("Percent"), - COUNT("Count") + COUNT("Count"), } private data class MetadataDefinition( val type: String, - val required: Boolean? + val required: Boolean?, ) private data class MetricDefinition( @@ -55,17 +59,17 @@ private data class MetricDefinition( val description: String, val unit: MetricUnit?, val metadata: List = listOf(), - val passive: Boolean = false + val passive: Boolean = false, ) private data class TelemetryDefinition( val types: List = listOf(), - val metrics: List + val metrics: List, ) data class TelemetrySchema( val types: List, - val metrics: List + val metrics: List, ) data class MetricSchema( @@ -73,14 +77,14 @@ data class MetricSchema( val description: String, val unit: MetricUnit?, val metadata: List, - val passive: Boolean = false + val passive: Boolean = false, ) fun MetricSchema.namespace() = name.split('_').first().toLowerCase() data class MetadataSchema( val type: TelemetryMetricType, - val required: Boolean? + val required: Boolean?, ) object TelemetryParser { @@ -94,44 +98,49 @@ object TelemetryParser { */ fun parseFiles( defaultResourcesFiles: List, - paths: List = listOf() + paths: List = listOf(), ): TelemetrySchema { val files = paths.map { it.readText() } + defaultResourcesFiles val rawSchema = JSONObject(JSONTokener(ResourceLoader.SCHEMA_FILE)) val schema: Schema = SchemaLoader.load(rawSchema) files.forEach { validate(it, schema) } - val telemetryDefinition = files.map { parse(it) }.fold(TelemetryDefinition(listOf(), listOf())) { it, it2 -> - TelemetryDefinition( - it.types.plus(it2.types), - it.metrics.plus(it2.metrics) - ) - }.let { - TelemetryDefinition( - it.types.distinctBy{ t -> t.name}, - it.metrics.distinctBy { m -> m.name } - ) - } + val telemetryDefinition = + files.map { parse(it) }.fold(TelemetryDefinition(listOf(), listOf())) { it, it2 -> + TelemetryDefinition( + it.types.plus(it2.types), + it.metrics.plus(it2.metrics), + ) + }.let { + TelemetryDefinition( + it.types.distinctBy { t -> t.name }, + it.metrics.distinctBy { m -> m.name }, + ) + } val metadataTypes = telemetryDefinition.types.associateBy { it.name } - val resolvedMetricTypes = telemetryDefinition.metrics.map { - MetricSchema( - it.name, - it.description, - it.unit, - it.metadata.map { metadata -> MetadataSchema(metadataTypes.getValue(metadata.type), metadata.required) }, - it.passive - ) - } + val resolvedMetricTypes = + telemetryDefinition.metrics.map { + MetricSchema( + it.name, + it.description, + it.unit, + it.metadata.map { metadata -> MetadataSchema(metadataTypes.getValue(metadata.type), metadata.required) }, + it.passive, + ) + } return TelemetrySchema( telemetryDefinition.types, - resolvedMetricTypes + resolvedMetricTypes, ) } - private fun validate(fileContents: String, schema: Schema) { + private fun validate( + fileContents: String, + schema: Schema, + ) { try { schema.validate(JSONObject(fileContents)) } catch (e: Exception) { diff --git a/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/GeneratorTest.kt b/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/GeneratorTest.kt index 2395c363..8f2a7e9b 100644 --- a/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/GeneratorTest.kt +++ b/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/GeneratorTest.kt @@ -3,18 +3,18 @@ package software.aws.toolkits.telemetry.generator -import java.io.File -import java.nio.file.Files -import java.nio.file.Paths -import kotlin.io.path.isRegularFile -import kotlin.io.path.readText -import kotlin.io.path.toPath import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.rules.TestName +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.isRegularFile +import kotlin.io.path.readText +import kotlin.io.path.toPath class GeneratorTest { @JvmField @@ -65,12 +65,18 @@ class GeneratorTest { } // inputPath and outputPath must be in test resources - private fun testGenerator(definitionsFile: String? = null, definitionsOverrides: List = listOf()) { + private fun testGenerator( + definitionsFile: String? = null, + definitionsOverrides: List = listOf(), + ) { val methodName = testName.methodName generateTelemetryFromFiles( - defaultDefinitions = listOf(this.javaClass.getResourceAsStream(definitionsFile ?: "/$methodName/input.json").use { it.bufferedReader().readText() }), + defaultDefinitions = + listOf( + this.javaClass.getResourceAsStream(definitionsFile ?: "/$methodName/input.json").use { it.bufferedReader().readText() }, + ), inputFiles = definitionsOverrides.map { File(javaClass.getResource(it).toURI()) }, - outputFolder = folder.root + outputFolder = folder.root, ) val outputRoot = Paths.get(folder.root.absolutePath) diff --git a/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/ParserTest.kt b/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/ParserTest.kt index 3b7008b6..bcd30c63 100644 --- a/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/ParserTest.kt +++ b/telemetry/jetbrains/src/test/kotlin/software/aws/toolkits/telemetry/generator/ParserTest.kt @@ -13,17 +13,17 @@ class ParserTest { TelemetryParser.parseFiles( listOf( """ - { - "types": [ { - "name": "result", - "allowedValues": ["Succeeded", "Failed", "Cancelled"], + "types": [ + { + "name": "result", + "allowedValues": ["Succeeded", "Failed", "Cancelled"], + } + ], + "metrics": [] } - ], - "metrics": [] - } - """.trimIndent() - ) + """.trimIndent(), + ), ) }.hasMessageContaining("required key [description] not found") } @@ -41,21 +41,20 @@ class ParserTest { TelemetryParser.parseFiles( listOf( """ - { - "types": [ { - "name": "result", - "type": "type that does not exist", - "description": "abc" + "types": [ + { + "name": "result", + "type": "type that does not exist", + "description": "abc" + } + ], + "metrics": [] } - ], - "metrics": [] - } - """.trimIndent() - ) + """.trimIndent(), + ), ) }.hasMessageContaining("type that does not exist is not a valid enum value") - } @Test @@ -63,11 +62,11 @@ class ParserTest { TelemetryParser.parseFiles( listOf( """ - { - "metrics": [] - } - """.trimIndent() - ) + { + "metrics": [] + } + """.trimIndent(), + ), ) } }