feat(query): Implement query endpoint

This commit is contained in:
js0ny 2025-11-21 21:46:10 +00:00
parent 5d82987cc6
commit aa9a870e94
9 changed files with 326 additions and 49 deletions

View file

@ -0,0 +1,44 @@
meta {
name: GT LT -contradict
type: http
seq: 3
}
post {
url: {{API_BASE}}/query
body: json
auth: inherit
}
body:json {
[
{
"attribute": "capacity",
"operator": ">",
"value": "8"
},
{
"attribute": "capacity",
"operator": "<",
"value": "8"
}
]
}
assert {
res.status: eq 200
res.body: length 0
}
tests {
test("Response body is a JSON array", function() {
expect(res.getBody()).to.be.an('array');
});
}
settings {
encodeUrl: true
timeout: 0
}

View file

@ -0,0 +1,53 @@
meta {
name: GT LT EQ
type: http
seq: 4
}
post {
url: {{API_BASE}}/query
body: json
auth: inherit
}
body:json {
[
{
"attribute": "capacity",
"operator": ">",
"value": "8"
},
{
"attribute": "maxMoves",
"operator": "<",
"value": "2000"
},
{
"attribute": "cooling",
"operator": "=",
"value": "true"
}
]
}
assert {
res.status: eq 200
res.body: length 1
}
tests {
test("Response body is a JSON array", function() {
expect(res.getBody()).to.be.an('array');
});
test("Array is not empty and contains Strings", function() {
const data = res.getBody();
expect(data[0]).to.be.a('string'); // data should be in string
});
}
settings {
encodeUrl: true
timeout: 0
}

View file

@ -0,0 +1,48 @@
meta {
name: GT LT
type: http
seq: 2
}
post {
url: {{API_BASE}}/query
body: json
auth: inherit
}
body:json {
[
{
"attribute": "capacity",
"operator": ">",
"value": "8"
},
{
"attribute": "maxMoves",
"operator": "<",
"value": "2000"
}
]
}
assert {
res.status: eq 200
res.body: length 2
}
tests {
test("Response body is a JSON array", function() {
expect(res.getBody()).to.be.an('array');
});
test("Array is not empty and contains Strings", function() {
const data = res.getBody();
expect(data[0]).to.be.a('string'); // data should be in string
});
}
settings {
encodeUrl: true
timeout: 0
}

View file

@ -0,0 +1,8 @@
meta {
name: [POST] query
seq: 4
}
auth {
mode: inherit
}

View file

@ -0,0 +1,53 @@
meta {
name: three eqs
type: http
seq: 1
}
post {
url: {{API_BASE}}/query
body: json
auth: inherit
}
body:json {
[
{
"attribute": "capacity",
"operator": "=",
"value": "20"
},
{
"attribute": "heating",
"operator": "=",
"value": "false"
},
{
"attribute": "cooling",
"operator": "=",
"value": "true"
}
]
}
assert {
res.status: eq 200
res.body: length 1
}
tests {
test("Response body is a JSON array", function() {
expect(res.getBody()).to.be.an('array');
});
test("Array is not empty and contains Strings", function() {
const data = res.getBody();
expect(data[0]).to.be.a('string'); // data should be in string
});
}
settings {
encodeUrl: true
timeout: 0
}

View file

@ -1,4 +1,7 @@
package io.github.js0ny.ilp_coursework.data;
// TODO: Convert operator to Enum
// import io.github.js0ny.ilp_coursework.util.AttrOperator;
public record AttrComparatorDto(String attribute, String operator, String value) {
}

View file

@ -4,6 +4,8 @@ import io.github.js0ny.ilp_coursework.data.AttrComparatorDto;
import io.github.js0ny.ilp_coursework.data.DroneDto;
import io.github.js0ny.ilp_coursework.util.AttrOperator;
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
import java.net.URI;
import java.util.Arrays;
import java.util.HashSet;
@ -41,10 +43,13 @@ public class DroneInfoService {
/**
* Return an array of ids of drones with/without cooling capability
* <p>
* Associated service method with {@code /dronesWithCooling/{state}}
*
* @param state determines the capability filtering
* @return if {@code state} is true, return ids of drones with cooling
* capability, else without cooling
* capability, else without cooling
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
*/
public String[] dronesWithCooling(boolean state) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
@ -53,7 +58,7 @@ public class DroneInfoService {
DroneDto[].class);
if (drones == null) {
return new String[] {};
return new String[]{};
}
return Arrays.stream(drones)
@ -64,6 +69,8 @@ public class DroneInfoService {
/**
* Return a {@link DroneDto}-style json data structure with the given {@code id}
* <p>
* Associated service method with {@code /droneDetails/{id}}
*
* @param id The id of the drone
* @return drone json body of given id
@ -72,6 +79,7 @@ public class DroneInfoService {
* @throws IllegalArgumentException when drone with given {@code id} cannot be
* found
* this should lead to a 404
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
*/
public DroneDto droneDetail(String id) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
@ -89,30 +97,68 @@ public class DroneInfoService {
}
}
// This will result in 404
throw new IllegalArgumentException(
"drone with that ID cannot be found");
}
/**
* Return an array of ids of drones with a given attribute name and value
* Return an array of ids of drones with a given attribute name and value.
* <p>
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @return array of drone ids matching the attribute name and value
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
*/
public String[] dronesWithAttribute(String attrName, String attrVal) {
// Call the helper with EQ operator
return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
}
/**
* Return an array of ids of drones which matches all given complex comparing rules
*
* @param attrComparators The filter rule with Name, Value and Operator
* @return array of drone ids that matches all rules
*/
public String[] dronesSatisfyingAttributes(AttrComparatorDto[] attrComparators) {
Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) {
String attribute = comparator.attribute();
String operator = comparator.operator();
String value = comparator.value();
AttrOperator op = AttrOperator.fromString(operator);
String[] ids = dronesWithAttributeCompared(attribute, value, op);
if (matchingDroneIds == null) {
matchingDroneIds = new HashSet<>(Arrays.asList(ids));
} else {
matchingDroneIds.retainAll(Arrays.asList(ids));
}
}
if (matchingDroneIds == null) {
return new String[]{};
}
return matchingDroneIds.toArray(String[]::new);
}
/**
* Helper that wraps the dynamic querying with different comparison operators
* <p>
* This method act as a concatenation of
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
* AttrOperator)}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @param op the comparison operator
* @return array of drone ids matching the attribute name and value (filtered by
* {@code op})
* {@code op})
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
* String,
* AttrOperator)
*/
private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
@ -122,7 +168,7 @@ public class DroneInfoService {
DroneDto[].class);
if (drones == null) {
return new String[] {};
return new String[]{};
}
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
@ -144,49 +190,7 @@ public class DroneInfoService {
.toArray(String[]::new);
}
// TODO: Implement this
public String[] dronesSatisfyingAttributes(AttrComparatorDto[] attrComparators) {
Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) {
String attribute = comparator.attribute();
String operator = comparator.operator();
String value = comparator.value();
AttrOperator op = AttrOperator.fromString(operator);
String[] ids = new String[] {};// Arrays.stream
// isValueMatched(attribute, value, op);
if (matchingDroneIds == null) {
matchingDroneIds = new HashSet<>(Arrays.asList(ids));
}
}
if (matchingDroneIds == null) {
return new String[] {};
}
return matchingDroneIds.toArray(String[]::new);
}
/**
* Helper for dynamic querying, to compare the json value with given value in
* {@code String}.
*
* @param node The {@code JsonNode} to be compared
* @param attrVal The Value passed, in {@code String}
* @return {@code true} if given values are equal, otherwise false.
*/
private boolean isValueMatched(JsonNode node, String attrVal, AttrOperator op) {
if (node.isTextual()) {
return node.asText().equals(attrVal);
} else if (node.isNumber()) {
return Double.compare(node.asDouble(), Double.parseDouble(attrVal)) == 0;
} else if (node.isBoolean()) {
return node.asText().equals(attrVal);
} else {
return false;
}
}
public int[] dronesMatchesRequirements() {
return new int[] {};
return new int[]{};
}
;
}

View file

@ -0,0 +1,64 @@
package io.github.js0ny.ilp_coursework.util;
import java.math.BigDecimal;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Comparator for attribute values in {@code JsonNode}.
*
* This is a helper for dynamic querying.
*/
public class AttrComparator {
/**
* Helper for dynamic querying, to compare the json value with given value in
* {@code String}.
*
* @param node The {@code JsonNode} to be compared
* @param attrVal The Value passed, in {@code String}
* @param op The comparison operator
* @return {@code true} if given values are equal, otherwise false.
*/
public static boolean isValueMatched(JsonNode node, String attrVal, AttrOperator op) {
if (node.isTextual()) {
return compareStrings(node.asText(), attrVal, op);
} else if (node.isNumber()) {
// return Double.compare(node.asDouble(), Double.parseDouble(attrVal)) == 0;
return compareNumbers(node.decimalValue(), new BigDecimal(attrVal), op);
} else if (node.isBoolean()) {
return compareBooleans(node.asBoolean(), Boolean.parseBoolean(attrVal), op);
} else {
return false;
}
}
private static boolean compareNumbers(BigDecimal nodeVal, BigDecimal attrVal, AttrOperator op) {
int comparison = nodeVal.compareTo(attrVal);
return switch (op) {
case EQ -> comparison == 0;
case GT -> comparison > 0;
case LT -> comparison < 0;
case NE -> comparison != 0;
};
}
private static boolean compareStrings(String nodeVal, String attrVal, AttrOperator op) {
return switch (op) {
case EQ -> nodeVal.equals(attrVal);
default -> !nodeVal.equals(attrVal);
// case NE -> !nodeVal.equals(attrVal);
// case GT -> !nodeVal.equals(attrVal);// > 0;
// case LT -> !nodeVal.equals(attrVal);// < 0;
};
}
private static boolean compareBooleans(boolean nodeVal, boolean attrVal, AttrOperator op) {
return switch (op) {
case EQ -> nodeVal == attrVal;
default -> nodeVal != attrVal;
// case NE -> nodeVal != attrVal;
// case GT -> !nodeVal && attrVal; // false < true
// case LT -> nodeVal && !attrVal; // true > false
};
}
}

View file

@ -2,7 +2,7 @@ package io.github.js0ny.ilp_coursework.util;
public enum AttrOperator {
EQ("="),
NEQ("!="),
NE("!="),
GT(">"),
LT("<");