Repository: bkbnio/kompendium (kotlin)

Issue:
Generate OpenAPI `operationId`
I am using https://orval.dev/ to generate `react-query` hooks based on the `openapi.json` file generated by Kompendium. Orval has support for using the OpenAPI "operationId" optional field to provide better names for the generated hooks.

From the Swagger docs:

```operationId
operationId is an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API.

/users:
  get:
    operationId: getUsers
    summary: Gets all users
    ...
  post:
    operationId: addUser
    summary: Adds a new user
    ...
/user/{id}:
  get:
    operationId: getUserById
    summary: Gets a user by user ID
    ...
Some common use cases for operationId are:

Some code generators use this value to name the corresponding methods in code.
Links can refer to the linked operations by operationId.
```

It would be awesome if one could specify `operationId` as part of `MethodInfo` so it is generated as part of the `openapi.json` spec, something like this:

```kotlin
            MethodInfo.GetInfo(
                summary = "Get details of a dataset",
                responseInfo = ResponseInfo(HttpStatusCode.OK, "Details for a given dataset"),
                operationId = "getDatasetDetailsById"
            )
```

Pull request description:
# Description

Adds support for the `operationId` field in the OpenAPI spec.

Closes #105 

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# How Has This Been Tested?

Added unit test for new parameter

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have updated the CHANGELOG and bumped the version
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9380c7cd08..eb3bd8d0b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## [1.10.0] - November 25th, 2021
+
+### Changed
+
+- Added `operationId` parameter to `MethodInfo`
+
 ## [1.9.2] - October 24th, 2021
 
 ### Changed
diff --git a/README.md b/README.md
index cdf3e92c43..ca377f8dfd 100644
--- a/README.md
+++ b/README.md
@@ -210,6 +210,7 @@ fun Application.mainModule() {
 val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
   summary = "Example Parameters",
   description = "A test for setting parameter examples",
+  operationId = "getExamples",
   responseInfo = ResponseInfo(
     status = 200,
     description = "nice",
diff --git a/gradle.properties b/gradle.properties
index 26cbe2ba79..c470ab58ac 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 # Kompendium
-project.version=1.9.2
+project.version=1.10.0
 # Kotlin
 kotlin.code.style=official
 # Gradle
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt
index 58a4a6a443..25612ee9cf 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/MethodParser.kt
@@ -49,6 +49,7 @@ object MethodParser {
   ) = OpenApiSpecPathItemOperation(
     summary = info.summary,
     description = info.description,
+    operationId = info.operationId,
     tags = info.tags,
     deprecated = info.deprecated,
     parameters = paramType.toParameterSpec(),
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/meta/MethodInfo.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/meta/MethodInfo.kt
index 93f2906a58..0b54858f19 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/meta/MethodInfo.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/models/meta/MethodInfo.kt
@@ -11,6 +11,7 @@ sealed class MethodInfo<TParam, TResp>(
   open val canThrow: Set<KClass<*>> = emptySet(),
   open val responseInfo: ResponseInfo<TResp>? = null,
   open val parameterExamples: Map<String, TParam> = emptyMap(),
+  open val operationId: String? = null
 ) {
 
   data class GetInfo<TParam, TResp>(
@@ -21,7 +22,8 @@ sealed class MethodInfo<TParam, TResp>(
     override val deprecated: Boolean = false,
     override val securitySchemes: Set<String> = emptySet(),
     override val canThrow: Set<KClass<*>> = emptySet(),
-    override val parameterExamples: Map<String, TParam> = emptyMap()
+    override val parameterExamples: Map<String, TParam> = emptyMap(),
+    override val operationId: String? = null
   ) : MethodInfo<TParam, TResp>(
     summary = summary,
     description = description,
@@ -30,7 +32,8 @@ sealed class MethodInfo<TParam, TResp>(
     securitySchemes = securitySchemes,
     canThrow = canThrow,
     responseInfo = responseInfo,
-    parameterExamples = parameterExamples
+    parameterExamples = parameterExamples,
+    operationId = operationId
   )
 
   data class PostInfo<TParam, TReq, TResp>(
@@ -42,7 +45,8 @@ sealed class MethodInfo<TParam, TResp>(
     override val deprecated: Boolean = false,
     override val securitySchemes: Set<String> = emptySet(),
     override val canThrow: Set<KClass<*>> = emptySet(),
-    override val parameterExamples: Map<String, TParam> = emptyMap()
+    override val parameterExamples: Map<String, TParam> = emptyMap(),
+    override val operationId: String? = null
   ) : MethodInfo<TParam, TResp>(
     summary = summary,
     description = description,
@@ -51,7 +55,8 @@ sealed class MethodInfo<TParam, TResp>(
     securitySchemes = securitySchemes,
     canThrow = canThrow,
     responseInfo = responseInfo,
-    parameterExamples = parameterExamples
+    parameterExamples = parameterExamples,
+    operationId = operationId
   )
 
   data class PutInfo<TParam, TReq, TResp>(
@@ -63,7 +68,8 @@ sealed class MethodInfo<TParam, TResp>(
     override val deprecated: Boolean = false,
     override val securitySchemes: Set<String> = emptySet(),
     override val canThrow: Set<KClass<*>> = emptySet(),
-    override val parameterExamples: Map<String, TParam> = emptyMap()
+    override val parameterExamples: Map<String, TParam> = emptyMap(),
+    override val operationId: String? = null
   ) : MethodInfo<TParam, TResp>(
     summary = summary,
     description = description,
@@ -71,7 +77,8 @@ sealed class MethodInfo<TParam, TResp>(
     deprecated = deprecated,
     securitySchemes = securitySchemes,
     canThrow = canThrow,
-    parameterExamples = parameterExamples
+    parameterExamples = parameterExamples,
+    operationId = operationId
   )
 
   data class DeleteInfo<TParam, TResp>(
@@ -82,7 +89,8 @@ sealed class MethodInfo<TParam, TResp>(
     override val deprecated: Boolean = false,
     override val securitySchemes: Set<String> = emptySet(),
     override val canThrow: Set<KClass<*>> = emptySet(),
-    override val parameterExamples: Map<String, TParam> = emptyMap()
+    override val parameterExamples: Map<String, TParam> = emptyMap(),
+    override val operationId: String? = null
   ) : MethodInfo<TParam, TResp>(
     summary = summary,
     description = description,
@@ -90,6 +98,7 @@ sealed class MethodInfo<TParam, TResp>(
     deprecated = deprecated,
     securitySchemes = securitySchemes,
     canThrow = canThrow,
-    parameterExamples = parameterExamples
+    parameterExamples = parameterExamples,
+    operationId = operationId
   )
 }


Test patch:
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
index 80b33f62bd..15aea7b87c 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/KompendiumTest.kt
@@ -46,6 +46,7 @@ import io.bkbn.kompendium.util.trailingSlash
 import io.bkbn.kompendium.util.undeclaredType
 import io.bkbn.kompendium.util.withDefaultParameter
 import io.bkbn.kompendium.util.withExamples
+import io.bkbn.kompendium.util.withOperationId
 
 internal class KompendiumTest {
 
@@ -139,7 +140,6 @@ internal class KompendiumTest {
     }
   }
 
-
   @Test
   fun `Notarized put does not interrupt the pipeline`() {
     withTestApplication({
@@ -363,6 +363,22 @@ internal class KompendiumTest {
     }
   }
 
+  @Test
+  fun `Can add operationId`() {
+    withTestApplication({
+      jacksonConfigModule()
+      docs()
+      withOperationId()
+    }) {
+      // do
+      val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
+
+      // expect
+      val expected = getFileSnapshot("notarized_get_with_operation_id.json").trim()
+      assertEquals(expected, json, "The received json spec should match the expected content")
+    }
+  }
+
   @Test
   fun `Generates the expected redoc`() {
     withTestApplication({
@@ -607,5 +623,4 @@ internal class KompendiumTest {
       redoc(oas)
     }
   }
-
 }
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
index 89d6d59d14..f54b4c48f3 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/util/TestModules.kt
@@ -265,6 +265,18 @@ fun Application.withDefaultParameter() {
   }
 }
 
+fun Application.withOperationId(){
+  routing {
+    route("/test") {
+      notarizedGet(
+        info = TestResponseInfo.testGetInfo.copy(operationId = "getTest")
+      ){
+        call.respond(HttpStatusCode.OK)
+      }
+    }
+  }
+}
+
 fun Application.nonRequiredParamsGet() {
   routing {
     route("/test/optional") {
diff --git a/kompendium-core/src/test/resources/notarized_get_with_operation_id.json b/kompendium-core/src/test/resources/notarized_get_with_operation_id.json
new file mode 100644
index 0000000000..416d7fe291
--- /dev/null
+++ b/kompendium-core/src/test/resources/notarized_get_with_operation_id.json
@@ -0,0 +1,88 @@
+{
+  "openapi" : "3.0.3",
+  "info" : {
+    "title" : "Test API",
+    "version" : "1.33.7",
+    "description" : "An amazing, fully-ish 😉 generated API spec",
+    "termsOfService" : "https://example.com",
+    "contact" : {
+      "name" : "Homer Simpson",
+      "url" : "https://gph.is/1NPUDiM",
+      "email" : "chunkylover53@aol.com"
+    },
+    "license" : {
+      "name" : "MIT",
+      "url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
+    }
+  },
+  "servers" : [ {
+    "url" : "https://myawesomeapi.com",
+    "description" : "Production instance of my API"
+  }, {
+    "url" : "https://staging.myawesomeapi.com",
+    "description" : "Where the fun stuff happens"
+  } ],
+  "paths" : {
+    "/test" : {
+      "get" : {
+        "tags" : [ ],
+        "summary" : "Another get test",
+        "description" : "testing more",
+        "operationId" : "getTest",
+        "parameters" : [ {
+          "name" : "a",
+          "in" : "path",
+          "schema" : {
+            "type" : "string"
+          },
+          "required" : true,
+          "deprecated" : false
+        }, {
+          "name" : "aa",
+          "in" : "query",
+          "schema" : {
+            "type" : "integer",
+            "format" : "int32"
+          },
+          "required" : true,
+          "deprecated" : false
+        } ],
+        "responses" : {
+          "200" : {
+            "description" : "A Successful Endeavor",
+            "content" : {
+              "application/json" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/TestResponse"
+                }
+              }
+            }
+          }
+        },
+        "deprecated" : false
+      }
+    }
+  },
+  "components" : {
+    "schemas" : {
+      "String" : {
+        "type" : "string"
+      },
+      "TestResponse" : {
+        "type" : "object",
+        "properties" : {
+          "c" : {
+            "$ref" : "#/components/schemas/String"
+          }
+        }
+      },
+      "Int" : {
+        "type" : "integer",
+        "format" : "int32"
+      }
+    },
+    "securitySchemes" : { }
+  },
+  "security" : [ ],
+  "tags" : [ ]
+}