Skip to content

Commit

Permalink
Resolve paths of the response entities, close 333
Browse files Browse the repository at this point in the history
  • Loading branch information
javiertuya committed Jan 29, 2025
1 parent 31fde32 commit 8d356a8
Show file tree
Hide file tree
Showing 17 changed files with 645 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class OaSchemaApi {
private OaSchemaIdResolver idResolver;
private OaSchemaFilter filter;
//custom logger for parse operations
private OaSchemaLogger oaLogger= new OaSchemaLogger(this.getClass());
private OaSchemaLogger oaLogger= new OaSchemaLogger();

/**
* Creates an instance for a given location (url or file) that contains the specification
Expand Down Expand Up @@ -103,6 +103,8 @@ public String getOaLogs() {
private OpenAPI parseOpenApi() {
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true); // needed to resolve references to schema objects
parseOptions.setResolveRequestBody(true); // to support reusable requests or responses
parseOptions.setResolveResponses(true);
return new OpenAPIV3Parser().read(location, null, parseOptions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Records significant errors and warnings produced during schema transformation
* that produce schemas potentially problematic when used for data generation
*/
public class OaSchemaLogger {

protected Logger standardLogger;
private List<String> logRecords;

public OaSchemaLogger(@SuppressWarnings("rawtypes") Class clazz) {
public OaSchemaLogger() {
logRecords = new ArrayList<>();
standardLogger = LoggerFactory.getLogger(clazz);
}

public void warn(String message, Object... args) {
public void warn(Logger logger, String message, Object... args) {
logRecords.add("WARN " + format(message, args));
standardLogger.error(message, args);
logger.error(message, args);
}

private String format(String message, Object... args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package giis.tdrules.client.oa.shared;

public class TransformException extends RuntimeException {
private static final long serialVersionUID = -3395114979531982805L;

public TransformException(Throwable e) {
super(e);
}
public TransformException(String message) {
super(message);
}
public TransformException(String message, Throwable cause) {
super(message + (cause== null ? "" : ". Caused by: " + cause.toString()), cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,84 @@
package giis.tdrules.client.oa.transform;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import giis.tdrules.client.oa.shared.OaSchemaLogger;
import giis.tdrules.client.oa.shared.TransformException;
import giis.tdrules.model.shared.ModelUtil;
import giis.tdrules.openapi.model.Ddl;
import giis.tdrules.openapi.model.TdEntity;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;

/**
* Determines the path info required by the POST and PUT operations associated to the
* entities in the TdSchema (with a request body that corresponds to some entity
* in the schema).
* Tuples of paths, path parameters, operations and entities (both request and response)
* from the OpenAPI model, indexed by entity name.
* Currently, does not track if each tuple is related to a request or response
*
* These operations are scanned and stored internally during instantiation
* and can be later used during the transformation of an entity.
* and can be later used during the transformation of an entity to determine
* the endpoint where POST for entities must be sent
*/
public class PathTransformer {
private static final String POST = "post";
private static final String PUT = "put";
private static final String REQUEST = "request";
private static final String RESPONSE = "response";

protected static final Logger log = LoggerFactory.getLogger(PathTransformer.class);

// Holds the path related data from the oaSchema for each entity in the schema
// (organized by http method)
Map<String, List<EntityPath>> entityPaths = new TreeMap<>();
// (organized by the entity name)
private EntityPaths entityPaths = new EntityPaths();
private OaSchemaLogger oaLogger;

public class EntityPaths {
Map<String, List<EntityPath>> epaths = new LinkedHashMap<>();

public List<EntityPath> get(String entityName) {
List<EntityPath> items = epaths.get(entityName);
return items == null ? new ArrayList<>() : items;
}
public void add(EntityPath epath) {
List<EntityPath> items = epaths.get(epath.entityName);
if (items == null) { //initialize entry if it is new
items = new ArrayList<>();
epaths.put(epath.entityName, items);
}
if (!containedIn(epath, items)) // Add only if not duplicated
items.add(epath);
}
private boolean containedIn(EntityPath epath, List<EntityPath> epaths) {
for (EntityPath item : epaths)
if (epath.entityName.equals(item.entityName) && epath.method.equals(item.method) && epath.path.equals(item.path))
return true;
return false;
}
}

public class EntityPath {
String entityName;
String method;
String path;
List<Parameter> oaParams;
}

public PathTransformer(Map<String, PathItem> pathItems) {
entityPaths.put("post", new ArrayList<>());
entityPaths.put("put", new ArrayList<>());
public PathTransformer(Map<String, PathItem> pathItems, OaSchemaLogger oaLogger) {
this.oaLogger = oaLogger;
this.initialize(pathItems);
}

Expand All @@ -51,55 +87,105 @@ private void initialize(Map<String, PathItem> pathItems) {
if (pathItems == null)
return;
for (Entry<String, PathItem> pathSchema : pathItems.entrySet()) {
addEntityPath("post", pathSchema.getKey(), pathSchema.getValue());
addEntityPath("put", pathSchema.getKey(), pathSchema.getValue());
addEntityPath(POST, REQUEST, pathSchema.getKey(), pathSchema.getValue());
addEntityPath(PUT, REQUEST, pathSchema.getKey(), pathSchema.getValue());
addEntityPath(POST, RESPONSE, pathSchema.getKey(), pathSchema.getValue());
addEntityPath(PUT, RESPONSE, pathSchema.getKey(), pathSchema.getValue());
}
}

// Adds an entity path object if it corresponds to a method that accepts a body
// with media type application/json
private void addEntityPath(String method, String oaKey, PathItem oaPath) {
log.trace("Check {} endpoint {}", method, oaKey);
Operation operation = null;
if ("post".equals(method))
operation = oaPath.getPost();
else if ("put".equals(method))
operation = oaPath.getPut();
private void addEntityPath(String method, String requestOrResponse, String oaKey, PathItem oaPath) {
String pathString = method + " " + oaKey + " (" + requestOrResponse + ")";
log.trace("Check {}", pathString);
Operation operation = getOaOperation(oaPath, method);
if (operation == null)
return;

String ref = getRequestBodyEntityRef(operation);
String ref = getBodyEntityRef(pathString, operation, requestOrResponse);
if (ref != null) {
log.trace("Found {}, body ref: {}", method, ref);
String entityName = ref.replace("#/components/schemas/", "");
log.debug("Add {} with path {} to entity {}", method, oaKey, entityName);
EntityPath entityPath = new EntityPath();
entityPath.entityName = entityName;
entityPath.method = method;
entityPath.path = oaKey;
entityPath.oaParams = operation.getParameters();
this.entityPaths.get(method).add(entityPath);
this.entityPaths.add(entityPath);
}
}
private String getRequestBodyEntityRef(Operation operation) {
if (operation == null)
return null;
RequestBody body = operation.getRequestBody();
if (body == null)

private String getBodyEntityRef(String pathString, Operation operation, String requestOrResponse) {
Content content = getOaBodyContent(pathString, operation, requestOrResponse);
if (content == null)
return null;
MediaType media = body.getContent().get("application/json");
MediaType media = getOaMediaType(pathString, content);
if (media == null)
return null;
return (String) warnAndReturnNull("Can't find an application/json media type for {}", pathString);
Schema<?> schema = media.getSchema();
if (schema == null)
return null;
return schema.get$ref();
return (String) warnAndReturnNull("Can't find a schema definition for {}", pathString);
String ref = schema.get$ref();
if (ref == null)
return (String) warnAndReturnNull("Can't find a schema with a valid ref for {}", pathString);
return ref;
}

private Operation getOaOperation(PathItem oaPath, String method) {
if (POST.equalsIgnoreCase(method))
return oaPath.getPost();
else if (PUT.equalsIgnoreCase(method))
return oaPath.getPut();
else
throw new TransformException("Operation not handled for method " + method);
}
private Content getOaBodyContent(String pathString, Operation operation, String requestOrResponse) {
if (REQUEST.equalsIgnoreCase(requestOrResponse)) {
RequestBody body = operation.getRequestBody();
return body == null
? (Content) warnAndReturnNull("Can't find the request body content for {}", pathString)
: body.getContent();
} else if (RESPONSE.equalsIgnoreCase(requestOrResponse)) {
// must select the first 2xx response using this method, null if not found
return getOaResponseContent(pathString, operation);
} else
throw new TransformException("Get body not handled for " + requestOrResponse);
}
private MediaType getOaMediaType(String pathString, Content content) {
MediaType media = content.get("application/json");
if (media == null) {
// Some specifications (market) do not specify media type for each response,
// but they are read as a media type range */* (could be also application/*), try this
media = content.get("*/*");
if (media != null)
warnAndReturnNull("Accepting media type range */* as media type for {}", pathString);

}
return media;
}
private Content getOaResponseContent(String pathString, Operation operation) {
ApiResponses responses = operation.getResponses();
if (responses == null)
return (Content) warnAndReturnNull("Can't find the responses for {}", pathString);
for (Entry<String, ApiResponse> response : responses.entrySet())
if (response.getKey().substring(0, 1).equals("2")) { // first 2XX response
Content content = response.getValue().getContent();
return content == null
? (Content) warnAndReturnNull("Can't find the {} response content for {}", response.getKey(), pathString)
: content;
}
return (Content) warnAndReturnNull("Can't find any 2XX response for {}", pathString);
}

/**
* Finds the paths that correspond to a given entity and method and adds the
* corresponding Ddl items to the model
*/
void addDdls(TdEntity entity, String method) {
for (EntityPath entityPath : this.entityPaths.get(method)) {
if (entityPath.entityName.equalsIgnoreCase(entity.getName())) {
for (EntityPath entityPath : this.entityPaths.get(entity.getName())) {
if (entityPath.entityName.equalsIgnoreCase(entity.getName()) && entityPath.method.equals(method)) {
entity.addDdlsItem(new Ddl().command(method).query(entityPath.path));
// note that there could be more than one item for a given entity and method.
}
Expand All @@ -111,8 +197,8 @@ void addDdls(TdEntity entity, String method) {
*/
List<Parameter> getPathParams(String entityName) {
List<Parameter> oaParams = new ArrayList<>();
for (EntityPath entityPath : entityPaths.get("post")) {
if (entityPath.entityName.equalsIgnoreCase(entityName)) {
for (EntityPath entityPath : entityPaths.get(entityName)) {
if (entityPath.entityName.equalsIgnoreCase(entityName) && entityPath.method.equals(POST)) {
for (Parameter oaParam : ModelUtil.safe(entityPath.oaParams)) {
if (oaParam.getIn().equals("path")) {
oaParams.add(oaParam);
Expand All @@ -123,4 +209,9 @@ List<Parameter> getPathParams(String entityName) {
return oaParams;
}

private Object warnAndReturnNull(String message, Object... args) {
oaLogger.warn(log, message, args);
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public TdSchema getTdSchema() {
public SchemaTransformer transform() {
// Before processing every item in the schema, gets an additional transformer
// to store the required information about paths (endpoint paths, path parameters)
PathTransformer pathTransformer = new PathTransformer(oaPaths);
PathTransformer pathTransformer = new PathTransformer(oaPaths, oaLogger);

for (Entry<String, Schema> oaSchema : oaSchemas.entrySet()) {
log.debug("Transform OA schema object: {}", oaSchema.getKey());
Expand Down Expand Up @@ -85,7 +85,7 @@ void fillMissingRid(TdEntity entity) {
log.debug("Try to link to missing Rid for array: {}", entity.getName());
TdEntity upstreamWithUid = upstreamAttr.findUpstreamWithUid(entity);
if (upstreamWithUid == null)
oaLogger.warn("Can't get the rid for array {} because it has not any upstream with uid", entity.getName());
oaLogger.warn(log, "Can't get the rid for array {} because it has not any upstream with uid", entity.getName());
else {
ct.linkArrayToContainerEntity(entity, upstreamWithUid);
// If the referenced entity is not the adjacent upstream, the mermaid drawing will show incorrect relations.
Expand All @@ -98,7 +98,7 @@ TdEntity getEntity(String name, Schema<?> oaSchema, PathTransformer pathTransfor
TdEntity entity = createNewEntity(name, upstream);
Map<String, Schema> oaAttributes = oaSchema.getProperties();
if (oaAttributes == null) {
oaLogger.warn("Open Api schema for {} does not have any property, generated entity will be empty", name);
oaLogger.warn(log, "Open Api schema for {} does not have any property, generated entity will be empty", name);
return entity;
}
// Gitlab rp issue #12: As the processing of each attribute is recursive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;

import giis.portable.util.FileUtil;
import giis.portable.util.Parameters;
Expand All @@ -30,6 +32,9 @@ public class Base {
protected static final String TEST_PATH_BENCHMARK="src/test/resources/bmk";
protected static final String TEST_PATH_OUTPUT="target";

@Rule
public TestName testName = new TestName();

@Before
public void setUp() {
String fileName = FileUtil.getPath(Parameters.getProjectRoot(), "..", "tdrules4.properties");
Expand Down
Loading

0 comments on commit 8d356a8

Please sign in to comment.