Skip to content
Open
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
2 changes: 1 addition & 1 deletion ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ path = "../native/build/libs/websocket-native-2.15.0-SNAPSHOT.jar"
groupId = "io.ballerina.stdlib"
artifactId = "http-native"
version = "2.15.0"
path = "./lib/http-native-2.15.0-20250922-135300-204bee5.jar"
path = "./lib/http-native-2.15.0-20250925-202800-b603d22.jar"

[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
Expand Down
2 changes: 1 addition & 1 deletion ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ path = "../compiler-plugin/build/libs/websocket-compiler-plugin-2.15.0-SNAPSHOT.
path = "../native/build/libs/websocket-native-2.15.0-SNAPSHOT.jar"

[[dependency]]
path = "./lib/http-native-2.15.0-20250922-135300-204bee5.jar"
path = "./lib/http-native-2.15.0-20250925-202800-b603d22.jar"
12 changes: 6 additions & 6 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.13.0-20250825-150500-2c7270f2"
distribution-version = "2201.13.0-20250924-081800-3dae8c03"

[[package]]
org = "ballerina"
Expand Down Expand Up @@ -47,7 +47,7 @@ modules = [
[[package]]
org = "ballerina"
name = "crypto"
version = "2.9.0"
version = "2.9.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "time"}
Expand All @@ -56,7 +56,7 @@ dependencies = [
[[package]]
org = "ballerina"
name = "data.jsondata"
version = "1.1.2"
version = "1.1.3"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.object"}
Expand Down Expand Up @@ -129,7 +129,7 @@ modules = [
[[package]]
org = "ballerina"
name = "jwt"
version = "2.15.0"
version = "2.15.1"
dependencies = [
{org = "ballerina", name = "cache"},
{org = "ballerina", name = "crypto"},
Expand Down Expand Up @@ -291,7 +291,7 @@ dependencies = [
[[package]]
org = "ballerina"
name = "os"
version = "1.10.0"
version = "1.10.1"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"}
Expand Down Expand Up @@ -324,7 +324,7 @@ modules = [
[[package]]
org = "ballerina"
name = "time"
version = "2.7.0"
version = "2.8.0"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]
Expand Down
1 change: 1 addition & 0 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ build.dependsOn ":${packageName}-test-utils:build"
build.dependsOn ":${packageName}-compiler-plugin:build"
test.dependsOn ":${packageName}-native:build"
test.dependsOn ":${packageName}-compiler-plugin:build"
test.dependsOn ":${packageName}-test-utils:build"

publishToMavenLocal.dependsOn build
publish.dependsOn build
122 changes: 122 additions & 0 deletions ballerina/tests/http_query_param_binding_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ decimal l88Cde = 1.0;
string l88Name = "";
boolean l88Bool = false;

// Enum types for testing enum query parameters
enum WsType {
ORDER_TYPE,
CARGO_TYPE
}

enum Status {
ACTIVE,
INACTIVE
}

// Global variables for enum tests
WsType capturedWsType = ORDER_TYPE;
Status capturedStatus = ACTIVE;
string capturedEnumId = "";
WsType? capturedOptionalType = ();

listener Listener l83 = new(21083);

service /onTextString on l83 {
Expand Down Expand Up @@ -124,6 +141,37 @@ service /onTextString on l93 {
}
}

// Enum query parameter services
listener Listener enumListener1 = new(21150);

service /ws on enumListener1 {
resource function get .(string id, WsType 'type) returns Service|UpgradeError {
capturedEnumId = id;
capturedWsType = 'type;
return new WsService83();
}
}

listener Listener enumListener2 = new(21151);

service /enumtest/multi on enumListener2 {
resource function get .(WsType wsType, Status status) returns Service|UpgradeError {
capturedWsType = wsType;
capturedStatus = status;
return new WsService83();
}
}

listener Listener enumListener3 = new(21152);

service /enumtest/optional on enumListener3 {
resource function get .(string id, WsType? 'type) returns Service|UpgradeError {
capturedEnumId = id;
capturedOptionalType = 'type;
return new WsService83();
}
}

service class WsService83 {
*Service;
remote isolated function onTextMessage(Caller caller, string data) returns string? {
Expand Down Expand Up @@ -267,3 +315,77 @@ public function testMandatoryDecimalQueryParamBindingError() returns Error? {
test:assertFail("Expected an resource not found error");
}
}

// Enum query parameter tests
@test:Config {}
public function testEnumQueryParamWithOrderType() returns Error? {
Client wsClient = check new("ws://localhost:21150/ws?id=123&type=ORDER_TYPE");
test:assertEquals(capturedEnumId, "123", "ID should match");
test:assertEquals(capturedWsType, ORDER_TYPE, "Type should be ORDER_TYPE");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}

@test:Config {}
public function testEnumQueryParamWithCargoType() returns Error? {
Client wsClient = check new("ws://localhost:21150/ws?id=456&type=CARGO_TYPE");
test:assertEquals(capturedEnumId, "456", "ID should match");
test:assertEquals(capturedWsType, CARGO_TYPE, "Type should be CARGO_TYPE");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}

@test:Config {}
public function testEnumQueryParamInvalidValue() returns Error? {
Client|Error wsClient = new("ws://localhost:21150/ws?id=789&type=INVALID_TYPE");
if wsClient is Error {
test:assertTrue(wsClient.message().includes("Invalid handshake"),
"Should fail with invalid handshake error for invalid enum value");
} else {
test:assertFail("Expected an error for invalid enum value");
}
}

@test:Config {}
public function testEnumQueryParamMissingValue() returns Error? {
Client|Error wsClient = new("ws://localhost:21150/ws?id=789");
if wsClient is Error {
test:assertTrue(wsClient.message().includes("Invalid handshake") ||
wsClient.message().includes("Bad Request"),
"Should fail with error for missing mandatory enum parameter");
} else {
test:assertFail("Expected an error for missing mandatory enum parameter");
}
}

@test:Config {}
public function testMultipleEnumQueryParams() returns Error? {
Client wsClient = check new("ws://localhost:21151/enumtest/multi?wsType=ORDER_TYPE&status=ACTIVE");
test:assertEquals(capturedWsType, ORDER_TYPE, "WsType should be ORDER_TYPE");
test:assertEquals(capturedStatus, ACTIVE, "Status should be ACTIVE");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}

@test:Config {}
public function testMultipleEnumQueryParamsDifferentValues() returns Error? {
Client wsClient = check new("ws://localhost:21151/enumtest/multi?wsType=CARGO_TYPE&status=INACTIVE");
test:assertEquals(capturedWsType, CARGO_TYPE, "WsType should be CARGO_TYPE");
test:assertEquals(capturedStatus, INACTIVE, "Status should be INACTIVE");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}

@test:Config {}
public function testOptionalEnumQueryParamWithValue() returns Error? {
capturedOptionalType = (); // Reset
Client wsClient = check new("ws://localhost:21152/enumtest/optional?id=opt1&type=ORDER_TYPE");
test:assertEquals(capturedEnumId, "opt1", "ID should match");
test:assertEquals(capturedOptionalType, ORDER_TYPE, "Optional type should be ORDER_TYPE");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}

@test:Config {}
public function testOptionalEnumQueryParamWithoutValue() returns Error? {
capturedOptionalType = ORDER_TYPE; // Reset to non-nil value
Client wsClient = check new("ws://localhost:21152/enumtest/optional?id=opt2");
test:assertEquals(capturedEnumId, "opt2", "ID should match");
test:assertEquals(capturedOptionalType, (), "Optional type should be nil when not provided");
error? result = wsClient->close(statusCode = 1000, reason = "Close the connection", timeout = 0);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public final class QueryParam {
}

private void validateQueryParamType() throws WebSocketConnectorException {
if (isValidBasicType(typeTag) || (typeTag == TypeTags.ARRAY_TAG && isValidBasicType(
((ArrayType) type).getElementType().getTag()))) {
if (isValidBasicType(typeTag) || WebSocketUtil.isEnumType(type) ||
(typeTag == TypeTags.ARRAY_TAG && isValidBasicType(
((ArrayType) type).getElementType().getTag()))) {
return;
}
throw new WebSocketConnectorException("Incompatible query parameter type: '" + type.getName() + "'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.ballerina.runtime.api.creators.ErrorCreator;
import io.ballerina.runtime.api.creators.TypeCreator;
import io.ballerina.runtime.api.creators.ValueCreator;
import io.ballerina.runtime.api.types.FiniteType;
import io.ballerina.runtime.api.types.IntersectionType;
import io.ballerina.runtime.api.types.MapType;
import io.ballerina.runtime.api.types.MethodType;
Expand Down Expand Up @@ -273,6 +274,9 @@ public static void dispatchUpgrade(WebSocketHandshaker webSocketHandshaker, WebS
} else {
if (qParamType.getTag() == STRING_TAG) {
bValues[index++] = queryValueArr.getBString(0);
} else if (WebSocketUtil.isEnumType(qParamType)) {
FiniteType finiteType = (FiniteType) TypeUtils.getReferredType(qParamType);
bValues[index++] = convertStringToEnum(queryValueArr.getBString(0), finiteType);
} else {
bValues[index++] = FromJsonStringWithType.fromJsonStringWithType(queryValueArr
.getBString(0), ValueCreator.createTypedescValue(qParamType));
Expand Down Expand Up @@ -1142,6 +1146,25 @@ private static void executeResource(WebSocketService wsService, BObject balservi
});
}

private static Object convertStringToEnum(BString queryValue, FiniteType finiteType) {
String stringValue = queryValue.getValue();

// Check if the string matches any enum value in the finite type's value space
for (Object value : finiteType.getValueSpace()) {
if (value instanceof BString) {
if (((BString) value).getValue().equals(stringValue)) {
return value;
}
} else if (value.toString().equals(stringValue)) {
return value;
}
}

// If no match found, throw an exception that will be caught and result in a 404
throw new IllegalArgumentException("Invalid enum value: '" + stringValue + "' for type: " +
finiteType.getName());
}

private static boolean isIsolated(BObject serviceObj, String remoteMethod) {
ObjectType serviceObjType = (ObjectType) TypeUtils.getReferredType(serviceObj.getType());
return serviceObjType.isIsolated() && serviceObjType.isIsolated(remoteMethod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.ballerina.runtime.api.Runtime;
import io.ballerina.runtime.api.creators.ErrorCreator;
import io.ballerina.runtime.api.creators.ValueCreator;
import io.ballerina.runtime.api.types.FiniteType;
import io.ballerina.runtime.api.types.ObjectType;
import io.ballerina.runtime.api.types.Type;
import io.ballerina.runtime.api.types.TypeTags;
Expand Down Expand Up @@ -559,6 +560,11 @@ public static Object getNegotiatedSubProtocol(Environment env, BObject wsSyncCli
return StringUtils.fromString((String) wsSyncClient.getNativeData(WebSocketConstants.NEGOTIATED_SUBPROTOCOL));
}

public static boolean isEnumType(Type type) {
Type referredType = TypeUtils.getReferredType(type);
return referredType instanceof FiniteType;
}

private WebSocketUtil() {
}
}