From c73a2171625dbb09e3f7293091676a844fb2f565 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Tue, 17 Dec 2024 14:37:12 +0900 Subject: [PATCH] add rss serialization --- gradle/libs.versions.toml | 2 + shared/build.gradle.kts | 1 + .../dimension/flare/data/network/rss/Rss.kt | 22 ++ .../flare/data/network/rss/model/Feed.kt | 359 ++++++++++++++++++ 4 files changed, 384 insertions(+) create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/Rss.kt create mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a5907d3a..6340ef2c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -172,6 +172,8 @@ precompose-molecule = { module = "moe.tlaster:precompose-molecule", version = "1 compose-cupertino = { module = "io.github.alexzhirkevich:cupertino-core", version = "0.1.0-alpha04" } +xmlUtil = { module = "io.github.pdvrieze.xmlutil:serialization", version = "0.90.3" } + [bundles] compose = ["ui", "ui-util", "ui-graphics", "ui-tooling", "ui-tooling-preview", "material3", "material3WindowSizeClass", "material3-adaptive-navigation-suite", "material3-adaptive", "material3-adaptive-navigation", "material3-adaptive-layout"] navigation = ["navigation-compose"] diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 90deb2dbd..eb0d889cc 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -71,6 +71,7 @@ kotlin { implementation(libs.sqlite.bundled) implementation(libs.datastore) implementation(libs.kotlinx.serialization.protobuf) + implementation(libs.xmlUtil) } } val commonTest by getting { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/Rss.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/Rss.kt new file mode 100644 index 000000000..896ad841b --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/Rss.kt @@ -0,0 +1,22 @@ +package dev.dimension.flare.data.network.rss + +import dev.dimension.flare.data.network.ktorClient +import dev.dimension.flare.data.network.rss.model.Feed +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import nl.adaptivity.xmlutil.serialization.XML + +internal object Rss { + private val xml by lazy { + XML { + defaultPolicy { + autoPolymorphic = true + ignoreNamespaces() + ignoreUnknownChildren() + } + defaultToGenericParser = true + } + } + + suspend fun fetch(url: String): Feed = xml.decodeFromString(Feed.serializer(), ktorClient().get(url).bodyAsText()) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt new file mode 100644 index 000000000..25f34c864 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/rss/model/Feed.kt @@ -0,0 +1,359 @@ +package dev.dimension.flare.data.network.rss.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.serialization.XmlSerialName +import nl.adaptivity.xmlutil.serialization.XmlValue + +@Serializable +internal sealed interface Feed { + @Serializable + @SerialName("feed") + data class Atom( + @XmlSerialName("id") + @XmlElement(true) + val id: String, + @XmlSerialName("title") + @XmlElement(true) + val title: Text, + @XmlSerialName("updated") + @XmlElement(true) + val updated: String, // ISO-8601 datetime string + @XmlSerialName("author") + @XmlElement(true) + val authors: List = emptyList(), + @XmlSerialName("contributor") + @XmlElement(true) + val contributors: List = emptyList(), + @XmlSerialName("category") + @XmlElement(true) + val categories: List = emptyList(), + @XmlSerialName("generator") + @XmlElement(true) + val generator: Generator? = null, + @XmlSerialName("icon") + @XmlElement(true) + val icon: String? = null, // URI + @XmlSerialName("logo") + @XmlElement(true) + val logo: String? = null, // URI + @XmlSerialName("rights") + @XmlElement(true) + val rights: Text? = null, + @XmlSerialName("subtitle") + @XmlElement(true) + val subtitle: Text? = null, + @XmlSerialName("link") + @XmlElement(true) + val links: List = emptyList(), + @XmlSerialName("entry") + @XmlElement(true) + val entries: List = emptyList(), + ) : Feed { + @Serializable + data class Entry( + @XmlSerialName("id") + @XmlElement(true) + val id: String, + @XmlSerialName("title") + @XmlElement(true) + val title: Text, + @XmlSerialName("updated") + @XmlElement(true) + val updated: String, // ISO-8601 datetime string + @XmlSerialName("author") + @XmlElement(true) + val authors: List = emptyList(), + @XmlSerialName("content") + @XmlElement(true) + val content: Content? = null, + @XmlSerialName("contributor") + @XmlElement(true) + val contributors: List = emptyList(), + @XmlSerialName("published") + @XmlElement(true) + val published: String? = null, // ISO-8601 datetime string + @XmlSerialName("rights") + @XmlElement(true) + val rights: Text? = null, + @XmlSerialName("source") + @XmlElement(true) + val source: Source? = null, + @XmlSerialName("summary") + @XmlElement(true) + val summary: Text? = null, + @XmlSerialName("link") + @XmlElement(true) + val links: List = emptyList(), + @XmlSerialName("category") + @XmlElement(true) + val categories: List = emptyList(), + ) + + @Serializable + data class Person( + @XmlSerialName("name") + @XmlElement(true) + val name: String, + @XmlSerialName("uri") + @XmlElement(true) + val uri: String? = null, + @XmlSerialName("email") + @XmlElement(true) + val email: String? = null, + ) + + @Serializable + data class Text( + @XmlValue + val value: String, + @XmlElement(false) + val type: TextType = TextType.TEXT, + ) + + @Serializable + enum class TextType { + @SerialName("text") + TEXT, + + @SerialName("html") + HTML, + + @SerialName("xhtml") + XHTML, + } + + @Serializable + data class Content( + @XmlValue + val value: String? = null, + @XmlElement(false) + val src: String? = null, // URI + @XmlElement(false) + val type: String? = null, + ) + + @Serializable + data class Category( + @XmlElement(false) + val term: String, + @XmlElement(false) + val scheme: String? = null, // URI + @XmlElement(false) + val label: String? = null, + ) + + @Serializable + data class Generator( + @XmlValue + val value: String, + @XmlElement(false) + val uri: String? = null, + @XmlElement(false) + val version: String? = null, + ) + + @Serializable + data class Link( + @XmlElement(false) + val href: String, // URI + @XmlElement(false) + val rel: String? = null, + @XmlElement(false) + val type: String? = null, + @XmlElement(false) + val hreflang: String? = null, + @XmlElement(false) + val title: String? = null, + @XmlElement(false) + val length: Int? = null, + ) + + @Serializable + data class Source( + @XmlSerialName("id") + @XmlElement(true) + val id: String? = null, + @XmlSerialName("title") + @XmlElement(true) + val title: Text? = null, + @XmlSerialName("updated") + @XmlElement(true) + val updated: String? = null, // ISO-8601 datetime string + @XmlSerialName("author") + @XmlElement(true) + val authors: List = emptyList(), + @XmlSerialName("contributor") + @XmlElement(true) + val contributors: List = emptyList(), + @XmlSerialName("category") + @XmlElement(true) + val categories: List = emptyList(), + @XmlSerialName("generator") + @XmlElement(true) + val generator: Generator? = null, + @XmlSerialName("icon") + @XmlElement(true) + val icon: String? = null, // URI + @XmlSerialName("logo") + @XmlElement(true) + val logo: String? = null, // URI + @XmlSerialName("rights") + @XmlElement(true) + val rights: Text? = null, + @XmlSerialName("subtitle") + @XmlElement(true) + val subtitle: Text? = null, + @XmlSerialName("link") + @XmlElement(true) + val links: List = emptyList(), + ) + } + + @Serializable + @SerialName("rss") + data class Rss20( + val version: String, + @XmlElement(true) + val channel: Channel, + ) : Feed { + @Serializable + @SerialName("channel") + data class Channel( + @XmlElement(true) + val title: String, + @XmlElement(true) + val link: String, + @XmlElement(true) + val description: String, + @XmlElement(true) + val language: String? = null, + @XmlElement(true) + val copyright: String? = null, + @XmlElement(true) + val managingEditor: String? = null, + @XmlElement(true) + val webMaster: String? = null, + @XmlElement(true) + val pubDate: String? = null, + @XmlElement(true) + val lastBuildDate: String? = null, + @XmlElement(true) + val category: String? = null, + @XmlElement(true) + val generator: String? = null, + @XmlElement(true) + val docs: String? = null, + @XmlElement(true) + val cloud: Cloud? = null, + @XmlElement(true) + val ttl: Int? = null, + @XmlElement(true) + val image: Image? = null, + @XmlElement(true) + val rating: String? = null, + @XmlElement(true) + val textInput: TextInput? = null, + @XmlElement(true) + val skipHours: List = emptyList(), + @XmlElement(true) + val skipDays: List = emptyList(), + @XmlSerialName(value = "item") + val items: List = emptyList(), + ) + + @Serializable + data class Cloud( + @XmlElement(true) + val domain: String, + @XmlElement(true) + val port: Int, + @XmlElement(true) + val path: String, + @XmlElement(true) + val registerProcedure: String, + @XmlElement(true) + val protocol: String, + ) + + @Serializable + data class Image( + @XmlElement(true) + val url: String, + @XmlElement(true) + val title: String? = null, + @XmlElement(true) + val link: String? = null, + @XmlElement(true) + val width: Int? = null, + @XmlElement(true) + val height: Int? = null, + @XmlElement(true) + val description: String? = null, + ) + + @Serializable + data class TextInput( + @XmlElement(true) + val title: String, + @XmlElement(true) + val description: String? = null, + @XmlElement(true) + val name: String, + @XmlElement(true) + val link: String? = null, + ) + + @Serializable + data class Item( + @XmlElement(true) + val title: String, + @XmlElement(true) + val link: String, + @XmlElement(true) + val description: String? = null, + @XmlElement(true) + val author: String? = null, + @XmlElement(true) + val category: String? = null, + @XmlElement(true) + val comments: String? = null, + @XmlElement(true) + val enclosure: Enclosure? = null, + @XmlElement(true) + val guid: Guid? = null, + @XmlElement(true) + val pubDate: String? = null, + @XmlElement(true) + val source: Source? = null, + ) + + @Serializable + data class Enclosure( + @XmlElement(true) + val url: String, + @XmlElement(true) + val length: Int, + @XmlElement(true) + val type: String, + ) + + @Serializable + @SerialName("guid") + data class Guid( + @XmlElement(false) + val isPermaLink: Boolean = false, + @XmlValue + val value: String, + ) + + @Serializable + data class Source( + @XmlElement(true) + val url: String, + @XmlElement(true) + val title: String? = null, + ) + } +}