Skip to content

Commit

Permalink
Merge pull request #1740 from ballerina-platform/example-annot
Browse files Browse the repository at this point in the history
Introduce OpenAPI example annotations
  • Loading branch information
TharmiganK authored Jul 25, 2024
2 parents d710e5b + 08c7ed7 commit 4d76108
Show file tree
Hide file tree
Showing 40 changed files with 2,382 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class Constants {
public static final String DOUBLE = "double";
public static final String HTTP_PAYLOAD = "http:Payload";
public static final String HTTP_SERVICE_CONFIG = "http:ServiceConfig";
public static final String OPENAPI = "openapi";
public static final String HTTP = "http";
public static final String BALLERINA = "ballerina";
public static final String EMPTY = "";
Expand Down Expand Up @@ -67,6 +68,14 @@ public final class Constants {
public static final String MEDIA_TYPE = "mediaType";
public static final String REGEX_INTERPOLATION_PATTERN = "^(?!.*\\$\\{).+$";
public static final String DATE_CONSTRAINT_ANNOTATION = "constraint:Date";
public static final String PATH = "path";
public static final String QUERY = "query";
public static final String HEADER = "header";
public static final String HTTP_PAYLOAD_TYPE = "HttpPayload";
public static final String HTTP_QUERY_TYPE = "HttpQuery";
public static final String HTTP_HEADER_TYPE = "HttpHeader";
public static final String EXAMPLE = "example";
public static final String REQUEST = "request";

private Constants() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import io.ballerina.openapi.service.mapper.constraint.ConstraintMapper;
import io.ballerina.openapi.service.mapper.constraint.ConstraintMapperImpl;
import io.ballerina.openapi.service.mapper.diagnostic.OpenAPIMapperDiagnostic;
import io.ballerina.openapi.service.mapper.example.OpenAPIExampleMapper;
import io.ballerina.openapi.service.mapper.example.OpenAPIExampleMapperImpl;
import io.ballerina.openapi.service.mapper.hateoas.HateoasMapper;
import io.ballerina.openapi.service.mapper.hateoas.HateoasMapperImpl;
import io.ballerina.openapi.service.mapper.interceptor.model.RequestParameterInfo;
Expand Down Expand Up @@ -84,10 +86,12 @@ public class ServiceMapperFactory {
private final HateoasMapper hateoasMapper;
private final InterceptorPipeline interceptorPipeline;
private final MetaInfoMapper metaInfoMapper;
private final OpenAPIExampleMapper exampleMapper;

public ServiceMapperFactory(OpenAPI openAPI, SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor,
List<OpenAPIMapperDiagnostic> diagnostics, ServiceDeclarationNode serviceDefinition) {
this.additionalData = new AdditionalData(semanticModel, moduleMemberVisitor, diagnostics);
List<OpenAPIMapperDiagnostic> diagnostics, ServiceDeclarationNode serviceDefinition,
boolean enableBallerinaExt) {
this.additionalData = new AdditionalData(semanticModel, moduleMemberVisitor, diagnostics, enableBallerinaExt);
this.treatNilableAsOptional = isTreatNilableAsOptionalParameter(serviceDefinition);
this.openAPI = openAPI;
this.interceptorPipeline = getInterceptorPipeline(serviceDefinition, additionalData);
Expand All @@ -96,6 +100,12 @@ public ServiceMapperFactory(OpenAPI openAPI, SemanticModel semanticModel, Module
this.constraintMapper = new ConstraintMapperImpl(openAPI, moduleMemberVisitor, diagnostics);
this.hateoasMapper = new HateoasMapperImpl();
this.metaInfoMapper = new MetaInfoMapperImpl();
this.exampleMapper = new OpenAPIExampleMapperImpl(openAPI, serviceDefinition, additionalData);
}

public ServiceMapperFactory(OpenAPI openAPI, SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor,
List<OpenAPIMapperDiagnostic> diagnostics, ServiceDeclarationNode serviceDefinition) {
this(openAPI, semanticModel, moduleMemberVisitor, diagnostics, serviceDefinition, false);
}

public ServersMapper getServersMapper(Set<ListenerDeclarationNode> endpoints, ServiceDeclarationNode serviceNode) {
Expand Down Expand Up @@ -170,6 +180,10 @@ public MetaInfoMapper getMetaInfoMapper() {
return metaInfoMapper;
}

public OpenAPIExampleMapper getExampleMapper() {
return exampleMapper;
}

private Components getComponents(OpenAPI openAPI) {
Components components = openAPI.getComponents();
if (Objects.isNull(components)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
import io.ballerina.openapi.service.mapper.diagnostic.DiagnosticMessages;
import io.ballerina.openapi.service.mapper.diagnostic.ExceptionDiagnostic;
import io.ballerina.openapi.service.mapper.diagnostic.OpenAPIMapperDiagnostic;
import io.ballerina.openapi.service.mapper.example.OpenAPIExampleMapper;
import io.ballerina.openapi.service.mapper.hateoas.HateoasMapper;
import io.ballerina.openapi.service.mapper.metainfo.MetaInfoMapper;
import io.ballerina.openapi.service.mapper.model.ModuleMemberVisitor;
import io.ballerina.openapi.service.mapper.model.OASGenerationMetaInfo;
import io.ballerina.openapi.service.mapper.model.OASResult;
import io.ballerina.openapi.service.mapper.type.extension.BallerinaTypeExtensioner;
import io.ballerina.projects.Module;
import io.ballerina.projects.Project;
import io.swagger.v3.oas.models.OpenAPI;
Expand Down Expand Up @@ -194,7 +196,7 @@ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo)
List<OpenAPIMapperDiagnostic> diagnostics = new ArrayList<>();
if (openapi.getPaths() == null) {
ServiceMapperFactory serviceMapperFactory = new ServiceMapperFactory(openapi, semanticModel,
moduleMemberVisitor, diagnostics, serviceDefinition);
moduleMemberVisitor, diagnostics, serviceDefinition, true);

ServersMapper serversMapperImpl = serviceMapperFactory.getServersMapper(listeners, serviceDefinition);
serversMapperImpl.setServers();
Expand All @@ -211,9 +213,14 @@ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo)
metaInfoMapper.setResourceMetaData(serviceDefinition, openapi, ballerinaFilePath);
diagnostics.addAll(metaInfoMapper.getDiagnostics());

OpenAPIExampleMapper exampleMapper = serviceMapperFactory.getExampleMapper();
exampleMapper.setExamples();

if (openapi.getComponents().getSchemas().isEmpty()) {
openapi.setComponents(null);
}
// Remove ballerina extensions
BallerinaTypeExtensioner.removeExtensions(openapi);
return new OASResult(openapi, diagnostics);
} else {
return new OASResult(openapi, oasResult.getDiagnostics());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,15 @@ public enum DiagnosticMessages {
OAS_CONVERTOR_130("OAS_CONVERTOR_130", "Generated OpenAPI definition does not contain the example" +
" details since the JSON value provided for the example has parser errors", DiagnosticSeverity.WARNING),
OAS_CONVERTOR_131("OAS_CONVERTOR_131", "`%s` example can not have a blank path",
DiagnosticSeverity.WARNING);
DiagnosticSeverity.WARNING),
OAS_CONVERTOR_132("OAS_CONVERTOR_132", "Generated OpenAPI definition does not contain the example " +
"for type: `%s`, since the example value has parser errors", DiagnosticSeverity.WARNING),
OAS_CONVERTOR_133("OAS_CONVERTOR_133", "Generated OpenAPI definition does not contain the example " +
"for record field: `%s.%s`, since the example value has parser errors", DiagnosticSeverity.WARNING),
OAS_CONVERTOR_134("OAS_CONVERTOR_134", "Generated OpenAPI definition does not contain the `%s` for" +
" `%s` parameter: `%s`, since the example value(s) has parser errors", DiagnosticSeverity.WARNING),
OAS_CONVERTOR_135("OAS_CONVERTOR_135", "Example skipped for request parameter: `%s`, since the request payload " +
"type implies more than one content-type", DiagnosticSeverity.WARNING);
private final String code;
private final String description;
private final DiagnosticSeverity severity;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.ballerina.openapi.service.mapper.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.compiler.api.symbols.AnnotationAttachmentSymbol;
import io.ballerina.compiler.api.symbols.RecordTypeSymbol;
import io.ballerina.compiler.api.symbols.Symbol;
import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol;
import io.ballerina.compiler.api.symbols.TypeSymbol;
import io.ballerina.compiler.api.values.ConstantValue;
import io.ballerina.openapi.service.mapper.diagnostic.OpenAPIMapperDiagnostic;
import io.ballerina.openapi.service.mapper.example.field.RecordFieldExampleMapper;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.wso2.ballerinalang.compiler.tree.BLangConstantValue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static io.ballerina.openapi.service.mapper.Constants.BALLERINA;
import static io.ballerina.openapi.service.mapper.Constants.EMPTY;
import static io.ballerina.openapi.service.mapper.Constants.OPENAPI;

/**
* This {@link CommonUtils} class represents the common utility functions of example mapper.
*
* @since 2.1.0
*/
public final class CommonUtils {

private static final String EXAMPLE_VALUES = "ExampleValues";
private static final String EXAMPLE_VALUE = "ExampleValue";
private static final String VALUE = "value";
private static final String QUOTE = "\"";
private static final String COMMA = ",";
private static final String BRACKET_START = "[";
private static final String BRACKET_END = "]";
private static final String COLON = ":";
private static final String BRACE_START = "{";
private static final String BRACE_END = "}";
public static final String INLINE_REC = "<inline-record>";

private CommonUtils() {
}

public static Optional<Object> extractOpenApiExampleValue(List<AnnotationAttachmentSymbol> annotations,
SemanticModel semanticModel)
throws JsonProcessingException {
Optional<AnnotationAttachmentSymbol> openAPIExample = annotations.stream()
.filter(annotAttachment -> isOpenAPIExampleAnnotation(annotAttachment, semanticModel))
.findFirst();
if (openAPIExample.isEmpty() || !openAPIExample.get().isConstAnnotation() ||
openAPIExample.get().attachmentValue().isEmpty()) {
return Optional.empty();
}

Object exampleValue = openAPIExample.get().attachmentValue().get().value();
return getExampleFromValue(exampleValue);
}

private static Optional<Object> getExampleFromValue(Object exampleValue) throws JsonProcessingException {
if (exampleValue instanceof ConstantValue constantExampleValue) {
exampleValue = constantExampleValue.value();
} else if (exampleValue instanceof BLangConstantValue constantExampleValue) {
exampleValue = constantExampleValue.value;
}

if (!(exampleValue instanceof HashMap<?, ?> exampleMap)) {
return Optional.empty();
}

Object value = exampleMap.get(VALUE);
if (Objects.isNull(value) || !(value instanceof ConstantValue)) {
return Optional.empty();
}

ObjectMapper objectMapper = new ObjectMapper();
return Optional.of(objectMapper.readValue(getJsonString(value), Object.class));
}

public static boolean isOpenAPIExampleAnnotation(AnnotationAttachmentSymbol annotAttachment,
SemanticModel semanticModel) {
return isOpenAPIAnnotation(annotAttachment, EXAMPLE_VALUE, semanticModel);
}

public static Optional<Map<String, Example>> extractOpenApiExampleValues(List<AnnotationAttachmentSymbol>
annotations,
SemanticModel semanticModel)
throws JsonProcessingException {
Optional<AnnotationAttachmentSymbol> openAPIExamples = annotations.stream()
.filter(annotAttachment -> isOpenAPIExamplesAnnotation(annotAttachment, semanticModel))
.findFirst();

if (openAPIExamples.isEmpty() || !openAPIExamples.get().isConstAnnotation() ||
openAPIExamples.get().attachmentValue().isEmpty()) {
return Optional.empty();
}

Object exampleValues = openAPIExamples.get().attachmentValue().get().value();
if (!(exampleValues instanceof HashMap<?, ?> exampleValuesMap)) {
return Optional.empty();
}

Map<String, Example> examplesMap = new HashMap<>();
for (Map.Entry<?, ?> entry : exampleValuesMap.entrySet()) {
Optional<Object> exampleValue = getExampleFromValue(entry.getValue());
if (exampleValue.isEmpty()) {
continue;
}
Example example = new Example();
example.setValue(exampleValue.get());
examplesMap.put(entry.getKey().toString(), example);
}
return Optional.of(examplesMap);
}

public static boolean isOpenAPIExamplesAnnotation(AnnotationAttachmentSymbol annotAttachment,
SemanticModel semanticModel) {
return isOpenAPIAnnotation(annotAttachment, EXAMPLE_VALUES, semanticModel);
}

private static boolean isOpenAPIAnnotation(AnnotationAttachmentSymbol annotAttachment, String annotationName,
SemanticModel semanticModel) {
if (annotAttachment.typeDescriptor().typeDescriptor().isEmpty()) {
return false;
}
Optional<Symbol> exampleValueSymbol = semanticModel.types().getTypeByName(BALLERINA, OPENAPI, EMPTY,
annotationName);
if (exampleValueSymbol.isEmpty() ||
!(exampleValueSymbol.get() instanceof TypeDefinitionSymbol serviceContractInfoType)) {
return false;
}
return annotAttachment.typeDescriptor().typeDescriptor().get()
.subtypeOf(serviceContractInfoType.typeDescriptor());
}

public static String getJsonString(Object value) {
if (value instanceof ConstantValue constantValue) {
return getJsonString(constantValue.value());
} else if (value instanceof BLangConstantValue constantValue) {
return getJsonString(constantValue.value);
} else if (value instanceof String stringValue) {
return QUOTE + stringValue + QUOTE;
} else if (value instanceof ArrayList<?> objects) {
return objects.stream()
.map(CommonUtils::getJsonString)
.collect(Collectors.joining(COMMA, BRACKET_START, BRACKET_END));
} else if (value instanceof HashMap<?, ?> hashMap) {
return hashMap.entrySet().stream()
.map(entry -> getJsonString(entry.getKey()) + COLON + getJsonString(entry.getValue()))
.collect(Collectors.joining(COMMA, BRACE_START, BRACE_END));
} else {
return value.toString();
}
}

public static void setExampleForInlineRecordFields(TypeSymbol type, Schema schema, SemanticModel semanticModel,
List<OpenAPIMapperDiagnostic> diagnostics) {
if (type instanceof RecordTypeSymbol recordType && Objects.nonNull(schema) &&
schema instanceof ObjectSchema objectSchema) {
ExampleAnnotationMapper recordExampleMapper = new RecordFieldExampleMapper(INLINE_REC, recordType,
objectSchema, semanticModel, diagnostics);
recordExampleMapper.setExample();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.ballerina.openapi.service.mapper.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.compiler.api.symbols.AnnotationAttachmentSymbol;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static io.ballerina.openapi.service.mapper.example.CommonUtils.extractOpenApiExampleValue;

/**
* This {@link ExampleAnnotationMapper} class represents the abstraction of OpenAPI example mapper.
*
* @since 2.1.0
*/
public abstract class ExampleAnnotationMapper {

private final SemanticModel semanticModel;

protected ExampleAnnotationMapper(SemanticModel semanticModel) {
this.semanticModel = semanticModel;
}

public SemanticModel getSemanticModel() {
return semanticModel;
}

public abstract void setExample();

public Optional<Object> extractExample(List<AnnotationAttachmentSymbol> annotations)
throws JsonProcessingException {
if (Objects.isNull(annotations)) {
return Optional.empty();
}
return extractOpenApiExampleValue(annotations, semanticModel);
}
}
Loading

0 comments on commit 4d76108

Please sign in to comment.