Skip to content

Commit

Permalink
Merge pull request #2153 from ballerina-platform/header-name
Browse files Browse the repository at this point in the history
Add header name mapping for record fields and add util functions for conversions
  • Loading branch information
TharmiganK authored Sep 23, 2024
2 parents b222df2 + 3b117aa commit 6004c86
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 48 deletions.
69 changes: 69 additions & 0 deletions ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ type Headers record {|
int req\-id;
|};

type HeadersWithName record {|
@http:Header {name: "user-id"}
string userId;
@http:Header {name: "req-id"}
int reqId;
|};

type ArrayHeaders record {|
string[] user\-id;
int[] req\-id;
Expand Down Expand Up @@ -102,6 +109,12 @@ type AlbumNotFound record {|
Headers headers;
|};

type AlbumNotFoundWithNamedHeaders record {|
*http:NotFound;
ErrorMessage body;
HeadersWithName headers;
|};

type AlbumNotFoundDefault record {|
*http:DefaultStatusCodeResponse;
ErrorMessage body;
Expand All @@ -114,6 +127,12 @@ type AlbumFound record {|
Headers headers;
|};

type AlbumFoundWithNamedHeaders record {|
*http:Ok;
Album body;
HeadersWithName headers;
|};

type AlbumFoundDefault record {|
*http:DefaultStatusCodeResponse;
Album body;
Expand Down Expand Up @@ -250,6 +269,19 @@ service /api on new http:Listener(statusCodeBindingPort2) {
headers: {user\-id: "user-1", req\-id: 1}
};
}

resource function get v1/albums/[string id]() returns AlbumFoundWithNamedHeaders|AlbumNotFoundWithNamedHeaders {
if albums.hasKey(id) {
return {
body: albums.get(id),
headers: {userId: "user-1", reqId: 1}
};
}
return {
body: {albumId: id, message: "Album not found"},
headers: {userId: "user-1", reqId: 1}
};
}
}

final http:StatusCodeClient albumClient = check new (string `localhost:${statusCodeBindingPort2}/api`);
Expand Down Expand Up @@ -601,3 +633,40 @@ function testStatusCodeBindingWithConstraintsFailure() returns error? {
test:assertFail("Invalid response type");
}
}

@test:Config {}
function testStatusCodeBindingWithNamedHeaders() returns error? {
AlbumFoundWithNamedHeaders albumFound = check albumClient->get("/v1/albums/1");
Album expectedAlbum = albums.get("1");
test:assertEquals(albumFound.body, expectedAlbum, "Invalid album returned");
test:assertEquals(albumFound.headers.userId, "user-1", "Invalid user-id header");
test:assertEquals(albumFound.headers.reqId, 1, "Invalid req-id header");
test:assertEquals(albumFound.mediaType, "application/json", "Invalid media type");

AlbumFoundWithNamedHeaders|AlbumNotFoundWithNamedHeaders res1 = check albumClient->/v1/albums/'1;
if res1 is AlbumFoundWithNamedHeaders {
test:assertEquals(res1.body, expectedAlbum, "Invalid album returned");
test:assertEquals(res1.headers.userId, "user-1", "Invalid user-id header");
test:assertEquals(res1.headers.reqId, 1, "Invalid req-id header");
test:assertEquals(res1.mediaType, "application/json", "Invalid media type");
} else {
test:assertFail("Invalid response type");
}

AlbumNotFoundWithNamedHeaders albumNotFound = check albumClient->/v1/albums/'4;
ErrorMessage expectedErrorMessage = {albumId: "4", message: "Album not found"};
test:assertEquals(albumNotFound.body, expectedErrorMessage, "Invalid error message");
test:assertEquals(albumNotFound.headers.userId, "user-1", "Invalid user-id header");
test:assertEquals(albumNotFound.headers.reqId, 1, "Invalid req-id header");
test:assertEquals(albumNotFound.mediaType, "application/json", "Invalid media type");

res1 = check albumClient->/v1/albums/'4;
if res1 is AlbumNotFoundWithNamedHeaders {
test:assertEquals(albumNotFound.body, expectedErrorMessage, "Invalid error message");
test:assertEquals(albumNotFound.headers.userId, "user-1", "Invalid user-id header");
test:assertEquals(albumNotFound.headers.reqId, 1, "Invalid req-id header");
test:assertEquals(albumNotFound.mediaType, "application/json", "Invalid media type");
} else {
test:assertFail("Invalid response type");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function testHeaderParamBindingCase8() returns error? {
}

@test:Config {}
function testHeaderParamBindingCase9() returns error? {
function testHeaderParamBindingCase91() returns error? {
map<string|string[]> headers = {header1: "value1", header2: "VALUE3", header3: ["1", "2", "3"], header4: ["VALUE1", "value2"]};
map<json> resPayload = check resourceHeaderParamBindingClient->/header/case9(headers);
test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: [1,2,3], header4: ["VALUE1", "value2"]});
Expand All @@ -122,6 +122,14 @@ function testHeaderParamBindingCase9() returns error? {
test:assertEquals(res.statusCode, 400);
}

@test:Config {}
function testHeaderParamBindingCase92() returns error? {
HeaderRecord headers = {header1: "value1", header2: "VALUE3", header3: [1, 2, 3], header4: ["VALUE1", "value2"]};
map<string|string[]> headerMap = http:getHeaderMap(headers);
map<json> resPayload = check resourceHeaderParamBindingClient->/header/case9(headerMap);
test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: [1,2,3], header4: ["VALUE1", "value2"]});
}

@test:Config {}
function testHeaderParamBindingCase10() returns error? {
int:Signed32 resPayload = check resourceHeaderParamBindingClient->/header/case10({header: "32"});
Expand Down Expand Up @@ -184,3 +192,36 @@ function testHeaderParamBindingCase14() returns error? {
res = check resourceHeaderParamBindingClient->/header/case14({header1: "ab", header2: "5000000000"});
test:assertEquals(res.statusCode, 400);
}

@test:Config {}
function testHeaderParamBindingCase151() returns error? {
HeaderRecordWithName headers = {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]};
map<json> resPayload = check resourceHeaderParamBindingClient->/header/case15(headers);
test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]});

headers = {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value5"]};
http:Response res = check resourceHeaderParamBindingClient->/header/case15(headers);
test:assertEquals(res.statusCode, 400);
}

@test:Config {}
function testHeaderParamBindingCase152() returns error? {
HeaderRecordWithType headers = {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]};
map<string|string[]> headerMap = http:getHeaderMap(headers);
map<json> resPayload = check resourceHeaderParamBindingClient->/header/case15(headerMap);
test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]});
}

@test:Config {}
function testHeaderParamBindingCase153() returns error? {
map<string|string[]> headerMap = {x\-header1: "value1", x\-header2: "VALUE3", x\-header3: ["VALUE1", "value2"]};
map<json> resPayload = check resourceHeaderParamBindingClient->/header/case15(headerMap);
test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]});
}

@test:Config {}
function testHeaderParamBindingCase154() returns error? {
map<string|string[]> headerMap = {header1: "value1", x\-header2: "VALUE3", x\-header3: ["VALUE1", "value2"]};
http:Response res = check resourceHeaderParamBindingClient->/header/case15(headerMap);
test:assertEquals(res.statusCode, 400);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ type HeaderRecord record {|
(EnumValue|Value)[] header4;
|};

type HeaderRecordWithName record {|
@http:Header {name: "x-header1"}
string header1;
@http:Header {name: "x-header2"}
string header2;
@http:Header {name: "x-header3"}
string[] header3;
|};

type HeaderRecordWithType record {|
@http:Header {name: "x-header1"}
"value1"|"value2" header1;
@http:Header {name: "x-header2"}
EnumValue header2;
@http:Header {name: "x-header3"}
(EnumValue|Value)[] header3;
|};

type UnionFiniteType EnumValue|Value;

type QueryRecord record {|
Expand Down Expand Up @@ -345,4 +363,8 @@ service /header on resourceParamBindingListener {
resource function get case14(@http:Header StringCharacter header1, @http:Header SmallInt header2) returns [StringCharacter, SmallInt] {
return [header1, header2];
}

resource function get case15(@http:Header HeaderRecordWithType header) returns map<json>|string {
return header;
}
}
52 changes: 52 additions & 0 deletions ballerina-tests/http-misc-tests/tests/http_header_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,55 @@ function testPassthruWithBody() returns error? {
}
}

type Headers record {|
@http:Header {name: "X-API-VERSION"}
string apiVersion;
@http:Header {name: "X-REQ-ID"}
int reqId;
@http:Header {name: "X-IDS"}
float[] ids;
|};

type HeadersNegative record {|
@http:Header
string header1;
@http:Header {name: ()}
string header2;
|};

@test:Config {}
function testGetHeadersMethod() {
Headers headers = {apiVersion: "v1", reqId: 123, ids: [1.0, 2.0, 3.0]};
map<string|string[]> expectedHeaderMap = {
"X-API-VERSION": "v1",
"X-REQ-ID": "123",
"X-IDS": ["1.0", "2.0", "3.0"]
};
test:assertEquals(http:getHeaderMap(headers), expectedHeaderMap, "Header map is not as expected");
}

@test:Config {}
function testGetHeadersMethodNegative() {
HeadersNegative headers = {header1: "header1", header2: "header2"};
test:assertEquals(http:getHeaderMap(headers), headers, "Header map is not as expected");
}

type Queries record {|
@http:Query { name: "XName" }
string name;
@http:Query { name: "XAge" }
int age;
@http:Query { name: "XIDs" }
float[] ids;
|};

@test:Config {}
function testGetQueryMapMethod() {
Queries queries = {name: "John", age: 30, ids: [1.0, 2.0, 3.0]};
map<anydata> expectedQueryMap = {
"XName": "John",
"XAge": 30,
"XIDs": [1.0, 2.0, 3.0]
};
test:assertEquals(http:getQueryMap(queries), expectedQueryMap, "Query map is not as expected");
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ public type RateLimitHeaders record {|
string[]? x\-rate\-limit\-types;
|};

public type RateLimitHeadersNew record {|
@http:Header {name: "x-rate-limit-id"}
string rateLimitId;
@http:Header {name: "x-rate-limit-remaining"}
int? rateLimitRemaining;
@http:Header {name: "x-rate-limit-types"}
string[]? rateLimitTypes;
|};

public type PureTypeHeaders record {|
string sid;
int iid;
Expand Down Expand Up @@ -126,6 +135,14 @@ service /headerRecord on HeaderBindingEP {
};
}

resource function get rateLimitHeadersNew(@http:Header RateLimitHeadersNew rateLimitHeaders) returns json {
return {
header1: rateLimitHeaders.rateLimitId,
header2: rateLimitHeaders.rateLimitRemaining,
header3: rateLimitHeaders.rateLimitTypes
};
}

resource function post ofStringOfPost(@http:Header RateLimitHeaders rateLimitHeaders) returns json {
return {
header1: rateLimitHeaders.x\-rate\-limit\-id,
Expand Down Expand Up @@ -506,6 +523,30 @@ function testHeaderRecordParam() returns error? {
common:assertJsonValue(response, "header3", ["weweq", "fefw"]);
}

@test:Config {}
function testHeaderRecordParamWithHeaderNameAnnotation() returns error? {
json response = check headerBindingClient->get("/headerRecord/rateLimitHeadersNew", {
"x-rate-limit-id": "dwqfec",
"x-rate-limit-remaining": "23",
"x-rate-limit-types": ["weweq", "fefw"]
});
common:assertJsonValue(response, "header1", "dwqfec");
common:assertJsonValue(response, "header2", 23);
common:assertJsonValue(response, "header3", ["weweq", "fefw"]);
}

@test:Config {}
function testHeaderRecordParamWithHeaderNameNotFound() returns error? {
http:Response response = check headerBindingClient->get("/headerRecord/rateLimitHeadersNew", {
"rate-limit-id": "dwqfec",
"x-rate-limit-remaining": "23",
"x-rate-limit-types": ["weweq", "fefw"]
});
test:assertEquals(response.statusCode, 400);
check common:assertJsonErrorPayload(check response.getJsonPayload(), "no header value found for 'rateLimitId'",
"Bad Request", 400, "/headerRecord/rateLimitHeadersNew", "GET");
}

@test:Config {}
function testHeaderRecordParamWithCastingError() returns error? {
http:Response response = check headerBindingClient->get("/headerRecord/rateLimitHeaders", {
Expand Down
6 changes: 3 additions & 3 deletions ballerina/http_annotation.bal
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ public type HttpHeader record {|
string name?;
|};

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

# Defines the query resource signature parameter.
#
Expand All @@ -121,7 +121,7 @@ public type HttpQuery record {|
string name?;
|};

# The annotation which is used to define the query resource signature parameter.
# The annotation which is used to define the query 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`
Expand Down
Loading

0 comments on commit 6004c86

Please sign in to comment.