From 3c722b3bb7048efb7f234a486a4537048e1f2ccb Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Sat, 22 Feb 2025 15:39:26 +0100 Subject: [PATCH] Update Dart duration serialization to be compatible with Kotlin ISO-8601 duration string parsing --- CHANGELOG.md | 1 + example/flutter/example/lib/main.dart | 8 +++++++ example/flutter/lib/MyTestModule.dart | 11 ++++++++- example/flutter/lib/models.dart | 11 ++++++++- .../flutterkmp/ksp/processor/DartGenerator.kt | 4 ++-- .../voize/flutterkmp/ksp/processor/utils.kt | 23 +++++++++++++++++-- 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22646d0..82f5fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## unreleased - Fix conflicts with same method names across different modules on iOS by prefixing method names +- Update Dart duration serialization to be compatible with Kotlin ISO-8601 duration string parsing ## v0.1.0-rc.2 diff --git a/example/flutter/example/lib/main.dart b/example/flutter/example/lib/main.dart index 198ffe3..42d6569 100644 --- a/example/flutter/example/lib/main.dart +++ b/example/flutter/example/lib/main.dart @@ -139,6 +139,7 @@ class _MyAppState extends State { print(await myTestModule.localDateMethod(DateTime.now())); print(await myTestModule.localTimeMethod(TimeOfDay.now())); print(await myTestModule.durationMethod(const Duration(seconds: 123))); + print(await myTestModule.durationMethod(const Duration(days: 1000))); print(await myTestModule.dateClassMethod(MyDateClass( DateTime.now(), @@ -147,6 +148,13 @@ class _MyAppState extends State { const Duration(seconds: 123), DateTime.now().toUtc()))); + print(await myTestModule.dateClassMethod(MyDateClass( + DateTime.now(), + TimeOfDay.now(), + DateTime.now(), + const Duration(days: 1000), + DateTime.now().toUtc()))); + final broadcaster = myTestModule.intEvents; // this starts a collect on the counter flow that is shared across all listeners diff --git a/example/flutter/lib/MyTestModule.dart b/example/flutter/lib/MyTestModule.dart index d74340e..1366ce6 100644 --- a/example/flutter/lib/MyTestModule.dart +++ b/example/flutter/lib/MyTestModule.dart @@ -349,7 +349,16 @@ final localDateSerialized = localDate.toIso8601String().split('T').first; return result; } Future durationMethod(Duration duration) async { - final durationSerialized = duration.toIso8601String(); + final durationSerialized = duration.toIso8601String().replaceFirstMapped(RegExp(r'P([^T]*)(T.*)?'), (m) { + int totalDays = 0; + String datePart = m[1]! + .replaceAllMapped(RegExp(r'(\d+)Y'), (y) { totalDays += int.parse(y[1]!) * 365; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)M'), (m) { totalDays += int.parse(m[1]!) * 30; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)W'), (w) { totalDays += int.parse(w[1]!) * 7; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)D'), (d) { totalDays += int.parse(d[1]!); return ''; }); + + return 'P' + (totalDays > 0 ? '${totalDays}D' : '') + (m[2] ?? ''); + }); final invokeResult = await methodChannelToNative.invokeMethod( 'MyTestModule_durationMethod', [durationSerialized], diff --git a/example/flutter/lib/models.dart b/example/flutter/lib/models.dart index 2d1b217..d688f78 100644 --- a/example/flutter/lib/models.dart +++ b/example/flutter/lib/models.dart @@ -85,7 +85,16 @@ MyDateClass(this.date, this.time, this.dateTime, this.duration, this.instant); static TimeOfDay _timeFromJson(String json) => TimeOfDay.fromDateTime(DateTime.parse("1998-01-01T$json:00.000")); - static String _durationToJson(Duration obj) => obj.toIso8601String(); + static String _durationToJson(Duration obj) => obj.toIso8601String().replaceFirstMapped(RegExp(r'P([^T]*)(T.*)?'), (m) { + int totalDays = 0; + String datePart = m[1]! + .replaceAllMapped(RegExp(r'(\d+)Y'), (y) { totalDays += int.parse(y[1]!) * 365; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)M'), (m) { totalDays += int.parse(m[1]!) * 30; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)W'), (w) { totalDays += int.parse(w[1]!) * 7; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)D'), (d) { totalDays += int.parse(d[1]!); return ''; }); + + return 'P' + (totalDays > 0 ? '${totalDays}D' : '') + (m[2] ?? ''); + }); static Duration _durationFromJson(String json) => parseIso8601Duration(json); diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt index 14224b1..29f1159 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt @@ -520,7 +520,7 @@ ${declaration.declarations.filterIsInstance() val serializedType: String, ) - val customSerializations = listOf( + private val customSerializations = listOf( // TimeOfDay is part of the material package and is not serializable by default CustomSerialization( detect = { prop -> @@ -548,7 +548,7 @@ ${declaration.declarations.filterIsInstance() this.toDartType() is DartType.Duration } }, - serializeFnString = { varName, _ -> "$varName.toIso8601String()" }, + serializeFnString = { varName, _ -> "$varName.toIso8601String().$patchIso8601DurationStringDartCode" }, deserializeFnString = { varName, _ -> "parseIso8601Duration($varName)" }, serializedType = "String", ), diff --git a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/utils.kt b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/utils.kt index 8376302..bb86437 100644 --- a/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/utils.kt +++ b/flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/utils.kt @@ -346,7 +346,7 @@ internal fun DartType.getDartSerializationStatement(varName: String): String? { is DartType.Primitive.Int, is DartType.Primitive.String -> return null } - is DartType.Duration -> "${varName}.toIso8601String()" + is DartType.Duration -> "${varName}.toIso8601String().$patchIso8601DurationStringDartCode" is DartType.TimeOfDay -> "\"${'$'}{$varName.hour.toString().padLeft(2, '0')}:${'$'}{$varName.minute.toString().padLeft(2, '0')}\"" is DartType.LocalDate -> "${varName}.toIso8601String().split('T').first" is DartType.DateTime, is DartType.LocalDateTime -> "$varName.toIso8601String()" @@ -362,4 +362,23 @@ internal fun DartType.getDartSerializationStatement(varName: String): String? { is DartType.List -> "jsonEncode(${transformEnumsForSerialization(varName, this)})" is DartType.Map -> "jsonEncode(${transformEnumsForSerialization(varName, this)})" } + ";" -} \ No newline at end of file +} + +/** + * The Pub package we use to parse Dart Durations to ISO-8601 strings, [iso_duration](https://github.com/pti/iso_duration), + * uses W, M, Y for day designators, which are not supported in kotlin.time.Duration ISO-8601 string parsing, + * see [here](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/-duration/-companion/parse-iso-string.html). + * + * This code runs as an extension on a "x.toIso8601String()" call to patch + * the ISO-8601 duration string to use D instead of W, M, Y with W=7D, M=30D, Y=365D. + */ +internal const val patchIso8601DurationStringDartCode = """replaceFirstMapped(RegExp(r'P([^T]*)(T.*)?'), (m) { + int totalDays = 0; + String datePart = m[1]! + .replaceAllMapped(RegExp(r'(\d+)Y'), (y) { totalDays += int.parse(y[1]!) * 365; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)M'), (m) { totalDays += int.parse(m[1]!) * 30; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)W'), (w) { totalDays += int.parse(w[1]!) * 7; return ''; }) + .replaceAllMapped(RegExp(r'(\d+)D'), (d) { totalDays += int.parse(d[1]!); return ''; }); + + return 'P' + (totalDays > 0 ? '${"$"}{totalDays}D' : '') + (m[2] ?? ''); + })"""