Skip to content

Commit

Permalink
Merge pull request #2143 from lnash94/query_field_annotation_fix
Browse files Browse the repository at this point in the history
[master] Add Query Parameter Annotation Mapping
  • Loading branch information
lnash94 authored Sep 18, 2024
2 parents 67c1e4b + 5fa4ddd commit 9906dbe
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ service on clientResourceMethodsServerEP {
}
}

service /query on clientResourceMethodsServerEP {

resource function get bar(string first\-name, string 'table, int age) returns string {
return string `Greetings! from query params: ${first\-name}, ${'table}, ${age.toString()}`;
}

resource function get bar/[int pathParam](string first\-name, string 'table, int age) returns string {
return string `Greetings! from query with path param, query: ${first\-name}, path: ${pathParam}`;
}

resource function post bar/[int pathParam](Person body, string first\-name, string 'table, int age) returns string {
return string `Greetings! from query with path param and request payload, query: ${first\-name}, payload.name: ${body.name}, path: ${pathParam}`;
}

resource function put bar/[int pathParam](Person body, string first\-name, int age) returns string {
return string `Greetings! from query with different annotation, query: ${first\-name}`;
}
}

@test:Config {}
function testClientGetResource() returns error? {
string response = check clientResourceMethodsClientEP->/foo/bar();
Expand Down Expand Up @@ -280,3 +299,112 @@ function testClientResourceWithBasicRestType() returns error? {
test:assertEquals(res["msg"], "Greetings! from mixed path params");
test:assertEquals(res["path"], "/bar/foo/45/34.5/45.6/true");
}

public type QueryParams record {|
@http:Query {name: "first-name"}
string first\-Name;
@http:Query {name: "table"}
string tableNo;
@http:Query {name: "age"}
int personAge;
|};


public type MetaInfo record {|
string name;
|};
public const annotation MetaInfo Meta on record field;
public type QueryWithDifferentAnnotation record {|
@http:Query {name: "first-name"}
@Meta {
name: "Potter"
}
string firstName;
int age;
|};

@test:Config {}
function testQueryParametersNameOverride() returns error? {
QueryParams queries = {
first\-Name: "Jhon",
tableNo: "10",
personAge: 29
};

string response = check clientResourceMethodsClientEP->/query/bar.get(params = queries);
test:assertEquals(response, "Greetings! from query params: Jhon, 10, 29");

response = check clientResourceMethodsClientEP->/query/bar/[99].get(params = queries);
test:assertEquals(response, "Greetings! from query with path param, query: Jhon, path: 99");

Person person = {
name: "Harry",
age: 29
};

response = check clientResourceMethodsClientEP->/query/bar/[99].post(person, params = queries);
test:assertEquals(response, "Greetings! from query with path param and request payload, query: Jhon, payload.name: Harry, path: 99");

QueryWithDifferentAnnotation annotQ = {
firstName: "Ron",
age: 29
};
response = check clientResourceMethodsClientEP->/query/bar/[99].put(person, params = annotQ);
test:assertEquals(response, "Greetings! from query with different annotation, query: Ron");
}

public type EmptyName record {|
@http:Query {name: ""}
string firstName;
int age;
|};

public type NoName record {|
@http:Query {}
string firstName;
int age;
|};

public type NilName record {|
@http:Query {name: ()}
string firstName;
int age;
|};

@test:Config
function testNegativeQueryParametersNameOverride() returns error? {
EmptyName emptyName = {
firstName: "Ron",
age: 29
};
Person person = {
name: "Harry",
age: 29
};

//with empty name
http:Response response = check clientResourceMethodsClientEP->/query/bar/[99].put(person, params = emptyName);
test:assertEquals(response.statusCode, 400);
common:assertHeaderValue(check response.getHeader(common:CONTENT_TYPE), common:APPLICATION_JSON);
check common:assertJsonErrorPayloadPartialMessage(check response.getJsonPayload(), "no query param value found for 'first-name'");

// with no name attribute
NoName noName = {
firstName: "Ron",
age: 29
};
response = check clientResourceMethodsClientEP->/query/bar/[99].put(person, params = noName);
test:assertEquals(response.statusCode, 400);
common:assertHeaderValue(check response.getHeader(common:CONTENT_TYPE), common:APPLICATION_JSON);
check common:assertJsonErrorPayloadPartialMessage(check response.getJsonPayload(), "no query param value found for 'first-name'");

// with nil name value
NilName nilName = {
firstName: "Ron",
age: 29
};
response = check clientResourceMethodsClientEP->/query/bar/[99].put(person, params = nilName);
test:assertEquals(response.statusCode, 400);
common:assertHeaderValue(check response.getHeader(common:CONTENT_TYPE), common:APPLICATION_JSON);
check common:assertJsonErrorPayloadPartialMessage(check response.getJsonPayload(), "no query param value found for 'first-name'");
}
8 changes: 6 additions & 2 deletions ballerina/http_annotation.bal
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,14 @@ public type HttpHeader record {|
public annotation HttpHeader Header on parameter;

# Defines the query resource signature parameter.
public type HttpQuery record {||};
#
# + name - Specifies the name of the query parameter
public type HttpQuery record {|
string name?;
|};

# The annotation which is used to define the query resource signature parameter.
public annotation HttpQuery Query on parameter;
public const annotation HttpQuery Query on parameter, record field;

# Defines the HTTP response cache configuration. By default the `no-cache` directive is setted to the `cache-control`
# header. In addition to that `etag` and `last-modified` headers are also added for cache validation.
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- [Add `anydata` support for `setPayload` methods in the request and response objects](https://github.com/ballerina-platform/ballerina-library/issues/6954)
- [Improve `@http:Query` annotation to overwrite the query parameter name in client] (https://github.com/ballerina-platform/ballerina-library/issues/6983)

## [2.12.0] - 2024-08-20

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public final class HttpConstants {
public static final BString ANN_FIELD_RESPOND_TYPE = StringUtils.fromString("respondType");
public static final BString ANN_FIELD_NAME = StringUtils.fromString("name");
public static final String ANN_NAME_CACHE = "Cache";
public static final String ANN_NAME_QUERY = "Query";

public static final String VALUE_ATTRIBUTE = "value";

Expand Down Expand Up @@ -628,6 +629,8 @@ public final class HttpConstants {
public static final String HTTP_VERSION_1_1 = "1.1";

public static final String CURRENT_TRANSACTION_CONTEXT_PROPERTY = "currentTrxContext";
public static final String REGEX_FOR_FIELD = "(\\$field\\$\\.)";
public static final String ESCAPE_SLASH = "\\\\";

@Deprecated
public static final String HTTP_MODULE_VERSION = "1.0.6";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.ballerina.runtime.api.PredefinedTypes;
import io.ballerina.runtime.api.TypeTags;
import io.ballerina.runtime.api.async.Callback;
import io.ballerina.runtime.api.types.RecordType;
import io.ballerina.runtime.api.utils.StringUtils;
import io.ballerina.runtime.api.values.BArray;
import io.ballerina.runtime.api.values.BError;
Expand All @@ -34,6 +35,7 @@
import io.ballerina.stdlib.http.api.HttpConstants;
import io.ballerina.stdlib.http.api.HttpErrorType;
import io.ballerina.stdlib.http.api.HttpUtil;
import io.ballerina.stdlib.http.api.nativeimpl.ModuleUtils;
import io.ballerina.stdlib.http.transport.contract.HttpClientConnector;
import io.ballerina.stdlib.http.transport.message.Http2PushPromise;
import io.ballerina.stdlib.http.transport.message.HttpCarbonMessage;
Expand All @@ -44,20 +46,26 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static io.ballerina.runtime.observability.ObservabilityConstants.KEY_OBSERVER_CONTEXT;
import static io.ballerina.stdlib.http.api.HttpConstants.AND_SIGN;
import static io.ballerina.stdlib.http.api.HttpConstants.ANN_NAME_QUERY;
import static io.ballerina.stdlib.http.api.HttpConstants.CLIENT_ENDPOINT_CONFIG;
import static io.ballerina.stdlib.http.api.HttpConstants.CLIENT_ENDPOINT_SERVICE_URI;
import static io.ballerina.stdlib.http.api.HttpConstants.COLON;
import static io.ballerina.stdlib.http.api.HttpConstants.CURRENT_TRANSACTION_CONTEXT_PROPERTY;
import static io.ballerina.stdlib.http.api.HttpConstants.EMPTY;
import static io.ballerina.stdlib.http.api.HttpConstants.EQUAL_SIGN;
import static io.ballerina.stdlib.http.api.HttpConstants.ESCAPE_SLASH;
import static io.ballerina.stdlib.http.api.HttpConstants.INBOUND_MESSAGE;
import static io.ballerina.stdlib.http.api.HttpConstants.MAIN_STRAND;
import static io.ballerina.stdlib.http.api.HttpConstants.ORIGIN_HOST;
import static io.ballerina.stdlib.http.api.HttpConstants.POOLED_BYTE_BUFFER_FACTORY;
import static io.ballerina.stdlib.http.api.HttpConstants.QUESTION_MARK;
import static io.ballerina.stdlib.http.api.HttpConstants.QUOTATION_MARK;
import static io.ballerina.stdlib.http.api.HttpConstants.REGEX_FOR_FIELD;
import static io.ballerina.stdlib.http.api.HttpConstants.REMOTE_ADDRESS;
import static io.ballerina.stdlib.http.api.HttpConstants.SINGLE_SLASH;
import static io.ballerina.stdlib.http.api.HttpConstants.SRC_HANDLER;
Expand Down Expand Up @@ -293,22 +301,67 @@ private static String[] getPathStringArray(BArray pathArray) {

private static String constructQueryString(BMap params) {
List<String> queryParams = new ArrayList<>();
Map<String, String> annotationValues = getQueryNameMapping(params);
BString[] keys = (BString[]) params.getKeys();
if (keys.length == 0) {
return "";
}
for (BString key : keys) {
Object value = params.get(key);
String queryName = key.getValue();
queryName = annotationValues.getOrDefault(queryName, queryName);
String valueString = value.toString();
if (value instanceof BArray) {
valueString = valueString.substring(1, valueString.length() - 1);
valueString = valueString.replace(QUOTATION_MARK, EMPTY);
}
queryParams.add(key.getValue() + EQUAL_SIGN + valueString);
queryParams.add(queryName + EQUAL_SIGN + valueString);
}
return String.join(AND_SIGN, queryParams);
}

/**
* This util function extracts the query name with the query annotation.
*
* @param params - Parameter map
* @return Map of string with overridden query param names
*/
private static Map<String, String> getQueryNameMapping(BMap params) {
Map<String, String> annotationValues = new HashMap<>();
RecordType queryRecord = (RecordType) params.getType();
BMap<BString, Object> queryFields = queryRecord.getAnnotations();

for (Map.Entry<BString, Object> qField: queryFields.entrySet()) {
BMap value = (BMap) qField.getValue();
Object[] keys = value.getKeys();
for (Object annotRef: keys) {
String refRegex = ModuleUtils.getHttpPackageIdentifier() + COLON + ANN_NAME_QUERY;
Pattern pattern = Pattern.compile(refRegex);
Matcher matcher = pattern.matcher(annotRef.toString());
if (matcher.find()) {
BMap refValue = (BMap) value.get(annotRef);
extractedFieldName(annotationValues, qField, refValue);
}
}
}
return annotationValues;
}

private static void extractedFieldName(Map<String, String> annotationValues, Map.Entry<BString, Object> qField,
BMap value) {
String[] parts = Pattern.compile(REGEX_FOR_FIELD).split(qField.getKey().getValue());
String fieldName = unescapeIdentifier(parts[1]);
Object overrideValue = value.get(HttpConstants.ANN_FIELD_NAME);
if (!(overrideValue instanceof BString overrideName)) {
return;
}
annotationValues.put(fieldName, overrideName.getValue());
}

public static String unescapeIdentifier(String parameterName) {
return parameterName.replaceAll(ESCAPE_SLASH, EMPTY);
}

private HttpClientAction() {
}
}

0 comments on commit 9906dbe

Please sign in to comment.