Skip to content

Commit

Permalink
fix: defer example parsing until end to await complete type information
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <michael@xlate.io>
  • Loading branch information
MikeEdgar committed Nov 14, 2024
1 parent 0fe0d03 commit 39f99e7
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 28 deletions.
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

0 comments on commit 39f99e7

Please sign in to comment.