Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add header name mapping for record fields and add util functions for conversions #2153

Merged
merged 15 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading