From aa9a870e9465b1d23489d233e0b597beeadfa661 Mon Sep 17 00:00:00 2001 From: js0ny Date: Fri, 21 Nov 2025 21:46:10 +0000 Subject: [PATCH] feat(query): Implement query endpoint --- ilp-cw-api/[POST] query/GT LT -contradict.bru | 44 ++++++++ ilp-cw-api/[POST] query/GT LT EQ.bru | 53 ++++++++++ ilp-cw-api/[POST] query/GT LT.bru | 48 +++++++++ ilp-cw-api/[POST] query/folder.bru | 8 ++ ilp-cw-api/[POST] query/three eqs.bru | 53 ++++++++++ .../data/AttrComparatorDto.java | 3 + .../service/DroneInfoService.java | 100 +++++++++--------- .../ilp_coursework/util/AttrComparator.java | 64 +++++++++++ .../ilp_coursework/util/AttrOperator.java | 2 +- 9 files changed, 326 insertions(+), 49 deletions(-) create mode 100644 ilp-cw-api/[POST] query/GT LT -contradict.bru create mode 100644 ilp-cw-api/[POST] query/GT LT EQ.bru create mode 100644 ilp-cw-api/[POST] query/GT LT.bru create mode 100644 ilp-cw-api/[POST] query/folder.bru create mode 100644 ilp-cw-api/[POST] query/three eqs.bru create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java diff --git a/ilp-cw-api/[POST] query/GT LT -contradict.bru b/ilp-cw-api/[POST] query/GT LT -contradict.bru new file mode 100644 index 0000000..263905e --- /dev/null +++ b/ilp-cw-api/[POST] query/GT LT -contradict.bru @@ -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 +} diff --git a/ilp-cw-api/[POST] query/GT LT EQ.bru b/ilp-cw-api/[POST] query/GT LT EQ.bru new file mode 100644 index 0000000..a86be2d --- /dev/null +++ b/ilp-cw-api/[POST] query/GT LT EQ.bru @@ -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 +} diff --git a/ilp-cw-api/[POST] query/GT LT.bru b/ilp-cw-api/[POST] query/GT LT.bru new file mode 100644 index 0000000..19d057d --- /dev/null +++ b/ilp-cw-api/[POST] query/GT LT.bru @@ -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 +} diff --git a/ilp-cw-api/[POST] query/folder.bru b/ilp-cw-api/[POST] query/folder.bru new file mode 100644 index 0000000..9f6a596 --- /dev/null +++ b/ilp-cw-api/[POST] query/folder.bru @@ -0,0 +1,8 @@ +meta { + name: [POST] query + seq: 4 +} + +auth { + mode: inherit +} diff --git a/ilp-cw-api/[POST] query/three eqs.bru b/ilp-cw-api/[POST] query/three eqs.bru new file mode 100644 index 0000000..b59ead6 --- /dev/null +++ b/ilp-cw-api/[POST] query/three eqs.bru @@ -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 +} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/AttrComparatorDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/AttrComparatorDto.java index 743e46c..478a570 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/AttrComparatorDto.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/AttrComparatorDto.java @@ -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) { } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java index d31d723..99508d5 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java @@ -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 + *

+ * 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} + *

+ * 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. + *

+ * 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 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 + *

+ * 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 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[]{}; } - - ; } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java new file mode 100644 index 0000000..a88f6b7 --- /dev/null +++ b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java @@ -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 + }; + } +} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/util/AttrOperator.java b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrOperator.java index b1d3719..4472c86 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/util/AttrOperator.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrOperator.java @@ -2,7 +2,7 @@ package io.github.js0ny.ilp_coursework.util; public enum AttrOperator { EQ("="), - NEQ("!="), + NE("!="), GT(">"), LT("<");