-
Notifications
You must be signed in to change notification settings - Fork 309
Kotlin: Support record & enum serialization #2753
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
base: main
Are you sure you want to change the base?
Changes from all commits
b06b7bd
b52a420
c487b93
4077557
855ae4e
c43dbde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to rename this parent directory now? maybe something like "generated_serialization" or similar - this each test having a unique name would be ok (eg, "test_codable" for swift and "test_serialization" for kotlin. I'm not too bothered if you don't want to do this here or would prefer it be done in a followup etc. (The name "codable" always gets me - I don't immediately think "serialization" when I see it :) |
||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import kotlinx.serialization.* | ||
| import kotlinx.serialization.json.* | ||
| import uniffi.codable_test.* | ||
|
|
||
| val simpleRecord = SimpleRecord( | ||
| string = "Test", | ||
| boolean = true, | ||
| integer = 1, | ||
| floatVar = 1.1, | ||
| vec = listOf(true), | ||
| ) | ||
|
|
||
| val jsonSimpleRecord = Json.encodeToString(simpleRecord) | ||
| val deserializedSimpleRecord = Json.decodeFromString<SimpleRecord>(jsonSimpleRecord) | ||
| assert(deserializedSimpleRecord.string == "Test") | ||
| assert(deserializedSimpleRecord.boolean) | ||
| assert(deserializedSimpleRecord.integer == 1) | ||
| assert(deserializedSimpleRecord.floatVar == 1.1) | ||
| assert(deserializedSimpleRecord.vec == listOf(true)) | ||
|
|
||
| // MultiLayerRecord | ||
| val multilayer = MultiLayerRecord( | ||
| simpleEnum = SimpleEnum.ONE, | ||
| reprU8 = ReprU8.TWO, | ||
| simpleRecord = simpleRecord, | ||
| ) | ||
|
|
||
| val jsonMultiLayerRecord = Json.encodeToString(multilayer) | ||
| val deserializedMultiLayerRecord = Json.decodeFromString<MultiLayerRecord>(jsonMultiLayerRecord) | ||
| assert(deserializedMultiLayerRecord.simpleEnum == SimpleEnum.ONE) | ||
| assert(deserializedMultiLayerRecord.reprU8 == ReprU8.TWO) | ||
| assert(deserializedMultiLayerRecord.simpleRecord == simpleRecord) | ||
|
|
||
| // RecordWithOptionals | ||
| val optionals = RecordWithOptionals( | ||
| string = null, | ||
| boolean = null, | ||
| integer = null, | ||
| floatVar = null, | ||
| vec = listOf("A", "B"), | ||
| ) | ||
|
|
||
| val implicitNulls = Json { explicitNulls = false } | ||
| val jsonRecordWithOptionals = implicitNulls.encodeToString(optionals) | ||
| assert(jsonRecordWithOptionals == """{"vec":["A","B"]}""") | ||
|
|
||
| // ComplexEnum | ||
| val withClassDiscriminator = Json { classDiscriminator = "#class" } | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will embed [bindings.kotlin.serialization.ComplexEnum.String]
serial_name = "complex_enum_string"which will put
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming you don't intend blocking this PR on that tweak, it might be worth opening a PR where we can discuss this, then add a short comment in the fixture itself which notes what you said and links to the PR, so people exploring this fixture also discovers this. Re the idea for customizing, that seems good - another alternative might be a format similar to what we did for renaming - https://mozilla.github.io/uniffi-rs/next/renaming.html - which would be more like: (I've no idea if that example makes actual sense in this context, but some degree of consistency makes some sense) |
||
| val complexEnum = ComplexEnum.String("test") | ||
| val jsonComplexEnum = withClassDiscriminator.encodeToString<ComplexEnum>(complexEnum) | ||
| val deserializedComplexEnum = withClassDiscriminator.decodeFromString<ComplexEnum>(jsonComplexEnum) | ||
| assert(deserializedComplexEnum == ComplexEnum.String("test")) | ||
|
|
||
| // SimpleError: Exceptions are not serializable in Kotlin | ||
| // val simpleException = SimpleException.OsException() | ||
| // val jsonSimpleException = Json.encodeToString(simpleException) | ||
| // val deserializedSimpleException = Json.decodeFromString<SimpleException>(jsonSimpleException) | ||
| // assert(deserializedSimpleException == SimpleException.OsException()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,4 @@ | ||
| uniffi::build_foreign_language_testcases!("tests/bindings/test_codable.swift",); | ||
| uniffi::build_foreign_language_testcases!( | ||
| "tests/bindings/test_codable.kts", | ||
| "tests/bindings/test_codable.swift", | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,5 @@ | ||
| [bindings.kotlin] | ||
| generate_serializable_types = true | ||
|
|
||
| [bindings.swift] | ||
| generate_codable_conformance = true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,8 @@ pub struct Config { | |
| disable_java_cleaner: bool, | ||
| #[serde(default)] | ||
| pub(super) rename: toml::Table, | ||
| #[serde(default)] | ||
| generate_serializable_types: bool, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any better names for this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe |
||
| } | ||
|
|
||
| impl Config { | ||
|
|
@@ -903,6 +905,78 @@ mod filters { | |
| let spaces = usize::try_from(*spaces).unwrap_or_default(); | ||
| Ok(textwrap::indent(&wrapped, &" ".repeat(spaces))) | ||
| } | ||
|
|
||
| fn serializable_type(ty: &Type, ci: &ComponentInterface) -> Result<bool, askama::Error> { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be okay to make
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, that would be fine and better than copying it |
||
| Ok(match ty { | ||
| Type::Object { .. } | Type::CallbackInterface { .. } => false, | ||
| Type::Record { name, .. } => serializable_record( | ||
| ci.get_record_definition(name) | ||
| .ok_or_else(|| to_askama_error(&format!("could not find record '{name}'")))?, | ||
| ci, | ||
| )?, | ||
| Type::Enum { name, .. } => serializable_enum( | ||
| ci.get_enum_definition(name) | ||
| .ok_or_else(|| to_askama_error(&format!("could not find enum '{name}'")))?, | ||
| ci, | ||
| )?, | ||
| Type::Optional { inner_type } | Type::Sequence { inner_type } => { | ||
| serializable_type(inner_type, ci)? | ||
| } | ||
| Type::Map { | ||
| key_type, | ||
| value_type, | ||
| } => serializable_type(key_type, ci)? && serializable_type(value_type, ci)?, | ||
| // Assume a custom type using a serializable type is also serializable. | ||
| Type::Custom { builtin, .. } => serializable_type(builtin, ci)?, | ||
| _ => true, | ||
| }) | ||
| } | ||
|
|
||
| pub fn serializable_record( | ||
| record: &Record, | ||
| ci: &ComponentInterface, | ||
| ) -> Result<bool, askama::Error> { | ||
| for field in record.fields() { | ||
| if !serializable_type(&field.as_type(), ci)? { | ||
| return Ok(false); | ||
| } | ||
| } | ||
| Ok(true) | ||
| } | ||
|
|
||
| pub fn serializable_enum(e: &Enum, ci: &ComponentInterface) -> Result<bool, askama::Error> { | ||
| if ci.is_name_used_as_error(e.name()) { | ||
| return Ok(false); | ||
| } | ||
|
|
||
| if e.is_flat() { | ||
| let Some(variant_discr_type) = e.variant_discr_type() else { | ||
| return Ok(true); | ||
| }; | ||
| return serializable_type(variant_discr_type, ci); | ||
| } | ||
|
|
||
| // Unlike records or enum variants, if any of the variants are serializable, the | ||
| // enum can be marked as serializable. | ||
| for variant in e.variants() { | ||
| if serializable_enum_variant(variant, ci)? { | ||
| return Ok(true); | ||
| } | ||
| } | ||
| Ok(false) | ||
| } | ||
|
|
||
| pub fn serializable_enum_variant( | ||
| variant: &Variant, | ||
| ci: &ComponentInterface, | ||
| ) -> Result<bool, askama::Error> { | ||
| for field in variant.fields() { | ||
| if !serializable_type(&field.as_type(), ci)? { | ||
| return Ok(false); | ||
| } | ||
| } | ||
| Ok(true) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest version of KotlinX Serialization is currently 1.9.0. Since the CI is also using KotlinX Coroutines 1.6.3, whose latest version is also 1.10.2, I chose a version released at the time, when Kotlin 2.0 had not been released yet.
Should we fix the Kotlin version and upgrade the dependencies here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense to me. Also, here I think it might be worth mentioning that the serialization libs are optional - eg, something like "Optionally, if you choose to use the
generate_serializable_typesoption so that serializable types are generated, you will need ..."?