Skip to content

Commit

Permalink
Fix: oas dereference path item is broken (#859)
Browse files Browse the repository at this point in the history
* fix: unescape json references special chars

Characters '/' and '~' are escaped with respectively '~1' and '~0' cf: https://swagger.io/docs/specification/v3_0/using-ref/ and must be unescaped accordingly.

* fix: handle dereference of path items

see Apicurio/apicurio-studio#3030

---------

Co-authored-by: Benjamin Ledentec <[email protected]>
  • Loading branch information
ben-lc and Benjamin Ledentec authored Jan 28, 2025
1 parent 4157a64 commit f24240e
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,9 @@ public boolean removeComponent(Document document, String name) {
if(removed != null) return true;
return model.components.messageBindings.remove(name) != null;
}

@Override
public boolean mergeNode(Node from, Node to) {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package io.apicurio.datamodels.openapi.visitors.dereference;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

import io.apicurio.datamodels.Library;
import io.apicurio.datamodels.core.models.Document;
Expand Down Expand Up @@ -31,4 +34,18 @@ protected void transform(Map<String, ? extends Node> source, Function<String, St
}
}
}

protected static <T extends Node> List<T> cloneNodes(List<T> nodes, Supplier<T> nodeSupplier) {
List<T> clones = new ArrayList<>(nodes.size());
for (T node : nodes) {
clones.add(cloneNode(node, nodeSupplier));
}
return clones;
}

protected static <T extends Node> T cloneNode(T node, Supplier<T> nodeSupplier) {
Node clone = Library.readNode(Library.writeNode(node), nodeSupplier.get());
//noinspection unchecked
return (T) clone;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,13 @@ public Document dereference() {
// TODO maybe avoid exceptions?
}
}

if (localRef == null) {

// if reference can't be attached to components try to merge external node
boolean canMerge = localRef == null && strategy.mergeNode(resolved, (Node) node);

if (localRef == null && !canMerge) {
unresolvable.add(node.getReference());
} else {
} else if (localRef != null) {
// success!
// rename the original reference
if(!nameReused) {
Expand All @@ -170,6 +173,9 @@ public Document dereference() {
processQueue.add(new Context(originalRef.getRef(), localRef.getNode()));
// remember, to prevent cycles
resolvedToLocalMap.put(originalRef.getRef(), localRef.getRef());
} else {
// in case of merge
processQueue.add(new Context(originalRef.getRef(), (Node) node));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ public interface IReferenceManipulationStrategy {
*/
boolean removeComponent(Document document, String name);

/**
* Merge nodes from given <code>from</code> node to give <code>to</code> node. Merge is non-recursive, a property of
* <code>from</code> node will be added to <code>to</code> node only it doesn't already appear in it.
*
* @return <code>true</code> if operation succeed, <code>false</code> otherwise
*/
boolean mergeNode(Node from, Node to);

class ReferenceAndNode {
private final String ref;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.apicurio.datamodels.openapi.v2.models.Oas20Document;
import io.apicurio.datamodels.openapi.v2.models.Oas20Parameter;
import io.apicurio.datamodels.openapi.v2.models.Oas20ParameterDefinition;
import io.apicurio.datamodels.openapi.v2.models.Oas20PathItem;
import io.apicurio.datamodels.openapi.v2.models.Oas20Response;
import io.apicurio.datamodels.openapi.v2.models.Oas20ResponseDefinition;
import io.apicurio.datamodels.openapi.v2.models.Oas20Schema;
Expand Down Expand Up @@ -97,4 +98,41 @@ public boolean removeComponent(Document document, String name) {
if (removed != null) return true;
return model.responses.items.remove(name) != null;
}

@Override
public boolean mergeNode(Node from, Node to) {
if (to instanceof Oas20PathItem && from instanceof Oas20PathItem) {
mergePathItem((Oas20PathItem) from, (Oas20PathItem) to);
return true;
}
return false;
}

private static void mergePathItem(Oas20PathItem from, Oas20PathItem to) {
if (to.get == null && from.get != null) {
to.get = cloneNode(from.get, () -> to.createOperation("get"));
}
if (to.put == null && from.put != null) {
to.put = cloneNode(from.put, () -> to.createOperation("put"));
}
if (to.post == null && from.post != null) {
to.post = cloneNode(from.post, () -> to.createOperation("post"));
}
if (to.delete == null && from.delete != null) {
to.delete = cloneNode(from.delete, () -> to.createOperation("delete"));
}
if (to.options == null && from.options != null) {
to.options = cloneNode(from.options, () -> to.createOperation("options"));
}
if (to.head == null && from.head != null) {
to.head = cloneNode(from.head, () -> to.createOperation("head"));
}
if (to.patch == null && from.patch != null) {
to.patch = cloneNode(from.patch, () -> to.createOperation("patch"));
}
if (to.parameters == null && from.parameters != null) {
to.parameters = cloneNodes(from.parameters, to::createParameter);
}
to.setReference(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
import io.apicurio.datamodels.openapi.v3.models.Oas30HeaderDefinition;
import io.apicurio.datamodels.openapi.v3.models.Oas30Link;
import io.apicurio.datamodels.openapi.v3.models.Oas30LinkDefinition;
import io.apicurio.datamodels.openapi.v3.models.Oas30Operation;
import io.apicurio.datamodels.openapi.v3.models.Oas30Parameter;
import io.apicurio.datamodels.openapi.v3.models.Oas30ParameterDefinition;
import io.apicurio.datamodels.openapi.v3.models.Oas30PathItem;
import io.apicurio.datamodels.openapi.v3.models.Oas30RequestBody;
import io.apicurio.datamodels.openapi.v3.models.Oas30RequestBodyDefinition;
import io.apicurio.datamodels.openapi.v3.models.Oas30Response;
Expand All @@ -37,6 +39,10 @@ public ReferenceAndNode attachAsComponent(Document document, String name, Node c
throw new IllegalArgumentException("Oas30Document expected.");
Oas30Document model = (Oas30Document) document;

if (model.components == null) {
model.components = model.createComponents();
}

if (component instanceof Oas30Schema) {
if (model.components.getSchemaDefinition(name) != null)
throw new IllegalArgumentException("Definition with that name already exists: " + name);
Expand Down Expand Up @@ -175,4 +181,54 @@ public boolean removeComponent(Document document, String name) {
if (removed != null) return true;
return model.components.callbacks.remove(name) != null;
}


@Override
public boolean mergeNode(Node from, Node to) {
if (to instanceof Oas30PathItem && from instanceof Oas30PathItem) {
mergePathItem((Oas30PathItem) from, (Oas30PathItem) to);
return true;
}
return false;
}

private static void mergePathItem(Oas30PathItem from, Oas30PathItem to) {
if (to.get == null && from.get != null) {
to.get = cloneNode(from.get, () -> to.createOperation("get"));
}
if (to.put == null && from.put != null) {
to.put = cloneNode(from.put, () -> to.createOperation("put"));
}
if (to.post == null && from.post != null) {
to.post = cloneNode(from.post, () -> to.createOperation("post"));
}
if (to.delete == null && from.delete != null) {
to.delete = cloneNode(from.delete, () -> to.createOperation("delete"));
}
if (to.options == null && from.options != null) {
to.options = cloneNode(from.options, () -> to.createOperation("options"));
}
if (to.head == null && from.head != null) {
to.head = cloneNode(from.head, () -> to.createOperation("head"));
}
if (to.patch == null && from.patch != null) {
to.patch = cloneNode(from.patch, () -> to.createOperation("patch"));
}
if (to.parameters == null && from.parameters != null) {
to.parameters = cloneNodes(from.parameters, to::createParameter);
}
if (to.summary == null && from.summary != null) {
to.summary = from.summary;
}
if (to.description == null && from.description != null) {
to.description = from.description;
}
if (to.trace == null && from.trace != null) {
to.trace = cloneNode(from.trace, () -> new Oas30Operation("trace"));
}
if (to.servers == null && from.servers != null) {
to.servers = from.servers;
}
to.setReference(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public String getName() {
if (rel == null)
throw new RuntimeException("No relative part in the reference.");
String[] parts = rel.split("/");
return parts[parts.length - 1];
return parts[parts.length - 1].replaceAll("~1", "/").replaceAll("~0", "~");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"swagger": "2.0",
"paths": {
"/pets": {
"get": {
"summary": "List Pets",
"description": "Returns all pets from the system that the user has access to",
"operationId": "listPets",
"parameters": [
{
"name": "token",
"in": "header",
"description": "token to be passed as a header",
"required": true,
"type": "array",
"items": {
"type": "integer",
"format": "int64"
},
"collectionFormat": "csv"
},
{
"name": "user",
"in": "body",
"description": "user to add to the system",
"required": true,
"schema": {
"$ref": "#/definitions/Widget20"
},
"allowEmptyValue": false
},
{
"$ref": "#/parameters/skipParam"
}
],
"responses": {
"200": {
"description": "A simple string response",
"schema": {
"$ref": "#/definitions/Widget20"
}
},
"500": {
"$ref": "#/responses/GeneralError"
}
}
},
"post": {
"summary": "Add Pet",
"description": "Add a pet",
"operationId": "AddPets",
"parameters": [
{
"name": "pet",
"in": "body",
"description": "pet to add to the system",
"required": true,
"schema": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
}
}
}
}
],
"responses": {
"200": {
"description": "A simple string response"
}
}
}
}
},
"definitions": {
"Widget20": {
"title": "Example Schema",
"type": "object",
"minProperties": 2,
"maxProperties": 3,
"readOnly": false,
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": [
"firstName",
"lastName"
]
}
},
"parameters": {
"skipParam": {
"name": "skip",
"in": "query",
"description": "number of items to skip",
"required": true,
"type": "integer",
"format": "int32"
}
},
"responses": {
"GeneralError": {
"description": "General Error",
"schema": {
"type": "string"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"swagger": "2.0",
"paths": {
"/pets": {
"$ref": "https://apis20.example.com/components.js#/paths/~1pets",
"post": {
"summary": "Add Pet",
"description": "Add a pet",
"operationId": "AddPets",
"parameters": [
{
"name": "pet",
"in": "body",
"description": "pet to add to the system",
"required": true,
"schema": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
}
}
}
}
],
"responses": {
"200": {
"description": "A simple string response"
}
}
}
}
}
}
Loading

0 comments on commit f24240e

Please sign in to comment.