diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java index 58dc070599..c7b8e4f23d 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OpenAPIParser.java @@ -19,13 +19,7 @@ import static io.jooby.internal.openapi.AsmUtils.toMap; import static java.util.Collections.singletonList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -517,24 +511,28 @@ private static void parameters( private static void parameter( ParserContext ctx, OperationExt operation, int index, Map parameterMap) { String name = (String) parameterMap.get("name"); - io.swagger.v3.oas.models.parameters.Parameter parameter; + io.swagger.v3.oas.models.parameters.Parameter existingParameter; if (name != null) { - parameter = + existingParameter = operation.getParameters().stream() .filter(it -> it.getName().equals(name)) .findFirst() .orElseGet(() -> operation.getParameter(index)); } else { - parameter = operation.getParameter(index); + existingParameter = operation.getParameter(index); } - if (parameter == null) { - throw new IllegalArgumentException( - "Parameter not found: " - + name - + " at position: " - + index - + " for annotation: " - + parameterMap); + io.swagger.v3.oas.models.parameters.Parameter parameter; + if (existingParameter == null) { + // Trust user, create a new parameter; + var parameterExt = new ParameterExt(); + arrayOrSchema(ctx, parameterMap).ifPresent(parameterExt::setSchema); + parameter = parameterExt; + var parameters = + new ArrayList<>(Optional.ofNullable(operation.getParameters()).orElse(List.of())); + parameters.add(index, parameter); + operation.setParameters(parameters); + } else { + parameter = existingParameter; } Optional.ofNullable(name).ifPresent(parameter::setName); stringValue(parameterMap, "description", parameter::setDescription); @@ -708,8 +706,11 @@ private static Optional toSchema( schemaType(ctx, annotation, "anyOf", schemaMap::put); schemaType(ctx, annotation, "oneOf", schemaMap::put); schemaType(ctx, annotation, "allOf", schemaMap::put); - if (schemaMap.isEmpty()) { + var type = (String) annotation.get("type"); + if (type != null) { + return Optional.ofNullable(ctx.schema(type)); + } return Optional.empty(); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java index 423e373607..811b9d5394 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ParserContext.java @@ -426,7 +426,14 @@ public Schema schema(String type) { if (schema != null) { return schema.toSchema(); } - return schema(javaType(type)); + return switch (type) { + // open-api types: + case "string" -> new StringSchema(); + case "boolean" -> new BooleanSchema(); + case "number" -> new NumberSchema(); + case "integer" -> new IntegerSchema(); + default -> schema(javaType(type)); + }; } public JavaType javaType(String type) { diff --git a/modules/jooby-openapi/src/test/java/issues/i3952/App3952.java b/modules/jooby-openapi/src/test/java/issues/i3952/App3952.java new file mode 100644 index 0000000000..b7807a6308 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3952/App3952.java @@ -0,0 +1,53 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3952; + +import io.jooby.Context; +import io.jooby.Jooby; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; + +public class App3952 extends Jooby { + { + path("/api", this::internalApiRoutes); + } + + private void internalApiRoutes() { + post("/getThing", App3952::getThing); + } + + @Operation( + summary = "Get a thing", + parameters = { + @Parameter( + name = "x-api-key", + description = "API Key", + in = ParameterIn.HEADER, + schema = @Schema(type = "string"), + required = true), + @Parameter( + name = "x-bool", + description = "Boolean key", + in = ParameterIn.HEADER, + schema = @Schema(type = "boolean")), + @Parameter( + name = "x-number", + description = "Number key", + in = ParameterIn.HEADER, + schema = @Schema(type = "number")), + @Parameter( + name = "x-integer", + description = "Int key", + in = ParameterIn.HEADER, + schema = @Schema(type = "integer"), + required = true) + }) + private static String getThing(Context context) { + return "works!"; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3952/Issue3952.java b/modules/jooby-openapi/src/test/java/issues/i3952/Issue3952.java new file mode 100644 index 0000000000..5a006ec9c8 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3952/Issue3952.java @@ -0,0 +1,63 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3952; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.jooby.openapi.OpenAPIResult; +import io.jooby.openapi.OpenAPITest; +import io.swagger.v3.oas.models.SpecVersion; + +public class Issue3952 { + @OpenAPITest(value = App3952.class, version = SpecVersion.V31) + public void shouldParseNestedPath(OpenAPIResult result) { + assertThat(result.toYaml()) + .isEqualToIgnoringNewLines( + """ + openapi: 3.1.0 + info: + title: 3952 API + description: 3952 API description + version: "1.0" + paths: + /api/getThing: + post: + summary: Get a thing + operationId: getThing + parameters: + - name: x-api-key + in: header + description: API Key + required: true + schema: + type: string + - name: x-bool + in: header + description: Boolean key + schema: + type: boolean + - name: x-number + in: header + description: Number key + schema: + type: number + - name: x-integer + in: header + description: Int key + required: true + schema: + type: integer + format: int32 + responses: + "200": + description: Success + content: + application/json: + schema: + type: string + """); + } +}