Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: defer example parsing until end to await complete type information #2071

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.media.Content;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;

import io.smallrye.openapi.internal.models.media.SchemaSupport;
import io.smallrye.openapi.model.BaseModel;
import io.smallrye.openapi.runtime.io.IOContext;
import io.smallrye.openapi.runtime.io.IoLogging;
import io.smallrye.openapi.runtime.io.ModelIO;
Expand Down Expand Up @@ -55,9 +59,10 @@ private Content read(AnnotationInstance[] annotations, Direction direction) {

if (contentType == null) {
for (String mimeType : getDefaultMimeTypes(direction)) {
content.addMediaType(mimeType, mediaTypeModel);
content.addMediaType(mimeType, maybeParseExamples(mimeType, mediaTypeModel, true));
}
} else {
maybeParseExamples(contentType, mediaTypeModel, false);
content.addMediaType(contentType, mediaTypeModel);
}
}
Expand All @@ -78,6 +83,27 @@ private String[] getDefaultMimeTypes(Direction direction) {
}
}

private MediaType maybeParseExamples(String contentType, MediaType model, boolean copyOnWrite) {
boolean parseExamples;

if (contentType.toUpperCase().contains("JSON")) {
parseExamples = true;
} else {
Schema schema = model.getSchema();
parseExamples = schema != null && SchemaSupport.getNonNullType(schema) != SchemaType.STRING;
}

if (parseExamples && (model.getExample() != null || model.getExamples() != null)) {
if (copyOnWrite) {
model = BaseModel.deepCopy(model, MediaType.class);
}

scannerContext().getUnparsedExamples().add(model);
}

return model;
}

static <T> T nonNullOrElse(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import io.smallrye.openapi.runtime.io.MapModelIO;
import io.smallrye.openapi.runtime.io.Names;
import io.smallrye.openapi.runtime.io.ReferenceIO;
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;

public class ExampleObjectIO<V, A extends V, O extends V, AB, OB> extends MapModelIO<Example, V, A, O, AB, OB>
implements ReferenceIO<V, A, O, AB, OB> {
Expand All @@ -31,32 +30,9 @@ public Example read(AnnotationInstance annotation) {
example.setRef(ReferenceType.EXAMPLE.refValue(annotation));
example.setSummary(value(annotation, PROP_SUMMARY));
example.setDescription(value(annotation, PROP_DESCRIPTION));
example.setValue(parseValue(value(annotation, PROP_VALUE)));
example.setValue(value(annotation, PROP_VALUE));
example.setExternalValue(value(annotation, PROP_EXTERNAL_VALUE));
example.setExtensions(extensionIO().readExtensible(annotation));
return example;
}

/**
* Reads an example value and decode it, the parsing is delegated to the extensions
* currently set in the scanner. The default value will parse the string using Jackson.
*
* @param value the value to decode
* @return a Java representation of the 'value' property, either a String or parsed value
*
*/
public Object parseValue(String value) {
Object parsedValue = value;

if (value != null) {
for (AnnotationScannerExtension e : scannerContext().getExtensions()) {
parsedValue = e.parseValue(value);
if (parsedValue != null) {
break;
}
}
}

return parsedValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public MediaType read(AnnotationInstance annotation) {
IoLogging.logger.singleAnnotationAs("@Content", "MediaType");
MediaType mediaType = OASFactory.createMediaType();
mediaType.setExamples(exampleObjectIO().readMap(annotation.value(PROP_EXAMPLES)));
mediaType.setExample(exampleObjectIO().parseValue(value(annotation, PROP_EXAMPLE)));
mediaType.setExample(value(annotation, PROP_EXAMPLE));
mediaType.setSchema(schemaIO().read(annotation.value(PROP_SCHEMA)));
mediaType.setEncoding(encodingIO().readMap(annotation.value(PROP_ENCODING)));
mediaType.setExtensions(extensionIO().readExtensible(annotation));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ protected boolean setProperty(Parameter model, AnnotationValue value) {
model.setExamples(exampleObjectIO().readMap(value));
return true;
case PROP_EXAMPLE:
model.setExample(exampleObjectIO().parseValue(value.asString()));
model.setExample(value.asString());
return true;
default:
break;
Expand All @@ -96,6 +96,14 @@ public Parameter read(AnnotationInstance annotation) {
Extensions.setParamRef(parameter, annotation.target());
}

if (parameter.getExample() != null || parameter.getExamples() != null) {
/*
* Save the parameter for later parsing. The schema may not yet be set
* so we do not know if it should be parsed.
*/
scannerContext().getUnparsedExamples().add(parameter);
}

return parameter;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -21,6 +22,10 @@
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Paths;
import org.eclipse.microprofile.openapi.models.examples.Example;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.tags.Tag;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
Expand All @@ -32,13 +37,15 @@
import io.smallrye.openapi.api.SmallRyeOASConfig;
import io.smallrye.openapi.api.util.ClassLoaderUtil;
import io.smallrye.openapi.api.util.MergeUtil;
import io.smallrye.openapi.internal.models.media.SchemaSupport;
import io.smallrye.openapi.runtime.io.Names;
import io.smallrye.openapi.runtime.io.OpenAPIDefinitionIO;
import io.smallrye.openapi.runtime.io.schema.SchemaConstant;
import io.smallrye.openapi.runtime.io.schema.SchemaFactory;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScanner;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerFactory;
import io.smallrye.openapi.runtime.util.ModelUtil;

/**
* Scans a deployment (using the archive and jandex annotation index) for OpenAPI annotations.
Expand Down Expand Up @@ -234,6 +241,7 @@ public OpenAPI scan(Predicate<String> filter) {

sortTags(annotationScannerContext, openApi);
sortMaps(openApi);
parseExamples();

return openApi;
}
Expand Down Expand Up @@ -384,4 +392,55 @@ private <P, V> void sort(P parent, Function<P, Map<String, V>> source, BiConsume

target.accept(parent, sorted);
}

private void parseExamples() {
for (Object model : annotationScannerContext.getUnparsedExamples()) {
Map<String, Example> examples = null;

if (model instanceof Parameter) {
Parameter param = (Parameter) model;

if (ModelUtil.getParameterSchemas(param).stream()
.map(s -> ModelUtil.dereference(annotationScannerContext.getOpenApi(), s))
.anyMatch(s -> SchemaSupport.getNonNullType(s) != SchemaType.STRING)) {
parseExample(param.getExample(), param::setExample);
examples = param.getExamples();
}
} else if (model instanceof MediaType) {
MediaType mediaType = (MediaType) model;
parseExample(mediaType.getExample(), mediaType::setExample);
examples = mediaType.getExamples();
}

if (examples != null) {
for (Example example : examples.values()) {
parseExample(example.getValue(), example::setValue);
}
}
}
}

/**
* Reads an example value and decodes it, the parsing is delegated to the
* extensions currently set in the scanner. The default value will parse the
* string using Jackson.
*
* @param value
* the value to decode
* @param setter
* the consumer/setter lambda where the parsed value is to be
* placed when non-null
*/
private void parseExample(Object value, Consumer<Object> setter) {
if (value instanceof String) {
for (AnnotationScannerExtension e : annotationScannerContext.getExtensions()) {
value = e.parseValue((String) value);

if (value != null) {
setter.accept(value);
break;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class AnnotationScannerContext {
private final IOContext<?, ?, ?, ?, ?> ioContext;

private final Map<String, MethodInfo> operationIdMap = new HashMap<>();
private final List<Object> unparsedExamples = new ArrayList<>();

public AnnotationScannerContext(FilteredIndexView index,
ClassLoader classLoader,
Expand Down Expand Up @@ -226,4 +227,8 @@ public Annotations annotations() {
public <V, A extends V, O extends V, AB, OB> IOContext<V, A, O, AB, OB> io() { // NOSONAR - ignore wildcards in return type
return (IOContext<V, A, O, AB, OB>) ioContext;
}

public List<Object> getUnparsedExamples() {
return unparsedExamples;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.smallrye.openapi.runtime.scanner;

import java.io.IOException;
import java.time.LocalDateTime;

import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.json.JSONException;
import org.junit.jupiter.api.Test;

class ExampleParseTests extends IndexScannerTestBase {

@Test
void testParametersExamplesParsedWhenJson() throws IOException, JSONException {
@jakarta.ws.rs.Path("examples")
class ExampleResource {
@Parameter(example = "2019-05-02T09:51:25.265", examples = {
@ExampleObject(name = "datetime", value = "2099-12-31T23:59:59.999")
})
@jakarta.ws.rs.QueryParam("createDateTimeMax")
public LocalDateTime createDateTimeMax;

@Parameter(schema = @Schema(type = SchemaType.OBJECT), example = "{ \"key\": \"value\" }", examples = {
@ExampleObject(name = "json", value = "{ \"key\": \"value\" }")
})
@jakarta.ws.rs.QueryParam("encodedJson")
public Object encodedJson;

@Parameter(schema = @Schema(type = SchemaType.STRING), example = "\"key\": \"value\"", examples = {
@ExampleObject(name = "keyValuePair", value = "\"key\": \"value\"")
})
@jakarta.ws.rs.QueryParam("keyValuePair")
public Object keyValuePair;

@Parameter(example = "3.1415")
@jakarta.ws.rs.QueryParam("floatingpoint")
public Float floatingpoint;

@jakarta.ws.rs.GET
@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
public jakarta.ws.rs.core.Response getExamples() {
return null;
}
}

assertJsonEquals("examples.parameters.json", ExampleResource.class);
}

@Test
void testResponseContentExampleParsedWhenJson() throws IOException, JSONException {
@jakarta.ws.rs.Path("examples")
class ExampleResource {
final String exampleIds = "1200635948\n" +
"1201860613\n" +
"1201901219";

@jakarta.ws.rs.GET
@jakarta.ws.rs.Produces({
jakarta.ws.rs.core.MediaType.TEXT_PLAIN,
jakarta.ws.rs.core.MediaType.APPLICATION_JSON,
})
@APIResponse(responseCode = "200", content = {
@Content(mediaType = jakarta.ws.rs.core.MediaType.TEXT_PLAIN, example = exampleIds, examples = {
@ExampleObject(name = "identifiers", value = exampleIds),
}),
@Content(mediaType = jakarta.ws.rs.core.MediaType.APPLICATION_JSON, example = "[ \"123\", \"456\" ]", examples = {
@ExampleObject(name = "identifiers", value = "[ \"1200635948\", \"1201860613\", \"1201901219\" ]"),
})
})
@APIResponse(responseCode = "206", description = "Partial Content", content = {
@Content(example = "1", examples = {
@ExampleObject(name = "integer", value = "1"),
}),
})
public jakarta.ws.rs.core.Response getExamples() {
return null;
}
}

assertJsonEquals("examples.responses.json", ExampleResource.class);
}
}
Loading