From 796d9b44546c002c95de397e91ddb242eb8e15e4 Mon Sep 17 00:00:00 2001 From: Joshua Sosso Date: Thu, 21 Nov 2024 23:09:18 -0600 Subject: [PATCH 1/3] use fieldname when serializing optional types --- languages/ts/ts-codegen/src/object.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/languages/ts/ts-codegen/src/object.ts b/languages/ts/ts-codegen/src/object.ts index 2edfadc4..3e9839d6 100644 --- a/languages/ts/ts-codegen/src/object.ts +++ b/languages/ts/ts-codegen/src/object.ts @@ -100,7 +100,7 @@ export function tsObjectFromSchema( rpcGenerators: context.rpcGenerators, }); if (prop.content) subContentParts.push(prop.content); - const fieldName = validVarName(camelCase(key)); + const fieldName = validVarName(camelCase(key, { normalize: true })); fieldParts.push( `${getJsDocComment(subSchema.metadata)}${fieldName}: ${prop.typeName},`, ); @@ -143,7 +143,7 @@ export function tsObjectFromSchema( rpcGenerators: context.rpcGenerators, }); if (prop.content) subContentParts.push(prop.content); - const fieldName = validVarName(camelCase(key)); + const fieldName = validVarName(camelCase(key, { normalize: true })); fieldParts.push( `${getJsDocComment(subSchema.metadata)}${fieldName}?: ${prop.typeName},`, ); @@ -153,15 +153,15 @@ export function tsObjectFromSchema( ${prop.fromJsonTemplate(`input.${key}`, tempKey)} }`); if (hasKey) { - toJsonParts.push(`if (typeof input.${key} !== 'undefined') { + toJsonParts.push(`if (typeof input.${fieldName} !== 'undefined') { json += \`,"${key}":\`; - ${prop.toJsonTemplate(`input.${key}`, "json", key)} + ${prop.toJsonTemplate(`input.${fieldName}`, "json", key)} }`); } else { - toJsonParts.push(`if (typeof input.${key} !== 'undefined') { + toJsonParts.push(`if (typeof input.${fieldName} !== 'undefined') { if (_hasKey) json += ','; json += '"${key}":'; - ${prop.toJsonTemplate(`input.${key}`, "json", key)} + ${prop.toJsonTemplate(`input.${fieldName}`, "json", key)} _hasKey = true; }`); } From 2af96d9e026064c88bcb874d762ff801a7c0a188 Mon Sep 17 00:00:00 2001 From: Joshua Sosso Date: Thu, 21 Nov 2024 23:09:34 -0600 Subject: [PATCH 2/3] add integration tests for case transformations --- tests/clients/dart/lib/test_client.rpc.dart | 250 ++++++++++++++ .../kotlin/src/main/kotlin/TestClient.rpc.kt | 256 +++++++++++++++ tests/clients/rust/src/test_client.g.rs | 304 ++++++++++++++++++ .../clients/swift/Sources/TestClient.g.swift | 234 ++++++++++++++ tests/clients/ts/testClient.rpc.ts | 258 +++++++++++++++ tests/clients/ts/testClient.test.ts | 24 ++ .../tests/sendObjectWithPascalCaseKeys.rpc.ts | 18 ++ .../tests/sendObjectWithSnakeCaseKeys.rpc.ts | 18 ++ 8 files changed, 1362 insertions(+) create mode 100644 tests/servers/ts/src/procedures/tests/sendObjectWithPascalCaseKeys.rpc.ts create mode 100644 tests/servers/ts/src/procedures/tests/sendObjectWithSnakeCaseKeys.rpc.ts diff --git a/tests/clients/dart/lib/test_client.rpc.dart b/tests/clients/dart/lib/test_client.rpc.dart index f3f7c665..48879805 100644 --- a/tests/clients/dart/lib/test_client.rpc.dart +++ b/tests/clients/dart/lib/test_client.rpc.dart @@ -141,6 +141,32 @@ class TestClientTestsService { ); } + Future sendObjectWithPascalCaseKeys( + ObjectWithPascalCaseKeys params) async { + return parsedArriRequest( + "$_baseUrl/rpcs/tests/send-object-with-pascal-case-keys", + method: HttpMethod.post, + httpClient: _httpClient, + headers: _headers, + clientVersion: _clientVersion, + params: params.toJson(), + parser: (body) => ObjectWithPascalCaseKeys.fromJsonString(body), + ); + } + + Future sendObjectWithSnakeCaseKeys( + ObjectWithSnakeCaseKeys params) async { + return parsedArriRequest( + "$_baseUrl/rpcs/tests/send-object-with-snake-case-keys", + method: HttpMethod.post, + httpClient: _httpClient, + headers: _headers, + clientVersion: _clientVersion, + params: params.toJson(), + parser: (body) => ObjectWithSnakeCaseKeys.fromJsonString(body), + ); + } + Future sendPartialObject( ObjectWithEveryOptionalType params) async { return parsedArriRequest( @@ -2734,6 +2760,230 @@ class ObjectWithEveryNullableTypeNestedArrayElementElement } } +class ObjectWithPascalCaseKeys implements ArriModel { + final DateTime createdAt; + final String displayName; + final String? phoneNumber; + final String? emailAddress; + final bool? isAdmin; + const ObjectWithPascalCaseKeys({ + required this.createdAt, + required this.displayName, + required this.phoneNumber, + this.emailAddress, + this.isAdmin, + }); + + factory ObjectWithPascalCaseKeys.empty() { + return ObjectWithPascalCaseKeys( + createdAt: DateTime.now(), + displayName: "", + phoneNumber: null, + ); + } + + factory ObjectWithPascalCaseKeys.fromJson(Map _input_) { + final createdAt = dateTimeFromDynamic(_input_["CreatedAt"], DateTime.now()); + final displayName = typeFromDynamic(_input_["DisplayName"], ""); + final phoneNumber = nullableTypeFromDynamic(_input_["PhoneNumber"]); + final emailAddress = + nullableTypeFromDynamic(_input_["EmailAddress"]); + final isAdmin = nullableTypeFromDynamic(_input_["IsAdmin"]); + return ObjectWithPascalCaseKeys( + createdAt: createdAt, + displayName: displayName, + phoneNumber: phoneNumber, + emailAddress: emailAddress, + isAdmin: isAdmin, + ); + } + + factory ObjectWithPascalCaseKeys.fromJsonString(String input) { + return ObjectWithPascalCaseKeys.fromJson(json.decode(input)); + } + + @override + Map toJson() { + final _output_ = { + "CreatedAt": createdAt.toUtc().toIso8601String(), + "DisplayName": displayName, + "PhoneNumber": phoneNumber, + }; + if (emailAddress != null) _output_["EmailAddress"] = emailAddress; + if (isAdmin != null) _output_["IsAdmin"] = isAdmin; + return _output_; + } + + @override + String toJsonString() { + return json.encode(toJson()); + } + + @override + String toUrlQueryParams() { + final _queryParts_ = []; + _queryParts_.add("CreatedAt=${createdAt.toUtc().toIso8601String()}"); + _queryParts_.add("DisplayName=$displayName"); + _queryParts_.add("PhoneNumber=$phoneNumber"); + if (emailAddress != null) _queryParts_.add("EmailAddress=$emailAddress"); + if (isAdmin != null) _queryParts_.add("IsAdmin=$isAdmin"); + return _queryParts_.join("&"); + } + + @override + ObjectWithPascalCaseKeys copyWith({ + DateTime? createdAt, + String? displayName, + String? Function()? phoneNumber, + String? Function()? emailAddress, + bool? Function()? isAdmin, + }) { + return ObjectWithPascalCaseKeys( + createdAt: createdAt ?? this.createdAt, + displayName: displayName ?? this.displayName, + phoneNumber: phoneNumber != null ? phoneNumber() : this.phoneNumber, + emailAddress: emailAddress != null ? emailAddress() : this.emailAddress, + isAdmin: isAdmin != null ? isAdmin() : this.isAdmin, + ); + } + + @override + List get props => [ + createdAt, + displayName, + phoneNumber, + emailAddress, + isAdmin, + ]; + + @override + bool operator ==(Object other) { + return other is ObjectWithPascalCaseKeys && + listsAreEqual(props, other.props); + } + + @override + int get hashCode => listToHashCode(props); + + @override + String toString() { + return "ObjectWithPascalCaseKeys ${toJsonString()}"; + } +} + +class ObjectWithSnakeCaseKeys implements ArriModel { + final DateTime createdAt; + final String displayName; + final String? phoneNumber; + final String? emailAddress; + final bool? isAdmin; + const ObjectWithSnakeCaseKeys({ + required this.createdAt, + required this.displayName, + required this.phoneNumber, + this.emailAddress, + this.isAdmin, + }); + + factory ObjectWithSnakeCaseKeys.empty() { + return ObjectWithSnakeCaseKeys( + createdAt: DateTime.now(), + displayName: "", + phoneNumber: null, + ); + } + + factory ObjectWithSnakeCaseKeys.fromJson(Map _input_) { + final createdAt = + dateTimeFromDynamic(_input_["created_at"], DateTime.now()); + final displayName = typeFromDynamic(_input_["display_name"], ""); + final phoneNumber = + nullableTypeFromDynamic(_input_["phone_number"]); + final emailAddress = + nullableTypeFromDynamic(_input_["email_address"]); + final isAdmin = nullableTypeFromDynamic(_input_["is_admin"]); + return ObjectWithSnakeCaseKeys( + createdAt: createdAt, + displayName: displayName, + phoneNumber: phoneNumber, + emailAddress: emailAddress, + isAdmin: isAdmin, + ); + } + + factory ObjectWithSnakeCaseKeys.fromJsonString(String input) { + return ObjectWithSnakeCaseKeys.fromJson(json.decode(input)); + } + + @override + Map toJson() { + final _output_ = { + "created_at": createdAt.toUtc().toIso8601String(), + "display_name": displayName, + "phone_number": phoneNumber, + }; + if (emailAddress != null) _output_["email_address"] = emailAddress; + if (isAdmin != null) _output_["is_admin"] = isAdmin; + return _output_; + } + + @override + String toJsonString() { + return json.encode(toJson()); + } + + @override + String toUrlQueryParams() { + final _queryParts_ = []; + _queryParts_.add("created_at=${createdAt.toUtc().toIso8601String()}"); + _queryParts_.add("display_name=$displayName"); + _queryParts_.add("phone_number=$phoneNumber"); + if (emailAddress != null) _queryParts_.add("email_address=$emailAddress"); + if (isAdmin != null) _queryParts_.add("is_admin=$isAdmin"); + return _queryParts_.join("&"); + } + + @override + ObjectWithSnakeCaseKeys copyWith({ + DateTime? createdAt, + String? displayName, + String? Function()? phoneNumber, + String? Function()? emailAddress, + bool? Function()? isAdmin, + }) { + return ObjectWithSnakeCaseKeys( + createdAt: createdAt ?? this.createdAt, + displayName: displayName ?? this.displayName, + phoneNumber: phoneNumber != null ? phoneNumber() : this.phoneNumber, + emailAddress: emailAddress != null ? emailAddress() : this.emailAddress, + isAdmin: isAdmin != null ? isAdmin() : this.isAdmin, + ); + } + + @override + List get props => [ + createdAt, + displayName, + phoneNumber, + emailAddress, + isAdmin, + ]; + + @override + bool operator ==(Object other) { + return other is ObjectWithSnakeCaseKeys && + listsAreEqual(props, other.props); + } + + @override + int get hashCode => listToHashCode(props); + + @override + String toString() { + return "ObjectWithSnakeCaseKeys ${toJsonString()}"; + } +} + class ObjectWithEveryOptionalType implements ArriModel { final dynamic any; final bool? boolean; diff --git a/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt b/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt index 66372667..db8c32c9 100644 --- a/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt +++ b/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt @@ -207,6 +207,50 @@ suspend fun deprecatedRpc(params: DeprecatedRpcParams): Unit { throw TestClientError.fromJson(response.bodyAsText()) } + suspend fun sendObjectWithPascalCaseKeys(params: ObjectWithPascalCaseKeys): ObjectWithPascalCaseKeys { + val response = __prepareRequest( + client = httpClient, + url = "$baseUrl/rpcs/tests/send-object-with-pascal-case-keys", + method = HttpMethod.Post, + params = params, + headers = headers?.invoke(), + ).execute() + if (response.headers["Content-Type"] != "application/json") { + throw TestClientError( + code = 0, + errorMessage = "Expected server to return Content-Type \"application/json\". Got \"${response.headers["Content-Type"]}\"", + data = JsonPrimitive(response.bodyAsText()), + stack = null, + ) + } + if (response.status.value in 200..299) { + return ObjectWithPascalCaseKeys.fromJson(response.bodyAsText()) + } + throw TestClientError.fromJson(response.bodyAsText()) + } + + suspend fun sendObjectWithSnakeCaseKeys(params: ObjectWithSnakeCaseKeys): ObjectWithSnakeCaseKeys { + val response = __prepareRequest( + client = httpClient, + url = "$baseUrl/rpcs/tests/send-object-with-snake-case-keys", + method = HttpMethod.Post, + params = params, + headers = headers?.invoke(), + ).execute() + if (response.headers["Content-Type"] != "application/json") { + throw TestClientError( + code = 0, + errorMessage = "Expected server to return Content-Type \"application/json\". Got \"${response.headers["Content-Type"]}\"", + data = JsonPrimitive(response.bodyAsText()), + stack = null, + ) + } + if (response.status.value in 200..299) { + return ObjectWithSnakeCaseKeys.fromJson(response.bodyAsText()) + } + throw TestClientError.fromJson(response.bodyAsText()) + } + suspend fun sendPartialObject(params: ObjectWithEveryOptionalType): ObjectWithEveryOptionalType { val response = __prepareRequest( client = httpClient, @@ -2740,6 +2784,218 @@ val timestamp: Instant? = when (__input.jsonObject["timestamp"]) { +data class ObjectWithPascalCaseKeys( + val createdAt: Instant, + val displayName: String, + val phoneNumber: String?, + val emailAddress: String? = null, + val isAdmin: Boolean? = null, +) : TestClientModel { + override fun toJson(): String { +var output = "{" +output += "\"CreatedAt\":" +output += "\"${timestampFormatter.format(createdAt)}\"" +output += ",\"DisplayName\":" +output += buildString { printQuoted(displayName) } +output += ",\"PhoneNumber\":" +output += when (phoneNumber) { + is String -> buildString { printQuoted(phoneNumber) } + else -> "null" + } +if (emailAddress != null) { + output += ",\"EmailAddress\":" + output += buildString { printQuoted(emailAddress) } + } +if (isAdmin != null) { + output += ",\"IsAdmin\":" + output += isAdmin + } +output += "}" +return output + } + + override fun toUrlQueryParams(): String { +val queryParts = mutableListOf() +queryParts.add( + "CreatedAt=${ + timestampFormatter.format(createdAt) + }" + ) +queryParts.add("DisplayName=$displayName") +queryParts.add("PhoneNumber=$phoneNumber") +if (emailAddress != null) { + queryParts.add("EmailAddress=$emailAddress") + } +if (isAdmin != null) { + queryParts.add("IsAdmin=$isAdmin") + } +return queryParts.joinToString("&") + } + + companion object Factory : TestClientModelFactory { + @JvmStatic + override fun new(): ObjectWithPascalCaseKeys { + return ObjectWithPascalCaseKeys( + createdAt = Instant.now(), + displayName = "", + phoneNumber = null, + ) + } + + @JvmStatic + override fun fromJson(input: String): ObjectWithPascalCaseKeys { + return fromJsonElement(JsonInstance.parseToJsonElement(input)) + } + + @JvmStatic + override fun fromJsonElement(__input: JsonElement, instancePath: String): ObjectWithPascalCaseKeys { + if (__input !is JsonObject) { + __logError("[WARNING] ObjectWithPascalCaseKeys.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty ObjectWithPascalCaseKeys.") + return new() + } +val createdAt: Instant = when (__input.jsonObject["CreatedAt"]) { + is JsonPrimitive -> + if (__input.jsonObject["CreatedAt"]!!.jsonPrimitive.isString) + Instant.parse(__input.jsonObject["CreatedAt"]!!.jsonPrimitive.content) + else + Instant.now() + else -> Instant.now() + } +val displayName: String = when (__input.jsonObject["DisplayName"]) { + is JsonPrimitive -> __input.jsonObject["DisplayName"]!!.jsonPrimitive.contentOrNull ?: "" + else -> "" + } +val phoneNumber: String? = when (__input.jsonObject["PhoneNumber"]) { + is JsonPrimitive -> __input.jsonObject["PhoneNumber"]!!.jsonPrimitive.contentOrNull + else -> null + } +val emailAddress: String? = when (__input.jsonObject["EmailAddress"]) { + is JsonPrimitive -> __input.jsonObject["EmailAddress"]!!.jsonPrimitive.contentOrNull + else -> null + } +val isAdmin: Boolean? = when (__input.jsonObject["IsAdmin"]) { + is JsonPrimitive -> __input.jsonObject["IsAdmin"]!!.jsonPrimitive.booleanOrNull + else -> null + } + return ObjectWithPascalCaseKeys( + createdAt, + displayName, + phoneNumber, + emailAddress, + isAdmin, + ) + } + } +} + + + +data class ObjectWithSnakeCaseKeys( + val createdAt: Instant, + val displayName: String, + val phoneNumber: String?, + val emailAddress: String? = null, + val isAdmin: Boolean? = null, +) : TestClientModel { + override fun toJson(): String { +var output = "{" +output += "\"created_at\":" +output += "\"${timestampFormatter.format(createdAt)}\"" +output += ",\"display_name\":" +output += buildString { printQuoted(displayName) } +output += ",\"phone_number\":" +output += when (phoneNumber) { + is String -> buildString { printQuoted(phoneNumber) } + else -> "null" + } +if (emailAddress != null) { + output += ",\"email_address\":" + output += buildString { printQuoted(emailAddress) } + } +if (isAdmin != null) { + output += ",\"is_admin\":" + output += isAdmin + } +output += "}" +return output + } + + override fun toUrlQueryParams(): String { +val queryParts = mutableListOf() +queryParts.add( + "created_at=${ + timestampFormatter.format(createdAt) + }" + ) +queryParts.add("display_name=$displayName") +queryParts.add("phone_number=$phoneNumber") +if (emailAddress != null) { + queryParts.add("email_address=$emailAddress") + } +if (isAdmin != null) { + queryParts.add("is_admin=$isAdmin") + } +return queryParts.joinToString("&") + } + + companion object Factory : TestClientModelFactory { + @JvmStatic + override fun new(): ObjectWithSnakeCaseKeys { + return ObjectWithSnakeCaseKeys( + createdAt = Instant.now(), + displayName = "", + phoneNumber = null, + ) + } + + @JvmStatic + override fun fromJson(input: String): ObjectWithSnakeCaseKeys { + return fromJsonElement(JsonInstance.parseToJsonElement(input)) + } + + @JvmStatic + override fun fromJsonElement(__input: JsonElement, instancePath: String): ObjectWithSnakeCaseKeys { + if (__input !is JsonObject) { + __logError("[WARNING] ObjectWithSnakeCaseKeys.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty ObjectWithSnakeCaseKeys.") + return new() + } +val createdAt: Instant = when (__input.jsonObject["created_at"]) { + is JsonPrimitive -> + if (__input.jsonObject["created_at"]!!.jsonPrimitive.isString) + Instant.parse(__input.jsonObject["created_at"]!!.jsonPrimitive.content) + else + Instant.now() + else -> Instant.now() + } +val displayName: String = when (__input.jsonObject["display_name"]) { + is JsonPrimitive -> __input.jsonObject["display_name"]!!.jsonPrimitive.contentOrNull ?: "" + else -> "" + } +val phoneNumber: String? = when (__input.jsonObject["phone_number"]) { + is JsonPrimitive -> __input.jsonObject["phone_number"]!!.jsonPrimitive.contentOrNull + else -> null + } +val emailAddress: String? = when (__input.jsonObject["email_address"]) { + is JsonPrimitive -> __input.jsonObject["email_address"]!!.jsonPrimitive.contentOrNull + else -> null + } +val isAdmin: Boolean? = when (__input.jsonObject["is_admin"]) { + is JsonPrimitive -> __input.jsonObject["is_admin"]!!.jsonPrimitive.booleanOrNull + else -> null + } + return ObjectWithSnakeCaseKeys( + createdAt, + displayName, + phoneNumber, + emailAddress, + isAdmin, + ) + } + } +} + + + data class ObjectWithEveryOptionalType( val any: JsonElement? = null, val boolean: Boolean? = null, diff --git a/tests/clients/rust/src/test_client.g.rs b/tests/clients/rust/src/test_client.g.rs index 0289dd6e..e615d135 100644 --- a/tests/clients/rust/src/test_client.g.rs +++ b/tests/clients/rust/src/test_client.g.rs @@ -201,6 +201,46 @@ impl TestClientTestsService { ) .await } + pub async fn send_object_with_pascal_case_keys( + &self, + params: ObjectWithPascalCaseKeys, + ) -> Result { + parsed_arri_request( + ArriParsedRequestOptions { + http_client: &self._config.http_client, + url: format!( + "{}/rpcs/tests/send-object-with-pascal-case-keys", + &self._config.base_url + ), + method: reqwest::Method::POST, + headers: self._config.headers.clone(), + client_version: "10".to_string(), + }, + Some(params), + |body| return ObjectWithPascalCaseKeys::from_json_string(body), + ) + .await + } + pub async fn send_object_with_snake_case_keys( + &self, + params: ObjectWithSnakeCaseKeys, + ) -> Result { + parsed_arri_request( + ArriParsedRequestOptions { + http_client: &self._config.http_client, + url: format!( + "{}/rpcs/tests/send-object-with-snake-case-keys", + &self._config.base_url + ), + method: reqwest::Method::POST, + headers: self._config.headers.clone(), + client_version: "10".to_string(), + }, + Some(params), + |body| return ObjectWithSnakeCaseKeys::from_json_string(body), + ) + .await + } pub async fn send_partial_object( &self, params: ObjectWithEveryOptionalType, @@ -2862,6 +2902,270 @@ impl ArriModel for ObjectWithEveryNullableTypeNestedArrayElementElement { } } +#[derive(Clone, Debug, PartialEq)] +pub struct ObjectWithPascalCaseKeys { + pub created_at: DateTime, + pub display_name: String, + pub phone_number: Option, + pub email_address: Option, + pub is_admin: Option, +} + +impl ArriModel for ObjectWithPascalCaseKeys { + fn new() -> Self { + Self { + created_at: DateTime::default(), + display_name: "".to_string(), + phone_number: None, + email_address: None, + is_admin: None, + } + } + fn from_json(input: serde_json::Value) -> Self { + match input { + serde_json::Value::Object(_val_) => { + let created_at = match _val_.get("CreatedAt") { + Some(serde_json::Value::String(created_at_val)) => { + DateTime::::parse_from_rfc3339(created_at_val) + .unwrap_or(DateTime::default()) + } + _ => DateTime::default(), + }; + let display_name = match _val_.get("DisplayName") { + Some(serde_json::Value::String(display_name_val)) => { + display_name_val.to_owned() + } + _ => "".to_string(), + }; + let phone_number = match _val_.get("PhoneNumber") { + Some(serde_json::Value::String(phone_number_val)) => { + Some(phone_number_val.to_owned()) + } + _ => None, + }; + let email_address = match _val_.get("EmailAddress") { + Some(serde_json::Value::String(email_address_val)) => { + Some(email_address_val.to_owned()) + } + _ => None, + }; + let is_admin = match _val_.get("IsAdmin") { + Some(serde_json::Value::Bool(is_admin_val)) => Some(is_admin_val.to_owned()), + _ => None, + }; + Self { + created_at, + display_name, + phone_number, + email_address, + is_admin, + } + } + _ => Self::new(), + } + } + fn from_json_string(input: String) -> Self { + match serde_json::from_str(input.as_str()) { + Ok(val) => Self::from_json(val), + _ => Self::new(), + } + } + fn to_json_string(&self) -> String { + let mut _json_output_ = "{".to_string(); + + _json_output_.push_str("\"CreatedAt\":"); + _json_output_.push_str(serialize_date_time(&self.created_at, true).as_str()); + _json_output_.push_str(",\"DisplayName\":"); + _json_output_.push_str(serialize_string(&self.display_name).as_str()); + _json_output_.push_str(",\"PhoneNumber\":"); + match &self.phone_number { + Some(phone_number_val) => { + _json_output_.push_str(serialize_string(phone_number_val).as_str()); + } + _ => { + _json_output_.push_str("null"); + } + }; + match &self.email_address { + Some(email_address_val) => { + _json_output_.push_str(",\"EmailAddress\":"); + _json_output_.push_str(serialize_string(email_address_val).as_str()) + } + _ => {} + }; + match &self.is_admin { + Some(is_admin_val) => { + _json_output_.push_str(",\"IsAdmin\":"); + _json_output_.push_str(is_admin_val.to_string().as_str()) + } + _ => {} + }; + _json_output_.push('}'); + _json_output_ + } + fn to_query_params_string(&self) -> String { + let mut _query_parts_: Vec = Vec::new(); + _query_parts_.push(format!( + "CreatedAt={}", + serialize_date_time(&self.created_at, false) + )); + _query_parts_.push(format!("DisplayName={}", &self.display_name)); + match &self.phone_number { + Some(phone_number_val) => { + _query_parts_.push(format!("PhoneNumber={}", phone_number_val)); + } + _ => { + _query_parts_.push("PhoneNumber=null".to_string()); + } + }; + match &self.email_address { + Some(email_address_val) => { + _query_parts_.push(format!("EmailAddress={}", email_address_val)); + } + _ => {} + }; + match &self.is_admin { + Some(is_admin_val) => { + _query_parts_.push(format!("IsAdmin={}", is_admin_val)); + } + _ => {} + }; + _query_parts_.join("&") + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ObjectWithSnakeCaseKeys { + pub created_at: DateTime, + pub display_name: String, + pub phone_number: Option, + pub email_address: Option, + pub is_admin: Option, +} + +impl ArriModel for ObjectWithSnakeCaseKeys { + fn new() -> Self { + Self { + created_at: DateTime::default(), + display_name: "".to_string(), + phone_number: None, + email_address: None, + is_admin: None, + } + } + fn from_json(input: serde_json::Value) -> Self { + match input { + serde_json::Value::Object(_val_) => { + let created_at = match _val_.get("created_at") { + Some(serde_json::Value::String(created_at_val)) => { + DateTime::::parse_from_rfc3339(created_at_val) + .unwrap_or(DateTime::default()) + } + _ => DateTime::default(), + }; + let display_name = match _val_.get("display_name") { + Some(serde_json::Value::String(display_name_val)) => { + display_name_val.to_owned() + } + _ => "".to_string(), + }; + let phone_number = match _val_.get("phone_number") { + Some(serde_json::Value::String(phone_number_val)) => { + Some(phone_number_val.to_owned()) + } + _ => None, + }; + let email_address = match _val_.get("email_address") { + Some(serde_json::Value::String(email_address_val)) => { + Some(email_address_val.to_owned()) + } + _ => None, + }; + let is_admin = match _val_.get("is_admin") { + Some(serde_json::Value::Bool(is_admin_val)) => Some(is_admin_val.to_owned()), + _ => None, + }; + Self { + created_at, + display_name, + phone_number, + email_address, + is_admin, + } + } + _ => Self::new(), + } + } + fn from_json_string(input: String) -> Self { + match serde_json::from_str(input.as_str()) { + Ok(val) => Self::from_json(val), + _ => Self::new(), + } + } + fn to_json_string(&self) -> String { + let mut _json_output_ = "{".to_string(); + + _json_output_.push_str("\"created_at\":"); + _json_output_.push_str(serialize_date_time(&self.created_at, true).as_str()); + _json_output_.push_str(",\"display_name\":"); + _json_output_.push_str(serialize_string(&self.display_name).as_str()); + _json_output_.push_str(",\"phone_number\":"); + match &self.phone_number { + Some(phone_number_val) => { + _json_output_.push_str(serialize_string(phone_number_val).as_str()); + } + _ => { + _json_output_.push_str("null"); + } + }; + match &self.email_address { + Some(email_address_val) => { + _json_output_.push_str(",\"email_address\":"); + _json_output_.push_str(serialize_string(email_address_val).as_str()) + } + _ => {} + }; + match &self.is_admin { + Some(is_admin_val) => { + _json_output_.push_str(",\"is_admin\":"); + _json_output_.push_str(is_admin_val.to_string().as_str()) + } + _ => {} + }; + _json_output_.push('}'); + _json_output_ + } + fn to_query_params_string(&self) -> String { + let mut _query_parts_: Vec = Vec::new(); + _query_parts_.push(format!( + "created_at={}", + serialize_date_time(&self.created_at, false) + )); + _query_parts_.push(format!("display_name={}", &self.display_name)); + match &self.phone_number { + Some(phone_number_val) => { + _query_parts_.push(format!("phone_number={}", phone_number_val)); + } + _ => { + _query_parts_.push("phone_number=null".to_string()); + } + }; + match &self.email_address { + Some(email_address_val) => { + _query_parts_.push(format!("email_address={}", email_address_val)); + } + _ => {} + }; + match &self.is_admin { + Some(is_admin_val) => { + _query_parts_.push(format!("is_admin={}", is_admin_val)); + } + _ => {} + }; + _query_parts_.join("&") + } +} + #[derive(Clone, Debug, PartialEq)] pub struct ObjectWithEveryOptionalType { pub any: Option, diff --git a/tests/clients/swift/Sources/TestClient.g.swift b/tests/clients/swift/Sources/TestClient.g.swift index 96d52914..e5a895d2 100644 --- a/tests/clients/swift/Sources/TestClient.g.swift +++ b/tests/clients/swift/Sources/TestClient.g.swift @@ -137,6 +137,28 @@ public class TestClientTestsService { ) return result } + public func sendObjectWithPascalCaseKeys(_ params: ObjectWithPascalCaseKeys) async throws -> ObjectWithPascalCaseKeys { + let result: ObjectWithPascalCaseKeys = try await parsedArriHttpRequest( + delegate: self.delegate, + url: "\(self.baseURL)/rpcs/tests/send-object-with-pascal-case-keys", + method: "POST", + headers: self.headers, + clientVersion: "10", + params: params + ) + return result + } + public func sendObjectWithSnakeCaseKeys(_ params: ObjectWithSnakeCaseKeys) async throws -> ObjectWithSnakeCaseKeys { + let result: ObjectWithSnakeCaseKeys = try await parsedArriHttpRequest( + delegate: self.delegate, + url: "\(self.baseURL)/rpcs/tests/send-object-with-snake-case-keys", + method: "POST", + headers: self.headers, + clientVersion: "10", + params: params + ) + return result + } public func sendPartialObject(_ params: ObjectWithEveryOptionalType) async throws -> ObjectWithEveryOptionalType { let result: ObjectWithEveryOptionalType = try await parsedArriHttpRequest( delegate: self.delegate, @@ -2522,6 +2544,218 @@ public struct ObjectWithEveryNullableTypeNestedArrayElementElement: ArriClientMo } +public struct ObjectWithPascalCaseKeys: ArriClientModel { + public var createdAt: Date = Date() + public var displayName: String = "" + public var phoneNumber: String? + public var emailAddress: String? + public var isAdmin: Bool? + public init( + createdAt: Date, + displayName: String, + phoneNumber: String?, + emailAddress: String?, + isAdmin: Bool? + ) { + self.createdAt = createdAt + self.displayName = displayName + self.phoneNumber = phoneNumber + self.emailAddress = emailAddress + self.isAdmin = isAdmin + } + public init() {} + public init(json: JSON) { + self.createdAt = parseDate(json["CreatedAt"].string ?? "") ?? Date() + self.displayName = json["DisplayName"].string ?? "" + if json["PhoneNumber"].string != nil { + self.phoneNumber = json["PhoneNumber"].string + } + if json["EmailAddress"].exists() { + self.emailAddress = json["EmailAddress"].string + } + if json["IsAdmin"].exists() { + self.isAdmin = json["IsAdmin"].bool + } + } + public init(JSONData: Data) { + do { + let json = try JSON(data: JSONData) + self.init(json: json) + } catch { + print("[WARNING] Error parsing JSON: \(error)") + self.init() + } + } + public init(JSONString: String) { + do { + let json = try JSON(data: JSONString.data(using: .utf8) ?? Data()) + self.init(json: json) + } catch { + print("[WARNING] Error parsing JSON: \(error)") + self.init() + } + } + public func toJSONString() -> String { + var __json = "{" + + __json += "\"CreatedAt\":" + __json += serializeDate(self.createdAt) + __json += ",\"DisplayName\":" + __json += serializeString(input: self.displayName) + __json += ",\"PhoneNumber\":" + if self.phoneNumber != nil { + __json += serializeString(input: self.phoneNumber!) + } else { + __json += "null" + } + if self.emailAddress != nil { + __json += ",\"EmailAddress\":" + __json += serializeString(input: self.emailAddress!) + } + if self.isAdmin != nil { + __json += ",\"IsAdmin\":" +__json += "\(self.isAdmin!)" + } + __json += "}" + return __json + } + public func toURLQueryParts() -> [URLQueryItem] { + var __queryParts: [URLQueryItem] = [] + __queryParts.append(URLQueryItem(name: "CreatedAt", value: serializeDate(self.createdAt, withQuotes: false))) + __queryParts.append(URLQueryItem(name: "DisplayName", value: self.displayName)) + if self.phoneNumber != nil { + __queryParts.append(URLQueryItem(name: "PhoneNumber", value: self.phoneNumber!)) + } else { + __queryParts.append(URLQueryItem(name: "PhoneNumber", value: "null")) + } + if self.emailAddress != nil { + __queryParts.append(URLQueryItem(name: "EmailAddress", value: self.emailAddress!)) + } + if self.isAdmin != nil { + __queryParts.append(URLQueryItem(name: "IsAdmin", value: "\(self.isAdmin!)")) + } + return __queryParts + } + public func clone() -> ObjectWithPascalCaseKeys { + + return ObjectWithPascalCaseKeys( + createdAt: self.createdAt, + displayName: self.displayName, + phoneNumber: self.phoneNumber, + emailAddress: self.emailAddress, + isAdmin: self.isAdmin + ) + } + +} + + +public struct ObjectWithSnakeCaseKeys: ArriClientModel { + public var createdAt: Date = Date() + public var displayName: String = "" + public var phoneNumber: String? + public var emailAddress: String? + public var isAdmin: Bool? + public init( + createdAt: Date, + displayName: String, + phoneNumber: String?, + emailAddress: String?, + isAdmin: Bool? + ) { + self.createdAt = createdAt + self.displayName = displayName + self.phoneNumber = phoneNumber + self.emailAddress = emailAddress + self.isAdmin = isAdmin + } + public init() {} + public init(json: JSON) { + self.createdAt = parseDate(json["created_at"].string ?? "") ?? Date() + self.displayName = json["display_name"].string ?? "" + if json["phone_number"].string != nil { + self.phoneNumber = json["phone_number"].string + } + if json["email_address"].exists() { + self.emailAddress = json["email_address"].string + } + if json["is_admin"].exists() { + self.isAdmin = json["is_admin"].bool + } + } + public init(JSONData: Data) { + do { + let json = try JSON(data: JSONData) + self.init(json: json) + } catch { + print("[WARNING] Error parsing JSON: \(error)") + self.init() + } + } + public init(JSONString: String) { + do { + let json = try JSON(data: JSONString.data(using: .utf8) ?? Data()) + self.init(json: json) + } catch { + print("[WARNING] Error parsing JSON: \(error)") + self.init() + } + } + public func toJSONString() -> String { + var __json = "{" + + __json += "\"created_at\":" + __json += serializeDate(self.createdAt) + __json += ",\"display_name\":" + __json += serializeString(input: self.displayName) + __json += ",\"phone_number\":" + if self.phoneNumber != nil { + __json += serializeString(input: self.phoneNumber!) + } else { + __json += "null" + } + if self.emailAddress != nil { + __json += ",\"email_address\":" + __json += serializeString(input: self.emailAddress!) + } + if self.isAdmin != nil { + __json += ",\"is_admin\":" +__json += "\(self.isAdmin!)" + } + __json += "}" + return __json + } + public func toURLQueryParts() -> [URLQueryItem] { + var __queryParts: [URLQueryItem] = [] + __queryParts.append(URLQueryItem(name: "created_at", value: serializeDate(self.createdAt, withQuotes: false))) + __queryParts.append(URLQueryItem(name: "display_name", value: self.displayName)) + if self.phoneNumber != nil { + __queryParts.append(URLQueryItem(name: "phone_number", value: self.phoneNumber!)) + } else { + __queryParts.append(URLQueryItem(name: "phone_number", value: "null")) + } + if self.emailAddress != nil { + __queryParts.append(URLQueryItem(name: "email_address", value: self.emailAddress!)) + } + if self.isAdmin != nil { + __queryParts.append(URLQueryItem(name: "is_admin", value: "\(self.isAdmin!)")) + } + return __queryParts + } + public func clone() -> ObjectWithSnakeCaseKeys { + + return ObjectWithSnakeCaseKeys( + createdAt: self.createdAt, + displayName: self.displayName, + phoneNumber: self.phoneNumber, + emailAddress: self.emailAddress, + isAdmin: self.isAdmin + ) + } + +} + + public struct ObjectWithEveryOptionalType: ArriClientModel { public var any: JSON? public var boolean: Bool? diff --git a/tests/clients/ts/testClient.rpc.ts b/tests/clients/ts/testClient.rpc.ts index b2dd0400..3b936745 100644 --- a/tests/clients/ts/testClient.rpc.ts +++ b/tests/clients/ts/testClient.rpc.ts @@ -169,6 +169,34 @@ export class TestClientTestsService { clientVersion: "10", }); } + async sendObjectWithPascalCaseKeys( + params: ObjectWithPascalCaseKeys, + ): Promise { + return arriRequest({ + url: `${this._baseUrl}/rpcs/tests/send-object-with-pascal-case-keys`, + method: "post", + headers: this._headers, + params: params, + responseFromJson: $$ObjectWithPascalCaseKeys.fromJson, + responseFromString: $$ObjectWithPascalCaseKeys.fromJsonString, + serializer: $$ObjectWithPascalCaseKeys.toJsonString, + clientVersion: "10", + }); + } + async sendObjectWithSnakeCaseKeys( + params: ObjectWithSnakeCaseKeys, + ): Promise { + return arriRequest({ + url: `${this._baseUrl}/rpcs/tests/send-object-with-snake-case-keys`, + method: "post", + headers: this._headers, + params: params, + responseFromJson: $$ObjectWithSnakeCaseKeys.fromJson, + responseFromString: $$ObjectWithSnakeCaseKeys.fromJsonString, + serializer: $$ObjectWithSnakeCaseKeys.toJsonString, + clientVersion: "10", + }); + } async sendPartialObject( params: ObjectWithEveryOptionalType, ): Promise { @@ -2876,6 +2904,236 @@ export const $$ObjectWithEveryNullableTypeNestedArrayElementElement: ArriModelVa }, }; +export interface ObjectWithPascalCaseKeys { + createdAt: Date; + displayName: string; + phoneNumber: string | null; + emailAddress?: string; + isAdmin?: boolean; +} +export const $$ObjectWithPascalCaseKeys: ArriModelValidator = + { + new(): ObjectWithPascalCaseKeys { + return { + createdAt: new Date(), + displayName: "", + phoneNumber: null, + }; + }, + validate(input): input is ObjectWithPascalCaseKeys { + return ( + isObject(input) && + input.createdAt instanceof Date && + typeof input.displayName === "string" && + (typeof input.phoneNumber === "string" || + input.phoneNumber === null) && + (typeof input.emailAddress === "string" || + typeof input.emailAddress === "undefined") && + (typeof input.isAdmin === "boolean" || + typeof input.isAdmin === "undefined") + ); + }, + fromJson(input): ObjectWithPascalCaseKeys { + let _CreatedAt: Date; + if (typeof input.CreatedAt === "string") { + _CreatedAt = new Date(input.CreatedAt); + } else if (input.CreatedAt instanceof Date) { + _CreatedAt = input.CreatedAt; + } else { + _CreatedAt = new Date(); + } + let _DisplayName: string; + if (typeof input.DisplayName === "string") { + _DisplayName = input.DisplayName; + } else { + _DisplayName = ""; + } + let _PhoneNumber: string | null; + if (typeof input.PhoneNumber === "string") { + _PhoneNumber = input.PhoneNumber; + } else { + _PhoneNumber = null; + } + let _EmailAddress: string | undefined; + if (typeof input.EmailAddress !== "undefined") { + if (typeof input.EmailAddress === "string") { + _EmailAddress = input.EmailAddress; + } else { + _EmailAddress = ""; + } + } + let _IsAdmin: boolean | undefined; + if (typeof input.IsAdmin !== "undefined") { + if (typeof input.IsAdmin === "boolean") { + _IsAdmin = input.IsAdmin; + } else { + _IsAdmin = false; + } + } + return { + createdAt: _CreatedAt, + displayName: _DisplayName, + phoneNumber: _PhoneNumber, + emailAddress: _EmailAddress, + isAdmin: _IsAdmin, + }; + }, + fromJsonString(input): ObjectWithPascalCaseKeys { + return $$ObjectWithPascalCaseKeys.fromJson(JSON.parse(input)); + }, + toJsonString(input): string { + let json = "{"; + json += '"CreatedAt":'; + json += `"${input.createdAt.toISOString()}"`; + json += ',"DisplayName":'; + json += serializeString(input.displayName); + json += ',"PhoneNumber":'; + if (typeof input.phoneNumber === "string") { + json += serializeString(input.phoneNumber); + } else { + json += "null"; + } + if (typeof input.emailAddress !== "undefined") { + json += `,"EmailAddress":`; + json += serializeString(input.emailAddress); + } + if (typeof input.isAdmin !== "undefined") { + json += `,"IsAdmin":`; + json += `${input.isAdmin}`; + } + json += "}"; + return json; + }, + toUrlQueryString(input): string { + const queryParts: string[] = []; + queryParts.push(`CreatedAt=${input.createdAt.toISOString()}`); + queryParts.push(`DisplayName=${input.displayName}`); + queryParts.push(`PhoneNumber=${input.phoneNumber}`); + if (typeof input.emailAddress !== "undefined") { + queryParts.push(`EmailAddress=${input.emailAddress}`); + } + if (typeof input.isAdmin !== "undefined") { + queryParts.push(`IsAdmin=${input.isAdmin}`); + } + return queryParts.join("&"); + }, + }; + +export interface ObjectWithSnakeCaseKeys { + createdAt: Date; + displayName: string; + phoneNumber: string | null; + emailAddress?: string; + isAdmin?: boolean; +} +export const $$ObjectWithSnakeCaseKeys: ArriModelValidator = + { + new(): ObjectWithSnakeCaseKeys { + return { + createdAt: new Date(), + displayName: "", + phoneNumber: null, + }; + }, + validate(input): input is ObjectWithSnakeCaseKeys { + return ( + isObject(input) && + input.createdAt instanceof Date && + typeof input.displayName === "string" && + (typeof input.phoneNumber === "string" || + input.phoneNumber === null) && + (typeof input.emailAddress === "string" || + typeof input.emailAddress === "undefined") && + (typeof input.isAdmin === "boolean" || + typeof input.isAdmin === "undefined") + ); + }, + fromJson(input): ObjectWithSnakeCaseKeys { + let _created_at: Date; + if (typeof input.created_at === "string") { + _created_at = new Date(input.created_at); + } else if (input.created_at instanceof Date) { + _created_at = input.created_at; + } else { + _created_at = new Date(); + } + let _display_name: string; + if (typeof input.display_name === "string") { + _display_name = input.display_name; + } else { + _display_name = ""; + } + let _phone_number: string | null; + if (typeof input.phone_number === "string") { + _phone_number = input.phone_number; + } else { + _phone_number = null; + } + let _email_address: string | undefined; + if (typeof input.email_address !== "undefined") { + if (typeof input.email_address === "string") { + _email_address = input.email_address; + } else { + _email_address = ""; + } + } + let _is_admin: boolean | undefined; + if (typeof input.is_admin !== "undefined") { + if (typeof input.is_admin === "boolean") { + _is_admin = input.is_admin; + } else { + _is_admin = false; + } + } + return { + createdAt: _created_at, + displayName: _display_name, + phoneNumber: _phone_number, + emailAddress: _email_address, + isAdmin: _is_admin, + }; + }, + fromJsonString(input): ObjectWithSnakeCaseKeys { + return $$ObjectWithSnakeCaseKeys.fromJson(JSON.parse(input)); + }, + toJsonString(input): string { + let json = "{"; + json += '"created_at":'; + json += `"${input.createdAt.toISOString()}"`; + json += ',"display_name":'; + json += serializeString(input.displayName); + json += ',"phone_number":'; + if (typeof input.phoneNumber === "string") { + json += serializeString(input.phoneNumber); + } else { + json += "null"; + } + if (typeof input.emailAddress !== "undefined") { + json += `,"email_address":`; + json += serializeString(input.emailAddress); + } + if (typeof input.isAdmin !== "undefined") { + json += `,"is_admin":`; + json += `${input.isAdmin}`; + } + json += "}"; + return json; + }, + toUrlQueryString(input): string { + const queryParts: string[] = []; + queryParts.push(`created_at=${input.createdAt.toISOString()}`); + queryParts.push(`display_name=${input.displayName}`); + queryParts.push(`phone_number=${input.phoneNumber}`); + if (typeof input.emailAddress !== "undefined") { + queryParts.push(`email_address=${input.emailAddress}`); + } + if (typeof input.isAdmin !== "undefined") { + queryParts.push(`is_admin=${input.isAdmin}`); + } + return queryParts.join("&"); + }, + }; + export interface ObjectWithEveryOptionalType { any?: any; boolean?: boolean; diff --git a/tests/clients/ts/testClient.test.ts b/tests/clients/ts/testClient.test.ts index 61fbf416..b333b094 100644 --- a/tests/clients/ts/testClient.test.ts +++ b/tests/clients/ts/testClient.test.ts @@ -7,6 +7,8 @@ import { type ObjectWithEveryNullableType, type ObjectWithEveryOptionalType, type ObjectWithEveryType, + ObjectWithPascalCaseKeys, + ObjectWithSnakeCaseKeys, type RecursiveObject, type RecursiveUnion, TestClient, @@ -128,6 +130,28 @@ test("can send/receive object every field type", async () => { const result = await client.tests.sendObject(input); expect(result).toStrictEqual(input); }); +test("can send/receive object with transformed keys", async () => { + const snakeCasePayload: ObjectWithSnakeCaseKeys = { + createdAt: new Date(), + displayName: "john doe", + emailAddress: "johndoe@gmail.com", + phoneNumber: null, + isAdmin: false, + }; + const snakeCaseResult = + await client.tests.sendObjectWithSnakeCaseKeys(snakeCasePayload); + expect(snakeCaseResult).toStrictEqual(snakeCasePayload); + const pascalCasePayload: ObjectWithPascalCaseKeys = { + createdAt: new Date(), + emailAddress: undefined, + isAdmin: undefined, + displayName: "john doe", + phoneNumber: "2112112111", + }; + const pascalCaseResult = + await client.tests.sendObjectWithPascalCaseKeys(pascalCasePayload); + expect(pascalCaseResult).toStrictEqual(pascalCasePayload); +}); test("returns error if sending nothing when RPC expects body", async () => { try { await ofetch(`${baseUrl}/rpcs/tests/send-object`, { diff --git a/tests/servers/ts/src/procedures/tests/sendObjectWithPascalCaseKeys.rpc.ts b/tests/servers/ts/src/procedures/tests/sendObjectWithPascalCaseKeys.rpc.ts new file mode 100644 index 00000000..cf8562f2 --- /dev/null +++ b/tests/servers/ts/src/procedures/tests/sendObjectWithPascalCaseKeys.rpc.ts @@ -0,0 +1,18 @@ +import { a } from "@arrirpc/schema"; +import { defineRpc } from "@arrirpc/server"; + +const ObjectWithPascalCaseKeys = a.object("ObjectWithPascalCaseKeys", { + CreatedAt: a.timestamp(), + DisplayName: a.string(), + EmailAddress: a.optional(a.string()), + PhoneNumber: a.nullable(a.string()), + IsAdmin: a.optional(a.boolean()), +}); + +export default defineRpc({ + params: ObjectWithPascalCaseKeys, + response: ObjectWithPascalCaseKeys, + handler({ params }) { + return params; + }, +}); diff --git a/tests/servers/ts/src/procedures/tests/sendObjectWithSnakeCaseKeys.rpc.ts b/tests/servers/ts/src/procedures/tests/sendObjectWithSnakeCaseKeys.rpc.ts new file mode 100644 index 00000000..50316d4f --- /dev/null +++ b/tests/servers/ts/src/procedures/tests/sendObjectWithSnakeCaseKeys.rpc.ts @@ -0,0 +1,18 @@ +import { a } from "@arrirpc/schema"; +import { defineRpc } from "@arrirpc/server"; + +const ObjectWithSnakeCaseKeys = a.object("ObjectWithSnakeCaseKeys", { + created_at: a.timestamp(), + display_name: a.string(), + email_address: a.optional(a.string()), + phone_number: a.nullable(a.string()), + is_admin: a.optional(a.boolean()), +}); + +export default defineRpc({ + params: ObjectWithSnakeCaseKeys, + response: ObjectWithSnakeCaseKeys, + handler({ params }) { + return params; + }, +}); From e3f44d1426a8fb22c50df730abebf8f52ae72f9e Mon Sep 17 00:00:00 2001 From: Joshua Sosso Date: Thu, 21 Nov 2024 23:21:04 -0600 Subject: [PATCH 3/3] add integration tests to go server --- languages/go/go-server/decode_json.go | 21 +++++++++++++-------- tests/servers/go/main.go | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/languages/go/go-server/decode_json.go b/languages/go/go-server/decode_json.go index 07ede256..d81b9d8f 100644 --- a/languages/go/go-server/decode_json.go +++ b/languages/go/go-server/decode_json.go @@ -605,14 +605,19 @@ func structFromJSON(data *gjson.Result, target reflect.Value, c *ValidationConte if !fieldMeta.IsExported() { continue } - switch c.KeyCasing { - case KeyCasingCamelCase: - fieldName = strcase.ToLowerCamel(fieldName) - case KeyCasingPascalCase: - case KeyCasingSnakeCase: - fieldName = strcase.ToSnake(fieldName) - default: - fieldName = strcase.ToLowerCamel(fieldName) + keyTag := fieldMeta.Tag.Get("key") + if len(keyTag) > 0 { + fieldName = keyTag + } else { + switch c.KeyCasing { + case KeyCasingCamelCase: + fieldName = strcase.ToLowerCamel(fieldName) + case KeyCasingPascalCase: + case KeyCasingSnakeCase: + fieldName = strcase.ToSnake(fieldName) + default: + fieldName = strcase.ToLowerCamel(fieldName) + } } jsonResult := data.Get(fieldName) enumValues := None[[]string]() diff --git a/tests/servers/go/main.go b/tests/servers/go/main.go index b6ec5327..f268387d 100644 --- a/tests/servers/go/main.go +++ b/tests/servers/go/main.go @@ -74,6 +74,8 @@ func main() { arri.ScopedRpc(&app, "tests", SendError, arri.RpcOptions{}) arri.ScopedRpc(&app, "tests", SendObject, arri.RpcOptions{}) arri.ScopedRpc(&app, "tests", SendObjectWithNullableFields, arri.RpcOptions{}) + arri.ScopedRpc(&app, "tests", SendObjectWithPascalCaseKeys, arri.RpcOptions{}) + arri.ScopedRpc(&app, "tests", SendObjectWithSnakeCaseKeys, arri.RpcOptions{}) arri.ScopedRpc(&app, "tests", SendPartialObject, arri.RpcOptions{}) arri.ScopedRpc(&app, "tests", SendRecursiveObject, arri.RpcOptions{}) arri.ScopedRpc(&app, "tests", SendRecursiveUnion, arri.RpcOptions{}) @@ -236,6 +238,30 @@ func SendObjectWithNullableFields(params ObjectWithEveryNullableType, _ AppConte return params, nil } +type ObjectWithPascalCaseKeys struct { + CreatedAt time.Time `key:"CreatedAt"` + DisplayName string `key:"DisplayName"` + EmailAddress arri.Option[string] `key:"EmailAddress"` + PhoneNumber arri.Nullable[string] `key:"PhoneNumber"` + IsAdmin arri.Option[bool] `key:"IsAdmin"` +} + +func SendObjectWithPascalCaseKeys(params ObjectWithPascalCaseKeys, _ AppContext) (ObjectWithPascalCaseKeys, arri.RpcError) { + return params, nil +} + +type ObjectWithSnakeCaseKeys struct { + CreatedAt time.Time `key:"created_at"` + DisplayName string `key:"display_name"` + EmailAddress arri.Option[string] `key:"email_address"` + PhoneNumber arri.Nullable[string] `key:"phone_number"` + IsAdmin arri.Option[bool] `key:"is_admin"` +} + +func SendObjectWithSnakeCaseKeys(params ObjectWithSnakeCaseKeys, _ AppContext) (ObjectWithSnakeCaseKeys, arri.RpcError) { + return params, nil +} + type ObjectWithEveryOptionalType struct { Any arri.Option[any] Boolean arri.Option[bool]