feat(query): Implement query endpoint
This commit is contained in:
parent
5d82987cc6
commit
aa9a870e94
9 changed files with 326 additions and 49 deletions
44
ilp-cw-api/[POST] query/GT LT -contradict.bru
Normal file
44
ilp-cw-api/[POST] query/GT LT -contradict.bru
Normal 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
|
||||
}
|
||||
53
ilp-cw-api/[POST] query/GT LT EQ.bru
Normal file
53
ilp-cw-api/[POST] query/GT LT EQ.bru
Normal 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
|
||||
}
|
||||
48
ilp-cw-api/[POST] query/GT LT.bru
Normal file
48
ilp-cw-api/[POST] query/GT LT.bru
Normal 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
|
||||
}
|
||||
8
ilp-cw-api/[POST] query/folder.bru
Normal file
8
ilp-cw-api/[POST] query/folder.bru
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
meta {
|
||||
name: [POST] query
|
||||
seq: 4
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
53
ilp-cw-api/[POST] query/three eqs.bru
Normal file
53
ilp-cw-api/[POST] query/three eqs.bru
Normal 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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||
*/
|
||||
public String[] dronesWithCooling(boolean state) {
|
||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||
|
|
@ -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})
|
||||
* @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);
|
||||
|
|
@ -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[]{};
|
||||
}
|
||||
|
||||
;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package io.github.js0ny.ilp_coursework.util;
|
|||
|
||||
public enum AttrOperator {
|
||||
EQ("="),
|
||||
NEQ("!="),
|
||||
NE("!="),
|
||||
GT(">"),
|
||||
LT("<");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue