Skip to content

Commit

Permalink
Merge pull request #1821 from ballerina-platform/type-api
Browse files Browse the repository at this point in the history
[Update 12] Introduce new APIs to convert a Ballerina Type Symbol to a Json Schema
  • Loading branch information
TharmiganK authored Feb 24, 2025
2 parents addcb73 + 3ba6e85 commit caaef29
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ public enum DiagnosticMessages {
OAS_CONVERTOR_138("OAS_CONVERTOR_138", "Failed to find the package to obtain the OpenAPI definition from the " +
"service contract type: '%s'", DiagnosticSeverity.ERROR),
OAS_CONVERTOR_139("OAS_CONVERTOR_139", "Failed to find the OpenAPI definition resource for the " +
"service contract type: '%s'", DiagnosticSeverity.ERROR);
"service contract type: '%s'", DiagnosticSeverity.ERROR),
OAS_CONVERTOR_140("OAS_CONVERTOR_140", "Failed to resolve recursive references for `expand: true` option",
DiagnosticSeverity.ERROR);

private final String code;
private final String description;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.openapi.service.mapper.diagnostic.OpenAPIMapperDiagnostic;

import java.util.ArrayList;
import java.util.List;

/**
Expand All @@ -28,11 +29,30 @@
* @param moduleMemberVisitor - The module member visitor.
* @param diagnostics - The list of diagnostics.
* @param enableBallerinaExt - The flag to enable ballerina extension in the type schema.
* @param enableExpansion - The flag to enable expansion in the reference type schema.
* @param visitedTypes - The list of visited types.
*
* @since 1.9.0
*/
public record AdditionalData(SemanticModel semanticModel,
ModuleMemberVisitor moduleMemberVisitor,
List<OpenAPIMapperDiagnostic> diagnostics,
boolean enableBallerinaExt) {
boolean enableBallerinaExt,
boolean enableExpansion,
List<String> visitedTypes) {

public AdditionalData(SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor) {
this(semanticModel, moduleMemberVisitor, new ArrayList<>(), false, false, new ArrayList<>());
}

public AdditionalData(SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor,
List<OpenAPIMapperDiagnostic> diagnostics, boolean enableBallerinaExt) {
this(semanticModel, moduleMemberVisitor, diagnostics, enableBallerinaExt, false, new ArrayList<>());
}

public AdditionalData(SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor,
List<OpenAPIMapperDiagnostic> diagnostics, boolean enableBallerinaExt,
boolean enableExpansion) {
this(semanticModel, moduleMemberVisitor, diagnostics, enableBallerinaExt, enableExpansion, new ArrayList<>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@
import java.util.Optional;
import java.util.Set;

import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getConstantValues;
import static io.ballerina.openapi.service.mapper.Constants.JSON_DATA;
import static io.ballerina.openapi.service.mapper.Constants.NAME_CONFIG;
import static io.ballerina.openapi.service.mapper.Constants.VALUE;
import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getConstantValues;
import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getNameFromAnnotation;
import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getRecordFieldTypeDescription;
import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getTypeName;

/**
* This {@link RecordTypeMapper} class represents the record type mapper.
Expand Down Expand Up @@ -121,11 +120,8 @@ static List<Schema> mapIncludedRecords(RecordTypeSymbol typeSymbol, Components c
// Type inclusion in a record is a TypeReferenceType and the referred type is a RecordType
if (typeInclusion.typeKind() == TypeDescKind.TYPE_REFERENCE &&
((TypeReferenceTypeSymbol) typeInclusion).typeDescriptor().typeKind() == TypeDescKind.RECORD) {
Schema includedRecordSchema = new Schema();
includedRecordSchema.set$ref(getTypeName(typeInclusion));
Schema includedRecordSchema = TypeMapperImpl.getTypeSchema(typeInclusion, components, additionalData);
allOfSchemaList.add(includedRecordSchema);
TypeMapperImpl.createComponentMapping((TypeReferenceTypeSymbol) typeInclusion,
components, additionalData);

RecordTypeSymbol includedRecordTypeSymbol = (RecordTypeSymbol) ((TypeReferenceTypeSymbol) typeInclusion)
.typeDescriptor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public static TypeSymbol getReferredType(TypeSymbol typeSymbol) {
if (referencedType.typeKind().equals(TypeDescKind.TYPE_REFERENCE)) {
return getReferredType(referencedType);
} else {
return typeSymbol;
return referencedType;
}
}
if (typeSymbol.typeKind().equals(TypeDescKind.INTERSECTION)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com).
*
* 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.type;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.media.Schema;

/**
* This {@link SchemaResult} record represents the result of the JSON schema.
* @param schema - The schema.
* @param components - The components.
*
* @since 2.3.0
*/
public record SchemaResult(Schema schema, Components components) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,12 @@ static void setDefaultValue(Schema schema, Object defaultValue) {
}
schema.setDefault(defaultValue);
}

SchemaResult getSchema(TypeSymbol typeSymbol, boolean enableExpansion) throws UnsupportedOperationException;

Schema getSchema(TypeSymbol typeSymbol) throws UnsupportedOperationException;

String getJsonSchemaString(TypeSymbol typeSymbol) throws UnsupportedOperationException;

String getJsonSchemaString(TypeSymbol typeSymbol, boolean enableExpansion) throws UnsupportedOperationException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package io.ballerina.openapi.service.mapper.type;

import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.compiler.api.symbols.ArrayTypeSymbol;
import io.ballerina.compiler.api.symbols.ErrorTypeSymbol;
import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol;
Expand All @@ -25,17 +26,31 @@
import io.ballerina.compiler.api.symbols.RecordTypeSymbol;
import io.ballerina.compiler.api.symbols.TableTypeSymbol;
import io.ballerina.compiler.api.symbols.TupleTypeSymbol;
import io.ballerina.compiler.api.symbols.TypeDescKind;
import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol;
import io.ballerina.compiler.api.symbols.TypeSymbol;
import io.ballerina.compiler.api.symbols.UnionTypeSymbol;
import io.ballerina.openapi.service.mapper.diagnostic.DiagnosticMessages;
import io.ballerina.openapi.service.mapper.diagnostic.ExceptionDiagnostic;
import io.ballerina.openapi.service.mapper.model.AdditionalData;
import io.ballerina.openapi.service.mapper.model.ModuleMemberVisitor;
import io.ballerina.openapi.service.mapper.type.extension.BallerinaTypeExtensioner;
import io.ballerina.openapi.service.mapper.utils.MapperCommonUtils;
import io.ballerina.projects.Project;
import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.OpenAPISchema2JsonSchema;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.media.Schema;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static io.ballerina.openapi.service.mapper.ServiceToOpenAPIMapper.extractNodesFromProject;
import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getTypeName;

/**
Expand All @@ -46,14 +61,97 @@
*/
public class TypeMapperImpl implements TypeMapper {

public static final String COMPONENTS_SCHEMAS = "#/components/schemas/";
public static final String DEFINITIONS = "#/definitions/";
public static final String DEFINITIONS_FIELD = "definitions";
public static final String JSON_SCHEMA_FIELD = "$schema";
public static final String JSON_SCHEMA_VERSION = "http://json-schema.org/draft-04/schema#";
private final Components components;
private final AdditionalData componentMapperData;
private final OpenAPISchema2JsonSchema converter = new OpenAPISchema2JsonSchema();

public TypeMapperImpl(Components components, AdditionalData componentMapperData) {
this.components = components;
this.componentMapperData = componentMapperData;
}

public TypeMapperImpl(SyntaxNodeAnalysisContext context) {
this.components = new Components().schemas(new HashMap<>());
SemanticModel semanticModel = context.semanticModel();
Project project = context.currentPackage().project();
ModuleMemberVisitor moduleMemberVisitor = extractNodesFromProject(project, semanticModel);
this.componentMapperData = new AdditionalData(semanticModel, moduleMemberVisitor);
}

public Schema getSchema(TypeSymbol typeSymbol) throws UnsupportedOperationException {
return getSchema(typeSymbol, true).schema();
}

public SchemaResult getSchema(TypeSymbol typeSymbol, boolean enableExpansion)
throws UnsupportedOperationException {
if (!isAnydata(typeSymbol)) {
throw new UnsupportedOperationException("Only 'anydata' type is supported for JSON schema generation.");
}
Components localComponents = new Components().schemas(new HashMap<>());
AdditionalData additionalData = cloneComponentMapperData(enableExpansion);
Schema schema = getTypeSchema(typeSymbol, localComponents, additionalData);
if (additionalData.enableExpansion() && additionalData.diagnostics().stream()
.anyMatch(diagnostic -> diagnostic.getCode().equals("OAS_CONVERTOR_140"))) {
throw new UnsupportedOperationException("Recursive references are not supported for JSON schema " +
"generation with reference expansion.");
}
BallerinaTypeExtensioner.removeExtensionFromSchema(schema);
return new SchemaResult(schema, localComponents);
}

public String getJsonSchemaString(TypeSymbol typeSymbol) throws UnsupportedOperationException {
return getJsonSchemaString(typeSymbol, false);
}

public String getJsonSchemaString(TypeSymbol typeSymbol, boolean enableExpansion)
throws UnsupportedOperationException {
SchemaResult result = getSchema(typeSymbol, enableExpansion);
converter.process(result.schema());
Map<String, Object> jsonSchema = result.schema().getJsonSchema();
jsonSchema.put(JSON_SCHEMA_FIELD, JSON_SCHEMA_VERSION);
if (componentMapperData.enableExpansion()) {
return getJsonString(jsonSchema);
}
populateDefinitions(result.components(), jsonSchema);
return getJsonString(jsonSchema);
}

private void populateDefinitions(Components components, Map<String, Object> jsonSchema) {
Map<String, Object> definitions = new HashMap<>();
Map<String, Schema> schemas = components.getSchemas();
BallerinaTypeExtensioner.removeExtensionFromComponents(components);
if (Objects.nonNull(schemas)) {
schemas.forEach((key, value) -> {
if (Objects.nonNull(value)) {
converter.process(value);
definitions.put(key, value.getJsonSchema());
}
});
}
if (!definitions.isEmpty()) {
jsonSchema.put(DEFINITIONS_FIELD, definitions);
}
}

private static String getJsonString(Map<String, Object> jsonSchema) {
String jsonSchemaString = Json.pretty(jsonSchema);
return jsonSchemaString.replace(COMPONENTS_SCHEMAS, DEFINITIONS);
}

private AdditionalData cloneComponentMapperData(boolean enableExpansion) {
return new AdditionalData(componentMapperData.semanticModel(), componentMapperData.moduleMemberVisitor(),
new ArrayList<>(), false, enableExpansion);
}

private boolean isAnydata(TypeSymbol typeSymbol) {
return typeSymbol.subtypeOf(componentMapperData.semanticModel().types().ANYDATA);
}

public Schema getTypeSchema(TypeSymbol typeSymbol) {
return getTypeSchema(typeSymbol, components, componentMapperData, false);
}
Expand All @@ -65,7 +163,23 @@ public static Schema getTypeSchema(TypeSymbol typeSymbol, Components components,

public static Schema getTypeSchema(TypeSymbol typeSymbol, Components components, AdditionalData componentMapperData,
boolean skipNilType) {
return switch (typeSymbol.typeKind()) {
String typeName = typeSymbol.typeKind().equals(TypeDescKind.TYPE_REFERENCE) ?
MapperCommonUtils.getTypeName(typeSymbol) : "";
if (componentMapperData.enableExpansion()) {
if (componentMapperData.visitedTypes().contains(MapperCommonUtils.getTypeName(typeSymbol))) {
ExceptionDiagnostic error = new ExceptionDiagnostic(DiagnosticMessages.OAS_CONVERTOR_140);
componentMapperData.diagnostics().add(error);
return null;
}
if (!typeName.isEmpty()) {
componentMapperData.visitedTypes().add(typeName);
}
TypeSymbol referredType = ReferenceTypeMapper.getReferredType(typeSymbol);
if (Objects.nonNull(referredType)) {
typeSymbol = referredType;
}
}
Schema schema = switch (typeSymbol.typeKind()) {
case MAP -> MapTypeMapper.getSchema((MapTypeSymbol) typeSymbol, components, componentMapperData);
case ARRAY ->
ArrayTypeMapper.getSchema((ArrayTypeSymbol) typeSymbol, components, componentMapperData);
Expand All @@ -86,6 +200,10 @@ public static Schema getTypeSchema(TypeSymbol typeSymbol, Components components,
ErrorTypeMapper.getSchema((ErrorTypeSymbol) typeSymbol, components, componentMapperData);
default -> SimpleTypeMapper.getTypeSchema(typeSymbol, componentMapperData);
};
if (componentMapperData.enableExpansion() && !typeName.isEmpty()) {
componentMapperData.visitedTypes().remove(typeName);
}
return schema;
}

protected static void createComponentMapping(TypeReferenceTypeSymbol typeSymbol, Components components,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -46,36 +47,67 @@ public static void addExtension(Schema schema, TypeSymbol typeSymbol) {
}

public static void removeExtensions(OpenAPI openAPI) {
removeExtensionsFromSchemas(openAPI, (extensions, orgName, moduleName) -> true);
removeExtensionsFromOpenAPI(openAPI, (extensions, orgName, moduleName) -> true);
}

public static void removeCurrentModuleTypeExtensions(OpenAPI openAPI, ModuleID moduleID) {
String orgName = moduleID.orgName();
String moduleName = moduleID.moduleName();

removeExtensionsFromSchemas(openAPI, (extensions, org, mod) -> fromSameModule(extensions, orgName,
removeExtensionsFromOpenAPI(openAPI, (extensions, org, mod) -> fromSameModule(extensions, orgName,
moduleName));
}

private static void removeExtensionsFromSchemas(OpenAPI openAPI, ExtensionRemovalCondition condition) {
private static void removeExtensionsFromOpenAPI(OpenAPI openAPI, ExtensionRemovalCondition condition) {
Components components = openAPI.getComponents();
if (Objects.isNull(components)) {
return;
}
removeExtensionFromComponents(components, condition);
}

public static void removeExtensionFromComponents(Components components) {
removeExtensionFromComponents(components, (extensions, orgName, moduleName) -> true);
}

private static void removeExtensionFromComponents(Components components, ExtensionRemovalCondition condition) {
Map<String, Schema> schemas = components.getSchemas();
if (Objects.isNull(schemas)) {
return;
}

schemas.forEach((key, schema) -> {
Map<?, ?> extensions = schema.getExtensions();
if (Objects.nonNull(extensions) && condition.shouldRemove(extensions, null, null)) {
extensions.remove(X_BALLERINA_TYPE);
}
removeExtensionFromSchema(schema, condition);
});
}

public static void removeExtensionFromSchema(Schema schema) {
removeExtensionFromSchema(schema, (extensions, orgName, moduleName) -> true);
}

private static void removeExtensionFromSchema(Schema schema, ExtensionRemovalCondition condition) {
Map<?, ?> extensions = schema.getExtensions();
if (Objects.nonNull(extensions) && condition.shouldRemove(extensions, null, null)) {
extensions.remove(X_BALLERINA_TYPE);
}
Map<String, Schema> properties = schema.getProperties();
if (Objects.nonNull(properties)) {
properties.values().forEach(value -> removeExtensionFromSchema(value, condition));
}
List<Schema> allOfSchemas = schema.getAllOf();
if (Objects.nonNull(allOfSchemas)) {
allOfSchemas.forEach(value -> removeExtensionFromSchema(value, condition));
}
List<Schema> oneOfSchemas = schema.getOneOf();
if (Objects.nonNull(oneOfSchemas)) {
oneOfSchemas.forEach(value -> removeExtensionFromSchema(value, condition));
}
List<Schema> anyOfSchemas = schema.getAnyOf();
if (Objects.nonNull(anyOfSchemas)) {
anyOfSchemas.forEach(value -> removeExtensionFromSchema(value, condition));
}
}

@FunctionalInterface
private interface ExtensionRemovalCondition {
boolean shouldRemove(Map<?, ?> extensions, String orgName, String moduleName);
Expand Down
Loading

0 comments on commit caaef29

Please sign in to comment.