Kotlin Object Multiplatform Mapper: сопоставляем коллекции правильно

Map'им List'ы в AI генераторе изображений

Map’им List’ы в AI генераторе изображений

После публикации первой статьи я получил несколько отзывов и предложений, некоторые из которых взял в разработку. Первой доработкой стал автоматический маппинг коллекций.

Iterrable как источник

Проведя небольшое исследование, я пришел к выводу, что проверять относится ли поле к коллекции лучше всего по наличию интерфейса Iterable в родителях. Это дает следующие возможности:

  1. Возможность вызывать такие методы как: toList (), toSet () и т.д.

  2. Имеется метод map для внутреннего сопоставления.

Сопоставляем коллекции с одинаковыми типами элементов

Тут, в целом, все тоже самое, что и было уже реализовано для сопоставления свойств в целом:

class SourceObject {
    val intList = listOf(1, 2, 3)
}

@KOMMMap(from = SourceObject::class)
data class DestinationObject(
    val intList: List
    @MapFrom("intList")
    val intSet: Set
)

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
)

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
)

public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
	stringList = intList.map { it.toString() }.toMutableList()
)

Да тоже ничего сложного! А что если свойство-исходник еще и nullable? При allowNotNullAssertion все вроде легко:

class SourceObject {
    val intList: List? = listOf(1, 2, 3)
}

@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))
data class DestinationObject(
    @MapFrom("intList")
    val stringList: MutableList
)

public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
	stringList = intList!!.map { it.toString() }.toMutableList()
)

Применим NullSubstitute. И вот тут начинается сложность, ибо надо все покрывать ?.:

class SourceObject {
    val intList: List? = listOf(1, 2, 3)
}

@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))
data class DestinationObject(
    @NullSubstitute(MapDefault(StringListResolver::class), "intList")
    val stringList: MutableList
)

public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
	stringList = intList?.map { it.toString() }?.toMutableList() ?: StringListResolver(null).resolve()
)

Однако нужно еще учесть нюанс, что map всегда возвращает List, и делать приведение в этом случае не нужно:

class SourceObject {
    val intSet: Set? = setOf(1, 2, 3)
}

@KOMMMap(from = SourceObject::class, config = MapConfiguration(allowNotNullAssertion = true))
data class DestinationObject(
    @NullSubstitute(MapDefault(StringListResolver::class), "intSet")
    val stringList: List
)

public fun SourceObject.toDestinationObject(): DestinationObject = DestinationObject(
	stringList = intSet?.map { it.toString() } ?: StringListResolver(null).resolve()
)

Что дальше

Следующим в работу может уйти сопоставления коллекций с другими свойствами. Тут придется повозиться с такими случаями, как String <-> List<*>, ибо нужно тогда указывать делитель. Может быть надо сделать глобальную настройку в KOMMMap, с возможностью перебить в аннотации самого свойства. Однако делать отдельную аннотацию для этого, выглядит не очень. Но и добавлять такую опцию в каждую существующую аннотацию (по примеру с name) тоже не выглядит идеальным решением. Плюс, совершенно не ясно, нужны ли случаи List → Int и прочее. Возможно, решение с abstract StringToList и abstract ListToString конверторами будет лучшим, хоть и заставит сделать небольшие дополнительные действия со стороны разработчика при использовании. Посмотрим!

Новая версия библиотеки 0.2.0, когда вы читаете эту статью, скорее всего уже доступна.

Буду держать в курсе развития библиотеки!

© Habrahabr.ru