diff --git a/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal b/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal index 0cda84e845..169581bf25 100644 --- a/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal +++ b/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal @@ -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; @@ -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; @@ -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; @@ -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`); @@ -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"); + } +} diff --git a/ballerina-tests/http-dispatching-tests/tests/header_params_binding_test.bal b/ballerina-tests/http-dispatching-tests/tests/header_params_binding_test.bal index f4984515e9..55cac4913f 100644 --- a/ballerina-tests/http-dispatching-tests/tests/header_params_binding_test.bal +++ b/ballerina-tests/http-dispatching-tests/tests/header_params_binding_test.bal @@ -112,7 +112,7 @@ function testHeaderParamBindingCase8() returns error? { } @test:Config {} -function testHeaderParamBindingCase9() returns error? { +function testHeaderParamBindingCase91() returns error? { map headers = {header1: "value1", header2: "VALUE3", header3: ["1", "2", "3"], header4: ["VALUE1", "value2"]}; map resPayload = check resourceHeaderParamBindingClient->/header/case9(headers); test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: [1,2,3], header4: ["VALUE1", "value2"]}); @@ -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 headerMap = http:getHeaderMap(headers); + map 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"}); @@ -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 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 headerMap = http:getHeaderMap(headers); + map resPayload = check resourceHeaderParamBindingClient->/header/case15(headerMap); + test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]}); +} + +@test:Config {} +function testHeaderParamBindingCase153() returns error? { + map headerMap = {x\-header1: "value1", x\-header2: "VALUE3", x\-header3: ["VALUE1", "value2"]}; + map resPayload = check resourceHeaderParamBindingClient->/header/case15(headerMap); + test:assertEquals(resPayload, {header1: "value1", header2: "VALUE3", header3: ["VALUE1", "value2"]}); +} + +@test:Config {} +function testHeaderParamBindingCase154() returns error? { + map headerMap = {header1: "value1", x\-header2: "VALUE3", x\-header3: ["VALUE1", "value2"]}; + http:Response res = check resourceHeaderParamBindingClient->/header/case15(headerMap); + test:assertEquals(res.statusCode, 400); +} diff --git a/ballerina-tests/http-dispatching-tests/tests/resource_params_binding_test_common.bal b/ballerina-tests/http-dispatching-tests/tests/resource_params_binding_test_common.bal index c703a67d3a..173e358a91 100644 --- a/ballerina-tests/http-dispatching-tests/tests/resource_params_binding_test_common.bal +++ b/ballerina-tests/http-dispatching-tests/tests/resource_params_binding_test_common.bal @@ -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 {| @@ -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|string { + return header; + } } diff --git a/ballerina-tests/http-misc-tests/tests/http_header_test.bal b/ballerina-tests/http-misc-tests/tests/http_header_test.bal index 308cd7b93a..72a0171459 100644 --- a/ballerina-tests/http-misc-tests/tests/http_header_test.bal +++ b/ballerina-tests/http-misc-tests/tests/http_header_test.bal @@ -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 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 expectedQueryMap = { + "XName": "John", + "XAge": 30, + "XIDs": [1.0, 2.0, 3.0] + }; + test:assertEquals(http:getQueryMap(queries), expectedQueryMap, "Query map is not as expected"); +} diff --git a/ballerina-tests/http2-tests/tests/http2_service_dispatching_header_param_binding_test.bal b/ballerina-tests/http2-tests/tests/http2_service_dispatching_header_param_binding_test.bal index a7e36ef880..6b76af00f5 100644 --- a/ballerina-tests/http2-tests/tests/http2_service_dispatching_header_param_binding_test.bal +++ b/ballerina-tests/http2-tests/tests/http2_service_dispatching_header_param_binding_test.bal @@ -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; @@ -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, @@ -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", { diff --git a/ballerina/http_annotation.bal b/ballerina/http_annotation.bal index 3319c0dae0..0de46cc306 100644 --- a/ballerina/http_annotation.bal +++ b/ballerina/http_annotation.bal @@ -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. # @@ -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` diff --git a/ballerina/http_commons.bal b/ballerina/http_commons.bal index 01a6f469f7..2789871d92 100644 --- a/ballerina/http_commons.bal +++ b/ballerina/http_commons.bal @@ -39,6 +39,58 @@ public isolated function parseHeader(string headerValue) returns HeaderValue[]|C name: "parseHeader" } external; +# Converts the headers represented as a map of `anydata` to a map of `string` or `string` array. The `value:toString` +# method will be used to convert the values to `string`. Additionally if the header name is specified by the +# `http:Header` annotation, then it will be used as the header name. +# ```ballerina +# type Headers record { +# @http:Header {name: "X-API-VERSION"} +# string apiVersion; +# int id; +# }; +# +# Headers headers = {apiVersion: "v1", id: 1}; +# map headersMap = http:getHeaderMap(headers); // { "X-API-VERSION": "v1", "id": "1" } +# ``` +# +# + headers - The headers represented as a map of anydata +# + return - A map of string or string array representing the headers +public isolated function getHeaderMap(map headers) returns map { + return map from var [key, value] in headers.entries() + select [getHeaderName(key, typeof headers), getHeaderValue(value)]; +} + +# If the query name is specified by the `http:Query` annotation, then this function will return the queries map +# with the specified query name. Otherwise, it will return the map as it is. +# ```ballerina +# type Queries record { +# @http:Query {name: "filter_ids"} +# string[] ids; +# }; +# +# Queries queries = {ids: ["1", "2"]}; +# map queriesMap = http:getQueryMap(queries); // { "filter_ids": ["1", "2"] } +# ``` +# +# + queries - The queries represented as a map of anydata +# + return - The queries map with names specified by the `http:Query` annotation +public isolated function getQueryMap(map queries) returns map { + return map from var [key, value] in queries.entries() + select [getQueryName(key, typeof queries), value]; +} + +isolated function getQueryName(string key, typedesc> queryParamType) returns string = @java:Method { + 'class: "io.ballerina.stdlib.http.api.nativeimpl.ExternUtils", + name: "getQueryName" +} external; + +isolated function getHeaderName(string key, typedesc> headerParamType) returns string = @java:Method { + 'class: "io.ballerina.stdlib.http.api.nativeimpl.ExternUtils", + name: "getHeaderName" +} external; + +isolated function getHeaderValue(anydata value) returns string|string[] => value is anydata[] ? value.'map(v => v.toString()) : value.toString(); + isolated function buildRequest(RequestMessage message, string? mediaType) returns Request|ClientError { Request request = new; if message is () { @@ -158,26 +210,18 @@ isolated function buildRequestWithHeaders(map? headers) returns } isolated function populateHeaders(Request request, map? headers) { - if headers is map { - foreach var [headerKey, headerValues] in headers.entries() { - foreach string headerValue in headerValues { - request.addHeader(headerKey, headerValue); + if headers is () { + return; + } + + foreach var [headerKey, headerValue] in getHeaderMap(headers).entries() { + if headerValue is string[] { + foreach string value in headerValue { + request.addHeader(headerKey, value); } - } - } else if headers is map { - foreach var [headerKey, headerValue] in headers.entries() { + } else { request.setHeader(headerKey, headerValue); } - } else if headers is map { - foreach var [headerKey, headerValue] in headers.entries() { - if headerValue is string[] { - foreach string value in headerValue { - request.addHeader(headerKey, value); - } - } else { - request.setHeader(headerKey, headerValue); - } - } } } diff --git a/ballerina/http_connection.bal b/ballerina/http_connection.bal index af5060aa88..3c640dfaf8 100644 --- a/ballerina/http_connection.bal +++ b/ballerina/http_connection.bal @@ -218,29 +218,11 @@ isolated function createStatusCodeResponse(StatusCodeResponse message, string? r response.statusCode = message.status.code; var headers = message?.headers; - if headers is map || headers is map || headers is map { - foreach var [headerKey, headerValues] in headers.entries() { - string[] mappedValues = headerValues.'map(val => val.toString()); - foreach string headerValue in mappedValues { - response.addHeader(headerKey, headerValue); - } - } - } else if headers is map || headers is map || headers is map { - foreach var [headerKey, headerValue] in headers.entries() { - response.setHeader(headerKey, headerValue.toString()); - } - } else if headers is map { - foreach var [headerKey, headerValue] in headers.entries() { - if headerValue is string[] || headerValue is int[] || headerValue is boolean[] { - string[] mappedValues = headerValue.'map(val => val.toString()); - foreach string value in mappedValues { - response.addHeader(headerKey, value); - } - } else { - response.setHeader(headerKey, headerValue.toString()); - } - } + if headers !is () { + map headerMap = getHeaderMap(headers); + setHeaders(headerMap, response); } + string? mediaType = retrieveMediaType(message, returnMediaType); setPayload(message?.body, response, mediaType, setETag, links); // Update content-type header according to the priority. (Highest to lowest) diff --git a/changelog.md b/changelog.md index e86f171661..6498c73ade 100644 --- a/changelog.md +++ b/changelog.md @@ -10,8 +10,10 @@ 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) -- [Improve `@http:Query` annotation to overwrite the query parameter name in service] (https://github.com/ballerina-platform/ballerina-library/issues/7006) +- [Improve `@http:Query` annotation to overwrite the query parameter name in client](https://github.com/ballerina-platform/ballerina-library/issues/6983) +- [Improve `@http:Query` annotation to overwrite the query parameter name in service](https://github.com/ballerina-platform/ballerina-library/issues/7006) +- [Add header name mapping support in record fields](https://github.com/ballerina-platform/ballerina-library/issues/7018) +- [Introduce util functions to convert query and header record with the `http:Query` and the `http:Header` annotations](https://github.com/ballerina-platform/ballerina-library/issues/7019) ## [2.12.0] - 2024-08-20 diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 81094bc2b4..0fe9d5f32f 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -800,7 +800,13 @@ resource function post album(@http:Header string referer) { resource function post product(@http:Header {name: "Accept"} string[] accept) { } +``` + +When the header parameter type is a record, the record fields represents each header values and the header name is +considered as the field name. However, the header annotation with a name field can be used to define the header +name whenever user needs some different variable name for the header. +```ballerina public type RateLimitHeaders record {| string x\-rate\-limit\-id; int x\-rate\-limit\-remaining; @@ -810,6 +816,19 @@ public type RateLimitHeaders record {| //Populate selected headers to a record resource function get price(@http:Header RateLimitHeaders rateLimitHeaders) { } + +public type RateLimitHeadersWithName 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; +|}; + +//Populate selected headers to a record. The header name is defined in the field level annotation +resource function get price(@http:Header RateLimitHeadersWithName rateLimitHeaders) { +} ``` If the requirement is to access all the header of the inbound request, it can be achieved through the `http:Headers` diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java index a1182d98d5..ac590a6d6f 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java @@ -55,6 +55,7 @@ import java.util.Optional; import java.util.Set; +import static io.ballerina.stdlib.http.api.HttpConstants.ANN_NAME_HEADER; import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS; import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE; import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_BODY_FIELD; @@ -406,7 +407,8 @@ private static Object createHeaderRecord(HttpHeaders httpHeaders, RecordType hea Field headerField = header.getValue(); Type headerFieldType = TypeUtils.getImpliedType(headerField.getFieldType()); - String headerName = header.getKey(); + BString headerFieldName = StringUtils.fromString(header.getKey()); + String headerName = ExternUtils.getName(headerFieldName, headersType, ANN_NAME_HEADER).getValue(); List headerValues = getHeader(httpHeaders, headerName); if (headerValues.isEmpty()) { @@ -422,7 +424,7 @@ private static Object createHeaderRecord(HttpHeaders httpHeaders, RecordType hea try { Object convertedValue = convertHeaderValues(headerValues, headerFieldType); - headerMap.put(StringUtils.fromString(headerName), convertedValue); + headerMap.put(headerFieldName, convertedValue); } catch (BError ex) { throw new StatusCodeBindingException(HEADER, String.format(HEADER_BINDING_FAILED_ERROR_MSG, headerName), ex); diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternUtils.java b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternUtils.java index ccf6873f56..fcd128b41b 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternUtils.java @@ -16,15 +16,33 @@ package io.ballerina.stdlib.http.api.nativeimpl; +import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; +import io.ballerina.stdlib.http.api.HttpConstants; import io.netty.handler.codec.http.HttpResponseStatus; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.http.api.HttpConstants.ANN_NAME_HEADER; +import static io.ballerina.stdlib.http.api.HttpConstants.ANN_NAME_QUERY; +import static io.ballerina.stdlib.http.api.HttpConstants.COLON; + /** * Contains external utility functions. */ public final class ExternUtils { + public static final String FIELD = "$field$.%s"; + private ExternUtils() {} /** @@ -49,4 +67,52 @@ public static BString getReasonFromStatusCode(Long statusCode) { return StringUtils.fromString(reasonPhrase); } + public static BString getHeaderName(BString originalName, BTypedesc headerTypeDesc) { + return getName(originalName, headerTypeDesc.getDescribingType(), ANN_NAME_HEADER); + } + + public static BString getQueryName(BString originalName, BTypedesc queryTypeDesc) { + return getName(originalName, queryTypeDesc.getDescribingType(), ANN_NAME_QUERY); + } + + public static BString getName(BString originalName, Type headerType, String annotationName) { + headerType = TypeUtils.getReferredType(headerType); + if (headerType.getTag() != TypeTags.RECORD_TYPE_TAG) { + return originalName; + } + + BMap annotations = ((RecordType) headerType).getAnnotations(); + if (Objects.isNull(annotations) || annotations.isEmpty()) { + return originalName; + } + + Object fieldAnnotations = annotations.get(StringUtils.fromString(String.format(FIELD, originalName))); + if (!(fieldAnnotations instanceof BMap fieldAnnotMap)) { + return originalName; + } + + return extractHttpAnnotation(fieldAnnotMap, annotationName) + .flatMap(ExternUtils::extractFieldName) + .orElse(originalName); + } + + private static Optional extractHttpAnnotation(BMap fieldAnnotMap, String annotationName) { + for (Object annotRef: fieldAnnotMap.getKeys()) { + String refRegex = ModuleUtils.getHttpPackageIdentifier() + COLON + annotationName; + Pattern pattern = Pattern.compile(refRegex); + Matcher matcher = pattern.matcher(annotRef.toString()); + if (matcher.find()) { + return Optional.of((BMap) fieldAnnotMap.get(annotRef)); + } + } + return Optional.empty(); + } + + private static Optional extractFieldName(BMap value) { + Object overrideValue = value.get(HttpConstants.ANN_FIELD_NAME); + if (!(overrideValue instanceof BString overrideName)) { + return Optional.empty(); + } + return Optional.of(overrideName); + } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java index e937454eea..e895a773de 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java @@ -27,12 +27,14 @@ import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.http.api.HttpConstants; import io.ballerina.stdlib.http.api.HttpUtil; +import io.ballerina.stdlib.http.api.nativeimpl.ExternUtils; import io.ballerina.stdlib.http.transport.message.HttpCarbonMessage; import io.netty.handler.codec.http.HttpHeaders; import java.util.ArrayList; import java.util.List; +import static io.ballerina.stdlib.http.api.HttpConstants.ANN_NAME_HEADER; import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_HEADER_BINDING_LISTENER_ERROR; import static io.ballerina.stdlib.http.api.service.signature.ParamUtils.castParam; import static io.ballerina.stdlib.http.api.service.signature.ParamUtils.castParamArray; @@ -139,7 +141,9 @@ private BMap processHeaderRecord(HeaderParam headerParam, HttpH int i = 0; for (String key : keys) { HeaderRecordParam.FieldParam field = headerRecordParam.getField(i++); - List headerValues = httpHeaders.getAll(key); + String headerName = ExternUtils.getName(StringUtils.fromString(key), recordType, + ANN_NAME_HEADER).getValue(); + List headerValues = httpHeaders.getAll(headerName); if (headerValues.isEmpty()) { if (field.isNilable() && treatNilableAsOptional) { recordValue.put(StringUtils.fromString(key), null);