Skip to content

Instantly share code, notes, and snippets.

@milovtim
Last active January 16, 2024 06:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milovtim/dc5a080d9abef682b42c47aed073a194 to your computer and use it in GitHub Desktop.
Save milovtim/dc5a080d9abef682b42c47aed073a194 to your computer and use it in GitHub Desktop.
Parse OpenAPI schema and print dot.separated field hierarchy of single schema
import java.util.*;
import java.util.stream.Collectors;
import io.swagger.v3.core.util.RefUtils;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.jooq.lambda.Seq;
import org.jooq.lambda.tuple.Tuple;
import org.jooq.lambda.tuple.Tuple2;
import static org.jooq.lambda.Seq.seq;
/**
*
*/
public class SchemaFieldsTraverseTool {
private final Map<String, JsonSchema> schemas;
private final String rootSchemaName;
public SchemaFieldsTraverseTool(String rootSchemaName,
Map<String, JsonSchema> schemas) {
this.rootSchemaName = rootSchemaName;
this.schemas = schemas;
}
public void collectTree() {
SchemaNode root = new SchemaNode(rootSchemaName);
schemas.get(rootSchemaName).getProperties()
.forEach((propName, propSchema) -> this.readNestedProperties(root, propName, ((JsonSchema) propSchema)));
levelOrderTraversal(root);
}
private static void levelOrderTraversal(SchemaNode rootNode) {
Queue<Tuple2<String, SchemaNode>> nodesQueue = new LinkedList<>();
nodesQueue.add(Tuple.tuple("", rootNode));
while (!nodesQueue.isEmpty()) {
Tuple2<String, SchemaNode> prefixedNode = nodesQueue.poll();
String prefix = prefixedNode.v1();
SchemaNode node = prefixedNode.v2();
//visit node
if (node.isLeaf()) {
System.out.printf("%s.%s(%s)%n", prefix, node.name, node.type);
}
prefixedNode.v2.getChildren().stream()
.map(schemaNode -> Tuple.tuple(prefix + "." + node.name, schemaNode))
.forEach(nodesQueue::add);
}
}
@SuppressWarnings("rawtypes")
private void readNestedProperties(SchemaNode root, String propName, JsonSchema propSchema) {
String newNodePropName = propName + ("array".equals(propSchema.getType()) ? "[]" : "");
final SchemaNode newNode = "".equals(propName)? root: root.addChild(newNodePropName);
lookupInnerProperties(propSchema)
.ifPresentOrElse(
//recursively iterate
namedSchemas -> {
namedSchemas.forEach(ns -> readNestedProperties(newNode, ns.v1(), ns.v2()));
},
//or just set type if primitive
() -> newNode.setType(propSchema.getType()));
}
private Optional<List<NamedSchema>> lookupInnerProperties(JsonSchema schema) {
if ("array".equals(schema.getType())) {
return Optional.of(List.of(
NamedSchema.of("", (JsonSchema)schema.getItems())));
}
if (schema.getProperties() != null) {
return Optional.ofNullable(getProps(schema)
.map(nameSchema -> NamedSchema.of(nameSchema.v1(), nameSchema.v2()))
.toList());
}
if (schema.get$ref() != null) {
String schemaName = RefUtils.extractSimpleName(schema.get$ref()).getLeft().toString();
return Optional.ofNullable(this.schemas.get(schemaName).getProperties())
.map(stringSchemaMap -> seq(stringSchemaMap)
.map(nameSchema -> NamedSchema.of(nameSchema.v1(), (JsonSchema) nameSchema.v2()))
.collect(Collectors.toList()));
}
return Optional.empty();
}
private static <T> Seq<Tuple2<String, JsonSchema>> getProps(Schema<T> schema) {
return seq(schema.getProperties())
.map(tpl -> new Tuple2<>(tpl.v1(), ((JsonSchema) tpl.v2())));
}
public static class NamedSchema extends Tuple2<String, JsonSchema> {
public NamedSchema(String v1, JsonSchema v2) {
super(v1, v2);
}
public static NamedSchema of(String name, JsonSchema schema) {
return new NamedSchema(name, schema);
}
}
@Getter
public static class SchemaNode {
private final String name;
@Setter
private String type;
private final List<SchemaNode> children = new ArrayList<>();
private SchemaNode(String name) {
this.name = name;
}
public static SchemaNode withName(String name) {
return new SchemaNode(name);
}
public SchemaNode addChild(String name) {
SchemaNode node = SchemaNode.withName(name);
this.children.add(node);
return node;
}
public boolean isLeaf() {
return this.children.isEmpty();
}
public int childrenCount() {
return children.size();
}
@Override
public String toString() {
return String.format("name:%s, type:%s, with %d children", this.getName(), this.getType(), this.childrenCount());
}
}
}
import java.util.Map;
import java.util.Optional;
import io.swagger.v3.core.util.RefUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.parser.OpenAPIV3Parser;
import org.jooq.lambda.tuple.Tuple2;
import org.junit.jupiter.api.Test;
import static org.jooq.lambda.Seq.seq;
class SchemaFieldsTraverseToolTest {
@Test
void name() {
Tuple2<String, Map<String, JsonSchema>> tuple2 = parseOpenApiSchemas();
if (tuple2 == null) return;
SchemaFieldsTraverseTool autocomplete = new SchemaFieldsTraverseTool(tuple2.v1, tuple2.v2);
autocomplete.collectTree();
}
private static Tuple2<String, Map<String, JsonSchema>> parseOpenApiSchemas() {
OpenAPI api = new OpenAPIV3Parser().read("upoa-common/input-schema.yaml");
//find schema name of given path (might be skipped if root schema is defined)
Optional<String> rootObjRefOpt = api.getPaths()
.entrySet()
.stream().filter(it ->
"/path/with/root/schema".equals(it.getKey())
).findFirst()
.map(Map.Entry::getValue)
.map(PathItem::getPost)
.map(Operation::getRequestBody)
.map(RequestBody::getContent)
.flatMap(content -> content.entrySet().stream()
.filter(stringMediaTypeEntry -> "application/json".equals(stringMediaTypeEntry.getKey()))
.findFirst()
)
.map(Map.Entry::getValue)
.map(MediaType::getSchema)
.map(Schema::get$ref);
if (rootObjRefOpt.isEmpty()) {
System.out.println("No root object found");
return null;
}
String rootObjRef = rootObjRefOpt.get();
Map<String, JsonSchema> schemasByName = getSchemasByName(api);
String rootSchemaName = RefUtils.extractSimpleName(rootObjRef).getLeft().toString();
return new Tuple2<>(rootSchemaName, schemasByName);
}
private static Map<String, JsonSchema> getSchemasByName(OpenAPI api) {
return seq(api.getComponents().getSchemas())
.map(tpl -> new Tuple2<>(tpl.v1(), ((JsonSchema) tpl.v2())))
.toMap(Tuple2::v1, Tuple2::v2);
}
}
@milovtim
Copy link
Author

Supports only single schema of array field

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment