Kotlin Object Multiplatform Mapper: сопоставляем коллекции правильно
Map'им List'ы в AI генераторе изображений
После публикации первой статьи я получил несколько отзывов и предложений, некоторые из которых взял в разработку. Первой доработкой стал автоматический маппинг коллекций.
Iterrable как источник
Проведя небольшое исследование, я пришел к выводу, что проверять относится ли поле к коллекции лучше всего по наличию интерфейса Iterable в родителях. Это дает следующие возможности:
- Возможность вызывать такие методы как: toList(), toSet() и т.д.
- Имеется метод map для внутреннего сопоставления.
Сопоставляем коллекции с одинаковыми типами элементов
Тут, в целом, все тоже самое, что и было уже реализовано для сопоставления свойств в целом:
class SourceObject { val intList = listOf(1, 2, 3)}@KOMMMap(from = SourceObject::class)data class DestinationObject( val intList: List<Int> @MapFrom("intList") val intSet: Set<Int>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( intList = intList, intSet = intList.toSet())
Nullable коллекция в не'nullable будет сопоставляться в таком случае по тем же правилам, что и другие свойства. Интересное начинается, когда...
Сопоставляем коллекции с разными типами элементов
class SourceObject { val intList = listOf(1, 2, 3)}@KOMMMap(from = SourceObject::class)data class DestinationObject( @MapFrom("intList") val stringList: List<String>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( stringList = intList.map { it.toString() })
И вроде все легко, но что, если свойство-приёмник и само по себе отличается от исходника:
class SourceObject { val intList = listOf(1, 2, 3)}@KOMMMap(from = SourceObject::class)data class DestinationObject( @MapFrom("intList") val stringList: MutableList<String>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( stringList = intList.map { it.toString() }.toMutableList())
Да тоже ничего сложного! А что если свойство-исходник еще и nullable? При allowNotNullAssertion все вроде легко:
class SourceObject { val intList: List<Int>? = listOf(1, 2, 3)}@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))data class DestinationObject( @MapFrom("intList") val stringList: MutableList<String>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( stringList = intList!!.map { it.toString() }.toMutableList())
Применим NullSubstitute. И вот тут начинается сложность, ибо надо все покрывать ?.:
class SourceObject { val intList: List<Int>? = listOf(1, 2, 3)}@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))data class DestinationObject( @NullSubstitute(MapDefault(StringListResolver::class), "intList") val stringList: MutableList<String>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( stringList = intList?.map { it.toString() }?.toMutableList() ?: StringListResolver(null).resolve())
Однако нужно еще учесть нюанс, что map всегда возвращает List, и делать приведение в этом случае не нужно:
class SourceObject { val intSet: Set<Int>? = setOf(1, 2, 3)}@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))data class DestinationObject( @NullSubstitute(MapDefault(StringListResolver::class), "intSet") val stringList: List<String>)public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject( stringList = intSet?.map { it.toString() } ?: StringListResolver(null).resolve())
Что дальше
Следующим в работу может уйти сопоставления коллекций с другими свойствами. Тут придется повозиться с такими случаями, как String <-> List<*>, ибо нужно тогда указывать делитель. Может быть надо сделать глобальную настройку в KOMMMap, с возможностью перебить в аннотации самого свойства. Однако делать отдельную аннотацию для этого, выглядит не очень. Но и добавлять такую опцию в каждую существующую аннотацию (по примеру с name) тоже не выглядит идеальным решением. Плюс, совершенно не ясно, нужны ли случаи List<Int> -> Int и прочее. Возможно, решение с abstract StringToList и abstract ListToString конверторами будет лучшим, хоть и заставит сделать небольшие дополнительные действия со стороны разработчика при использовании. Посмотрим!
Новая версия библиотеки 0.2.0, когда вы читаете эту статью, скорее всего уже доступна.