diff --git a/core/src/main/java/io/smallrye/openapi/api/models/media/SchemaImpl.java b/core/src/main/java/io/smallrye/openapi/api/models/media/SchemaImpl.java index bd7ee085e..1bf71aafa 100644 --- a/core/src/main/java/io/smallrye/openapi/api/models/media/SchemaImpl.java +++ b/core/src/main/java/io/smallrye/openapi/api/models/media/SchemaImpl.java @@ -59,11 +59,16 @@ public class SchemaImpl extends ExtensibleImpl implements Schema, ModelI private Boolean nullable; private Boolean writeOnly; private Boolean deprecated; + private int modCount; public static boolean isNamed(Schema schema) { return schema instanceof SchemaImpl && ((SchemaImpl) schema).name != null; } + public static int getModCount(Schema schema) { + return schema instanceof SchemaImpl ? ((SchemaImpl) schema).modCount : -1; + } + public SchemaImpl(String name) { this.name = name; } @@ -76,6 +81,10 @@ public String getName() { return name; } + private void incrementModCount() { + modCount++; + } + /** * @see org.eclipse.microprofile.openapi.models.Reference#getRef() */ @@ -92,6 +101,7 @@ public void setRef(String ref) { if (ref != null && !ref.contains("/")) { ref = OpenApiConstants.REF_PREFIX_SCHEMA + ref; } + incrementModCount(); this.ref = ref; } @@ -108,6 +118,7 @@ public Discriminator getDiscriminator() { */ @Override public void setDiscriminator(Discriminator discriminator) { + incrementModCount(); this.discriminator = discriminator; } @@ -124,6 +135,7 @@ public String getTitle() { */ @Override public void setTitle(String title) { + incrementModCount(); this.title = title; } @@ -140,6 +152,7 @@ public Object getDefaultValue() { */ @Override public void setDefaultValue(Object defaultValue) { + incrementModCount(); this.defaultValue = defaultValue; } @@ -156,6 +169,7 @@ public List getEnumeration() { */ @Override public void setEnumeration(List enumeration) { + incrementModCount(); this.enumeration = ModelUtil.replace(enumeration, ArrayList::new); } @@ -164,6 +178,7 @@ public void setEnumeration(List enumeration) { */ @Override public Schema addEnumeration(Object enumeration) { + incrementModCount(); this.enumeration = ModelUtil.add(enumeration, this.enumeration, ArrayList::new); return this; } @@ -173,6 +188,7 @@ public Schema addEnumeration(Object enumeration) { */ @Override public void removeEnumeration(Object enumeration) { + incrementModCount(); ModelUtil.remove(this.enumeration, enumeration); } @@ -189,6 +205,7 @@ public BigDecimal getMultipleOf() { */ @Override public void setMultipleOf(BigDecimal multipleOf) { + incrementModCount(); this.multipleOf = multipleOf; } @@ -205,6 +222,7 @@ public BigDecimal getMaximum() { */ @Override public void setMaximum(BigDecimal maximum) { + incrementModCount(); this.maximum = maximum; } @@ -221,6 +239,7 @@ public Boolean getExclusiveMaximum() { */ @Override public void setExclusiveMaximum(Boolean exclusiveMaximum) { + incrementModCount(); this.exclusiveMaximum = exclusiveMaximum; } @@ -237,6 +256,7 @@ public BigDecimal getMinimum() { */ @Override public void setMinimum(BigDecimal minimum) { + incrementModCount(); this.minimum = minimum; } @@ -253,6 +273,7 @@ public Boolean getExclusiveMinimum() { */ @Override public void setExclusiveMinimum(Boolean exclusiveMinimum) { + incrementModCount(); this.exclusiveMinimum = exclusiveMinimum; } @@ -269,6 +290,7 @@ public Integer getMaxLength() { */ @Override public void setMaxLength(Integer maxLength) { + incrementModCount(); this.maxLength = maxLength; } @@ -285,6 +307,7 @@ public Integer getMinLength() { */ @Override public void setMinLength(Integer minLength) { + incrementModCount(); this.minLength = minLength; } @@ -301,6 +324,7 @@ public String getPattern() { */ @Override public void setPattern(String pattern) { + incrementModCount(); this.pattern = pattern; } @@ -317,6 +341,7 @@ public Integer getMaxItems() { */ @Override public void setMaxItems(Integer maxItems) { + incrementModCount(); this.maxItems = maxItems; } @@ -333,6 +358,7 @@ public Integer getMinItems() { */ @Override public void setMinItems(Integer minItems) { + incrementModCount(); this.minItems = minItems; } @@ -349,6 +375,7 @@ public Boolean getUniqueItems() { */ @Override public void setUniqueItems(Boolean uniqueItems) { + incrementModCount(); this.uniqueItems = uniqueItems; } @@ -365,6 +392,7 @@ public Integer getMaxProperties() { */ @Override public void setMaxProperties(Integer maxProperties) { + incrementModCount(); this.maxProperties = maxProperties; } @@ -381,6 +409,7 @@ public Integer getMinProperties() { */ @Override public void setMinProperties(Integer minProperties) { + incrementModCount(); this.minProperties = minProperties; } @@ -397,6 +426,7 @@ public List getRequired() { */ @Override public void setRequired(List required) { + incrementModCount(); this.required = ModelUtil.replace(required, ArrayList::new); } @@ -405,6 +435,7 @@ public void setRequired(List required) { */ @Override public Schema addRequired(String required) { + incrementModCount(); this.required = ModelUtil.add(required, this.required, ArrayList::new); return this; } @@ -414,6 +445,7 @@ public Schema addRequired(String required) { */ @Override public void removeRequired(String required) { + incrementModCount(); ModelUtil.remove(this.required, required); } @@ -430,6 +462,7 @@ public SchemaType getType() { */ @Override public void setType(SchemaType type) { + incrementModCount(); this.type = type; } @@ -446,6 +479,7 @@ public Schema getNot() { */ @Override public void setNot(Schema not) { + incrementModCount(); this.not = not; } @@ -462,6 +496,7 @@ public Map getProperties() { */ @Override public void setProperties(Map properties) { + incrementModCount(); this.properties = ModelUtil.replace(properties, LinkedHashMap::new); } @@ -471,6 +506,7 @@ public void setProperties(Map properties) { */ @Override public Schema addProperty(String key, Schema propertySchema) { + incrementModCount(); this.properties = ModelUtil.add(key, propertySchema, this.properties, LinkedHashMap::new); return this; } @@ -480,6 +516,7 @@ public Schema addProperty(String key, Schema propertySchema) { */ @Override public void removeProperty(String key) { + incrementModCount(); ModelUtil.remove(this.properties, key); } @@ -498,6 +535,7 @@ public Boolean getAdditionalPropertiesBoolean() { */ @Override public void setAdditionalPropertiesSchema(Schema additionalProperties) { + incrementModCount(); this.additionalPropertiesBoolean = null; this.additionalPropertiesSchema = additionalProperties; } @@ -507,6 +545,7 @@ public void setAdditionalPropertiesSchema(Schema additionalProperties) { */ @Override public void setAdditionalPropertiesBoolean(Boolean additionalProperties) { + incrementModCount(); this.additionalPropertiesSchema = null; this.additionalPropertiesBoolean = additionalProperties; } @@ -524,6 +563,7 @@ public String getDescription() { */ @Override public void setDescription(String description) { + incrementModCount(); this.description = description; } @@ -540,6 +580,7 @@ public String getFormat() { */ @Override public void setFormat(String format) { + incrementModCount(); this.format = format; } @@ -556,6 +597,7 @@ public Boolean getNullable() { */ @Override public void setNullable(Boolean nullable) { + incrementModCount(); this.nullable = nullable; } @@ -572,6 +614,7 @@ public Boolean getReadOnly() { */ @Override public void setReadOnly(Boolean readOnly) { + incrementModCount(); this.readOnly = readOnly; } @@ -588,6 +631,7 @@ public Boolean getWriteOnly() { */ @Override public void setWriteOnly(Boolean writeOnly) { + incrementModCount(); this.writeOnly = writeOnly; } @@ -604,6 +648,7 @@ public Object getExample() { */ @Override public void setExample(Object example) { + incrementModCount(); this.example = example; } @@ -620,6 +665,7 @@ public ExternalDocumentation getExternalDocs() { */ @Override public void setExternalDocs(ExternalDocumentation externalDocs) { + incrementModCount(); this.externalDocs = externalDocs; } @@ -636,6 +682,7 @@ public Boolean getDeprecated() { */ @Override public void setDeprecated(Boolean deprecated) { + incrementModCount(); this.deprecated = deprecated; } @@ -652,6 +699,7 @@ public XML getXml() { */ @Override public void setXml(XML xml) { + incrementModCount(); this.xml = xml; } @@ -668,6 +716,7 @@ public Schema getItems() { */ @Override public void setItems(Schema items) { + incrementModCount(); this.items = items; } @@ -684,6 +733,7 @@ public List getAllOf() { */ @Override public void setAllOf(List allOf) { + incrementModCount(); this.allOf = ModelUtil.replace(allOf, ArrayList::new); } @@ -692,6 +742,7 @@ public void setAllOf(List allOf) { */ @Override public Schema addAllOf(Schema allOf) { + incrementModCount(); this.allOf = ModelUtil.add(allOf, this.allOf, ArrayList::new); return this; } @@ -701,6 +752,7 @@ public Schema addAllOf(Schema allOf) { */ @Override public void removeAllOf(Schema allOf) { + incrementModCount(); ModelUtil.remove(this.allOf, allOf); } @@ -717,6 +769,7 @@ public List getAnyOf() { */ @Override public void setAnyOf(List anyOf) { + incrementModCount(); this.anyOf = ModelUtil.replace(anyOf, ArrayList::new); } @@ -725,6 +778,7 @@ public void setAnyOf(List anyOf) { */ @Override public Schema addAnyOf(Schema anyOf) { + incrementModCount(); this.anyOf = ModelUtil.add(anyOf, this.anyOf, ArrayList::new); return this; } @@ -734,6 +788,7 @@ public Schema addAnyOf(Schema anyOf) { */ @Override public void removeAnyOf(Schema anyOf) { + incrementModCount(); ModelUtil.remove(this.anyOf, anyOf); } @@ -750,6 +805,7 @@ public List getOneOf() { */ @Override public void setOneOf(List oneOf) { + incrementModCount(); this.oneOf = ModelUtil.replace(oneOf, ArrayList::new); } @@ -758,6 +814,7 @@ public void setOneOf(List oneOf) { */ @Override public Schema addOneOf(Schema oneOf) { + incrementModCount(); this.oneOf = ModelUtil.add(oneOf, this.oneOf, ArrayList::new); return this; } @@ -767,6 +824,7 @@ public Schema addOneOf(Schema oneOf) { */ @Override public void removeOneOf(Schema oneOf) { + incrementModCount(); ModelUtil.remove(this.oneOf, oneOf); } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java index b97a70d90..6ba98ecbf 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java @@ -24,8 +24,10 @@ */ public class ResourceParameters { - private static final Comparator PARAMETER_COMPARATOR = Comparator.comparing(Parameter::getIn) - .thenComparing(Parameter::getName); + public static final Comparator PARAMETER_COMPARATOR = Comparator + .comparing(Parameter::getRef, Comparator.nullsFirst(Comparator.naturalOrder())) + .thenComparing(Parameter::getIn, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Parameter::getName, Comparator.nullsLast(Comparator.naturalOrder())); static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\{(\\w[\\w\\.-]*)\\}"); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 5cbf6d786..9ddb31580 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -10,7 +10,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -156,17 +155,27 @@ protected static class ParameterContextKey { final String name; final In location; final Style style; + final String ref; public ParameterContextKey(String name, In location, Style style) { this.name = name; this.location = location; this.style = style; + this.ref = null; + } + + public ParameterContextKey(Parameter oaiParam) { + this.name = oaiParam.getName(); + this.location = oaiParam.getIn(); + this.style = styleOf(oaiParam); + this.ref = oaiParam.getRef(); } public ParameterContextKey(ParameterContext context) { this.name = context.name; this.location = context.location; this.style = context.style; + this.ref = context.oaiParam != null ? context.oaiParam.getRef() : null; } @Override @@ -174,21 +183,34 @@ public boolean equals(Object obj) { if (obj instanceof ParameterContextKey) { ParameterContextKey other = (ParameterContextKey) obj; + if (isNull() && other.isNull()) { + return this == other; + } + + if (ref != null) { + return ref.equals(other.ref); + } + return Objects.equals(this.name, other.name) && Objects.equals(this.location, other.location) && Objects.equals(this.style, other.style); } + return false; } @Override public int hashCode() { - return Objects.hash(name, location, style); + return isNull() ? super.hashCode() : Objects.hash(name, location, style, ref); } @Override public String toString() { - return "name: " + name + "; in: " + location; + return "name: " + name + "; in: " + location + "; style: " + style + "; ref: " + ref; + } + + public boolean isNull() { + return name == null && location == null && style == null && ref == null; } } @@ -232,7 +254,7 @@ protected void processOperationParameters(MethodInfo resourceMethod, ResourcePar // Phase II - Read method argument @Parameter and framework's annotations resourceMethod.annotations() .stream() - .filter(a -> !a.target().equals(resourceMethod)) + .filter(a -> !JandexUtil.equals(a.target(), resourceMethod)) .forEach(annotation -> { /* * This condition exists to support @Parameters wrapper annotation @@ -248,7 +270,7 @@ protected void processOperationParameters(MethodInfo resourceMethod, ResourcePar // Phase III - Read method @Parameter(s) annotations resourceMethod.annotations() .stream() - .filter(a -> a.target().equals(resourceMethod)) + .filter(a -> JandexUtil.equals(a.target(), resourceMethod)) .filter(a -> openApiParameterAnnotations.contains(a.name())) .forEach(this::readParameterAnnotation); @@ -391,8 +413,7 @@ protected List getParameters(MethodInfo resourceMethod) { .stream() .map(context -> this.mapParameter(resourceMethod, context)) .filter(Objects::nonNull) - .sorted(Comparator.comparing(Parameter::getIn) - .thenComparing(Parameter::getName)) + .sorted(ResourceParameters.PARAMETER_COMPARATOR) .collect(Collectors.toList()); return parameters.isEmpty() ? null : parameters; @@ -499,17 +520,7 @@ private Parameter mapParameter(MethodInfo resourceMethod, ParameterContext conte } if (param.getSchema() != null) { - //TODO: Test BV annotations on all target types - BeanValidationScanner.applyConstraints(context.target, - param.getSchema(), - param.getName(), - (target, name) -> { - if (param.getRequired() == null) { - param.setRequired(Boolean.TRUE); - } - }); - - setDefaultValue(param.getSchema(), context.defaultValue); + augmentParamSchema(param, context); } if (param.getRequired() == null && TypeUtil.isOptional(context.targetType)) { @@ -519,6 +530,61 @@ private Parameter mapParameter(MethodInfo resourceMethod, ParameterContext conte return param; } + /** + * Scan the AnnotationTarget associated with Parameter param for supported + * default value or Bean Validation annotations that may alter the schema. When + * found, update param's schema either directly or, when a ref is present, by + * creating a union of the local and reference schemas using allOf. + * + * @param param the Parameter containing a schema for update + * @param context ParameterContext associated with param + */ + void augmentParamSchema(Parameter param, ParameterContext context) { + Schema paramSchema = param.getSchema(); + String ref = paramSchema.getRef(); + Schema localSchema; + + if (ref != null) { + Type paramType = TypeUtil.unwrapType(context.targetType); + + /* + * Lookup the schema `type` from components (if available) or guess the type if + * the `ref` is not available. + */ + Schema refSchema = ModelUtil.getComponent(scannerContext.getOpenApi(), ref); + + if (refSchema != null) { + localSchema = new SchemaImpl().type(refSchema.getType()); + } else { + localSchema = new SchemaImpl().type(SchemaType + .valueOf(TypeUtil.getTypeAttributes(paramType).get(SchemaConstant.PROP_TYPE).toString().toUpperCase())); + } + } else { + localSchema = paramSchema; + } + + int modCount = SchemaImpl.getModCount(localSchema); + + BeanValidationScanner.applyConstraints(context.target, localSchema, param.getName(), + (target, name) -> { + if (param.getRequired() == null) { + param.setRequired(Boolean.TRUE); + } + }); + + setDefaultValue(localSchema, context.defaultValue); + + if (localOnlySchemaModified(paramSchema, localSchema, modCount)) { + // Add new `allOf` schema, erasing `type` derived above from the local schema + Schema allOf = new SchemaImpl().addAllOf(paramSchema).addAllOf(localSchema.type(null)); + param.setSchema(allOf); + } + } + + boolean localOnlySchemaModified(Schema paramSchema, Schema localSchema, int originalModCount) { + return localSchema != paramSchema && originalModCount != -1 && SchemaImpl.getModCount(localSchema) > originalModCount; + } + /** * Converts the collection of parameter annotations to properties set on the * given schema. @@ -587,7 +653,7 @@ private void setRequired(String name, Schema schema) { } private void setDefaultValue(Schema schema, Object defaultValue) { - if (schema.getDefaultValue() == null) { + if (schema.getDefaultValue() == null && defaultValue != null) { schema.setDefaultValue(defaultValue); } } @@ -661,10 +727,11 @@ protected void addEncoding(Map encodings, String paramName, An * @see org.eclipse.microprofile.openapi.annotations.parameters.Parameter#in() */ protected boolean isIgnoredParameter(Parameter parameter, AnnotationTarget resourceMethod) { - String paramName = parameter.getName(); - In paramIn = parameter.getIn(); + final String ref = parameter.getRef(); + final String paramName = parameter.getName(); + final In paramIn = parameter.getIn(); - if (paramIn == null) { + if (paramIn == null && ref == null) { /* * Per @Parameter JavaDoc, ignored when empty string (i.e., unspecified). * This may occur when @Parameter is specified without a matching framework @@ -680,7 +747,7 @@ protected boolean isIgnoredParameter(Parameter parameter, AnnotationTarget resou /* * Name is REQUIRED unless it is a reference. */ - if ((paramName == null || paramName.trim().isEmpty()) && parameter.getRef() == null) { + if ((paramName == null || paramName.trim().isEmpty()) && ref == null) { return true; } @@ -791,7 +858,7 @@ protected abstract void readAnnotatedType(AnnotationInstance annotation, Annotat * @param param the {@link Parameter} * @return the param's style, the default style defined based on in, or null if in is not defined. */ - protected Style styleOf(Parameter param) { + protected static Style styleOf(Parameter param) { if (param.getStyle() != null) { return param.getStyle(); } @@ -1064,6 +1131,21 @@ protected static Type getType(AnnotationTarget target) { return type; } + protected boolean isReadableParameterAnnotation(DotName name) { + return ParameterConstant.DOTNAME_PARAMETER.equals(name) && readerFunction != null; + } + + protected void readParameterAnnotation(AnnotationInstance annotation, boolean overriddenParametersOnly) { + Parameter oaiParam = readerFunction.apply(annotation); + + readParameter(new ParameterContextKey(oaiParam), + oaiParam, + null, + null, + annotation.target(), + overriddenParametersOnly); + } + /** * Merges MP-OAI {@link Parameter}s and framework-specific parameters for the same {@link In} and name, * and {@link Style}. When overriddenParametersOnly is true, new parameters not already known @@ -1162,49 +1244,82 @@ ParameterContext getParameterContext(ParameterContextKey key, AnnotationTarget t context = params .values() .stream() + // Only attempt to match parameters without `ref` specified + .filter(c -> c.oaiParam == null || c.oaiParam.getRef() == null) .filter(c -> haveSameAnnotatedTarget(c, target, key.name)) + .filter(c -> attributesCompatible(c.location, key.location)) + .filter(c -> attributesCompatible(c.style, key.style)) .findFirst() - .orElse(null); - } - - if (context == null) { - /* - * Allow a match on just the name and style if one of the Parameter.In values - * is not specified - */ - context = params.values().stream().filter(c -> { - if (c.location == null || key.location == null) { - return Objects.equals(c.name, key.name) && Objects.equals(c.style, key.style); - } - return false; - }).findFirst().orElse(null); - + .orElseGet(() -> params.values() + .stream() + .filter(c -> nameAndStyleMatch(c, key)) + .findFirst() + .orElse(null)); } return context; } boolean haveSameAnnotatedTarget(ParameterContext context, AnnotationTarget target, String name) { - /* - * Consider names to match if one is unspecified or they are equal. - */ - boolean nameMatches = (context.name == null || name == null || Objects.equals(context.name, name)); + boolean sameTarget = false; if (JandexUtil.equals(target, context.target)) { + if (target.kind() != Kind.METHOD) { + // Both annotations are set on the same method argument or field + sameTarget = true; + } else { + /* + * Method targets: consider names to match if *one* is unspecified or they are equal. + */ + if (name != null) { + sameTarget = context.name == null || Objects.equals(name, context.name); + } else if (context.name != null) { + sameTarget = true; + } + } + } else if (JandexUtil.equals(targetMethod(target), targetMethod(context.target))) { /* - * The name must match for annotations on a method because it is - * ambiguous which parameters is being referenced. + * One annotation is on the method and the other is on a parameter. Require that + * the names are both given and match. */ - return nameMatches || target.kind() != Kind.METHOD; + sameTarget = name != null && name.equals(context.name); } - if (nameMatches && target.kind() == Kind.METHOD && context.target.kind() == Kind.METHOD_PARAMETER) { - return context.target.asMethodParameter().method().equals(target); + return sameTarget; + } + + boolean attributesCompatible(Object a1, Object a2) { + return a1 == null || a2 == null || a1.equals(a2); + } + + boolean nameAndStyleMatch(ParameterContext context, ParameterContextKey key) { + /* + * Allow a match on just the name and style if one of the Parameter.In values + * is not specified + */ + if ((context.location == null || key.location == null) && key.name != null && key.style != null) { + return key.name.equals(context.name) && key.style.equals(context.style); } return false; } + /** + * Obtain the MethodInfo associated with the annotation target. + * + * @param target annotated item. Only method and method parameter targets. + * @return the MethodInfo associated with the target, or null if target is not a method or parameter. + */ + static MethodInfo targetMethod(AnnotationTarget target) { + if (target.kind() == Kind.METHOD) { + return target.asMethod(); + } + if (target.kind() == Kind.METHOD_PARAMETER) { + return target.asMethodParameter().method(); + } + return null; + } + /** * Scans for class level parameters on the given class argument and its ancestors. * diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java index 5bbab7622..68947631e 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java @@ -57,6 +57,15 @@ public enum RefType { RefType(String componentPath) { this.componentPath = componentPath; } + + public static RefType fromComponentPath(String path) { + for (RefType ref : values()) { + if (ref.componentPath.equals(path)) { + return ref; + } + } + return null; + } } /** @@ -510,7 +519,10 @@ public static Map inheritanceChain(IndexView index, ClassInfo k } public static boolean equals(AnnotationTarget t1, AnnotationTarget t2) { - if (t1.kind() != t2.kind()) { + if (t1 == t2) { + return true; + } + if (t1 == null || t2 == null || t1.kind() != t2.kind()) { return false; } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java index cd9a614f4..19bae2125 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java @@ -27,6 +27,7 @@ import io.smallrye.openapi.api.models.media.MediaTypeImpl; import io.smallrye.openapi.api.models.responses.APIResponsesImpl; import io.smallrye.openapi.api.util.MergeUtil; +import io.smallrye.openapi.runtime.util.JandexUtil.RefType; /** * Class with some convenience methods useful for working with the OAI data model. @@ -84,6 +85,68 @@ public static Components components(OpenAPI openApi) { return openApi.getComponents(); } + /** + * Gets the component type specified by the given `ref` from the OpenAPI model. + * + * @param the type of the component map's entry values + * @param openApi containing OpenAPI model + * @param ref reference path to retrieve + * @return the component referenced by ref if present, otherwise null + */ + @SuppressWarnings("unchecked") + public static T getComponent(OpenAPI openApi, String ref) { + final Components components = openApi.getComponents(); + Map types = null; + T value = null; + + if (components != null && ref.startsWith("#")) { + String[] split = ref.split("/"); + + if (split.length > 1) { + String name = split[split.length - 1]; + RefType type = RefType.fromComponentPath(split[split.length - 2]); + + if (type != null) { + switch (type) { + case CALLBACK: + types = (Map) components.getCallbacks(); + break; + case EXAMPLE: + types = (Map) components.getExamples(); + break; + case HEADER: + types = (Map) components.getHeaders(); + break; + case LINK: + types = (Map) components.getLinks(); + break; + case PARAMETER: + types = (Map) components.getParameters(); + break; + case REQUEST_BODY: + types = (Map) components.getRequestBodies(); + break; + case RESPONSE: + types = (Map) components.getResponses(); + break; + case SCHEMA: + types = (Map) components.getSchemas(); + break; + case SECURITY_SCHEME: + types = (Map) components.getSecuritySchemes(); + break; + default: + break; + } + } + + value = types != null ? types.get(name) : null; + } + } + + return value; + } + /** * Gets the {@link Paths} from the OAI model. If it doesn't exist, creates it. * diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java index 7287158ba..f418778ef 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java @@ -29,6 +29,7 @@ import io.smallrye.openapi.api.OpenApiConfigImpl; import io.smallrye.openapi.api.models.ComponentsImpl; import io.smallrye.openapi.api.models.OpenAPIImpl; +import io.smallrye.openapi.runtime.io.CurrentScannerInfo; import io.smallrye.openapi.runtime.io.Format; import io.smallrye.openapi.runtime.io.OpenApiSerializer; @@ -39,6 +40,7 @@ public class IndexScannerTestBase { @After public void removeSchemaRegistry() { SchemaRegistry.remove(); + CurrentScannerInfo.remove(); } protected static String pathOf(Class clazz) { diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java index 4b1294897..5cdc46d48 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsParameterProcessor.java @@ -158,15 +158,8 @@ protected void readAnnotatedType(AnnotationInstance annotation, AnnotationInstan boolean overriddenParametersOnly) { DotName name = annotation.name(); - if (ParameterConstant.DOTNAME_PARAMETER.equals(name) && readerFunction != null) { - Parameter oaiParam = readerFunction.apply(annotation); - - readParameter(new ParameterContextKey(oaiParam.getName(), oaiParam.getIn(), styleOf(oaiParam)), - oaiParam, - null, - null, - annotation.target(), - overriddenParametersOnly); + if (isReadableParameterAnnotation(name)) { + readParameterAnnotation(annotation, overriddenParametersOnly); } else { FrameworkParameter frameworkParam = JaxRsParameter.forName(name); diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java index e4e47c3f9..650542f06 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java @@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalLong; @@ -31,17 +30,21 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Application; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Request; +import org.eclipse.microprofile.openapi.annotations.Components; import org.eclipse.microprofile.openapi.annotations.ExternalDocumentation; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; import org.eclipse.microprofile.openapi.annotations.enums.ParameterStyle; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.info.Info; import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Encoding; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -71,7 +74,7 @@ public class ParameterScanTests extends IndexScannerTestBase { private static void test(String expectedResource, Class... classes) throws IOException, JSONException { Index index = indexOf(classes); - OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(dynamicConfig(new HashMap()), index); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); OpenAPI result = scanner.scan(); printToConsole(result); assertJsonEquals(expectedResource, result); @@ -261,6 +264,16 @@ public void testSerializedIndexParameterAnnotations() throws IOException, JSONEx assertJsonEquals("params.serialized-annotation-index.json", result); } + @Test + public void testParameterRefOnly() throws IOException, JSONException { + test("params.parameter-ref-property.json", ParameterRefTestApplication.class, ParameterRefTestResource.class); + } + + @Test + public void testDefaultEnumValue() throws IOException, JSONException { + test("params.local-schema-attributes.json", DefaultEnumTestResource.class, DefaultEnumTestResource.MyEnum.class); + } + /***************** Test models and resources below. ***********************/ public static class Widget { @@ -973,4 +986,53 @@ public static class GreetingMessage { String message; } } + + @OpenAPIDefinition(info = @Info(title = "title", version = "1"), components = @Components(parameters = { + @Parameter(name = "queryParam1", in = ParameterIn.QUERY), + @Parameter(name = "pathParam2", in = ParameterIn.PATH, description = "`pathParam2` with info in components") })) + static class ParameterRefTestApplication extends Application { + } + + @Path("/{pathParam1}/{pathParam2}") + static class ParameterRefTestResource { + @GET + @Path("one") + @Parameter(ref = "queryParam1") + String exampleEndpoint1(@PathParam("pathParam1") String pathParam1, + @PathParam("pathParam2") String pathParam2) { + return null; + } + + @GET + @Path("/two") + @Parameter(name = "pathParam1", style = ParameterStyle.SIMPLE) + @Parameter(ref = "pathParam2") + // `name` on `queryParam1` ref ignored + @Parameter(ref = "queryParam1", name = "queryParamOne") + @Parameter(in = ParameterIn.COOKIE, description = "Ignored: missing key attributes") + @Parameter(in = ParameterIn.DEFAULT, description = "Ignored: missing key attributes") + @Parameter(in = ParameterIn.HEADER, description = "Ignored: missing key attributes") + @Parameter(in = ParameterIn.PATH, description = "Ignored: missing key attributes") + String exampleEndpoint2(@PathParam("pathParam1") String pathParam1, + @Parameter(hidden = true) @PathParam("pathParam2") String pathParam2) { + return null; + } + } + + @Path("/enum-default-param") + static class DefaultEnumTestResource { + public enum MyEnum { + CAT, + DOG, + BAR, + FOO + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello(@QueryParam("q0") String q0, + @Parameter(required = true) @QueryParam(value = "q1") @Size(min = 3, max = 3) @DefaultValue("DOG") Optional q1) { + return "myEnum = " + q1; + } + } } diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.local-schema-attributes.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.local-schema-attributes.json new file mode 100644 index 000000000..b4006ee96 --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.local-schema-attributes.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.3", + "paths": { + "/enum-default-param": { + "get": { + "parameters": [ + { + "name": "q0", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "q1", + "in": "query", + "required": true, + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MyEnum" + }, + { + "default": "DOG", + "maxLength": 3, + "minLength": 3 + } + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MyEnum": { + "enum": [ + "BAR", + "CAT", + "DOG", + "FOO" + ], + "type": "string" + } + } + } +} diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json new file mode 100644 index 000000000..d06be7169 --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json @@ -0,0 +1,92 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "title", + "version": "1" + }, + "paths": { + "/{pathParam1}/{pathParam2}/one": { + "get": { + "parameters": [ + { + "name": "pathParam1", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pathParam2", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/queryParam1" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/{pathParam1}/{pathParam2}/two": { + "get": { + "parameters": [ + { + "name": "pathParam1", + "in": "path", + "style": "simple", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/pathParam2" + }, + { + "$ref": "#/components/parameters/queryParam1" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "queryParam1": { + "name": "queryParam1", + "in": "query" + }, + "pathParam2": { + "name": "pathParam2", + "in": "path", + "description": "`pathParam2` with info in components" + } + } + } +} diff --git a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java index e9bc9d971..81296925f 100644 --- a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java +++ b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringParameterProcessor.java @@ -89,15 +89,8 @@ protected void readAnnotatedType(AnnotationInstance annotation, AnnotationInstan boolean overriddenParametersOnly) { DotName name = annotation.name(); - if (ParameterConstant.DOTNAME_PARAMETER.equals(name) && readerFunction != null) { - Parameter oaiParam = readerFunction.apply(annotation); - - readParameter(new ParameterContextKey(oaiParam.getName(), oaiParam.getIn(), styleOf(oaiParam)), - oaiParam, - null, - null, - annotation.target(), - overriddenParametersOnly); + if (isReadableParameterAnnotation(name)) { + readParameterAnnotation(annotation, overriddenParametersOnly); } else { FrameworkParameter frameworkParam = SpringParameter.forName(name); diff --git a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java index 61939b03d..560ee8c51 100644 --- a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java +++ b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxParameterProcessor.java @@ -91,14 +91,8 @@ protected void readAnnotatedType(AnnotationInstance annotation, AnnotationInstan DotName name = annotation.name(); - if (ParameterConstant.DOTNAME_PARAMETER.equals(name) && readerFunction != null) { - Parameter oaiParam = readerFunction.apply(annotation); - readParameter(new ParameterContextKey(oaiParam.getName(), oaiParam.getIn(), styleOf(oaiParam)), - oaiParam, - null, - null, /* defaultValue */ - annotation.target(), - overriddenParametersOnly); + if (isReadableParameterAnnotation(name)) { + readParameterAnnotation(annotation, overriddenParametersOnly); } else if (VertxConstants.PARAM.equals(name) && annotation.value() != null) { String parameterName = annotation.value().asString(); String path = null;