Repository: bkbnio/kompendium
Pull request #207 (2022-02-23 10:43:58)

Fix (#194) to support Location classes located in other non-location classes (e.g. inside Object)
This PR fixes https://github.com/bkbnio/kompendium/issues/194

Implementation notes:
- Stops processing parent classes for path calculation if they are not `@Location` annotated


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfabc395a..738922301 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # Changelog
 
 ## Unreleased
+- Fixed support Location classes located in other non-location classes
 
 ### Added
 
diff --git a/kompendium-locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt b/kompendium-locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt
index 7b9e9cb96..d23f206ec 100644
--- a/kompendium-locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt
+++ b/kompendium-locations/src/main/kotlin/io/bkbn/kompendium/locations/LocationMethodParser.kt
@@ -50,7 +50,7 @@ object LocationMethodParser : IMethodParser {
   fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
     val locationAnnotation = this.findAnnotation<Location>()
     require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
-    val parent = this.java.declaringClass?.kotlin
+    val parent = this.java.declaringClass?.kotlin?.takeIf { it.hasAnnotation<Location>() }
     val newSuffix = locationAnnotation.path.plus(suffix)
     return when (parent) {
       null -> newSuffix
diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
index e69337b5c..a2727f9ab 100644
--- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
+++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt
@@ -69,7 +69,7 @@ private fun Application.mainModule() {
 }
 
 private object LocationsToC {
-  val testLocation = GetInfo<TestLocations, LocationModels.ExampleResponse>(
+  val testLocation = GetInfo<TestLocationsParent.TestLocations, LocationModels.ExampleResponse>(
     summary = "Shallow",
     description = "Ez Pz Lemon Squeezy",
     responseInfo = ResponseInfo(
@@ -77,7 +77,7 @@ private object LocationsToC {
       description = "Great!"
     )
   )
-  val testNestLocation = GetInfo<TestLocations.NestedTestLocations, LocationModels.ExampleResponse>(
+  val testNestLocation = GetInfo<TestLocationsParent.TestLocations.NestedTestLocations, LocationModels.ExampleResponse>(
     summary = "Nested",
     description = "Gettin' scary",
     responseInfo = ResponseInfo(
@@ -85,7 +85,7 @@ private object LocationsToC {
       description = "Hmmm"
     )
   )
-  val ohBoiUCrazy = GetInfo<TestLocations.NestedTestLocations.OhBoiUCrazy, LocationModels.ExampleResponse>(
+  val ohBoiUCrazy = GetInfo<TestLocationsParent.TestLocations.NestedTestLocations.OhBoiUCrazy, LocationModels.ExampleResponse>(
     summary = "Example Deeply Nested",
     description = "We deep now",
     responseInfo = ResponseInfo(
@@ -99,23 +99,28 @@ private object LocationsToC {
 // For more info make sure to read through the Ktor location docs
 // Additionally, make sure to note that even though we define the locations here, we still must annotate fields
 // with KompendiumParam!!!
-@Location("test/{name}")
-data class TestLocations(
-  @Param(ParamType.PATH)
-  val name: String,
-) {
-  @Location("/spaghetti")
-  data class NestedTestLocations(
-    @Param(ParamType.QUERY)
-    val idk: Int,
-    val parent: TestLocations
+object TestLocationsParent {
+
+  @Location("test/{name}")
+  data class TestLocations(
+    @Param(ParamType.PATH)
+    val name: String,
   ) {
-    @Location("/hehe/{madness}")
-    data class OhBoiUCrazy(
-      @Param(ParamType.PATH)
-      val madness: Boolean,
-      val parent: NestedTestLocations
-    )
+
+    @Location("/spaghetti")
+    data class NestedTestLocations(
+      @Param(ParamType.QUERY)
+      val idk: Int,
+      val parent: TestLocations
+    ) {
+
+      @Location("/hehe/{madness}")
+      data class OhBoiUCrazy(
+        @Param(ParamType.PATH)
+        val madness: Boolean,
+        val parent: NestedTestLocations
+      )
+    }
   }
 }
 


Test patch:
diff --git a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt
index cd415c420..c45ad217f 100644
--- a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt
+++ b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt
@@ -5,6 +5,7 @@ import io.bkbn.kompendium.locations.util.locationsConfig
 import io.bkbn.kompendium.locations.util.notarizedDeleteNestedLocation
 import io.bkbn.kompendium.locations.util.notarizedDeleteSimpleLocation
 import io.bkbn.kompendium.locations.util.notarizedGetNestedLocation
+import io.bkbn.kompendium.locations.util.notarizedGetNestedLocationFromNonLocationClass
 import io.bkbn.kompendium.locations.util.notarizedGetSimpleLocation
 import io.bkbn.kompendium.locations.util.notarizedPostNestedLocation
 import io.bkbn.kompendium.locations.util.notarizedPostSimpleLocation
@@ -70,5 +71,12 @@ class KompendiumLocationsTest : DescribeSpec({
         notarizedDeleteNestedLocation()
       }
     }
+    it("Can notarize a get with a nested location nested in a non-location class") {
+      // act
+      openApiTestAllSerializers("notarized_get_nested_location_from_non_location_class.json") {
+        locationsConfig()
+        notarizedGetNestedLocationFromNonLocationClass()
+      }
+    }
   }
 })
diff --git a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt
index e07412131..a0aee790d 100644
--- a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt
+++ b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModels.kt
@@ -10,5 +10,13 @@ data class SimpleLoc(@Param(ParamType.PATH) val name: String) {
   data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc)
 }
 
+object NonLocationObject {
+  @Location("/test/{name}")
+  data class SimpleLoc(@Param(ParamType.PATH) val name: String) {
+    @Location("/nesty")
+    data class NestedLoc(@Param(ParamType.QUERY) val isCool: Boolean, val parent: SimpleLoc)
+  }
+}
+
 data class SimpleResponse(val result: Boolean)
 data class SimpleRequest(val input: String)
diff --git a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt
index b7382f3b7..3345dc126 100644
--- a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt
+++ b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestModules.kt
@@ -95,3 +95,13 @@ fun Application.notarizedDeleteNestedLocation() {
     }
   }
 }
+
+fun Application.notarizedGetNestedLocationFromNonLocationClass() {
+  routing {
+    route("/test") {
+      notarizedGet(TestResponseInfo.testGetNestedLocationFromNonLocationClass) {
+        call.respondText { "hey dude ‼️ congratz on the get request" }
+      }
+    }
+  }
+}
diff --git a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt
index a01513fed..652583f2c 100644
--- a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt
+++ b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/util/TestResponseInfo.kt
@@ -1,6 +1,5 @@
 package io.bkbn.kompendium.locations.util
 
-import io.bkbn.kompendium.core.metadata.method.MethodInfo
 import io.bkbn.kompendium.core.metadata.RequestInfo
 import io.bkbn.kompendium.core.metadata.ResponseInfo
 import io.bkbn.kompendium.core.metadata.method.DeleteInfo
@@ -86,4 +85,13 @@ object TestResponseInfo {
       description = "A successful endeavor"
     )
   )
+
+  val testGetNestedLocationFromNonLocationClass = GetInfo<NonLocationObject.SimpleLoc.NestedLoc, SimpleResponse>(
+    summary = "Location Test",
+    description = "A cool test",
+    responseInfo = ResponseInfo(
+      status = HttpStatusCode.OK,
+      description = "A successful endeavor"
+    )
+  )
 }
diff --git a/kompendium-locations/src/test/resources/notarized_get_nested_location_from_non_location_class.json b/kompendium-locations/src/test/resources/notarized_get_nested_location_from_non_location_class.json
new file mode 100644
index 000000000..4a9adabcc
--- /dev/null
+++ b/kompendium-locations/src/test/resources/notarized_get_nested_location_from_non_location_class.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/test/{name}/nesty": {
+      "get": {
+        "tags": [],
+        "summary": "Location Test",
+        "description": "A cool test",
+        "parameters": [
+          {
+            "name": "isCool",
+            "in": "query",
+            "schema": {
+              "type": "boolean"
+            },
+            "required": true,
+            "deprecated": false
+          },
+          {
+            "name": "name",
+            "in": "path",
+            "schema": {
+              "type": "string"
+            },
+            "required": true,
+            "deprecated": false
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A successful endeavor",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SimpleResponse"
+                }
+              }
+            }
+          }
+        },
+        "deprecated": false
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "SimpleResponse": {
+        "properties": {
+          "result": {
+            "type": "boolean"
+          }
+        },
+        "required": [
+          "result"
+        ],
+        "type": "object"
+      }
+    },
+    "securitySchemes": {}
+  },
+  "security": [],
+  "tags": []
+}