Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,102 @@
import java.util.stream.Collectors;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty;

/**
* Value type read from incoming JSON and used for resolving into actual {@link UpdateOperation}s
* with validation and additional context.
*/
@JsonDeserialize(using = UpdateClauseDeserializer.class)
@Schema(
name = "UpdateClause",
type = SchemaType.OBJECT,
implementation = Object.class,
examples =
"""
additionalProperties = Schema.False.class,
description =
"A Data API update object mapping operators such as $set and $unset to objects of document paths and values. Operators may be combined on distinct paths. Tables support only $set, $unset, $push, and $pullAll; collections support every operator except $pullAll.",
properties = {
@SchemaProperty(
name = UpdateOperator.Names.SET,
type = SchemaType.OBJECT,
description =
"Sets paths to JSON values, including null, arrays, objects, and extended JSON."),
@SchemaProperty(
name = UpdateOperator.Names.SET_ON_INSERT,
type = SchemaType.OBJECT,
description = "Sets paths only when an upsert inserts a document."),
@SchemaProperty(
name = UpdateOperator.Names.UNSET,
type = SchemaType.OBJECT,
description = "Removes paths. The values supplied for them are ignored."),
@SchemaProperty(
name = UpdateOperator.Names.INC,
type = SchemaType.OBJECT,
description = "Maps paths to numeric increments, including fractions."),
@SchemaProperty(
name = UpdateOperator.Names.MUL,
type = SchemaType.OBJECT,
description = "Maps paths to numeric multipliers, including fractions."),
@SchemaProperty(
name = UpdateOperator.Names.MIN,
type = SchemaType.OBJECT,
description = "Updates paths when the supplied values are smaller."),
@SchemaProperty(
name = UpdateOperator.Names.MAX,
type = SchemaType.OBJECT,
description = "Updates paths when the supplied values are larger."),
@SchemaProperty(
name = UpdateOperator.Names.RENAME,
type = SchemaType.OBJECT,
description = "Maps existing paths to new path strings."),
@SchemaProperty(
name = UpdateOperator.Names.CURRENT_DATE,
type = SchemaType.OBJECT,
description = "Sets paths to the current date using true or {\"$type\": \"date\"}."),
@SchemaProperty(
name = UpdateOperator.Names.POP,
type = SchemaType.OBJECT,
description = "Removes the first (-1) or last (1) element of arrays."),
@SchemaProperty(
name = UpdateOperator.Names.PUSH,
type = SchemaType.OBJECT,
description =
"Appends values to arrays, or to list, set, and map columns. Use $each for multiple values; only collections accept $position."),
@SchemaProperty(
name = UpdateOperator.Names.ADD_TO_SET,
type = SchemaType.OBJECT,
description = "Adds unique array values. Use {\"$each\": [...]} for multiple values."),
@SchemaProperty(
name = UpdateOperator.Names.PULL_ALL,
type = SchemaType.OBJECT,
description =
"Tables only. Removes the listed values from list and set columns, and the listed keys from map columns.")
},
examples = {
"""
{
"$set" : {"location": "New York"},
"$unset" : {"new_data": 1}
"$unset" : {"obsolete": 1}
}
""")
public record UpdateClause(EnumMap<UpdateOperator, ObjectNode> updateOperationDefs) {
""",
"""
{"$inc": {"score": 0.25}, "$mul": {"price": 1.5},
"$min": {"lowest": 2.5}, "$max": {"highest": 9.5}}
""",
"""
{"$push": {"items": {"$each": [1, 2], "$position": 0}},
"$addToSet": {"tags": {"$each": ["a", "b"]}},
"$pop": {"history": -1}, "$rename": {"oldName": "newName"}}
""",
"""
{"$currentDate": {"updated": true},
"$setOnInsert": {"created": {"$date": 123456}}, "$set": {"optional": null}}
""",
"{}"
})
// Hide parsed operations from schemas without changing Jackson serialization.
public record UpdateClause(
@Schema(hidden = true) EnumMap<UpdateOperator, ObjectNode> updateOperationDefs) {
/**
* Method that will validate update operation definitions of the clause and construct an ordered
* set of {@link UpdateOperation}s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,83 +7,83 @@

/** List of update operator that's supported in update. */
public enum UpdateOperator {
ADD_TO_SET("$addToSet") {
ADD_TO_SET(Names.ADD_TO_SET) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return AddToSetOperation.construct(arguments);
}
},

CURRENT_DATE("$currentDate") {
CURRENT_DATE(Names.CURRENT_DATE) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return CurrentDateOperation.construct(arguments);
}
},

INC("$inc") {
INC(Names.INC) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return IncOperation.construct(arguments);
}
},

MAX("$max") {
MAX(Names.MAX) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return MinMaxOperation.constructMax(arguments);
}
},

MIN("$min") {
MIN(Names.MIN) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return MinMaxOperation.constructMin(arguments);
}
},

MUL("$mul") {
MUL(Names.MUL) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return MulOperation.construct(arguments);
}
},

POP("$pop") {
POP(Names.POP) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return PopOperation.construct(arguments);
}
},

PUSH("$push") {
PUSH(Names.PUSH) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return PushOperation.construct(arguments);
}
},

RENAME("$rename") {
RENAME(Names.RENAME) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return RenameOperation.construct(arguments);
}
},

SET("$set") {
SET(Names.SET) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return SetOperation.constructSet(arguments);
}
},
SET_ON_INSERT("$setOnInsert") {
SET_ON_INSERT(Names.SET_ON_INSERT) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return SetOperation.constructSetOnInsert(arguments);
}
},

UNSET("$unset") {
UNSET(Names.UNSET) {
@Override
public UpdateOperation resolveOperation(ObjectNode arguments) {
return UnsetOperation.construct(arguments);
Expand All @@ -92,7 +92,23 @@ public UpdateOperation resolveOperation(ObjectNode arguments) {

// Then operators that we recognize but do not (yet) support

PULL_ALL("$pullAll");
PULL_ALL(Names.PULL_ALL);

public interface Names {
Comment thread
erichare marked this conversation as resolved.
Resolved
String ADD_TO_SET = "$addToSet";
String CURRENT_DATE = "$currentDate";
String INC = "$inc";
String MAX = "$max";
String MIN = "$min";
String MUL = "$mul";
String POP = "$pop";
String PUSH = "$push";
String RENAME = "$rename";
String SET = "$set";
String SET_ON_INSERT = "$setOnInsert";
String UNSET = "$unset";
String PULL_ALL = "$pullAll";
}

private String apiName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,29 @@ void shouldEscapeComponentNamesInReferences() {
assertEquals("#/components/schemas/A~1B~0C", McpOpenApiSchemaAdapter.componentRef("A/B~C"));
}

@Test
void shouldPreserveFractionalSchemaBoundsAndDefault() throws Exception {
var document = fixtureDocument();
((ObjectNode) document.path("components").path("schemas"))
.putObject("Fractional")
.put("type", "number")
.put("minimum", 0.25)
.put("maximum", 1.75)
.put("default", 0.5);
var schema =
bundle(MAPPER.readTree("{\"$ref\":\"#/components/schemas/Fractional\"}"), document);
var component = schema.at(schema.path("$ref").asText().substring(1));

assertEquals(0.25, component.path("minimum").doubleValue());
assertEquals(1.75, component.path("maximum").doubleValue());
assertEquals(0.5, component.path("default").doubleValue());
assertAccepts(schema, "0.25");
assertAccepts(schema, "1.75");
assertAccepts(schema, component.path("default").toString());
assertRejects(schema, "0.125");
assertRejects(schema, "1.875");
}

/** Bundles a copy of {@code input}, so tests can compare the result with the input. */
private static ObjectNode bundle(JsonNode input, JsonNode document) {
ObjectNode schema = input.deepCopy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
import io.quarkus.test.junit.TestProfile;
import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile;
import io.vertx.core.json.JsonObject;
import java.util.HashSet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/** Checks the schemas clients receive from the packaged application's tools/list endpoint. */
@QuarkusIntegrationTest
Expand Down Expand Up @@ -149,6 +152,52 @@ void findAndRerankPreservesNestedConstraints() {
.thenAssertResults();
}

@ParameterizedTest
@ValueSource(strings = {"updateOne", "updateMany", "findOneAndUpdate"})
void updateToolsAdvertiseAcceptedOperatorSyntax(String tool) {
mcpClient
.when()
.toolsList(
page -> {
var schema = schemaFor(page, tool);
var document = json(page.findByName(tool).inputSchema());
var update = document.path("properties").path("update");
var visited = new HashSet<String>();
while (update.has("$ref")) {
var reference = update.path("$ref").asText();
assertTrue(reference.startsWith("#/"), reference);
assertTrue(visited.add(reference), "Circular update schema reference");
update = document.at(reference.substring(1));
assertFalse(update.isMissingNode(), reference);
}
var examples = update.path("examples");
assertFalse(examples.isEmpty(), "Update tools must advertise usable examples");
var arguments =
new JsonObject()
.put("keyspace", "default_keyspace")
.put("collection", "schema_test");
for (var example : examples) {
arguments.put("update", new JsonObject(example.toString()));
assertAccepts(schema, arguments);
assertAccepts(schema, arguments.copy().putNull("filter"));
}
for (var malformed :
new String[] {
"{\"updateOperationDefs\":{}}",
"{\"$unknown\":{}}",
"{\"$set\":null}",
"{\"$set\":5}",
"{\"$set\":[]}"
}) {
assertRejects(schema, arguments.copy().put("update", new JsonObject(malformed)));
}
arguments.remove("update");
assertRejects(schema, arguments);
assertRejects(schema, arguments.copy().putNull("update"));
})
.thenAssertResults();
}

private static JsonSchema schemaFor(ToolsPage page, String name) {
var tool = page.findByName(name);
assertNotNull(tool, name);
Expand Down
Loading
Loading