Skip to content

Commit e1794c4

Browse files
Fix multiple parameters in path parsing (#135)
* use regex to extract parameters from path in OpenApi 2.0 generator * use regex to extract parameters from path in OpenApi 3.0 generator * fixed typo in multi param test in OpenAPI 2.0 generator added OpenAPI 3.0 multi param test * fix typo
1 parent d68049a commit e1794c4

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ import io.swagger.models.properties.PropertyBuilder
3434
import io.swagger.util.Json
3535
import java.util.Comparator.comparing
3636
import java.util.Comparator.comparingInt
37+
import java.util.regex.Pattern
3738

3839
object OpenApi20Generator {
3940

4041
private const val API_KEY_SECURITY_NAME = "api_key"
4142
private const val BASIC_SECURITY_NAME = "basic"
43+
private val PATH_PARAMETER_PATTERN = """\{([^/}]+)}""".toRegex()
4244
internal fun generate(
4345
resources: List<ResourceModel>,
4446
basePath: String? = null,
@@ -322,10 +324,9 @@ object OpenApi20Generator {
322324
}
323325

324326
private fun extractPathParameters(resourceModel: ResourceModel): List<PathParameter> {
325-
val pathParameterNames = resourceModel.request.path
326-
.split("/")
327-
.filter { it.startsWith("{") && it.endsWith("}") }
328-
.map { it.removePrefix("{").removeSuffix("}") }
327+
val pathParameterNames = PATH_PARAMETER_PATTERN.findAll(resourceModel.request.path)
328+
.map { matchResult -> matchResult.groupValues[1] }
329+
.toList()
329330

330331
return pathParameterNames.map { parameterName ->
331332
resourceModel.request.pathParameters

restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ class OpenApi20GeneratorTest {
106106
thenPathParametersExist(openapi, api)
107107
}
108108

109+
@Test
110+
fun `should extract multiple path parameters from path as fallback`() {
111+
val api = givenGetProductResourceModelWithMultiplePathParameters()
112+
113+
val openapi = whenOpenApiObjectGenerated(api)
114+
115+
thenMultiplePathParametersExist(openapi, api)
116+
}
117+
109118
@Test
110119
fun `should convert resource with head http method`() {
111120
val api = givenHeadResourceModel()
@@ -270,6 +279,13 @@ class OpenApi20GeneratorTest {
270279
then(pathParameter.name).isEqualTo("id")
271280
}
272281

282+
private fun thenMultiplePathParametersExist(openapi: Swagger, api: List<ResourceModel>) {
283+
val path = openapi.paths.getValue(api[0].request.path).get
284+
then(path.parameters).hasSize(2)
285+
then(path.parameters[0].name).isEqualTo("id")
286+
then(path.parameters[1].name).isEqualTo("subId")
287+
}
288+
273289
private fun thenApiSpecificationWithoutJsonSchemaButWithExamplesIsGenerated(
274290
openapi: Swagger,
275291
api: List<ResourceModel>
@@ -448,6 +464,18 @@ class OpenApi20GeneratorTest {
448464
)
449465
}
450466

467+
private fun givenGetProductResourceModelWithMultiplePathParameters(): List<ResourceModel> {
468+
return listOf(
469+
ResourceModel(
470+
operationId = "test",
471+
privateResource = false,
472+
deprecated = false,
473+
request = getProductRequestWithMultiplePathParameters(),
474+
response = getProduct200Response(getProductPayloadExample())
475+
)
476+
)
477+
}
478+
451479
private fun givenPostProductResourceModelWithoutFieldDescriptors(): List<ResourceModel> {
452480
return listOf(
453481
ResourceModel(
@@ -788,6 +816,22 @@ class OpenApi20GeneratorTest {
788816
)
789817
}
790818

819+
private fun getProductRequestWithMultiplePathParameters(): RequestModel {
820+
return RequestModel(
821+
path = "/products/{id}-{subId}",
822+
method = HTTPMethod.GET,
823+
contentType = "application/json",
824+
securityRequirements = SecurityRequirements(
825+
type = OAUTH2,
826+
requiredScopes = listOf("prod:r")
827+
),
828+
headers = listOf(),
829+
pathParameters = listOf(),
830+
requestParameters = listOf(),
831+
requestFields = listOf()
832+
)
833+
}
834+
791835
private fun postProductRequest(schema: Schema? = null, path: String = "/products"): RequestModel {
792836
return RequestModel(
793837
path = path,

restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import io.swagger.v3.oas.models.tags.Tag
4141

4242
object OpenApi3Generator {
4343

44+
private val PATH_PARAMETER_PATTERN = """\{([^/}]+)}""".toRegex()
4445
internal fun generate(
4546
resources: List<ResourceModel>,
4647
servers: List<Server>,
@@ -344,10 +345,9 @@ object OpenApi3Generator {
344345
}
345346

346347
private fun extractPathParameters(resourceModel: ResourceModel): List<PathParameter> {
347-
val pathParameterNames = resourceModel.request.path
348-
.split("/")
349-
.filter { it.startsWith("{") && it.endsWith("}") }
350-
.map { it.removePrefix("{").removeSuffix("}") }
348+
val pathParameterNames = PATH_PARAMETER_PATTERN.findAll(resourceModel.request.path)
349+
.map { matchResult -> matchResult.groupValues[1] }
350+
.toList()
351351

352352
return pathParameterNames.map { parameterName ->
353353
resourceModel.request.pathParameters

restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ class OpenApi3GeneratorTest {
177177
thenOpenApiSpecIsValid()
178178
}
179179

180+
@Test
181+
fun `should extract multiple parameters when seperated by delimiter`(){
182+
givenResourceWithMultiplePathParameters()
183+
184+
whenOpenApiObjectGenerated()
185+
186+
thenMultiplePathParametersExist()
187+
188+
}
189+
180190
fun thenGetProductByIdOperationIsValid() {
181191
val productGetByIdPath = "paths./products/{id}.get"
182192
then(openApiJsonPathContext.read<List<String>>("$productGetByIdPath.tags")).isNotNull()
@@ -206,6 +216,12 @@ class OpenApi3GeneratorTest {
206216
then(openApiJsonPathContext.read<List<List<String>>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly("prod:r")
207217
}
208218

219+
private fun thenMultiplePathParametersExist() {
220+
val productMultiparamPath = "paths./products/{id}-{subId}.get"
221+
then(openApiJsonPathContext.read<List<String>>("$productMultiparamPath.parameters[?(@.name == 'id')].in")).containsOnly("path")
222+
then(openApiJsonPathContext.read<List<String>>("$productMultiparamPath.parameters[?(@.name == 'subId')].in")).containsOnly("path")
223+
}
224+
209225
private fun thenServersPresent() {
210226
then(openApiJsonPathContext.read<List<String>>("servers[*].url")).contains("https://localhost/api")
211227
}
@@ -441,6 +457,21 @@ class OpenApi3GeneratorTest {
441457
)
442458
}
443459

460+
private fun givenResourceWithMultiplePathParameters() {
461+
resources = listOf(
462+
ResourceModel(
463+
operationId = "test",
464+
summary = "summary",
465+
description = "description",
466+
privateResource = false,
467+
deprecated = false,
468+
tags = setOf("tag1", "tag2"),
469+
request = getProductRequestWithMultiplePathParameters(),
470+
response = getProductResponse()
471+
)
472+
)
473+
}
474+
444475
private fun givenDeleteProductResourceModel() {
445476
resources = listOf(
446477
ResourceModel(
@@ -688,6 +719,34 @@ class OpenApi3GeneratorTest {
688719
)
689720
}
690721

722+
private fun getProductRequestWithMultiplePathParameters (getSecurityRequirement: () -> SecurityRequirements = ::getOAuth2SecurityRequirement): RequestModel {
723+
return RequestModel(
724+
path = "/products/{id}-{subId}",
725+
method = HTTPMethod.GET,
726+
securityRequirements = getSecurityRequirement(),
727+
headers = listOf(
728+
HeaderDescriptor(
729+
name = "Authorization",
730+
description = "Access token",
731+
type = "string",
732+
optional = false,
733+
example = "some example"
734+
)
735+
),
736+
pathParameters = emptyList(),
737+
requestParameters = listOf(
738+
ParameterDescriptor(
739+
name = "locale",
740+
description = "Localizes the product fields to the given locale code",
741+
type = "STRING",
742+
optional = true,
743+
ignored = false
744+
)
745+
),
746+
requestFields = listOf()
747+
)
748+
}
749+
691750
private fun getProductRequest(getSecurityRequirement: () -> SecurityRequirements = ::getOAuth2SecurityRequirement): RequestModel {
692751
return RequestModel(
693752
path = "/products/{id}",

0 commit comments

Comments
 (0)