From a31d12fd28b63142fbb3d331be12b38227f5806d Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sat, 24 Apr 2021 20:31:44 +0200 Subject: [PATCH 01/18] Started with Podcastindex phase 2, adds Geo URI implementation --- .../model/podcastindex/EpisodePodcastindex.kt | 10 +- .../stalla/model/podcastindex/GeoLocation.kt | 56 +++++ .../podcastindex/OpenStreetMapFeature.kt | 17 ++ .../dev/stalla/model/podcastindex/OsmType.kt | 24 ++ .../model/podcastindex/PodcastPodcastindex.kt | 6 +- .../model/podcastindex/PodcastindexEpisode.kt | 14 ++ .../podcastindex/PodcastindexLocation.kt | 14 ++ .../model/podcastindex/PodcastindexPerson.kt | 20 ++ .../model/podcastindex/PodcastindexSeason.kt | 14 ++ .../kotlin/dev/stalla/parser/GeoUriParser.kt | 205 ++++++++++++++++++ .../dev/stalla/parser/GeoUriParserTest.kt | 39 ++++ 11 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt create mode 100644 src/main/kotlin/dev/stalla/parser/GeoUriParser.kt create mode 100644 src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/EpisodePodcastindex.kt b/src/main/kotlin/dev/stalla/model/podcastindex/EpisodePodcastindex.kt index 98361083..78c3c49e 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/EpisodePodcastindex.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/EpisodePodcastindex.kt @@ -13,13 +13,21 @@ import dev.stalla.model.BuilderFactory * @property transcripts The transcript information for the episode. * @property soundbites The soundbites information for the episode. * @property chapters The chapters information for the episode. + * @property persons TODO + * @property location TODO + * @property season TODO + * @property episode TODO * * @since 1.0.0 */ public data class EpisodePodcastindex( val transcripts: List = emptyList(), val soundbites: List = emptyList(), - val chapters: Chapters? = null + val chapters: Chapters? = null, + val persons: List = emptyList(), + val location: PodcastindexLocation? = null, + val season: PodcastindexSeason? = null, + val episode: PodcastindexEpisode? = null ) { /** Provides a builder for the [EpisodePodcastindex] class. */ diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt new file mode 100644 index 00000000..edfb2671 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -0,0 +1,56 @@ +package dev.stalla.model.podcastindex + +import dev.stalla.model.TypeFactory +import dev.stalla.parser.GeoUriParser +import java.util.Locale + +// TODO https://en.wikipedia.org/wiki/Geo_URI_scheme +// RFC 5870: https://tools.ietf.org/html/rfc5870 + +/** + * TODO. + * + * @property coordA TODO. + * @property coordB TODO. + * @property coordC TODO. + * @property parameters TODO. + */ +public class GeoLocation internal constructor( + public val coordA: Double, + public val coordB: Double, + public val coordC: Double? = null, + public val crs: String? = null, + public val uncertainty: Double? = null, + public val parameters: Map = emptyMap() +) { + + /** + * TODO. + * + * @property key TODO. + * @property value TODO. + */ + public data class Parameter(val key: String, val value: String) { + override fun equals(other: Any?): Boolean { + return other is Parameter && + other.key.equals(key, ignoreCase = true) && + other.value.equals(value, ignoreCase = true) + } + + override fun hashCode(): Int { + var result = key.toLowerCase(Locale.ROOT).hashCode() + result += 31 * result + value.toLowerCase(Locale.ROOT).hashCode() + return result + } + } + + // TODO + + /** + * TODO extend TypeFactory. + */ + public companion object Factory : TypeFactory { + @JvmStatic + override fun of(rawValue: String?): GeoLocation? = rawValue?.let { value -> GeoUriParser.parse(value) } + } +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt new file mode 100644 index 00000000..707b1dbd --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt @@ -0,0 +1,17 @@ +package dev.stalla.model.podcastindex + +// What OpenStreetMap calls "OSM type and OSM id". +// See: https://github.com/Podcastindex-org/podcast-namespace/issues/138#issue-758103104 + +/** + * @property osmType A one-character description of the type of OSM point. One of the supported [OsmType]s. + * @property osmId The ID of the OpenStreetMap feature that is described. + * @property osmRevision An optional revision ID for an OSM object, preceded by a hash.. + */ +public data class OpenStreetMapFeature( + val osmType: OsmType, + val osmId: Int, + val osmRevision: String? +) { + // TODO +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt new file mode 100644 index 00000000..09bbf812 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt @@ -0,0 +1,24 @@ +package dev.stalla.model.podcastindex + +import dev.stalla.model.TypeFactory + +/** + * TODO. + * + * @property type TODO. + */ +public enum class OsmType(public val type: String) { + Node("N"), + Way("W"), + Relation("R"); + + /** + * TODO. + */ + public companion object Factory : TypeFactory { + @JvmStatic + override fun of(rawValue: String?): OsmType? = rawValue?.let { + values().find { t -> t.type.equals(it, ignoreCase = true) } + } + } +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastPodcastindex.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastPodcastindex.kt index 73b25775..b92faef1 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastPodcastindex.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastPodcastindex.kt @@ -12,12 +12,16 @@ import dev.stalla.model.BuilderFactory * * @property locked The lock status of the podcast. * @property funding The funding information for the podcast. + * @property persons TODO + * @property location TODO * * @since 1.0.0 */ public data class PodcastPodcastindex( val locked: Locked? = null, - val funding: List = emptyList() + val funding: List = emptyList(), + val persons: List = emptyList(), + val location: PodcastindexLocation? = null ) { /** Provides a builder for the [PodcastPodcastindex] class. */ diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt new file mode 100644 index 00000000..72de0706 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt @@ -0,0 +1,14 @@ +package dev.stalla.model.podcastindex + +/** + * TODO. + * + * @property number TODO. + * @property display TODO. + */ +public data class PodcastindexEpisode( + val number: Double, + val display: String? +) { + // TODO +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt new file mode 100644 index 00000000..f3afd1ab --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt @@ -0,0 +1,14 @@ +package dev.stalla.model.podcastindex + +/** + * @property name Human-readable place name. + * @property geo TODO. + * @property osm TODO. + */ +public data class PodcastindexLocation( + val name: String, + val geo: GeoLocation?, + val osm: OpenStreetMapFeature? +) { + // TODO +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt new file mode 100644 index 00000000..365f6ff3 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt @@ -0,0 +1,20 @@ +package dev.stalla.model.podcastindex + +/** + * TODO. + * + * @property name TODO. + * @property role TODO. + * @property group TODO. + * @property img TODO. + * @property href TODO. + */ +public data class PodcastindexPerson( + val name: String, + val role: String?, + val group: String?, + val img: String?, + val href: String? +) { + // TODO +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt new file mode 100644 index 00000000..d42df926 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt @@ -0,0 +1,14 @@ +package dev.stalla.model.podcastindex + +/** + * TODO. + * + * @property number TODO. + * @property name TODO. + */ +public data class PodcastindexSeason( + val number: Double, + val name: String? +) { + // TODO +} diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt new file mode 100644 index 00000000..9464d783 --- /dev/null +++ b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt @@ -0,0 +1,205 @@ +package dev.stalla.parser + +import dev.stalla.model.podcastindex.GeoLocation +import java.util.regex.Matcher +import java.util.regex.Pattern + +internal object GeoUriParser { + + /* + private val validParameterValueCharacters: Set by lazy { + val valid = mutableSetOf() + for (c in 'a'..'z') valid.add(c) + for (c in 'A'..'Z') valid.add(c) + for (c in '0'..'9') valid.add(c) + for (c in "!$&'()*+-.:[]_~") valid.add(c) + valid + } + */ + + private val hexPattern: Pattern = Pattern.compile("(?i)%([0-9a-f]{2})") + + private const val PARAM_CRS = "crs" + private const val PARAM_UNCERTAINTY = "u" + + @Suppress("ComplexMethod", "NestedBlockDepth") + internal fun parse(value: String): GeoLocation? { + // URI format: geo:LAT,LONG;prop1=value1;prop2=value2 + val scheme = "geo:" + if (value.length < scheme.length || !value.substring(0, scheme.length).equals(scheme, ignoreCase = true)) { + // not a geo URI + return null + } + val builder = Builder() + val buffer = StringBuilder() + var paramName: String? = null + var coordinatesDone = false + for (i in scheme.length until value.length) { + val c = value[i] + if (c == ',' && !coordinatesDone) { + handleEndOfCoordinate(buffer, builder) + continue + } + if (c == ';') { + if (coordinatesDone) { + handleEndOfParameter(buffer, paramName, builder) + paramName = null + } else { + handleEndOfCoordinate(buffer, builder) + if (builder.coordB == null) { + return null + } + coordinatesDone = true + } + continue + } + if (c == '=' && coordinatesDone && paramName == null) { + paramName = buffer.getAndClear() + continue + } + buffer.append(c) + } + if (coordinatesDone) { + handleEndOfParameter(buffer, paramName, builder) + } else { + handleEndOfCoordinate(buffer, builder) + if (builder.coordB == null) { + return null + } + } + return builder.build() + } + + private fun handleEndOfCoordinate(buffer: StringBuilder, builder: Builder) { + val s: String = buffer.getAndClear() + if (builder.coordA == null) { + try { + builder.coordA(s.toDouble()) + } catch (e: NumberFormatException) { + return + } + return + } + if (builder.coordB == null) { + try { + builder.coordB(s.toDouble()) + } catch (e: NumberFormatException) { + return + } + return + } + if (builder.coordC == null) { + try { + builder.coordC(s.toDouble()) + } catch (e: NumberFormatException) { + return + } + return + } + } + + private fun addParameter(name: String, value: String, builder: Builder) { + val decodedValue = decodeParameterValue(value) + if (PARAM_CRS.equals(name, ignoreCase = true)) { + builder.crs(decodedValue) + return + } + if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { + try { + builder.uncertainty(decodedValue.toDouble()) + return + } catch (e: NumberFormatException) { + // if it can't be parsed, then treat it as an ordinary parameter + } + } + builder.parameter(name, decodedValue) + } + + /* + private fun encodeParameterValue(value: String): String = StringBuilder().apply { + for (i in value.indices) { + val c = value[i] + if (validParameterValueCharacters.contains(c)) { + append(c) + } else { + val hex = c.toInt().toString(16) + append('%') + append(hex) + } + } + }.toString() + */ + + private fun decodeParameterValue(value: String): String { + val m: Matcher = hexPattern.matcher(value) + val sb = StringBuffer() + while (m.find()) { + val hex: Int = m.group(1).toInt(16) + m.appendReplacement(sb, hex.toChar().toString()) + } + m.appendTail(sb) + return sb.toString() + } + + private fun handleEndOfParameter(buffer: StringBuilder, paramName: String?, builder: Builder) { + val s = buffer.getAndClear() + if (paramName == null) { + if (s.isNotEmpty()) { + addParameter(s, "", builder) + } + return + } + addParameter(paramName, s, builder) + } + + private fun StringBuilder.getAndClear(): String { + val string: String = toString() + clear() + return string + } + + private class Builder { + + var coordA: Double? = null + private set + var coordB: Double? = null + private set + var coordC: Double? = null + private set + private var crs: String? = null + private var uncertainty: Double? = null + private var parameters: MutableMap = mutableMapOf() + + fun coordA(coordA: Double): Builder = apply { this.coordA = coordA } + + fun coordB(coordB: Double): Builder = apply { this.coordB = coordB } + + fun coordC(coordC: Double?): Builder = apply { this.coordC = coordC } + + fun crs(crs: String?): Builder = apply { this.crs = crs } + + fun uncertainty(uncertainty: Double?): Builder = apply { this.uncertainty = uncertainty } + + fun parameter(name: String, value: String?): Builder = apply { + if (value == null) { + parameters.remove(name) + } else { + parameters[name] = value + } + } + + @Suppress("SwallowedException") + fun build(): GeoLocation? = try { + GeoLocation( + coordA = coordA!!, + coordB = coordB!!, + coordC = coordC, + crs = crs, + uncertainty = uncertainty, + parameters = parameters + ) + } catch (ex: NullPointerException) { + null + } + } +} diff --git a/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt b/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt new file mode 100644 index 00000000..1e3ed572 --- /dev/null +++ b/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt @@ -0,0 +1,39 @@ +package dev.stalla.parser + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.prop +import dev.stalla.model.podcastindex.GeoLocation +import org.junit.jupiter.api.Test + +class GeoUriParserTest { + + @Test + fun `should parse a Geo URI with A and B correctly`() { + assertAll { + assertThat(GeoUriParser.parse("geo:37.786971,-122.399677")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(37.786971) + prop(GeoLocation::coordB).isEqualTo(-122.399677) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + } + + @Test + fun `test 2`() { + assertAll { + assertThat(GeoUriParser.parse("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(48.198634) + prop(GeoLocation::coordB).isEqualTo(16.371648) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + } +} From cdd39962203e591d849d369c32994d8d3edde0a3 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 13:37:03 +0200 Subject: [PATCH 02/18] GeoLocation builder class like for all other entities; add more tests --- .../dev/stalla/builder/GeoLocationBuilder.kt | 50 ++++ .../ValidatingGeoLocationBuilder.kt | 81 +++++++ .../stalla/model/podcastindex/GeoLocation.kt | 167 ++++++++++++- .../kotlin/dev/stalla/parser/GeoUriParser.kt | 191 +++++---------- src/test/kotlin/dev/stalla/Assertions.kt | 11 + .../dev/stalla/parser/GeoUriParserTest.kt | 225 +++++++++++++++++- 6 files changed, 564 insertions(+), 161 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt diff --git a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt new file mode 100644 index 00000000..a06e417d --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt @@ -0,0 +1,50 @@ +package dev.stalla.builder + +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.util.whenNotNull + +/** + * Builder for constructing [GeoLocation] instances. + * + * @since 1.1.0 + */ +public interface GeoLocationBuilder : Builder { + + public fun coordA(coordA: Double): GeoLocationBuilder + + public fun coordB(coordB: Double): GeoLocationBuilder + + public fun coordC(coordC: Double?): GeoLocationBuilder + + public fun crs(crs: String?): GeoLocationBuilder + + public fun uncertainty(uncertainty: Double?): GeoLocationBuilder + + public fun addParameter(name: String, value: String): GeoLocationBuilder + + public fun addParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = + apply { addParameter(parameter.key, parameter.value) } + + public fun addAllParameters(parameters: List): GeoLocationBuilder = + apply { parameters.forEach(::addParameter) } + + public fun removeParameter(name: String): GeoLocationBuilder + + public fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder + + public fun hasCoordA(): Boolean + + public fun hasCoordB(): Boolean + + public fun hasCoordC(): Boolean + + override fun applyFrom(prototype: GeoLocation?): GeoLocationBuilder = + whenNotNull(prototype) { location -> + coordA(location.coordA) + coordB(location.coordB) + coordC(location.coordC) + crs(location.crs) + uncertainty(location.uncertainty) + addAllParameters(location.parameters) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt new file mode 100644 index 00000000..efbe5898 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -0,0 +1,81 @@ +package dev.stalla.builder.validating + +import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeoLocation.Factory.PARAM_CRS +import dev.stalla.model.podcastindex.GeoLocation.Factory.PARAM_UNCERTAINTY + +public class ValidatingGeoLocationBuilder : GeoLocationBuilder { + + private var coordA: Double? = null + private var coordB: Double? = null + private var coordC: Double? = null + private var crs: String? = null + private var uncertainty: Double? = null + private var parameters: MutableMap = mutableMapOf() + + override fun coordA(coordA: Double): GeoLocationBuilder = apply { this.coordA = coordA } + + override fun coordB(coordB: Double): GeoLocationBuilder = apply { this.coordB = coordB } + + override fun coordC(coordC: Double?): GeoLocationBuilder = apply { this.coordC = coordC } + + override fun crs(crs: String?): GeoLocationBuilder = apply { this.crs = crs } + + override fun uncertainty(uncertainty: Double?): GeoLocationBuilder = apply { this.uncertainty = uncertainty } + + override fun addParameter(name: String, value: String): GeoLocationBuilder = apply { + if (PARAM_CRS.equals(name, ignoreCase = true)) { + crs(value) + return@apply + } + if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { + try { + uncertainty(value.toDouble()) + return@apply + } catch (e: NumberFormatException) { + // if it can't be parsed, then treat it as an ordinary parameter + } + } + parameters[name] = value + } + + override fun removeParameter(name: String): GeoLocationBuilder = apply { + if (PARAM_CRS.equals(name, ignoreCase = true)) { + crs(null) + } + if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { + uncertainty(null) + } + parameters.remove(name) + } + + override fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = + removeParameter(parameter.key) + + override fun hasCoordA(): Boolean = coordA != null + + override fun hasCoordB(): Boolean = coordB != null + + override fun hasCoordC(): Boolean = coordC != null + + override val hasEnoughDataToBuild: Boolean + get() = coordA != null && coordB != null + + override fun build(): GeoLocation? { + if (!hasEnoughDataToBuild) return null + + return try { + GeoLocation( + coordA = coordA!!, + coordB = coordB!!, + coordC = coordC, + crs = crs, + uncertainty = uncertainty, + parameters = parameters + ) + } catch (ex: NullPointerException) { + null + } + } +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index edfb2671..085853bf 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -1,29 +1,55 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.builder.validating.ValidatingGeoLocationBuilder +import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory +import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_WGS84 import dev.stalla.parser.GeoUriParser +import dev.stalla.util.InternalAPI +import dev.stalla.util.containsMediaTypeSeparatorSymbol import java.util.Locale - -// TODO https://en.wikipedia.org/wiki/Geo_URI_scheme -// RFC 5870: https://tools.ietf.org/html/rfc5870 +import kotlin.contracts.contract +import kotlin.math.absoluteValue /** * TODO. * - * @property coordA TODO. - * @property coordB TODO. - * @property coordC TODO. + * @property coordA Latitude. + * @property coordB Longitude. + * @property coordC Altitude. + * @property crs The coordinate reference system - defaults to ([CRS_WGS84]) if not set. + * @property uncertainty TODO. * @property parameters TODO. + * + * @see [RFC 5870](https://tools.ietf.org/html/rfc5870) */ -public class GeoLocation internal constructor( +public class GeoLocation private constructor( public val coordA: Double, public val coordB: Double, public val coordC: Double? = null, public val crs: String? = null, public val uncertainty: Double? = null, - public val parameters: Map = emptyMap() + public val parameters: List = emptyList() ) { + @InternalAPI + internal constructor( + coordA: Double, + coordB: Double, + coordC: Double?, + crs: String?, + uncertainty: Double?, + parameters: Map + ) : this( + coordA = coordA, + coordB = coordB, + coordC = coordC, + crs = crs, + uncertainty = uncertainty, + parameters = parameters.map { Parameter(it.key, it.value) } + ) + /** * TODO. * @@ -33,7 +59,7 @@ public class GeoLocation internal constructor( public data class Parameter(val key: String, val value: String) { override fun equals(other: Any?): Boolean { return other is Parameter && - other.key.equals(key, ignoreCase = true) && + other.key == key && other.value.equals(value, ignoreCase = true) } @@ -44,13 +70,130 @@ public class GeoLocation internal constructor( } } - // TODO + /** + * The first value for the parameter with [key] comparing + * case-insensitively or `null` if no such parameters found. + */ + public fun parameter(key: String): String? = + parameters.firstOrNull { it.key.equals(key, ignoreCase = true) }?.value + + /** Creates a copy of `this` type with an added parameter of [key] and [value]. */ + public fun withParameter(key: String, value: String): GeoLocation { + if (key.isBlank() || key.containsMediaTypeSeparatorSymbol()) return this + if (value.isBlank()) return this + if (hasParameter(key, value)) return this + + return GeoLocation(coordA, coordB, coordC, crs, uncertainty, parameters + Parameter(key, value)) + } + + /** Creates a copy of `this` type without any parameters.*/ + public fun withoutParameters(): GeoLocation = GeoLocation(coordA, coordB, coordC) + + /** Checks if `this` type matches a [pattern] type taking parameters into account. */ + public fun match(pattern: GeoLocation?): Boolean { + contract { + returns(true) implies (pattern != null) + } + + if (pattern == null) return false + if (this == pattern) return true + if (coordA.absoluteValue == 90.0 && matchCrs(crs, pattern.crs)) { + // Special "poles" rule for WGS-84 applies - longitude is to be ignored + return coordA == pattern.coordA && + coordC == pattern.coordC && + uncertainty == pattern.uncertainty && + match(parameters, pattern.parameters) + } + + return coordA == pattern.coordA && + coordB == pattern.coordB && + coordC == pattern.coordC && + crs.equals(pattern.crs, ignoreCase = true) && + uncertainty == pattern.uncertainty && + match(parameters, pattern.parameters) + } + + /** Checks if `this` type matches a [pattern] type taking parameters into account. */ + public fun match(pattern: String): Boolean = match(of(pattern)) + + private fun match(parameters1: List, parameters2: List): Boolean { + for ((patternName, patternValue) in parameters1) { + val value = parameter(patternName, parameters2) + val matches = value != null && value == patternValue + + if (!matches) return false + } + return true + } + + private fun matchCrs(crs1: String?, crs2: String?): Boolean { + return (crs1 == null || crs1.equals(CRS_WGS84, ignoreCase = true)) + && (crs2 == null || crs2.equals(CRS_WGS84, ignoreCase = true)) + } + + private fun parameter(key: String, elements: List): String? = + elements.firstOrNull { it.key.equals(key, ignoreCase = true) }?.value + + private fun hasParameter(key: String, value: String): Boolean = when (parameters.size) { + 0 -> false + 1 -> parameters[0].let { param -> + param.key.equals(key, ignoreCase = true) && param.value.equals(value, ignoreCase = true) + } + else -> parameters.any { param -> + param.key.equals(key, ignoreCase = true) && param.value.equals(value, ignoreCase = true) + } + } + + override fun toString(): String = StringBuilder().apply { + append("geo:$coordA,$coordB") + if (coordC != null) append(",$coordC") + if (crs != null) append(";$PARAM_CRS=$crs") + if (uncertainty != null) append(";$PARAM_UNCERTAINTY=$uncertainty") + for ((key, value) in parameters) append(";$key=$value") + }.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GeoLocation + + if (coordA != other.coordA) return false + if (coordB != other.coordB) return false + if (coordC != other.coordC) return false + if (crs != other.crs) return false + if (uncertainty != other.uncertainty) return false + if (parameters != other.parameters) return false + + return true + } + + override fun hashCode(): Int { + var result = coordA.hashCode() + result = 31 * result + coordB.hashCode() + result = 31 * result + (coordC?.hashCode() ?: 0) + result = 31 * result + (crs?.hashCode() ?: 0) + result = 31 * result + (uncertainty?.hashCode() ?: 0) + result = 31 * result + parameters.hashCode() + return result + } /** - * TODO extend TypeFactory. + * TODO. */ - public companion object Factory : TypeFactory { + public companion object Factory : BuilderFactory, TypeFactory { + + internal const val PARAM_CRS = "crs" + internal const val PARAM_UNCERTAINTY = "u" + + /** Returns a builder implementation for building [GeoLocation] model instances. */ + @JvmStatic + override fun builder(): GeoLocationBuilder = ValidatingGeoLocationBuilder() + @JvmStatic override fun of(rawValue: String?): GeoLocation? = rawValue?.let { value -> GeoUriParser.parse(value) } + + /** The World Geodetic System 1984 coordinate reference system used by GPS. */ + public const val CRS_WGS84: String = "WGS84" } } diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt index 9464d783..0532a376 100644 --- a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt @@ -1,27 +1,24 @@ package dev.stalla.parser +import dev.stalla.builder.GeoLocationBuilder import dev.stalla.model.podcastindex.GeoLocation -import java.util.regex.Matcher +import dev.stalla.util.InternalAPI import java.util.regex.Pattern +/** + * Parser implementation for [GeoLocation] values, as defined in [RFC 5870](https://tools.ietf.org/html/rfc5870). + * + * The parsing logic is inspired by the + * [GeoUri](https://github.com/mangstadt/ez-vcard/blob/master/src/main/java/ezvcard/util/GeoUri.java) + * class of the [ez-vcard](https://github.com/mangstadt/ez-vcard) project. + * Special thanks to the ez-vcard contributors. + */ +@InternalAPI internal object GeoUriParser { - /* - private val validParameterValueCharacters: Set by lazy { - val valid = mutableSetOf() - for (c in 'a'..'z') valid.add(c) - for (c in 'A'..'Z') valid.add(c) - for (c in '0'..'9') valid.add(c) - for (c in "!$&'()*+-.:[]_~") valid.add(c) - valid - } - */ - private val hexPattern: Pattern = Pattern.compile("(?i)%([0-9a-f]{2})") - private const val PARAM_CRS = "crs" - private const val PARAM_UNCERTAINTY = "u" - + @InternalAPI @Suppress("ComplexMethod", "NestedBlockDepth") internal fun parse(value: String): GeoLocation? { // URI format: geo:LAT,LONG;prop1=value1;prop2=value2 @@ -30,25 +27,23 @@ internal object GeoUriParser { // not a geo URI return null } - val builder = Builder() + val builder = GeoLocation.builder() val buffer = StringBuilder() var paramName: String? = null var coordinatesDone = false for (i in scheme.length until value.length) { val c = value[i] if (c == ',' && !coordinatesDone) { - handleEndOfCoordinate(buffer, builder) + builder.handleEndOfCoordinate(buffer) continue } if (c == ';') { if (coordinatesDone) { - handleEndOfParameter(buffer, paramName, builder) + builder.handleEndOfParameter(buffer, paramName) paramName = null } else { - handleEndOfCoordinate(buffer, builder) - if (builder.coordB == null) { - return null - } + builder.handleEndOfCoordinate(buffer) + if (!builder.hasCoordB()) return null coordinatesDone = true } continue @@ -60,146 +55,70 @@ internal object GeoUriParser { buffer.append(c) } if (coordinatesDone) { - handleEndOfParameter(buffer, paramName, builder) + builder.handleEndOfParameter(buffer, paramName) } else { - handleEndOfCoordinate(buffer, builder) - if (builder.coordB == null) { - return null - } + builder.handleEndOfCoordinate(buffer) + if (!builder.hasCoordB()) return null } return builder.build() } - private fun handleEndOfCoordinate(buffer: StringBuilder, builder: Builder) { + private fun String.asDoubleOrNull(): Double? = try { + toDouble() + } catch (e: NumberFormatException) { + null + } + + private fun GeoLocationBuilder.handleEndOfCoordinate(buffer: StringBuilder) { val s: String = buffer.getAndClear() - if (builder.coordA == null) { - try { - builder.coordA(s.toDouble()) - } catch (e: NumberFormatException) { - return - } + if (!hasCoordA()) { + val a = s.asDoubleOrNull() ?: return + coordA(a) return } - if (builder.coordB == null) { - try { - builder.coordB(s.toDouble()) - } catch (e: NumberFormatException) { - return - } + if (!hasCoordB()) { + val b = s.asDoubleOrNull() ?: return + coordB(b) return } - if (builder.coordC == null) { - try { - builder.coordC(s.toDouble()) - } catch (e: NumberFormatException) { - return - } + if (!hasCoordC()) { + val c = s.asDoubleOrNull() ?: return + coordC(c) return } } - private fun addParameter(name: String, value: String, builder: Builder) { - val decodedValue = decodeParameterValue(value) - if (PARAM_CRS.equals(name, ignoreCase = true)) { - builder.crs(decodedValue) - return - } - if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { - try { - builder.uncertainty(decodedValue.toDouble()) - return - } catch (e: NumberFormatException) { - // if it can't be parsed, then treat it as an ordinary parameter + private fun GeoLocationBuilder.handleEndOfParameter(buffer: StringBuilder, paramName: String?) { + val s = buffer.getAndClear() + if (paramName == null) { + if (s.isNotEmpty()) { + addParameterDecodeValue(s, "") } + return } - builder.parameter(name, decodedValue) + addParameterDecodeValue(paramName, s) } - /* - private fun encodeParameterValue(value: String): String = StringBuilder().apply { - for (i in value.indices) { - val c = value[i] - if (validParameterValueCharacters.contains(c)) { - append(c) - } else { - val hex = c.toInt().toString(16) - append('%') - append(hex) - } - } - }.toString() - */ - - private fun decodeParameterValue(value: String): String { - val m: Matcher = hexPattern.matcher(value) - val sb = StringBuffer() - while (m.find()) { - val hex: Int = m.group(1).toInt(16) - m.appendReplacement(sb, hex.toChar().toString()) + private fun GeoLocationBuilder.addParameterDecodeValue(name: String, value: String?) { + if (value == null) { + removeParameter(name) + return } - m.appendTail(sb) - return sb.toString() + addParameter(name, decodeParameterValue(value)) } - private fun handleEndOfParameter(buffer: StringBuilder, paramName: String?, builder: Builder) { - val s = buffer.getAndClear() - if (paramName == null) { - if (s.isNotEmpty()) { - addParameter(s, "", builder) - } - return + private fun decodeParameterValue(value: String): String = StringBuffer().apply { + val matcher = hexPattern.matcher(value) + while (matcher.find()) { + val hex: Int = matcher.group(1).toInt(16) + matcher.appendReplacement(this, hex.toChar().toString()) } - addParameter(paramName, s, builder) - } + matcher.appendTail(this) + }.toString() private fun StringBuilder.getAndClear(): String { val string: String = toString() clear() return string } - - private class Builder { - - var coordA: Double? = null - private set - var coordB: Double? = null - private set - var coordC: Double? = null - private set - private var crs: String? = null - private var uncertainty: Double? = null - private var parameters: MutableMap = mutableMapOf() - - fun coordA(coordA: Double): Builder = apply { this.coordA = coordA } - - fun coordB(coordB: Double): Builder = apply { this.coordB = coordB } - - fun coordC(coordC: Double?): Builder = apply { this.coordC = coordC } - - fun crs(crs: String?): Builder = apply { this.crs = crs } - - fun uncertainty(uncertainty: Double?): Builder = apply { this.uncertainty = uncertainty } - - fun parameter(name: String, value: String?): Builder = apply { - if (value == null) { - parameters.remove(name) - } else { - parameters[name] = value - } - } - - @Suppress("SwallowedException") - fun build(): GeoLocation? = try { - GeoLocation( - coordA = coordA!!, - coordB = coordB!!, - coordC = coordC, - crs = crs, - uncertainty = uncertainty, - parameters = parameters - ) - } catch (ex: NullPointerException) { - null - } - } } diff --git a/src/test/kotlin/dev/stalla/Assertions.kt b/src/test/kotlin/dev/stalla/Assertions.kt index 6fd8f557..49375e17 100644 --- a/src/test/kotlin/dev/stalla/Assertions.kt +++ b/src/test/kotlin/dev/stalla/Assertions.kt @@ -7,6 +7,7 @@ import dev.stalla.builder.Builder import dev.stalla.dom.asListOfNodes import dev.stalla.dom.asString import dev.stalla.model.MediaType +import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.util.FeedNamespace import dev.stalla.util.FeedNamespace.Companion.matches import org.w3c.dom.Attr @@ -220,3 +221,13 @@ internal fun Assert.doesNotMatchSymmetrically(expected: MediaType?) = ) } } + +/** Asserts that [GeoLocation.match] matches the expected value. */ +internal fun Assert.matchPattern(expected: GeoLocation) = given { geoLocation -> + if (geoLocation.match(expected)) return@given + expected( + "to be: '$expected' but was: '$geoLocation'", + expected = expected, + actual = geoLocation + ) +} diff --git a/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt b/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt index 1e3ed572..ea5ad39b 100644 --- a/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt +++ b/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt @@ -3,10 +3,13 @@ package dev.stalla.parser import assertk.all import assertk.assertAll import assertk.assertThat +import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNull +import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.model.podcastindex.GeoLocation import org.junit.jupiter.api.Test @@ -15,25 +18,221 @@ class GeoUriParserTest { @Test fun `should parse a Geo URI with A and B correctly`() { - assertAll { - assertThat(GeoUriParser.parse("geo:37.786971,-122.399677")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(37.786971) - prop(GeoLocation::coordB).isEqualTo(-122.399677) - prop(GeoLocation::coordC).isNull() - prop(GeoLocation::parameters).isEmpty() - } + assertThat(GeoUriParser.parse("geo:37.786971,-122.399677")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(37.786971) + prop(GeoLocation::coordB).isEqualTo(-122.399677) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `should parse a Geo URI with A and B and C correctly`() { + assertThat(GeoUriParser.parse("geo:40.714623,-74.006605,1.1")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(40.714623) + prop(GeoLocation::coordB).isEqualTo(-74.006605) + prop(GeoLocation::coordC).isEqualTo(1.1) + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).isEmpty() } } @Test fun `test 2`() { + assertThat(GeoUriParser.parse("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(48.198634) + prop(GeoLocation::coordB).isEqualTo(16.371648) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::crs).isEqualTo("wgs84") + prop(GeoLocation::uncertainty).isEqualTo(40.0) + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse all`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isEqualTo("wgs84") + prop(GeoLocation::uncertainty).isEqualTo(12.0) + prop(GeoLocation::parameters).hasSize(1) + prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") + } + } + + @Test + fun `parse no params`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isEqualTo("wgs84") + prop(GeoLocation::uncertainty).isEqualTo(12.0) + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isEqualTo("wgs84") + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or crs`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isEqualTo(12.0) + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u or crs`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u or crs or coordC`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).isEmpty() + } + } + + @Test + fun `parse invalid uncertainty`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78;u=invalid")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).hasSize(1) + prop("parameter") { GeoLocation::parameter.call(it, "u") }.isNotNull().isEqualTo("invalid") + } + } + + @Test + fun `parse no params or u or crs or coordsC or coordB`() { + assertThat(GeoUriParser.parse("geo:12.34")).isNull() + } + + @Test + fun `parse no params or u or crs or coordsC or coordB or coordA`() { + assertThat(GeoUriParser.parse("geo:")).isNull() + } + + @Test + fun `parse not geo uri(`() { + assertThat(GeoUriParser.parse("https://stalla.dev")).isNull() + } + + @Test + fun `parse decode special chars in param value`() { + assertThat(GeoUriParser.parse("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(56.78) + prop(GeoLocation::coordC).isNull() + prop(GeoLocation::crs).isNull() + prop(GeoLocation::uncertainty).isNull() + prop(GeoLocation::parameters).hasSize(1) + prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("with = special & chars") + } + } + + @Test + fun `multiple params`() { + assertThat(GeoUriParser.parse("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { + prop(GeoLocation::coordA).isEqualTo(12.34) + prop(GeoLocation::coordB).isEqualTo(45.67) + prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::crs).isEqualTo("theCrs") + prop(GeoLocation::uncertainty).isEqualTo(12.0) + prop(GeoLocation::parameters).hasSize(2) + prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") + prop("parameter") { GeoLocation::parameter.call(it, "param2") }.isNotNull().isEqualTo("value2") + } + } + + @Test + fun `WGS84 pole rule`() { + val geoLocation1 = GeoUriParser.parse("geo:90,-22.43;crs=WGS84") + val geoLocation2 = GeoUriParser.parse("geo:90,46;crs=WGS84") assertAll { - assertThat(GeoUriParser.parse("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(48.198634) - prop(GeoLocation::coordB).isEqualTo(16.371648) - prop(GeoLocation::coordC).isNull() - prop(GeoLocation::parameters).isEmpty() - } + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() } } + + @Test + fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { + val geoLocation1 = GeoUriParser.parse("geo:66,30;u=6.500;FOo=this%2dthat") + val geoLocation2 = GeoUriParser.parse("geo:66.0,30;u=6.5;foo=this-that") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter order is insignificant`() { + val geoLocation1 = GeoUriParser.parse("geo:47,11;foo=blue;bar=white") + val geoLocation2 = GeoUriParser.parse("geo:47,11;bar=white;foo=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter keys are case-insensitive`() { + val geoLocation1 = GeoUriParser.parse("geo:22,0;bar=blue") + val geoLocation2 = GeoUriParser.parse("geo:22,0;BAR=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter values are case-sensitive`() { + val geoLocation1 = GeoUriParser.parse("geo:22,0;bar=BLUE") + val geoLocation2 = GeoUriParser.parse("geo:22,0;bar=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } + } From a8058be185975b8f2d89968cf508ee5f9cce0402 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 14:37:01 +0200 Subject: [PATCH 03/18] Setup location parsing, but missing OSM implementation --- .../builder/PodcastindexLocationBuilder.kt | 23 ++++++ .../episode/EpisodePodcastindexBuilder.kt | 7 ++ .../episode/ProvidingEpisodeBuilder.kt | 5 ++ .../podcast/PodcastPodcastindexBuilder.kt | 8 ++ .../podcast/ProvidingPodcastBuilder.kt | 5 ++ .../ValidatingGeoLocationBuilder.kt | 20 ++--- .../ValidatingPodcastindexLocationBuilder.kt | 34 +++++++++ .../ValidatingEpisodePodcastindexBuilder.kt | 15 ++-- .../ValidatingPodcastPodcastindexBuilder.kt | 15 ++-- .../stalla/model/podcastindex/GeoLocation.kt | 3 +- .../podcastindex/PodcastindexLocation.kt | 12 ++- .../kotlin/dev/stalla/parser/GeoUriParser.kt | 9 ++- .../parser/namespace/PodcastindexParser.kt | 75 +++++++++++++------ 13 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt new file mode 100644 index 00000000..0f9370cc --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt @@ -0,0 +1,23 @@ +package dev.stalla.builder + +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.util.whenNotNull + +public interface PodcastindexLocationBuilder : Builder { + + public fun name(name: String): PodcastindexLocationBuilder + + public fun geo(geo: GeoLocation?): PodcastindexLocationBuilder + + public fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder + + override fun applyFrom(prototype: PodcastindexLocation?): PodcastindexLocationBuilder = + whenNotNull(prototype) { location -> + name(location.name) + geo(GeoLocation.builder().applyFrom(location.geo).build()!!) + // TODO osm or osmBuilder ?! + } + +} diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt index d413202d..e1c3d004 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt @@ -1,8 +1,11 @@ package dev.stalla.builder.episode import dev.stalla.builder.Builder +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.model.podcastindex.Chapters import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.util.asBuilders import dev.stalla.util.whenNotNull @@ -52,10 +55,14 @@ public interface EpisodePodcastindexBuilder : Builder { transcriptBuilders.forEach(::addTranscriptBuilder) } + /** Set the [EpisodePodcastindexBuilder]. */ + public fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder + override fun applyFrom(prototype: EpisodePodcastindex?): EpisodePodcastindexBuilder = whenNotNull(prototype) { podcast -> chaptersBuilder(Chapters.builder().applyFrom(podcast.chapters)) addAllSoundbiteBuilders(podcast.soundbites.asBuilders()) addAllTranscriptBuilders(podcast.transcripts.asBuilders()) + locationBuilder(PodcastindexLocation.builder().applyFrom(podcast.location)) } } diff --git a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt index 9d49a227..667d9685 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt @@ -1,8 +1,10 @@ package dev.stalla.builder.episode import dev.stalla.builder.AtomPersonBuilderProvider +import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.util.InternalAPI @@ -32,4 +34,7 @@ internal interface ProvidingEpisodeBuilder : EpisodeBuilder, AtomPersonBuilderPr /** Creates an instance of [EpisodePodcastindexSoundbiteBuilder] to use with this builder. */ fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder + + /** Creates an instance of [PodcastindexLocationBuilder] to use with this builder. */ + fun createLocationBuilder(): PodcastindexLocationBuilder } diff --git a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt index f4418673..a6a548e1 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt @@ -1,8 +1,12 @@ package dev.stalla.builder.podcast import dev.stalla.builder.Builder +import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.Locked import dev.stalla.model.podcastindex.PodcastPodcastindex +import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.util.asBuilders import dev.stalla.util.whenNotNull @@ -32,9 +36,13 @@ public interface PodcastPodcastindexBuilder : Builder { fundingBuilders.forEach(::addFundingBuilder) } + /** Set the [PodcastindexLocationBuilder]. */ + public fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder + override fun applyFrom(prototype: PodcastPodcastindex?): PodcastPodcastindexBuilder = whenNotNull(prototype) { podcast -> lockedBuilder(Locked.builder().applyFrom(podcast.locked)) addAllFundingBuilders(podcast.funding.asBuilders()) + locationBuilder(PodcastindexLocation.builder().applyFrom(podcast.location)) } } diff --git a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt index 4b624cd1..8c5bd883 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt @@ -1,8 +1,10 @@ package dev.stalla.builder.podcast import dev.stalla.builder.AtomPersonBuilderProvider +import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder @@ -25,4 +27,7 @@ internal interface ProvidingPodcastBuilder : PodcastBuilder, AtomPersonBuilderPr /** Creates an instance of [PodcastPodcastindexFundingBuilder] to use with this builder. */ fun createFundingBuilder(): PodcastPodcastindexFundingBuilder + + /** Creates an instance of [PodcastindexLocationBuilder] to use with this builder. */ + fun createLocationBuilder(): PodcastindexLocationBuilder } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt index efbe5898..b24ee2e1 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -65,17 +65,13 @@ public class ValidatingGeoLocationBuilder : GeoLocationBuilder { override fun build(): GeoLocation? { if (!hasEnoughDataToBuild) return null - return try { - GeoLocation( - coordA = coordA!!, - coordB = coordB!!, - coordC = coordC, - crs = crs, - uncertainty = uncertainty, - parameters = parameters - ) - } catch (ex: NullPointerException) { - null - } + return GeoLocation( + coordA = coordA ?: return null, + coordB = coordB ?: return null, + coordC = coordC, + crs = crs, + uncertainty = uncertainty, + parameters = parameters // this secondary constructor will apply .asUnmodifiable() + ) } } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt new file mode 100644 index 00000000..47e37c13 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt @@ -0,0 +1,34 @@ +package dev.stalla.builder.validating + +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.util.InternalAPI + +@InternalAPI +internal class ValidatingPodcastindexLocationBuilder : PodcastindexLocationBuilder { + + private lateinit var nameValue: String + private var geoValue: GeoLocation? = null + private var osmValue: OpenStreetMapFeature? = null + + override fun name(name: String): PodcastindexLocationBuilder = apply { this.nameValue = name } + + override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geoValue = geo } + + override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osmValue = osm } + + override val hasEnoughDataToBuild: Boolean + get() = ::nameValue.isInitialized + + override fun build(): PodcastindexLocation? { + if (!hasEnoughDataToBuild) return null + + return PodcastindexLocation( + name = nameValue, + geo = geoValue, + osm = osmValue + ) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt index 58662b38..ea5283c5 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt @@ -1,5 +1,6 @@ package dev.stalla.builder.validating.episode +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.episode.EpisodePodcastindexBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder @@ -12,6 +13,7 @@ import dev.stalla.util.asUnmodifiable internal class ValidatingEpisodePodcastindexBuilder : EpisodePodcastindexBuilder { private var chaptersBuilderValue: EpisodePodcastindexChaptersBuilder? = null + private var locationBuilderValue: PodcastindexLocationBuilder? = null private val transcriptBuilders: MutableList = mutableListOf() private val soundbiteBuilders: MutableList = mutableListOf() @@ -30,20 +32,23 @@ internal class ValidatingEpisodePodcastindexBuilder : EpisodePodcastindexBuilder transcriptBuilders.add(transcriptBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder = + apply { this.locationBuilderValue = locationBuilder } + override val hasEnoughDataToBuild: Boolean get() = chaptersBuilderValue?.hasEnoughDataToBuild == true || transcriptBuilders.any { it.hasEnoughDataToBuild } || - soundbiteBuilders.any { it.hasEnoughDataToBuild } + soundbiteBuilders.any { it.hasEnoughDataToBuild } || + locationBuilderValue?.hasEnoughDataToBuild == true override fun build(): EpisodePodcastindex? { - if (!hasEnoughDataToBuild) { - return null - } + if (!hasEnoughDataToBuild) return null return EpisodePodcastindex( transcripts = transcriptBuilders.mapNotNull { it.build() }.asUnmodifiable(), soundbites = soundbiteBuilders.mapNotNull { it.build() }.asUnmodifiable(), - chapters = chaptersBuilderValue?.build() + chapters = chaptersBuilderValue?.build(), + location = locationBuilderValue?.build() ) } } diff --git a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt index f41f089a..de5dca7e 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt @@ -1,5 +1,6 @@ package dev.stalla.builder.validating.podcast +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.builder.podcast.PodcastPodcastindexLockedBuilder @@ -11,6 +12,7 @@ import dev.stalla.util.asUnmodifiable internal class ValidatingPodcastPodcastindexBuilder : PodcastPodcastindexBuilder { private lateinit var lockedBuilderValue: PodcastPodcastindexLockedBuilder + private var locationBuilderValue: PodcastindexLocationBuilder? = null private val fundingBuilders: MutableList = mutableListOf() override fun lockedBuilder(lockedBuilder: PodcastPodcastindexLockedBuilder): PodcastPodcastindexBuilder = @@ -19,19 +21,22 @@ internal class ValidatingPodcastPodcastindexBuilder : PodcastPodcastindexBuilder override fun addFundingBuilder(fundingBuilder: PodcastPodcastindexFundingBuilder): PodcastPodcastindexBuilder = apply { fundingBuilders.add(fundingBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder = + apply { this.locationBuilderValue = locationBuilder } + override val hasEnoughDataToBuild: Boolean get() = ::lockedBuilderValue.isInitialized && lockedBuilderValue.hasEnoughDataToBuild || - fundingBuilders.any { it.hasEnoughDataToBuild } + fundingBuilders.any { it.hasEnoughDataToBuild } || + locationBuilderValue?.hasEnoughDataToBuild == true override fun build(): PodcastPodcastindex? { - if (!hasEnoughDataToBuild) { - return null - } + if (!hasEnoughDataToBuild) return null return PodcastPodcastindex( locked = if (::lockedBuilderValue.isInitialized) lockedBuilderValue.build() else null, - funding = fundingBuilders.mapNotNull { it.build() }.asUnmodifiable() + funding = fundingBuilders.mapNotNull { it.build() }.asUnmodifiable(), + location = locationBuilderValue?.build() ) } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index 085853bf..5108d35a 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -7,6 +7,7 @@ import dev.stalla.model.TypeFactory import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_WGS84 import dev.stalla.parser.GeoUriParser import dev.stalla.util.InternalAPI +import dev.stalla.util.asUnmodifiable import dev.stalla.util.containsMediaTypeSeparatorSymbol import java.util.Locale import kotlin.contracts.contract @@ -47,7 +48,7 @@ public class GeoLocation private constructor( coordC = coordC, crs = crs, uncertainty = uncertainty, - parameters = parameters.map { Parameter(it.key, it.value) } + parameters = parameters.map { Parameter(it.key, it.value) }.asUnmodifiable() ) /** diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt index f3afd1ab..4a3dd36c 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt @@ -1,5 +1,9 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.model.BuilderFactory + /** * @property name Human-readable place name. * @property geo TODO. @@ -10,5 +14,11 @@ public data class PodcastindexLocation( val geo: GeoLocation?, val osm: OpenStreetMapFeature? ) { - // TODO + /** Provides a builder for the [PodcastindexLocation] class. */ + public companion object Factory : BuilderFactory { + + /** Returns a builder implementation for building [PodcastindexLocation] model instances. */ + @JvmStatic + override fun builder(): PodcastindexLocationBuilder = ValidatingPodcastindexLocationBuilder() + } } diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt index 0532a376..980acac4 100644 --- a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt @@ -4,6 +4,7 @@ import dev.stalla.builder.GeoLocationBuilder import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.util.InternalAPI import java.util.regex.Pattern +import kotlin.contracts.contract /** * Parser implementation for [GeoLocation] values, as defined in [RFC 5870](https://tools.ietf.org/html/rfc5870). @@ -20,7 +21,13 @@ internal object GeoUriParser { @InternalAPI @Suppress("ComplexMethod", "NestedBlockDepth") - internal fun parse(value: String): GeoLocation? { + internal fun parse(value: String?): GeoLocation? { + contract { + returnsNotNull() implies (value != null) + } + + if (value == null) return null + // URI format: geo:LAT,LONG;prop1=value1;prop2=value2 val scheme = "geo:" if (value.length < scheme.length || !value.substring(0, scheme.length).equals(scheme, ignoreCase = true)) { diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index c0b6354c..7a76d004 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -1,5 +1,7 @@ package dev.stalla.parser.namespace +import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder @@ -11,8 +13,11 @@ import dev.stalla.dom.getAttributeByName import dev.stalla.dom.getAttributeValueByName import dev.stalla.dom.parseAsMediaTypeOrNull import dev.stalla.dom.textAsBooleanOrNull +import dev.stalla.dom.textOrNull import dev.stalla.model.StyledDuration +import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.TranscriptType +import dev.stalla.parser.GeoUriParser import dev.stalla.parser.NamespaceParser import dev.stalla.util.FeedNamespace import dev.stalla.util.InternalAPI @@ -45,32 +50,16 @@ internal object PodcastindexParser : NamespaceParser() { } ?: return builder.podcastPodcastindexBuilder.addFundingBuilder(fundingBuilder) } + "location" -> { + val locationBuilder = ifCanBeParsed { + toLocationBuilder(builder.createLocationBuilder()) + } ?: return + builder.podcastPodcastindexBuilder.locationBuilder(locationBuilder) + } else -> pass } } - private fun Node.toLockedBuilder( - lockedBuilder: PodcastPodcastindexLockedBuilder - ): PodcastPodcastindexLockedBuilder? { - val owner = getAttributeByName("owner")?.value.trimmedOrNullIfBlank() - val locked = textAsBooleanOrNull() - - if (!allNotNull(owner, locked)) return null - return lockedBuilder.owner(owner) - .locked(locked) - } - - private fun Node.toFundingBuilder( - fundingBuilder: PodcastPodcastindexFundingBuilder - ): PodcastPodcastindexFundingBuilder? { - val url = getAttributeByName("url")?.value.trimmedOrNullIfBlank() - val message = textContent.trimmedOrNullIfBlank() - - if (!allNotNull(url, message)) return null - return fundingBuilder.url(url) - .message(message) - } - override fun Node.parseItemData(builder: ProvidingEpisodeBuilder) { when (localName) { "chapters" -> { @@ -91,10 +80,52 @@ internal object PodcastindexParser : NamespaceParser() { } ?: return builder.podcastindexBuilder.addTranscriptBuilder(transcriptBuilder) } + "location" -> { + val locationBuilder = ifCanBeParsed { + toLocationBuilder(builder.createLocationBuilder()) + } ?: return + builder.podcastindexBuilder.locationBuilder(locationBuilder) + } else -> pass } } + private fun Node.toLocationBuilder( + locationBuilder: PodcastindexLocationBuilder + ): PodcastindexLocationBuilder? { + val name = textOrNull() ?: return null + val geoValue = getAttributeByName("geo")?.value.trimmedOrNullIfBlank() + val osmValue = getAttributeByName("osm")?.value.trimmedOrNullIfBlank() + + val osm = null // TODO parse osmValue to OpenStreetMapFeature? + + return locationBuilder.name(name) + .geo(GeoUriParser.parse(geoValue)) + .osm(osm) + } + + private fun Node.toLockedBuilder( + lockedBuilder: PodcastPodcastindexLockedBuilder + ): PodcastPodcastindexLockedBuilder? { + val owner = getAttributeByName("owner")?.value.trimmedOrNullIfBlank() + val locked = textAsBooleanOrNull() + + if (!allNotNull(owner, locked)) return null + return lockedBuilder.owner(owner) + .locked(locked) + } + + private fun Node.toFundingBuilder( + fundingBuilder: PodcastPodcastindexFundingBuilder + ): PodcastPodcastindexFundingBuilder? { + val url = getAttributeByName("url")?.value.trimmedOrNullIfBlank() + val message = textContent.trimmedOrNullIfBlank() + + if (!allNotNull(url, message)) return null + return fundingBuilder.url(url) + .message(message) + } + private fun Node.toChaptersBuilder( chaptersBuilder: EpisodePodcastindexChaptersBuilder ): EpisodePodcastindexChaptersBuilder? { From 9aac57b883cc321f0b2fbb7b0648bbd631df257c Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 16:30:32 +0200 Subject: [PATCH 04/18] Implement OpenStreetMap feature parsing --- .../episode/ValidatingEpisodeBuilder.kt | 5 ++ .../podcast/ValidatingPodcastBuilder.kt | 5 ++ .../podcastindex/OpenStreetMapFeature.kt | 31 +++++-- .../dev/stalla/model/podcastindex/OsmType.kt | 9 ++ .../dev/stalla/parser/OsmFeatureParser.kt | 83 +++++++++++++++++++ .../parser/namespace/PodcastindexParser.kt | 5 +- .../fake/FakePodcastindexLocationBuilder.kt | 42 ++++++++++ .../fake/episode/FakeEpisodeBuilder.kt | 4 + .../episode/FakeEpisodePodcastindexBuilder.kt | 19 +++-- .../fake/podcast/FakePodcastBuilder.kt | 4 + .../podcast/FakePodcastPodcastindexBuilder.kt | 22 +++-- .../dev/stalla/parser/OsmFeatureParserTest.kt | 78 +++++++++++++++++ 12 files changed, 287 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt create mode 100644 src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt create mode 100644 src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt index 7641eb5d..e9e4f516 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt @@ -4,6 +4,7 @@ import dev.stalla.builder.AtomBuilder import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBitloveBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -23,6 +24,7 @@ import dev.stalla.builder.validating.ValidatingAtomBuilder import dev.stalla.builder.validating.ValidatingAtomPersonBuilder import dev.stalla.builder.validating.ValidatingHrefOnlyImageBuilder import dev.stalla.builder.validating.ValidatingLinkBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder import dev.stalla.builder.validating.ValidatingRssCategoryBuilder import dev.stalla.model.Episode import dev.stalla.util.InternalAPI @@ -105,6 +107,9 @@ internal class ValidatingEpisodeBuilder : ProvidingEpisodeBuilder { override fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder = ValidatingEpisodePodcastindexSoundbiteBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = + ValidatingPodcastindexLocationBuilder() + override val hasEnoughDataToBuild: Boolean get() = ::titleValue.isInitialized && ::enclosureBuilderValue.isInitialized && diff --git a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt index db00d212..30620903 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt @@ -4,6 +4,7 @@ import dev.stalla.builder.AtomBuilder import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -21,6 +22,7 @@ import dev.stalla.builder.validating.ValidatingAtomBuilder import dev.stalla.builder.validating.ValidatingAtomPersonBuilder import dev.stalla.builder.validating.ValidatingHrefOnlyImageBuilder import dev.stalla.builder.validating.ValidatingLinkBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder import dev.stalla.builder.validating.ValidatingRssCategoryBuilder import dev.stalla.builder.validating.ValidatingRssImageBuilder import dev.stalla.model.Podcast @@ -115,6 +117,9 @@ internal class ValidatingPodcastBuilder : ProvidingPodcastBuilder { override fun createFundingBuilder(): PodcastPodcastindexFundingBuilder = ValidatingPodcastPodcastindexFundingBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = + ValidatingPodcastindexLocationBuilder() + override val hasEnoughDataToBuild: Boolean get() = episodeBuilders.any { it.hasEnoughDataToBuild } && ::titleValue.isInitialized && ::descriptionValue.isInitialized && diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt index 707b1dbd..ddb8902c 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt @@ -1,17 +1,34 @@ package dev.stalla.model.podcastindex +import dev.stalla.model.TypeFactory +import dev.stalla.parser.OsmFeatureParser +import java.math.BigInteger + // What OpenStreetMap calls "OSM type and OSM id". // See: https://github.com/Podcastindex-org/podcast-namespace/issues/138#issue-758103104 /** - * @property osmType A one-character description of the type of OSM point. One of the supported [OsmType]s. - * @property osmId The ID of the OpenStreetMap feature that is described. - * @property osmRevision An optional revision ID for an OSM object, preceded by a hash.. + * @property type A one-character description of the type of OSM point. One of the supported [OsmType]s. + * @property id The ID of the OpenStreetMap feature that is described. + * @property revision An optional revision ID for an OSM object, preceded by a hash. */ public data class OpenStreetMapFeature( - val osmType: OsmType, - val osmId: Int, - val osmRevision: String? + val type: OsmType, + val id: BigInteger, + val revision: String? ) { - // TODO + + override fun toString(): String = StringBuilder().apply { + append(type.type) + append(id) + if (revision != null) append("#$revision") + }.toString() + + /** + * TODO. + */ + public companion object Factory : TypeFactory { + @JvmStatic + override fun of(rawValue: String?): OpenStreetMapFeature? = rawValue?.let { value -> OsmFeatureParser.parse(value) } + } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt index 09bbf812..046a9eda 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt @@ -20,5 +20,14 @@ public enum class OsmType(public val type: String) { override fun of(rawValue: String?): OsmType? = rawValue?.let { values().find { t -> t.type.equals(it, ignoreCase = true) } } + + /** + * Factory method that returns the instance matching the [rawValue] parameter, if any. + * + * @param rawValue The case insensitive char representation of the instance. + * @return The instance matching [rawValue], or `null` if no matching instance exists. + */ + @JvmStatic + public fun of(rawValue: Char?): OsmType? = of(rawValue?.toString()) } } diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt new file mode 100644 index 00000000..589e78f2 --- /dev/null +++ b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt @@ -0,0 +1,83 @@ +package dev.stalla.parser + +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.OsmType +import dev.stalla.util.InternalAPI +import java.lang.StringBuilder +import java.math.BigInteger +import kotlin.contracts.contract + +@InternalAPI +internal object OsmFeatureParser { + + private enum class ParsingMode { + Type, Id, Revision + } + + internal fun parse(value: String?): OpenStreetMapFeature? { + contract { + returnsNotNull() implies (value != null) + } + + if (value == null) return null + + val builder = Builder() + val idBuffer = StringBuilder() + val revisionBuffer = StringBuilder() + var mode = ParsingMode.Type + + for (c in value) { + when (mode) { + ParsingMode.Type -> { + builder.type(c.toString()) + mode = ParsingMode.Id + } + ParsingMode.Id -> when { + c == '#' -> mode = ParsingMode.Revision + c.isNoDigit() -> return null + else -> idBuffer.append(c) + } + ParsingMode.Revision -> revisionBuffer.append(c) + } + } + + builder.id(idBuffer.stringOrNullIfEmpty()) + builder.revision(revisionBuffer.stringOrNullIfEmpty()) + + return builder.build() + } + + @OptIn(ExperimentalStdlibApi::class) + private fun Char.isNoDigit(): Boolean = digitToIntOrNull() == null + + private fun String.asBigIntegerOrNull(): BigInteger? = try { + BigInteger(this) + } catch (e: NumberFormatException) { + null + } + + private fun StringBuilder.stringOrNullIfEmpty(): String? = if (isNotEmpty()) toString() else null + + private class Builder { + + private var type: String? = null + private var id: String? = null + private var revision: String? = null + + fun type(type: String?): Builder = apply { this.type = type } + + fun id(id: String?): Builder = apply { this.id = id } + + fun revision(revision: String?): Builder = apply { this.revision = revision } + + fun build(): OpenStreetMapFeature? { + return OpenStreetMapFeature( + type = OsmType.of(type) ?: return null, + id = id?.asBigIntegerOrNull() ?: return null, + revision = revision + ) + } + + } + +} diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index 7a76d004..39e3be89 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -19,6 +19,7 @@ import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.parser.GeoUriParser import dev.stalla.parser.NamespaceParser +import dev.stalla.parser.OsmFeatureParser import dev.stalla.util.FeedNamespace import dev.stalla.util.InternalAPI import dev.stalla.util.allNotNull @@ -97,11 +98,9 @@ internal object PodcastindexParser : NamespaceParser() { val geoValue = getAttributeByName("geo")?.value.trimmedOrNullIfBlank() val osmValue = getAttributeByName("osm")?.value.trimmedOrNullIfBlank() - val osm = null // TODO parse osmValue to OpenStreetMapFeature? - return locationBuilder.name(name) .geo(GeoUriParser.parse(geoValue)) - .osm(osm) + .osm(OsmFeatureParser.parse(osmValue)) } private fun Node.toLockedBuilder( diff --git a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt new file mode 100644 index 00000000..93d2ce92 --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt @@ -0,0 +1,42 @@ +package dev.stalla.builder.fake + +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.PodcastindexLocation + +internal class FakePodcastindexLocationBuilder: FakeBuilder(), PodcastindexLocationBuilder { + + var nameValue: String? = null + var geoValue: GeoLocation? = null + var osmValue: OpenStreetMapFeature? = null + + override fun name(name: String): PodcastindexLocationBuilder = apply { this.nameValue = name } + + override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geoValue = geo } + + override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osmValue = osm } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FakePodcastindexLocationBuilder + + if (nameValue != other.nameValue) return false + if (geoValue != other.geoValue) return false + if (osmValue != other.osmValue) return false + + return true + } + + override fun hashCode(): Int { + var result = nameValue?.hashCode() ?: 0 + result = 31 * result + (geoValue?.hashCode() ?: 0) + result = 31 * result + (osmValue?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "FakePodcastindexLocationBuilder(nameValue=$nameValue, geoValue=$geoValue, osmValue=$osmValue)" + } +} diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt index 5863880f..81bf12f2 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt @@ -3,6 +3,7 @@ package dev.stalla.builder.fake.episode import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBuilder import dev.stalla.builder.episode.EpisodeEnclosureBuilder @@ -17,6 +18,7 @@ import dev.stalla.builder.fake.FakeAtomPersonBuilder import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.fake.FakeHrefOnlyImageBuilder import dev.stalla.builder.fake.FakeLinkBuilder +import dev.stalla.builder.fake.FakePodcastindexLocationBuilder import dev.stalla.builder.fake.FakeRssCategoryBuilder import dev.stalla.model.Episode import java.time.temporal.TemporalAccessor @@ -93,6 +95,8 @@ internal class FakeEpisodeBuilder : FakeBuilder(), ProvidingEpisodeBuil override fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder = FakeEpisodePodcastindexSoundbiteBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = FakePodcastindexLocationBuilder() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is FakeEpisodeBuilder) return false diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt index 974fd952..d381035f 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt @@ -1,5 +1,6 @@ package dev.stalla.builder.fake.episode +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.episode.EpisodePodcastindexBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder @@ -10,7 +11,7 @@ import dev.stalla.model.podcastindex.EpisodePodcastindex internal class FakeEpisodePodcastindexBuilder : FakeBuilder(), EpisodePodcastindexBuilder { var chaptersBuilderValue: EpisodePodcastindexChaptersBuilder? = null - + var locationBuilderValue: PodcastindexLocationBuilder? = null val transcriptBuilders: MutableList = mutableListOf() val soundbiteBuilders: MutableList = mutableListOf() @@ -26,11 +27,18 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder transcriptBuilders.add(transcriptBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder = apply { + this.locationBuilderValue = locationBuilder + } + override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is FakeEpisodePodcastindexBuilder) return false + if (javaClass != other?.javaClass) return false + + other as FakeEpisodePodcastindexBuilder if (chaptersBuilderValue != other.chaptersBuilderValue) return false + if (locationBuilderValue != other.locationBuilderValue) return false if (transcriptBuilders != other.transcriptBuilders) return false if (soundbiteBuilders != other.soundbiteBuilders) return false @@ -39,12 +47,13 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder override fun hashCode(): Int { var result = chaptersBuilderValue?.hashCode() ?: 0 + result = 31 * result + (locationBuilderValue?.hashCode() ?: 0) result = 31 * result + transcriptBuilders.hashCode() result = 31 * result + soundbiteBuilders.hashCode() return result } - override fun toString() = - "FakeEpisodePodcastBuilder(chaptersBuilder=$chaptersBuilderValue, transcriptBuilders=$transcriptBuilders, " + - "soundbiteBuilders=$soundbiteBuilders)" + override fun toString(): String { + return "FakeEpisodePodcastindexBuilder(chaptersBuilderValue=$chaptersBuilderValue, locationBuilderValue=$locationBuilderValue, transcriptBuilders=$transcriptBuilders, soundbiteBuilders=$soundbiteBuilders)" + } } diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt index e2cb2da2..8836c0ca 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt @@ -3,6 +3,7 @@ package dev.stalla.builder.fake.podcast import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -11,6 +12,7 @@ import dev.stalla.builder.fake.FakeAtomPersonBuilder import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.fake.FakeHrefOnlyImageBuilder import dev.stalla.builder.fake.FakeLinkBuilder +import dev.stalla.builder.fake.FakePodcastindexLocationBuilder import dev.stalla.builder.fake.FakeRssCategoryBuilder import dev.stalla.builder.fake.FakeRssImageBuilder import dev.stalla.builder.podcast.PodcastBuilder @@ -104,6 +106,8 @@ internal class FakePodcastBuilder : FakeBuilder(), ProvidingPodcastBuil override fun createFundingBuilder(): PodcastPodcastindexFundingBuilder = FakePodcastPodcastindexFundingBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = FakePodcastindexLocationBuilder() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is FakePodcastBuilder) return false diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt index 28677bba..1785a51e 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt @@ -1,5 +1,6 @@ package dev.stalla.builder.fake.podcast +import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder @@ -9,6 +10,7 @@ import dev.stalla.model.podcastindex.PodcastPodcastindex internal class FakePodcastPodcastindexBuilder : FakeBuilder(), PodcastPodcastindexBuilder { var lockedBuilderValue: PodcastPodcastindexLockedBuilder? = null + var locationBuilderValue: PodcastindexLocationBuilder? = null val fundingBuilders: MutableList = mutableListOf() override fun lockedBuilder(lockedBuilder: PodcastPodcastindexLockedBuilder): PodcastPodcastindexBuilder = apply { @@ -19,21 +21,31 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder fundingBuilders.add(fundingBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder = apply { + this.locationBuilderValue = locationBuilder + } + override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is FakePodcastPodcastindexBuilder) return false + if (javaClass != other?.javaClass) return false + + other as FakePodcastPodcastindexBuilder - if (fundingBuilders != other.fundingBuilders) return false if (lockedBuilderValue != other.lockedBuilderValue) return false + if (locationBuilderValue != other.locationBuilderValue) return false + if (fundingBuilders != other.fundingBuilders) return false return true } override fun hashCode(): Int { - var result = fundingBuilders.hashCode() - result = 31 * result + lockedBuilderValue.hashCode() + var result = lockedBuilderValue?.hashCode() ?: 0 + result = 31 * result + (locationBuilderValue?.hashCode() ?: 0) + result = 31 * result + fundingBuilders.hashCode() return result } - override fun toString() = "FakePodcastPodcastBuilder(fundingBuilders=$fundingBuilders, lockedBuilder=$lockedBuilderValue)" + override fun toString(): String { + return "FakePodcastPodcastindexBuilder(lockedBuilderValue=$lockedBuilderValue, locationBuilderValue=$locationBuilderValue, fundingBuilders=$fundingBuilders)" + } } diff --git a/src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt b/src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt new file mode 100644 index 00000000..2a5637d7 --- /dev/null +++ b/src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt @@ -0,0 +1,78 @@ +package dev.stalla.parser + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.prop +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.OsmType +import org.junit.jupiter.api.Test +import java.math.BigInteger + +class OsmFeatureParserTest { + + @Test + fun `test 1`() { + assertThat(OsmFeatureParser.parse("R148838")).isNotNull().all { + prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) + prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("148838")) + prop(OpenStreetMapFeature::revision).isNull() + } + } + + @Test + fun `test 2`() { + assertThat(OsmFeatureParser.parse("W5013364")).isNotNull().all { + prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Way) + prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("5013364")) + prop(OpenStreetMapFeature::revision).isNull() + } + } + + @Test + fun `test 3`() { + assertThat(OsmFeatureParser.parse("R7444#188")).isNotNull().all { + prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) + prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("7444")) + prop(OpenStreetMapFeature::revision).isEqualTo("188") + } + } + + @Test + fun `test 3_5`() { + assertThat(OsmFeatureParser.parse("X")).isNull() + } + + @Test + fun `test 4`() { + assertThat(OsmFeatureParser.parse("X12345")).isNull() + } + + @Test + fun `test 5`() { + assertThat(OsmFeatureParser.parse("R")).isNull() + } + + @Test + fun `test 6`() { + assertThat(OsmFeatureParser.parse("R#188")).isNull() + } + + @Test + fun `test 7`() { + assertThat(OsmFeatureParser.parse("Rabc")).isNull() + } + + @Test + fun `test 8`() { + assertThat(OsmFeatureParser.parse("Rabc")).isNull() + } + + @Test + fun `test 9`() { + assertThat(OsmFeatureParser.parse("Rabc#123")).isNull() + } + +} From fb2f21a7fcacaddae964327518642fe9c80e87d1 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 16:54:09 +0200 Subject: [PATCH 05/18] OpenStreetMap feature gets a dedicated builder as well, for consistency --- .../builder/OpenStreetMapFeatureBuilder.kt | 27 ++++++++++ .../ValidatingGeoLocationBuilder.kt | 1 + .../ValidatingOpenStreetMapFeatureBuilder.kt | 35 ++++++++++++ .../podcastindex/OpenStreetMapFeature.kt | 15 ++++-- .../dev/stalla/parser/OsmFeatureParser.kt | 44 +++++---------- .../GeoLocationTest.kt} | 54 +++++++++---------- .../OpenStreetMapFeatureTest.kt} | 26 ++++----- 7 files changed, 127 insertions(+), 75 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt rename src/test/kotlin/dev/stalla/{parser/GeoUriParserTest.kt => model/GeoLocationTest.kt} (77%) rename src/test/kotlin/dev/stalla/{parser/OsmFeatureParserTest.kt => model/OpenStreetMapFeatureTest.kt} (63%) diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt new file mode 100644 index 00000000..1b8d6750 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt @@ -0,0 +1,27 @@ +package dev.stalla.builder + +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.OsmType +import dev.stalla.util.whenNotNull +import java.math.BigInteger + +/** + * Builder for constructing [OpenStreetMapFeature] instances. + * + * @since 1.1.0 + */ +public interface OpenStreetMapFeatureBuilder : Builder { + + public fun type(type: OsmType): OpenStreetMapFeatureBuilder + + public fun id(id: BigInteger): OpenStreetMapFeatureBuilder + + public fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder + + override fun applyFrom(prototype: OpenStreetMapFeature?): OpenStreetMapFeatureBuilder = + whenNotNull(prototype) { feature -> + type(feature.type) + id(feature.id) + revision(feature.revision) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt index b24ee2e1..76c6c1bb 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -74,4 +74,5 @@ public class ValidatingGeoLocationBuilder : GeoLocationBuilder { parameters = parameters // this secondary constructor will apply .asUnmodifiable() ) } + } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt new file mode 100644 index 00000000..3b3ea754 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt @@ -0,0 +1,35 @@ +package dev.stalla.builder.validating + +import dev.stalla.builder.OpenStreetMapFeatureBuilder +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.OsmType +import dev.stalla.util.InternalAPI +import java.math.BigInteger + +@InternalAPI +internal class ValidatingOpenStreetMapFeatureBuilder : OpenStreetMapFeatureBuilder { + + private lateinit var typeValue: OsmType + private lateinit var idValue: BigInteger + private var revisionValue: BigInteger? = null + + override fun type(type: OsmType): OpenStreetMapFeatureBuilder = apply { this.typeValue = type } + + override fun id(id: BigInteger): OpenStreetMapFeatureBuilder = apply { this.idValue = id } + + override fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder = apply { this.revisionValue = revision } + + override val hasEnoughDataToBuild: Boolean + get() = ::typeValue.isInitialized && ::idValue.isInitialized + + override fun build(): OpenStreetMapFeature? { + if (!hasEnoughDataToBuild) return null + + return OpenStreetMapFeature( + type = typeValue, + id = idValue, + revision = revisionValue + ) + } + +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt index ddb8902c..118f9369 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt @@ -1,12 +1,12 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.OpenStreetMapFeatureBuilder +import dev.stalla.builder.validating.ValidatingOpenStreetMapFeatureBuilder +import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory import dev.stalla.parser.OsmFeatureParser import java.math.BigInteger -// What OpenStreetMap calls "OSM type and OSM id". -// See: https://github.com/Podcastindex-org/podcast-namespace/issues/138#issue-758103104 - /** * @property type A one-character description of the type of OSM point. One of the supported [OsmType]s. * @property id The ID of the OpenStreetMap feature that is described. @@ -15,7 +15,7 @@ import java.math.BigInteger public data class OpenStreetMapFeature( val type: OsmType, val id: BigInteger, - val revision: String? + val revision: BigInteger? ) { override fun toString(): String = StringBuilder().apply { @@ -27,7 +27,12 @@ public data class OpenStreetMapFeature( /** * TODO. */ - public companion object Factory : TypeFactory { + public companion object Factory : BuilderFactory, TypeFactory { + + /** Returns a builder implementation for building [OpenStreetMapFeature] model instances. */ + @JvmStatic + override fun builder(): OpenStreetMapFeatureBuilder = ValidatingOpenStreetMapFeatureBuilder() + @JvmStatic override fun of(rawValue: String?): OpenStreetMapFeature? = rawValue?.let { value -> OsmFeatureParser.parse(value) } } diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt index 589e78f2..36ccb90b 100644 --- a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt @@ -14,6 +14,7 @@ internal object OsmFeatureParser { Type, Id, Revision } + @InternalAPI internal fun parse(value: String?): OpenStreetMapFeature? { contract { returnsNotNull() implies (value != null) @@ -21,7 +22,7 @@ internal object OsmFeatureParser { if (value == null) return null - val builder = Builder() + val builder = OpenStreetMapFeature.builder() val idBuffer = StringBuilder() val revisionBuffer = StringBuilder() var mode = ParsingMode.Type @@ -29,7 +30,8 @@ internal object OsmFeatureParser { for (c in value) { when (mode) { ParsingMode.Type -> { - builder.type(c.toString()) + val type = OsmType.of(c) ?: return null + builder.type(type) mode = ParsingMode.Id } ParsingMode.Id -> when { @@ -41,8 +43,9 @@ internal object OsmFeatureParser { } } - builder.id(idBuffer.stringOrNullIfEmpty()) - builder.revision(revisionBuffer.stringOrNullIfEmpty()) + val idValue = idBuffer.stringOrNullIfEmpty().asBigIntegerOrNull() ?: return null + builder.id(idValue) + builder.revision(revisionBuffer.stringOrNullIfEmpty().asBigIntegerOrNull()) return builder.build() } @@ -50,34 +53,15 @@ internal object OsmFeatureParser { @OptIn(ExperimentalStdlibApi::class) private fun Char.isNoDigit(): Boolean = digitToIntOrNull() == null - private fun String.asBigIntegerOrNull(): BigInteger? = try { - BigInteger(this) - } catch (e: NumberFormatException) { - null + private fun String?.asBigIntegerOrNull(): BigInteger? { + if (this == null) return null + return try { + BigInteger(this) + } catch (e: NumberFormatException) { + null + } } private fun StringBuilder.stringOrNullIfEmpty(): String? = if (isNotEmpty()) toString() else null - private class Builder { - - private var type: String? = null - private var id: String? = null - private var revision: String? = null - - fun type(type: String?): Builder = apply { this.type = type } - - fun id(id: String?): Builder = apply { this.id = id } - - fun revision(revision: String?): Builder = apply { this.revision = revision } - - fun build(): OpenStreetMapFeature? { - return OpenStreetMapFeature( - type = OsmType.of(type) ?: return null, - id = id?.asBigIntegerOrNull() ?: return null, - revision = revision - ) - } - - } - } diff --git a/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt similarity index 77% rename from src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt rename to src/test/kotlin/dev/stalla/model/GeoLocationTest.kt index ea5ad39b..626a0408 100644 --- a/src/test/kotlin/dev/stalla/parser/GeoUriParserTest.kt +++ b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt @@ -1,4 +1,4 @@ -package dev.stalla.parser +package dev.stalla.model import assertk.all import assertk.assertAll @@ -14,11 +14,11 @@ import assertk.assertions.prop import dev.stalla.model.podcastindex.GeoLocation import org.junit.jupiter.api.Test -class GeoUriParserTest { +class GeoLocationTest { @Test fun `should parse a Geo URI with A and B correctly`() { - assertThat(GeoUriParser.parse("geo:37.786971,-122.399677")).isNotNull().all { + assertThat(GeoLocation.of("geo:37.786971,-122.399677")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(37.786971) prop(GeoLocation::coordB).isEqualTo(-122.399677) prop(GeoLocation::coordC).isNull() @@ -30,7 +30,7 @@ class GeoUriParserTest { @Test fun `should parse a Geo URI with A and B and C correctly`() { - assertThat(GeoUriParser.parse("geo:40.714623,-74.006605,1.1")).isNotNull().all { + assertThat(GeoLocation.of("geo:40.714623,-74.006605,1.1")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(40.714623) prop(GeoLocation::coordB).isEqualTo(-74.006605) prop(GeoLocation::coordC).isEqualTo(1.1) @@ -42,7 +42,7 @@ class GeoUriParserTest { @Test fun `test 2`() { - assertThat(GeoUriParser.parse("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { + assertThat(GeoLocation.of("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(48.198634) prop(GeoLocation::coordB).isEqualTo(16.371648) prop(GeoLocation::coordC).isNull() @@ -54,7 +54,7 @@ class GeoUriParserTest { @Test fun `parse all`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -67,7 +67,7 @@ class GeoUriParserTest { @Test fun `parse no params`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -79,7 +79,7 @@ class GeoUriParserTest { @Test fun `parse no params or u`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -91,7 +91,7 @@ class GeoUriParserTest { @Test fun `parse no params or crs`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -103,7 +103,7 @@ class GeoUriParserTest { @Test fun `parse no params or u or crs`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78,-21.43")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78,-21.43")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -115,7 +115,7 @@ class GeoUriParserTest { @Test fun `parse no params or u or crs or coordC`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isNull() @@ -127,7 +127,7 @@ class GeoUriParserTest { @Test fun `parse invalid uncertainty`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78;u=invalid")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78;u=invalid")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isNull() @@ -140,22 +140,22 @@ class GeoUriParserTest { @Test fun `parse no params or u or crs or coordsC or coordB`() { - assertThat(GeoUriParser.parse("geo:12.34")).isNull() + assertThat(GeoLocation.of("geo:12.34")).isNull() } @Test fun `parse no params or u or crs or coordsC or coordB or coordA`() { - assertThat(GeoUriParser.parse("geo:")).isNull() + assertThat(GeoLocation.of("geo:")).isNull() } @Test fun `parse not geo uri(`() { - assertThat(GeoUriParser.parse("https://stalla.dev")).isNull() + assertThat(GeoLocation.of("https://stalla.dev")).isNull() } @Test fun `parse decode special chars in param value`() { - assertThat(GeoUriParser.parse("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(56.78) prop(GeoLocation::coordC).isNull() @@ -168,7 +168,7 @@ class GeoUriParserTest { @Test fun `multiple params`() { - assertThat(GeoUriParser.parse("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { + assertThat(GeoLocation.of("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(12.34) prop(GeoLocation::coordB).isEqualTo(45.67) prop(GeoLocation::coordC).isEqualTo(-21.43) @@ -182,8 +182,8 @@ class GeoUriParserTest { @Test fun `WGS84 pole rule`() { - val geoLocation1 = GeoUriParser.parse("geo:90,-22.43;crs=WGS84") - val geoLocation2 = GeoUriParser.parse("geo:90,46;crs=WGS84") + val geoLocation1 = GeoLocation.of("geo:90,-22.43;crs=WGS84") + val geoLocation2 = GeoLocation.of("geo:90,46;crs=WGS84") assertAll { assertThat(geoLocation1).isNotNull() assertThat(geoLocation2).isNotNull() @@ -193,8 +193,8 @@ class GeoUriParserTest { @Test fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { - val geoLocation1 = GeoUriParser.parse("geo:66,30;u=6.500;FOo=this%2dthat") - val geoLocation2 = GeoUriParser.parse("geo:66.0,30;u=6.5;foo=this-that") + val geoLocation1 = GeoLocation.of("geo:66,30;u=6.500;FOo=this%2dthat") + val geoLocation2 = GeoLocation.of("geo:66.0,30;u=6.5;foo=this-that") assertAll { assertThat(geoLocation1).isNotNull() assertThat(geoLocation2).isNotNull() @@ -204,8 +204,8 @@ class GeoUriParserTest { @Test fun `parameter order is insignificant`() { - val geoLocation1 = GeoUriParser.parse("geo:47,11;foo=blue;bar=white") - val geoLocation2 = GeoUriParser.parse("geo:47,11;bar=white;foo=blue") + val geoLocation1 = GeoLocation.of("geo:47,11;foo=blue;bar=white") + val geoLocation2 = GeoLocation.of("geo:47,11;bar=white;foo=blue") assertAll { assertThat(geoLocation1).isNotNull() assertThat(geoLocation2).isNotNull() @@ -215,8 +215,8 @@ class GeoUriParserTest { @Test fun `parameter keys are case-insensitive`() { - val geoLocation1 = GeoUriParser.parse("geo:22,0;bar=blue") - val geoLocation2 = GeoUriParser.parse("geo:22,0;BAR=blue") + val geoLocation1 = GeoLocation.of("geo:22,0;bar=blue") + val geoLocation2 = GeoLocation.of("geo:22,0;BAR=blue") assertAll { assertThat(geoLocation1).isNotNull() assertThat(geoLocation2).isNotNull() @@ -226,8 +226,8 @@ class GeoUriParserTest { @Test fun `parameter values are case-sensitive`() { - val geoLocation1 = GeoUriParser.parse("geo:22,0;bar=BLUE") - val geoLocation2 = GeoUriParser.parse("geo:22,0;bar=blue") + val geoLocation1 = GeoLocation.of("geo:22,0;bar=BLUE") + val geoLocation2 = GeoLocation.of("geo:22,0;bar=blue") assertAll { assertThat(geoLocation1).isNotNull() assertThat(geoLocation2).isNotNull() diff --git a/src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt b/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt similarity index 63% rename from src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt rename to src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt index 2a5637d7..3fc0b287 100644 --- a/src/test/kotlin/dev/stalla/parser/OsmFeatureParserTest.kt +++ b/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt @@ -1,4 +1,4 @@ -package dev.stalla.parser +package dev.stalla.model import assertk.all import assertk.assertThat @@ -11,11 +11,11 @@ import dev.stalla.model.podcastindex.OsmType import org.junit.jupiter.api.Test import java.math.BigInteger -class OsmFeatureParserTest { +class OpenStreetMapFeatureTest { @Test fun `test 1`() { - assertThat(OsmFeatureParser.parse("R148838")).isNotNull().all { + assertThat(OpenStreetMapFeature.of("R148838")).isNotNull().all { prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("148838")) prop(OpenStreetMapFeature::revision).isNull() @@ -24,7 +24,7 @@ class OsmFeatureParserTest { @Test fun `test 2`() { - assertThat(OsmFeatureParser.parse("W5013364")).isNotNull().all { + assertThat(OpenStreetMapFeature.of("W5013364")).isNotNull().all { prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Way) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("5013364")) prop(OpenStreetMapFeature::revision).isNull() @@ -33,46 +33,46 @@ class OsmFeatureParserTest { @Test fun `test 3`() { - assertThat(OsmFeatureParser.parse("R7444#188")).isNotNull().all { + assertThat(OpenStreetMapFeature.of("R7444#188")).isNotNull().all { prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("7444")) - prop(OpenStreetMapFeature::revision).isEqualTo("188") + prop(OpenStreetMapFeature::revision).isEqualTo(BigInteger("188")) } } @Test fun `test 3_5`() { - assertThat(OsmFeatureParser.parse("X")).isNull() + assertThat(OpenStreetMapFeature.of("X")).isNull() } @Test fun `test 4`() { - assertThat(OsmFeatureParser.parse("X12345")).isNull() + assertThat(OpenStreetMapFeature.of("X12345")).isNull() } @Test fun `test 5`() { - assertThat(OsmFeatureParser.parse("R")).isNull() + assertThat(OpenStreetMapFeature.of("R")).isNull() } @Test fun `test 6`() { - assertThat(OsmFeatureParser.parse("R#188")).isNull() + assertThat(OpenStreetMapFeature.of("R#188")).isNull() } @Test fun `test 7`() { - assertThat(OsmFeatureParser.parse("Rabc")).isNull() + assertThat(OpenStreetMapFeature.of("Rabc")).isNull() } @Test fun `test 8`() { - assertThat(OsmFeatureParser.parse("Rabc")).isNull() + assertThat(OpenStreetMapFeature.of("Rabc")).isNull() } @Test fun `test 9`() { - assertThat(OsmFeatureParser.parse("Rabc#123")).isNull() + assertThat(OpenStreetMapFeature.of("Rabc#123")).isNull() } } From bd0ccada4a440ef67c512291640491d6a1e33d05 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 21:35:42 +0200 Subject: [PATCH 06/18] Implement the remaining builders for Podcastindex phase 2 --- .../builder/OpenStreetMapFeatureBuilder.kt | 4 +- .../builder/PodcastindexLocationBuilder.kt | 1 - .../builder/PodcastindexPersonBuilder.kt | 26 ++++++++++ .../EpisodePodcastindexEpisodeBuilder.kt | 18 +++++++ .../EpisodePodcastindexSeasonBuilder.kt | 18 +++++++ .../ValidatingOpenStreetMapFeatureBuilder.kt | 12 ++--- .../ValidatingPodcastindexLocationBuilder.kt | 12 ++--- .../ValidatingPodcastindexPersonBuilder.kt | 41 +++++++++++++++ ...datingEpisodePodcastindexEpisodeBuilder.kt | 28 ++++++++++ ...idatingEpisodePodcastindexSeasonBuilder.kt | 28 ++++++++++ .../stalla/model/podcastindex/GeoLocation.kt | 6 ++- ...OsmType.kt => OpenStreetMapElementType.kt} | 11 ++-- .../podcastindex/OpenStreetMapFeature.kt | 8 ++- .../model/podcastindex/PodcastindexEpisode.kt | 16 +++++- .../podcastindex/PodcastindexLocation.kt | 4 ++ .../model/podcastindex/PodcastindexPerson.kt | 16 +++++- .../model/podcastindex/PodcastindexSeason.kt | 15 +++++- .../dev/stalla/parser/OsmFeatureParser.kt | 4 +- .../fake/FakePodcastindexLocationBuilder.kt | 28 +++++----- .../fake/FakePodcastindexPersonBuilder.kt | 51 +++++++++++++++++++ .../FakeEpisodePodcastindexEpisodeBuilder.kt | 37 ++++++++++++++ .../FakeEpisodePodcastindexSeasonBuilder.kt | 37 ++++++++++++++ .../stalla/model/OpenStreetMapFeatureTest.kt | 8 +-- 23 files changed, 383 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt rename src/main/kotlin/dev/stalla/model/podcastindex/{OsmType.kt => OpenStreetMapElementType.kt} (61%) create mode 100644 src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexPersonBuilder.kt create mode 100644 src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexEpisodeBuilder.kt create mode 100644 src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt index 1b8d6750..b73a4378 100644 --- a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt @@ -1,7 +1,7 @@ package dev.stalla.builder import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OsmType +import dev.stalla.model.podcastindex.OpenStreetMapElementType import dev.stalla.util.whenNotNull import java.math.BigInteger @@ -12,7 +12,7 @@ import java.math.BigInteger */ public interface OpenStreetMapFeatureBuilder : Builder { - public fun type(type: OsmType): OpenStreetMapFeatureBuilder + public fun type(type: OpenStreetMapElementType): OpenStreetMapFeatureBuilder public fun id(id: BigInteger): OpenStreetMapFeatureBuilder diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt index 0f9370cc..e5e5cc73 100644 --- a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt @@ -19,5 +19,4 @@ public interface PodcastindexLocationBuilder : Builder { geo(GeoLocation.builder().applyFrom(location.geo).build()!!) // TODO osm or osmBuilder ?! } - } diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt new file mode 100644 index 00000000..54c80c1d --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt @@ -0,0 +1,26 @@ +package dev.stalla.builder + +import dev.stalla.model.podcastindex.PodcastindexPerson +import dev.stalla.util.whenNotNull + +public interface PodcastindexPersonBuilder : Builder { + + public fun name(name: String): PodcastindexPersonBuilder + + public fun role(role: String?): PodcastindexPersonBuilder + + public fun group(group: String?): PodcastindexPersonBuilder + + public fun img(img: String?): PodcastindexPersonBuilder + + public fun href(href: String?): PodcastindexPersonBuilder + + override fun applyFrom(prototype: PodcastindexPerson?): PodcastindexPersonBuilder = + whenNotNull(prototype) { person -> + name(person.name) + role(person.role) + group(person.group) + img(person.img) + href(person.href) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt new file mode 100644 index 00000000..f7977895 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt @@ -0,0 +1,18 @@ +package dev.stalla.builder.episode + +import dev.stalla.builder.Builder +import dev.stalla.model.podcastindex.PodcastindexEpisode +import dev.stalla.util.whenNotNull + +public interface EpisodePodcastindexEpisodeBuilder : Builder { + + public fun number(number: Double): EpisodePodcastindexEpisodeBuilder + + public fun display(display: String?): EpisodePodcastindexEpisodeBuilder + + override fun applyFrom(prototype: PodcastindexEpisode?): EpisodePodcastindexEpisodeBuilder = + whenNotNull(prototype) { episode -> + number(episode.number) + display(episode.display) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt new file mode 100644 index 00000000..0dd642d0 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt @@ -0,0 +1,18 @@ +package dev.stalla.builder.episode + +import dev.stalla.builder.Builder +import dev.stalla.model.podcastindex.PodcastindexSeason +import dev.stalla.util.whenNotNull + +public interface EpisodePodcastindexSeasonBuilder : Builder { + + public fun number(number: Double): EpisodePodcastindexSeasonBuilder + + public fun name(name: String?): EpisodePodcastindexSeasonBuilder + + override fun applyFrom(prototype: PodcastindexSeason?): EpisodePodcastindexSeasonBuilder = + whenNotNull(prototype) { season -> + number(season.number) + name(season.name) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt index 3b3ea754..c0e0755b 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt @@ -2,22 +2,22 @@ package dev.stalla.builder.validating import dev.stalla.builder.OpenStreetMapFeatureBuilder import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OsmType +import dev.stalla.model.podcastindex.OpenStreetMapElementType import dev.stalla.util.InternalAPI import java.math.BigInteger @InternalAPI internal class ValidatingOpenStreetMapFeatureBuilder : OpenStreetMapFeatureBuilder { - private lateinit var typeValue: OsmType + private lateinit var typeValue: OpenStreetMapElementType private lateinit var idValue: BigInteger - private var revisionValue: BigInteger? = null + private var revision: BigInteger? = null - override fun type(type: OsmType): OpenStreetMapFeatureBuilder = apply { this.typeValue = type } + override fun type(type: OpenStreetMapElementType): OpenStreetMapFeatureBuilder = apply { this.typeValue = type } override fun id(id: BigInteger): OpenStreetMapFeatureBuilder = apply { this.idValue = id } - override fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder = apply { this.revisionValue = revision } + override fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder = apply { this.revision = revision } override val hasEnoughDataToBuild: Boolean get() = ::typeValue.isInitialized && ::idValue.isInitialized @@ -28,7 +28,7 @@ internal class ValidatingOpenStreetMapFeatureBuilder : OpenStreetMapFeatureBuild return OpenStreetMapFeature( type = typeValue, id = idValue, - revision = revisionValue + revision = revision ) } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt index 47e37c13..323b0b94 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt @@ -10,14 +10,14 @@ import dev.stalla.util.InternalAPI internal class ValidatingPodcastindexLocationBuilder : PodcastindexLocationBuilder { private lateinit var nameValue: String - private var geoValue: GeoLocation? = null - private var osmValue: OpenStreetMapFeature? = null + private var geo: GeoLocation? = null + private var osm: OpenStreetMapFeature? = null override fun name(name: String): PodcastindexLocationBuilder = apply { this.nameValue = name } - override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geoValue = geo } + override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } - override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osmValue = osm } + override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osm = osm } override val hasEnoughDataToBuild: Boolean get() = ::nameValue.isInitialized @@ -27,8 +27,8 @@ internal class ValidatingPodcastindexLocationBuilder : PodcastindexLocationBuild return PodcastindexLocation( name = nameValue, - geo = geoValue, - osm = osmValue + geo = geo, + osm = osm ) } } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilder.kt new file mode 100644 index 00000000..0a3d18c9 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilder.kt @@ -0,0 +1,41 @@ +package dev.stalla.builder.validating + +import dev.stalla.builder.PodcastindexPersonBuilder +import dev.stalla.model.podcastindex.PodcastindexPerson +import dev.stalla.util.InternalAPI + +@InternalAPI +internal class ValidatingPodcastindexPersonBuilder : PodcastindexPersonBuilder { + + private lateinit var nameValue: String + + private var role: String? = null + private var group: String? = null + private var img: String? = null + private var href: String? = null + + override fun name(name: String): PodcastindexPersonBuilder = apply { this.nameValue = name } + + override fun role(role: String?): PodcastindexPersonBuilder = apply { this.role = role } + + override fun group(group: String?): PodcastindexPersonBuilder = apply { this.group = group } + + override fun img(img: String?): PodcastindexPersonBuilder = apply { this.img = img } + + override fun href(href: String?): PodcastindexPersonBuilder = apply { this.href = href } + + override val hasEnoughDataToBuild: Boolean + get() = ::nameValue.isInitialized + + override fun build(): PodcastindexPerson? { + if (!hasEnoughDataToBuild) return null + + return PodcastindexPerson( + name = nameValue, + role = role, + group = group, + img = img, + href = href + ) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilder.kt new file mode 100644 index 00000000..44959e15 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilder.kt @@ -0,0 +1,28 @@ +package dev.stalla.builder.validating.episode + +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.model.podcastindex.PodcastindexEpisode +import dev.stalla.util.InternalAPI + +@InternalAPI +internal class ValidatingEpisodePodcastindexEpisodeBuilder : EpisodePodcastindexEpisodeBuilder { + + private var number: Double? = null + private var display: String? = null + + override fun number(number: Double): EpisodePodcastindexEpisodeBuilder = apply { this.number = number } + + override fun display(display: String?): EpisodePodcastindexEpisodeBuilder = apply { this.display = display } + + override val hasEnoughDataToBuild: Boolean + get() = number != null + + override fun build(): PodcastindexEpisode? { + if (!hasEnoughDataToBuild) return null + + return PodcastindexEpisode( + number = number ?: return null, + display = display + ) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt new file mode 100644 index 00000000..10c99e72 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt @@ -0,0 +1,28 @@ +package dev.stalla.builder.validating.episode + +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder +import dev.stalla.model.podcastindex.PodcastindexSeason +import dev.stalla.util.InternalAPI + +@InternalAPI +internal class ValidatingEpisodePodcastindexSeasonBuilder : EpisodePodcastindexSeasonBuilder { + + private var number: Double? = null + private var name: String? = null + + override fun number(number: Double): EpisodePodcastindexSeasonBuilder = apply { this.number = number } + + override fun name(name: String?): EpisodePodcastindexSeasonBuilder = apply { this.name = name } + + override val hasEnoughDataToBuild: Boolean + get() = number != null + + override fun build(): PodcastindexSeason? { + if (!hasEnoughDataToBuild) return null + + return PodcastindexSeason( + number = number ?: return null, + name = name + ) + } +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index 5108d35a..6001357b 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -24,6 +24,7 @@ import kotlin.math.absoluteValue * @property parameters TODO. * * @see [RFC 5870](https://tools.ietf.org/html/rfc5870) + * @since 1.1.0 */ public class GeoLocation private constructor( public val coordA: Double, @@ -184,6 +185,9 @@ public class GeoLocation private constructor( */ public companion object Factory : BuilderFactory, TypeFactory { + /** The World Geodetic System 1984 coordinate reference system used by GPS. */ + public const val CRS_WGS84: String = "WGS84" + internal const val PARAM_CRS = "crs" internal const val PARAM_UNCERTAINTY = "u" @@ -194,7 +198,5 @@ public class GeoLocation private constructor( @JvmStatic override fun of(rawValue: String?): GeoLocation? = rawValue?.let { value -> GeoUriParser.parse(value) } - /** The World Geodetic System 1984 coordinate reference system used by GPS. */ - public const val CRS_WGS84: String = "WGS84" } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt similarity index 61% rename from src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt rename to src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt index 046a9eda..dfcf9b10 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OsmType.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt @@ -6,8 +6,11 @@ import dev.stalla.model.TypeFactory * TODO. * * @property type TODO. + * + * @see https://wiki.openstreetmap.org/wiki/Elements + * @since 1.1.0 */ -public enum class OsmType(public val type: String) { +public enum class OpenStreetMapElementType(public val type: String) { Node("N"), Way("W"), Relation("R"); @@ -15,9 +18,9 @@ public enum class OsmType(public val type: String) { /** * TODO. */ - public companion object Factory : TypeFactory { + public companion object Factory : TypeFactory { @JvmStatic - override fun of(rawValue: String?): OsmType? = rawValue?.let { + override fun of(rawValue: String?): OpenStreetMapElementType? = rawValue?.let { values().find { t -> t.type.equals(it, ignoreCase = true) } } @@ -28,6 +31,6 @@ public enum class OsmType(public val type: String) { * @return The instance matching [rawValue], or `null` if no matching instance exists. */ @JvmStatic - public fun of(rawValue: Char?): OsmType? = of(rawValue?.toString()) + public fun of(rawValue: Char?): OpenStreetMapElementType? = of(rawValue?.toString()) } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt index 118f9369..18523a54 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt @@ -8,12 +8,16 @@ import dev.stalla.parser.OsmFeatureParser import java.math.BigInteger /** - * @property type A one-character description of the type of OSM point. One of the supported [OsmType]s. + * TODO. + * + * @property type A one-character description of the type of OSM point. One of the supported [OpenStreetMapElementType]s. * @property id The ID of the OpenStreetMap feature that is described. * @property revision An optional revision ID for an OSM object, preceded by a hash. + * + * @since 1.1.0 */ public data class OpenStreetMapFeature( - val type: OsmType, + val type: OpenStreetMapElementType, val id: BigInteger, val revision: BigInteger? ) { diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt index 72de0706..8e6e9009 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt @@ -1,14 +1,28 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexEpisodeBuilder +import dev.stalla.model.BuilderFactory + /** * TODO. * * @property number TODO. * @property display TODO. + * + * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#episode + * @since 1.1.0 */ public data class PodcastindexEpisode( val number: Double, val display: String? ) { - // TODO + + /** Provides a builder for the [PodcastindexEpisode] class. */ + public companion object Factory : BuilderFactory { + + /** Returns a builder implementation for building [PodcastindexEpisode] model instances. */ + @JvmStatic + override fun builder(): EpisodePodcastindexEpisodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt index 4a3dd36c..746cc3a1 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt @@ -5,9 +5,13 @@ import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder import dev.stalla.model.BuilderFactory /** + * TODO. + * * @property name Human-readable place name. * @property geo TODO. * @property osm TODO. + * + * @since 1.1.0 */ public data class PodcastindexLocation( val name: String, diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt index 365f6ff3..8f97458a 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt @@ -1,5 +1,11 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.PodcastindexPersonBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder +import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeasonBuilder +import dev.stalla.model.BuilderFactory + /** * TODO. * @@ -8,6 +14,8 @@ package dev.stalla.model.podcastindex * @property group TODO. * @property img TODO. * @property href TODO. + * + * @since 1.1.0 */ public data class PodcastindexPerson( val name: String, @@ -16,5 +24,11 @@ public data class PodcastindexPerson( val img: String?, val href: String? ) { - // TODO + /** Provides a builder for the [PodcastindexPerson] class. */ + public companion object Factory : BuilderFactory { + + /** Returns a builder implementation for building [PodcastindexPerson] model instances. */ + @JvmStatic + override fun builder(): PodcastindexPersonBuilder = ValidatingPodcastindexPersonBuilder() + } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt index d42df926..a1140288 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt @@ -1,14 +1,27 @@ package dev.stalla.model.podcastindex +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder +import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeasonBuilder +import dev.stalla.model.BuilderFactory + /** * TODO. * * @property number TODO. * @property name TODO. + * + * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#season + * @since 1.1.0 */ public data class PodcastindexSeason( val number: Double, val name: String? ) { - // TODO + /** Provides a builder for the [PodcastindexSeason] class. */ + public companion object Factory : BuilderFactory { + + /** Returns a builder implementation for building [PodcastindexSeason] model instances. */ + @JvmStatic + override fun builder(): EpisodePodcastindexSeasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + } } diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt index 36ccb90b..43fab69c 100644 --- a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt @@ -1,7 +1,7 @@ package dev.stalla.parser import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OsmType +import dev.stalla.model.podcastindex.OpenStreetMapElementType import dev.stalla.util.InternalAPI import java.lang.StringBuilder import java.math.BigInteger @@ -30,7 +30,7 @@ internal object OsmFeatureParser { for (c in value) { when (mode) { ParsingMode.Type -> { - val type = OsmType.of(c) ?: return null + val type = OpenStreetMapElementType.of(c) ?: return null builder.type(type) mode = ParsingMode.Id } diff --git a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt index 93d2ce92..0332aad9 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt @@ -5,38 +5,38 @@ import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.PodcastindexLocation -internal class FakePodcastindexLocationBuilder: FakeBuilder(), PodcastindexLocationBuilder { +internal class FakePodcastindexLocationBuilder : FakeBuilder(), PodcastindexLocationBuilder { - var nameValue: String? = null - var geoValue: GeoLocation? = null - var osmValue: OpenStreetMapFeature? = null + var name: String? = null + var geo: GeoLocation? = null + var osm: OpenStreetMapFeature? = null - override fun name(name: String): PodcastindexLocationBuilder = apply { this.nameValue = name } + override fun name(name: String): PodcastindexLocationBuilder = apply { this.name = name } - override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geoValue = geo } + override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } - override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osmValue = osm } + override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osm = osm } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as FakePodcastindexLocationBuilder - if (nameValue != other.nameValue) return false - if (geoValue != other.geoValue) return false - if (osmValue != other.osmValue) return false + if (name != other.name) return false + if (geo != other.geo) return false + if (osm != other.osm) return false return true } override fun hashCode(): Int { - var result = nameValue?.hashCode() ?: 0 - result = 31 * result + (geoValue?.hashCode() ?: 0) - result = 31 * result + (osmValue?.hashCode() ?: 0) + var result = name?.hashCode() ?: 0 + result = 31 * result + (geo?.hashCode() ?: 0) + result = 31 * result + (osm?.hashCode() ?: 0) return result } override fun toString(): String { - return "FakePodcastindexLocationBuilder(nameValue=$nameValue, geoValue=$geoValue, osmValue=$osmValue)" + return "FakePodcastindexLocationBuilder(name=$name, geo=$geo, osm=$osm)" } } diff --git a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexPersonBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexPersonBuilder.kt new file mode 100644 index 00000000..dd1a86aa --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexPersonBuilder.kt @@ -0,0 +1,51 @@ +package dev.stalla.builder.fake + +import dev.stalla.builder.PodcastindexPersonBuilder +import dev.stalla.model.podcastindex.PodcastindexPerson + +internal class FakePodcastindexPersonBuilder : FakeBuilder(), PodcastindexPersonBuilder { + + var name: String? = null + var role: String? = null + var group: String? = null + var img: String? = null + var href: String? = null + + override fun name(name: String): PodcastindexPersonBuilder = apply { this.name = name } + + override fun role(role: String?): PodcastindexPersonBuilder = apply { this.role = role } + + override fun group(group: String?): PodcastindexPersonBuilder = apply { this.group = group } + + override fun img(img: String?): PodcastindexPersonBuilder = apply { this.img = img } + + override fun href(href: String?): PodcastindexPersonBuilder = apply { this.href = href } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FakePodcastindexPersonBuilder + + if (name != other.name) return false + if (role != other.role) return false + if (group != other.group) return false + if (img != other.img) return false + if (href != other.href) return false + + return true + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (role?.hashCode() ?: 0) + result = 31 * result + (group?.hashCode() ?: 0) + result = 31 * result + (img?.hashCode() ?: 0) + result = 31 * result + (href?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "FakePodcastindexPersonBuilder(name=$name, role=$role, group=$group, img=$img, href=$href)" + } +} diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexEpisodeBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexEpisodeBuilder.kt new file mode 100644 index 00000000..e69f7e9d --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexEpisodeBuilder.kt @@ -0,0 +1,37 @@ +package dev.stalla.builder.fake.episode + +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.fake.FakeBuilder +import dev.stalla.model.podcastindex.PodcastindexEpisode + +internal class FakeEpisodePodcastindexEpisodeBuilder : FakeBuilder(), EpisodePodcastindexEpisodeBuilder { + + var number: Double? = null + var display: String? = null + + override fun number(number: Double): EpisodePodcastindexEpisodeBuilder = apply { this.number = number } + + override fun display(display: String?): EpisodePodcastindexEpisodeBuilder = apply { this.display = display } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FakeEpisodePodcastindexEpisodeBuilder + + if (number != other.number) return false + if (display != other.display) return false + + return true + } + + override fun hashCode(): Int { + var result = number?.hashCode() ?: 0 + result = 31 * result + (display?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "FakeEpisodePodcastindexEpisodeBuilder(number=$number, display=$display)" + } +} diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt new file mode 100644 index 00000000..f30d35bb --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt @@ -0,0 +1,37 @@ +package dev.stalla.builder.fake.episode + +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder +import dev.stalla.builder.fake.FakeBuilder +import dev.stalla.model.podcastindex.PodcastindexSeason + +internal class FakeEpisodePodcastindexSeasonBuilder : FakeBuilder(), EpisodePodcastindexSeasonBuilder { + + var number: Double? = null + var name: String? = null + + override fun number(number: Double): EpisodePodcastindexSeasonBuilder = apply { this.number = number } + + override fun name(name: String?): EpisodePodcastindexSeasonBuilder = apply { this.name = name } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FakeEpisodePodcastindexSeasonBuilder + + if (number != other.number) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = number?.hashCode() ?: 0 + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "FakeEpisodePodcastindexSeasonBuilder(number=$number, name=$name)" + } +} diff --git a/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt b/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt index 3fc0b287..02a6e15f 100644 --- a/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt +++ b/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt @@ -7,7 +7,7 @@ import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.prop import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OsmType +import dev.stalla.model.podcastindex.OpenStreetMapElementType import org.junit.jupiter.api.Test import java.math.BigInteger @@ -16,7 +16,7 @@ class OpenStreetMapFeatureTest { @Test fun `test 1`() { assertThat(OpenStreetMapFeature.of("R148838")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) + prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Relation) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("148838")) prop(OpenStreetMapFeature::revision).isNull() } @@ -25,7 +25,7 @@ class OpenStreetMapFeatureTest { @Test fun `test 2`() { assertThat(OpenStreetMapFeature.of("W5013364")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Way) + prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Way) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("5013364")) prop(OpenStreetMapFeature::revision).isNull() } @@ -34,7 +34,7 @@ class OpenStreetMapFeatureTest { @Test fun `test 3`() { assertThat(OpenStreetMapFeature.of("R7444#188")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OsmType.Relation) + prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Relation) prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("7444")) prop(OpenStreetMapFeature::revision).isEqualTo(BigInteger("188")) } From b23e378d78c7fb9f3e695a895c0673f4bbe9b063 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Sun, 25 Apr 2021 22:27:56 +0200 Subject: [PATCH 07/18] Parse person/season/episode --- .../episode/EpisodePodcastindexBuilder.kt | 39 +++++++- .../episode/ProvidingEpisodeBuilder.kt | 10 +++ .../podcast/PodcastPodcastindexBuilder.kt | 18 ++++ .../podcast/ProvidingPodcastBuilder.kt | 4 + .../episode/ValidatingEpisodeBuilder.kt | 13 +++ .../ValidatingEpisodePodcastindexBuilder.kt | 37 ++++++-- .../podcast/ValidatingPodcastBuilder.kt | 5 ++ .../ValidatingPodcastPodcastindexBuilder.kt | 18 ++-- .../dev/stalla/dom/DomParsingExtensions.kt | 8 ++ .../parser/namespace/PodcastindexParser.kt | 90 ++++++++++++++++--- .../dev/stalla/util/CollectionsExtensions.kt | 9 ++ 11 files changed, 222 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt index e1c3d004..ac6d9ff5 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexBuilder.kt @@ -2,10 +2,12 @@ package dev.stalla.builder.episode import dev.stalla.builder.Builder import dev.stalla.builder.PodcastindexLocationBuilder -import dev.stalla.builder.podcast.PodcastPodcastindexBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.model.podcastindex.Chapters import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.PodcastindexEpisode import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexSeason import dev.stalla.util.asBuilders import dev.stalla.util.whenNotNull @@ -17,7 +19,7 @@ import dev.stalla.util.whenNotNull public interface EpisodePodcastindexBuilder : Builder { /** - * Set the [EpisodePodcastindexChaptersBuilder] for the Podcastindex namespace `` info. + * Set the [EpisodePodcastindexChaptersBuilder] for the Podcastindex namespace `` info. */ public fun chaptersBuilder(chaptersBuilder: EpisodePodcastindexChaptersBuilder): EpisodePodcastindexBuilder @@ -55,14 +57,45 @@ public interface EpisodePodcastindexBuilder : Builder { transcriptBuilders.forEach(::addTranscriptBuilder) } - /** Set the [EpisodePodcastindexBuilder]. */ + /** + * Adds the [PodcastindexPersonBuilder] for the + * `` info to the list of person builders. + */ + public fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): EpisodePodcastindexBuilder + + /** + * Adds all of the [PodcastindexPersonBuilder] for the + * `` info to the list of person builders. + */ + public fun addAllPersonBuilders( + personBuilders: List + ): EpisodePodcastindexBuilder = apply { + personBuilders.forEach(::addPersonBuilder) + } + + /** + * Set the [PodcastindexLocationBuilder] for the Podcastindex namespace `` info. + */ public fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder + /** + * Set the [EpisodePodcastindexSeasonBuilder] for the Podcastindex namespace `` info. + */ + public fun seasonBuilder(seasonBuilder: EpisodePodcastindexSeasonBuilder): EpisodePodcastindexBuilder + + /** + * Set the [EpisodePodcastindexEpisodeBuilder] for the Podcastindex namespace `` info. + */ + public fun episodeBuilder(episodeBuilder: EpisodePodcastindexEpisodeBuilder): EpisodePodcastindexBuilder + override fun applyFrom(prototype: EpisodePodcastindex?): EpisodePodcastindexBuilder = whenNotNull(prototype) { podcast -> chaptersBuilder(Chapters.builder().applyFrom(podcast.chapters)) addAllSoundbiteBuilders(podcast.soundbites.asBuilders()) addAllTranscriptBuilders(podcast.transcripts.asBuilders()) + addAllPersonBuilders(podcast.persons.asBuilders()) locationBuilder(PodcastindexLocation.builder().applyFrom(podcast.location)) + seasonBuilder(PodcastindexSeason.builder().applyFrom(podcast.season)) + episodeBuilder(PodcastindexEpisode.builder().applyFrom(podcast.episode)) } } diff --git a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt index 667d9685..ea4531f3 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt @@ -5,6 +5,7 @@ import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.util.InternalAPI @@ -35,6 +36,15 @@ internal interface ProvidingEpisodeBuilder : EpisodeBuilder, AtomPersonBuilderPr /** Creates an instance of [EpisodePodcastindexSoundbiteBuilder] to use with this builder. */ fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder + /** Creates an instance of [PodcastindexPersonBuilder] to use with this builder. */ + fun createPersonBuilder(): PodcastindexPersonBuilder + /** Creates an instance of [PodcastindexLocationBuilder] to use with this builder. */ fun createLocationBuilder(): PodcastindexLocationBuilder + + /** Creates an instance of [EpisodePodcastindexSeasonBuilder] to use with this builder. */ + fun createSeasonBuilder(): EpisodePodcastindexSeasonBuilder + + /** Creates an instance of [EpisodePodcastindexEpisodeBuilder] to use with this builder. */ + fun createEpisodeBuilder(): EpisodePodcastindexEpisodeBuilder } diff --git a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt index a6a548e1..761db57f 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt @@ -3,6 +3,7 @@ package dev.stalla.builder.podcast import dev.stalla.builder.Builder import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.Locked import dev.stalla.model.podcastindex.PodcastPodcastindex @@ -36,6 +37,22 @@ public interface PodcastPodcastindexBuilder : Builder { fundingBuilders.forEach(::addFundingBuilder) } + /** + * Adds the [PodcastindexPersonBuilder] for the + * `` info to the list of person builders. + */ + public fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): PodcastPodcastindexBuilder + + /** + * Adds all of the [PodcastindexPersonBuilder] for the + * `` info to the list of person builders. + */ + public fun addAllPersonBuilders( + personBuilders: List + ): PodcastPodcastindexBuilder = apply { + personBuilders.forEach(::addPersonBuilder) + } + /** Set the [PodcastindexLocationBuilder]. */ public fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder @@ -43,6 +60,7 @@ public interface PodcastPodcastindexBuilder : Builder { whenNotNull(prototype) { podcast -> lockedBuilder(Locked.builder().applyFrom(podcast.locked)) addAllFundingBuilders(podcast.funding.asBuilders()) + addAllPersonBuilders(podcast.persons.asBuilders()) locationBuilder(PodcastindexLocation.builder().applyFrom(podcast.location)) } } diff --git a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt index 8c5bd883..08d30ee0 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt @@ -5,6 +5,7 @@ import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder @@ -28,6 +29,9 @@ internal interface ProvidingPodcastBuilder : PodcastBuilder, AtomPersonBuilderPr /** Creates an instance of [PodcastPodcastindexFundingBuilder] to use with this builder. */ fun createFundingBuilder(): PodcastPodcastindexFundingBuilder + /** Creates an instance of [PodcastindexPersonBuilder] to use with this builder. */ + fun createPersonBuilder(): PodcastindexPersonBuilder + /** Creates an instance of [PodcastindexLocationBuilder] to use with this builder. */ fun createLocationBuilder(): PodcastindexLocationBuilder } diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt index e9e4f516..bab7f0cd 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodeBuilder.kt @@ -5,6 +5,7 @@ import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBitloveBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -15,6 +16,8 @@ import dev.stalla.builder.episode.EpisodeGuidBuilder import dev.stalla.builder.episode.EpisodeItunesBuilder import dev.stalla.builder.episode.EpisodePodcastindexBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder import dev.stalla.builder.episode.EpisodePodloveBuilder @@ -25,6 +28,7 @@ import dev.stalla.builder.validating.ValidatingAtomPersonBuilder import dev.stalla.builder.validating.ValidatingHrefOnlyImageBuilder import dev.stalla.builder.validating.ValidatingLinkBuilder import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.builder.validating.ValidatingRssCategoryBuilder import dev.stalla.model.Episode import dev.stalla.util.InternalAPI @@ -107,9 +111,18 @@ internal class ValidatingEpisodeBuilder : ProvidingEpisodeBuilder { override fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder = ValidatingEpisodePodcastindexSoundbiteBuilder() + override fun createPersonBuilder(): PodcastindexPersonBuilder = + ValidatingPodcastindexPersonBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = ValidatingPodcastindexLocationBuilder() + override fun createSeasonBuilder(): EpisodePodcastindexSeasonBuilder = + ValidatingEpisodePodcastindexSeasonBuilder() + + override fun createEpisodeBuilder(): EpisodePodcastindexEpisodeBuilder = + ValidatingEpisodePodcastindexEpisodeBuilder() + override val hasEnoughDataToBuild: Boolean get() = ::titleValue.isInitialized && ::enclosureBuilderValue.isInitialized && diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt index ea5283c5..d07a2f27 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilder.kt @@ -1,8 +1,11 @@ package dev.stalla.builder.validating.episode import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.episode.EpisodePodcastindexBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder import dev.stalla.model.podcastindex.EpisodePodcastindex @@ -12,13 +15,16 @@ import dev.stalla.util.asUnmodifiable @InternalAPI internal class ValidatingEpisodePodcastindexBuilder : EpisodePodcastindexBuilder { - private var chaptersBuilderValue: EpisodePodcastindexChaptersBuilder? = null - private var locationBuilderValue: PodcastindexLocationBuilder? = null + private var chaptersBuilder: EpisodePodcastindexChaptersBuilder? = null + private var locationBuilder: PodcastindexLocationBuilder? = null + private var seasonBuilder: EpisodePodcastindexSeasonBuilder? = null + private var episodeBuilder: EpisodePodcastindexEpisodeBuilder? = null private val transcriptBuilders: MutableList = mutableListOf() private val soundbiteBuilders: MutableList = mutableListOf() + private val personBuilders: MutableList = mutableListOf() override fun chaptersBuilder(chaptersBuilder: EpisodePodcastindexChaptersBuilder): EpisodePodcastindexBuilder = - apply { this.chaptersBuilderValue = chaptersBuilder } + apply { this.chaptersBuilder = chaptersBuilder } override fun addSoundbiteBuilder( soundbiteBuilder: EpisodePodcastindexSoundbiteBuilder @@ -32,14 +38,26 @@ internal class ValidatingEpisodePodcastindexBuilder : EpisodePodcastindexBuilder transcriptBuilders.add(transcriptBuilder) } + override fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): EpisodePodcastindexBuilder = + apply { this.personBuilders.add(personBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder = - apply { this.locationBuilderValue = locationBuilder } + apply { this.locationBuilder = locationBuilder } + + override fun seasonBuilder(seasonBuilder: EpisodePodcastindexSeasonBuilder): EpisodePodcastindexBuilder = + apply { this.seasonBuilder = seasonBuilder } + + override fun episodeBuilder(episodeBuilder: EpisodePodcastindexEpisodeBuilder): EpisodePodcastindexBuilder = + apply { this.episodeBuilder = episodeBuilder } override val hasEnoughDataToBuild: Boolean - get() = chaptersBuilderValue?.hasEnoughDataToBuild == true || + get() = chaptersBuilder?.hasEnoughDataToBuild == true || transcriptBuilders.any { it.hasEnoughDataToBuild } || soundbiteBuilders.any { it.hasEnoughDataToBuild } || - locationBuilderValue?.hasEnoughDataToBuild == true + personBuilders.any { it.hasEnoughDataToBuild } || + locationBuilder?.hasEnoughDataToBuild == true || + seasonBuilder?.hasEnoughDataToBuild == true || + episodeBuilder?.hasEnoughDataToBuild == true override fun build(): EpisodePodcastindex? { if (!hasEnoughDataToBuild) return null @@ -47,8 +65,11 @@ internal class ValidatingEpisodePodcastindexBuilder : EpisodePodcastindexBuilder return EpisodePodcastindex( transcripts = transcriptBuilders.mapNotNull { it.build() }.asUnmodifiable(), soundbites = soundbiteBuilders.mapNotNull { it.build() }.asUnmodifiable(), - chapters = chaptersBuilderValue?.build(), - location = locationBuilderValue?.build() + chapters = chaptersBuilder?.build(), + persons = personBuilders.mapNotNull { it.build() }.asUnmodifiable(), + location = locationBuilder?.build(), + season = seasonBuilder?.build(), + episode = episodeBuilder?.build() ) } } diff --git a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt index 30620903..5e17f5d8 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastBuilder.kt @@ -5,6 +5,7 @@ import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -23,6 +24,7 @@ import dev.stalla.builder.validating.ValidatingAtomPersonBuilder import dev.stalla.builder.validating.ValidatingHrefOnlyImageBuilder import dev.stalla.builder.validating.ValidatingLinkBuilder import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.builder.validating.ValidatingRssCategoryBuilder import dev.stalla.builder.validating.ValidatingRssImageBuilder import dev.stalla.model.Podcast @@ -117,6 +119,9 @@ internal class ValidatingPodcastBuilder : ProvidingPodcastBuilder { override fun createFundingBuilder(): PodcastPodcastindexFundingBuilder = ValidatingPodcastPodcastindexFundingBuilder() + override fun createPersonBuilder(): PodcastindexPersonBuilder = + ValidatingPodcastindexPersonBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = ValidatingPodcastindexLocationBuilder() diff --git a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt index de5dca7e..766358a3 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilder.kt @@ -1,6 +1,7 @@ package dev.stalla.builder.validating.podcast import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.builder.podcast.PodcastPodcastindexLockedBuilder @@ -12,8 +13,9 @@ import dev.stalla.util.asUnmodifiable internal class ValidatingPodcastPodcastindexBuilder : PodcastPodcastindexBuilder { private lateinit var lockedBuilderValue: PodcastPodcastindexLockedBuilder - private var locationBuilderValue: PodcastindexLocationBuilder? = null + private var locationBuilder: PodcastindexLocationBuilder? = null private val fundingBuilders: MutableList = mutableListOf() + private val personBuilders: MutableList = mutableListOf() override fun lockedBuilder(lockedBuilder: PodcastPodcastindexLockedBuilder): PodcastPodcastindexBuilder = apply { this.lockedBuilderValue = lockedBuilder } @@ -21,14 +23,17 @@ internal class ValidatingPodcastPodcastindexBuilder : PodcastPodcastindexBuilder override fun addFundingBuilder(fundingBuilder: PodcastPodcastindexFundingBuilder): PodcastPodcastindexBuilder = apply { fundingBuilders.add(fundingBuilder) } + override fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): PodcastPodcastindexBuilder = + apply { personBuilders.add(personBuilder) } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder = - apply { this.locationBuilderValue = locationBuilder } + apply { this.locationBuilder = locationBuilder } override val hasEnoughDataToBuild: Boolean - get() = ::lockedBuilderValue.isInitialized && - lockedBuilderValue.hasEnoughDataToBuild || + get() = ::lockedBuilderValue.isInitialized && lockedBuilderValue.hasEnoughDataToBuild || fundingBuilders.any { it.hasEnoughDataToBuild } || - locationBuilderValue?.hasEnoughDataToBuild == true + personBuilders.any { it.hasEnoughDataToBuild } || + locationBuilder?.hasEnoughDataToBuild == true override fun build(): PodcastPodcastindex? { if (!hasEnoughDataToBuild) return null @@ -36,7 +41,8 @@ internal class ValidatingPodcastPodcastindexBuilder : PodcastPodcastindexBuilder return PodcastPodcastindex( locked = if (::lockedBuilderValue.isInitialized) lockedBuilderValue.build() else null, funding = fundingBuilders.mapNotNull { it.build() }.asUnmodifiable(), - location = locationBuilderValue?.build() + persons = personBuilders.mapNotNull { it.build() }.asUnmodifiable(), + location = locationBuilder?.build() ) } } diff --git a/src/main/kotlin/dev/stalla/dom/DomParsingExtensions.kt b/src/main/kotlin/dev/stalla/dom/DomParsingExtensions.kt index 09577e11..710c4b4b 100644 --- a/src/main/kotlin/dev/stalla/dom/DomParsingExtensions.kt +++ b/src/main/kotlin/dev/stalla/dom/DomParsingExtensions.kt @@ -59,6 +59,14 @@ internal fun String?.parseAsBooleanOrNull(): Boolean? = @InternalAPI internal fun Node.parseAsInt(): Int? = textOrNull()?.toIntOrNull() +/** + * Extracts the text content of a DOM node, and transforms it to an Int instance. + * + * @return The DOM node content as an [Double], or `null` if conversion failed. + */ +@InternalAPI +internal fun Node.parseAsDouble(): Double? = textOrNull()?.toDoubleOrNull() + /** * Extracts the text content of a DOM node, and parses it as a [TemporalAccessor] instance * if possible. diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index 39e3be89..edc90644 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -2,7 +2,10 @@ package dev.stalla.parser.namespace import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder import dev.stalla.builder.episode.ProvidingEpisodeBuilder @@ -11,11 +14,14 @@ import dev.stalla.builder.podcast.PodcastPodcastindexLockedBuilder import dev.stalla.builder.podcast.ProvidingPodcastBuilder import dev.stalla.dom.getAttributeByName import dev.stalla.dom.getAttributeValueByName +import dev.stalla.dom.parseAsDouble +import dev.stalla.dom.parseAsInt import dev.stalla.dom.parseAsMediaTypeOrNull import dev.stalla.dom.textAsBooleanOrNull import dev.stalla.dom.textOrNull import dev.stalla.model.StyledDuration import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.parser.GeoUriParser import dev.stalla.parser.NamespaceParser @@ -51,6 +57,12 @@ internal object PodcastindexParser : NamespaceParser() { } ?: return builder.podcastPodcastindexBuilder.addFundingBuilder(fundingBuilder) } + "person" -> { + val personBuilder = ifCanBeParsed { + toPersonBuilder(builder.createPersonBuilder()) + } ?: return + builder.podcastPodcastindexBuilder.addPersonBuilder(personBuilder) + } "location" -> { val locationBuilder = ifCanBeParsed { toLocationBuilder(builder.createLocationBuilder()) @@ -81,28 +93,34 @@ internal object PodcastindexParser : NamespaceParser() { } ?: return builder.podcastindexBuilder.addTranscriptBuilder(transcriptBuilder) } + "person" -> { + val personBuilder = ifCanBeParsed { + toPersonBuilder(builder.createPersonBuilder()) + } ?: return + builder.podcastindexBuilder.addPersonBuilder(personBuilder) + } "location" -> { val locationBuilder = ifCanBeParsed { toLocationBuilder(builder.createLocationBuilder()) } ?: return builder.podcastindexBuilder.locationBuilder(locationBuilder) } + "season" -> { + val seasonBuilder = ifCanBeParsed { + toSeasonBuilder(builder.createSeasonBuilder()) + } ?: return + builder.podcastindexBuilder.seasonBuilder(seasonBuilder) + } + "episode" -> { + val episodeBuilder = ifCanBeParsed { + toEpisodeBuilder(builder.createEpisodeBuilder()) + } ?: return + builder.podcastindexBuilder.episodeBuilder(episodeBuilder) + } else -> pass } } - private fun Node.toLocationBuilder( - locationBuilder: PodcastindexLocationBuilder - ): PodcastindexLocationBuilder? { - val name = textOrNull() ?: return null - val geoValue = getAttributeByName("geo")?.value.trimmedOrNullIfBlank() - val osmValue = getAttributeByName("osm")?.value.trimmedOrNullIfBlank() - - return locationBuilder.name(name) - .geo(GeoUriParser.parse(geoValue)) - .osm(OsmFeatureParser.parse(osmValue)) - } - private fun Node.toLockedBuilder( lockedBuilder: PodcastPodcastindexLockedBuilder ): PodcastPodcastindexLockedBuilder? { @@ -176,4 +194,52 @@ internal object PodcastindexParser : NamespaceParser() { .language(language) .rel(rel) } + + private fun Node.toLocationBuilder( + locationBuilder: PodcastindexLocationBuilder + ): PodcastindexLocationBuilder? { + val name = textOrNull() ?: return null + val geoValue = getAttributeByName("geo")?.value.trimmedOrNullIfBlank() + val osmValue = getAttributeByName("osm")?.value.trimmedOrNullIfBlank() + + return locationBuilder.name(name) + .geo(GeoUriParser.parse(geoValue)) + .osm(OsmFeatureParser.parse(osmValue)) + } + + private fun Node.toPersonBuilder( + personBuilder: PodcastindexPersonBuilder + ): PodcastindexPersonBuilder? { + val name = textOrNull() ?: return null + val role = getAttributeByName("role")?.value.trimmedOrNullIfBlank() + val group = getAttributeByName("group")?.value.trimmedOrNullIfBlank() + val img = getAttributeByName("img")?.value.trimmedOrNullIfBlank() + val href = getAttributeByName("href")?.value.trimmedOrNullIfBlank() + + return personBuilder.name(name) + .role(role) + .group(group) + .img(img) + .href(href) + } + + private fun Node.toSeasonBuilder( + seasonBuilder: EpisodePodcastindexSeasonBuilder + ): EpisodePodcastindexSeasonBuilder? { + val number = parseAsDouble() ?: return null + val name = getAttributeByName("name")?.value.trimmedOrNullIfBlank() + + return seasonBuilder.number(number) + .name(name) + } + + private fun Node.toEpisodeBuilder( + episodeBuilder: EpisodePodcastindexEpisodeBuilder + ): EpisodePodcastindexEpisodeBuilder? { + val number = parseAsDouble() ?: return null + val display = getAttributeByName("display")?.value.trimmedOrNullIfBlank() + + return episodeBuilder.number(number) + .display(display) + } } diff --git a/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt b/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt index 0069047b..3b06c363 100644 --- a/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt +++ b/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt @@ -1,5 +1,7 @@ package dev.stalla.util +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBuilder import dev.stalla.builder.episode.EpisodeEnclosureBuilder @@ -9,6 +11,8 @@ import dev.stalla.builder.episode.EpisodePodloveSimpleChapterBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.model.Episode import dev.stalla.model.podcastindex.Funding +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.Soundbite import dev.stalla.model.podcastindex.Transcript import dev.stalla.model.podlove.SimpleChapter @@ -43,6 +47,11 @@ internal fun List.asBuilders(): List.asBuilders(): List = map(Funding.builder()::applyFrom) +/** Transforms this list into a list of [PodcastindexPersonBuilder]. */ +@InternalAPI +@JvmName("asPersonBuilders") +internal fun List.asBuilders(): List = map(PodcastindexPerson.builder()::applyFrom) + /** Transforms this list into a list of [EpisodePodloveSimpleChapterBuilder]. */ @InternalAPI @JvmName("asSimpleChapterBuilders") From 8a2c9e78e46823fbe0d1590cc81c42af39c2907a Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Mon, 26 Apr 2021 19:47:08 +0200 Subject: [PATCH 08/18] Add writing of new podcastindex tags --- .../dev/stalla/util/NumberExtensions.kt | 24 +++++++ .../writer/namespace/PodcastindexWriter.kt | 70 ++++++++++++++++--- 2 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/util/NumberExtensions.kt diff --git a/src/main/kotlin/dev/stalla/util/NumberExtensions.kt b/src/main/kotlin/dev/stalla/util/NumberExtensions.kt new file mode 100644 index 00000000..c1411fbb --- /dev/null +++ b/src/main/kotlin/dev/stalla/util/NumberExtensions.kt @@ -0,0 +1,24 @@ +package dev.stalla.util + +import java.math.BigInteger +import kotlin.contracts.contract + +@InternalAPI +internal fun Int?.asBigIntegerOrNull(): BigInteger? { + contract { + returnsNotNull() implies (this@asBigIntegerOrNull != null) + } + + if (this == null) return null + return BigInteger.valueOf(this.toLong()) +} + +@InternalAPI +internal fun Long?.asBigIntegerOrNull(): BigInteger? { + contract { + returnsNotNull() implies (this@asBigIntegerOrNull != null) + } + + if (this == null) return null + return BigInteger.valueOf(this) +} diff --git a/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt b/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt index 47935aa9..3e0c95ac 100644 --- a/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt +++ b/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt @@ -4,6 +4,8 @@ import dev.stalla.dom.appendElement import dev.stalla.model.Episode import dev.stalla.model.Podcast import dev.stalla.model.podcastindex.Chapters +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.util.BooleanStringStyle import dev.stalla.util.FeedNamespace import dev.stalla.util.InternalAPI @@ -24,16 +26,16 @@ internal object PodcastindexWriter : NamespaceWriter() { override val namespace = FeedNamespace.PODCAST override fun Element.appendPodcastData(podcast: Podcast) { - val podcastNs = podcast.podcastindex ?: return + val podcastindex = podcast.podcastindex ?: return - if (podcastNs.locked != null && podcastNs.locked.owner.isNotBlank()) { + if (podcastindex.locked != null && podcastindex.locked.owner.isNotBlank()) { appendElement("locked", namespace) { - setAttribute("owner", podcastNs.locked.owner.trim()) - textContent = podcastNs.locked.locked.asBooleanString(BooleanStringStyle.YES_NO) + setAttribute("owner", podcastindex.locked.owner.trim()) + textContent = podcastindex.locked.locked.asBooleanString(BooleanStringStyle.YES_NO) } } - for (funding in podcastNs.funding) { + for (funding in podcastindex.funding) { if (funding.url.isBlank() || funding.message.isBlank()) continue appendElement("funding", namespace) { @@ -41,19 +43,23 @@ internal object PodcastindexWriter : NamespaceWriter() { textContent = funding.message.trim() } } + + appendPodcastindexPersonData(podcastindex.persons) + + appendPodcastindexLocationData(podcastindex.location) } override fun Element.appendEpisodeData(episode: Episode) { - val podcastNs = episode.podcastindex ?: return + val podcastindex = episode.podcastindex ?: return - if (podcastNs.chapters != null && podcastNs.chapters.canBeWritten()) { + if (podcastindex.chapters != null && podcastindex.chapters.canBeWritten()) { appendElement("chapters", namespace) { - setAttribute("url", podcastNs.chapters.url.trim()) - setAttribute("type", podcastNs.chapters.type.toString()) + setAttribute("url", podcastindex.chapters.url.trim()) + setAttribute("type", podcastindex.chapters.type.toString()) } } - for (transcript in podcastNs.transcripts) { + for (transcript in podcastindex.transcripts) { if (transcript.url.isBlank()) continue appendElement("transcript", namespace) { @@ -64,7 +70,7 @@ internal object PodcastindexWriter : NamespaceWriter() { } } - for (soundbite in podcastNs.soundbites) { + for (soundbite in podcastindex.soundbites) { if (soundbite.startTime.isNegative || !soundbite.duration.isStrictlyPositive) continue appendElement("soundbite", namespace) { @@ -73,6 +79,48 @@ internal object PodcastindexWriter : NamespaceWriter() { if (soundbite.title != null) textContent = soundbite.title.trim() } } + + appendPodcastindexPersonData(podcastindex.persons) + + appendPodcastindexLocationData(podcastindex.location) + + if (podcastindex.season != null) { + appendElement("season", namespace) { + if (podcastindex.season.name != null) setAttribute("name", podcastindex.season.name.trim()) + textContent = podcastindex.season.number.toString() + } + } + + if (podcastindex.episode != null) { + appendElement("episode", namespace) { + if (podcastindex.episode.display != null) setAttribute("display", podcastindex.episode.display.trim()) + textContent = podcastindex.episode.number.toString() + } + } + } + + private fun Element.appendPodcastindexPersonData(persons: List) { + for (person in persons) { + if (person.name.isBlank()) continue + + appendElement("person", namespace) { + if (person.role != null) setAttribute("role", person.role.trim()) + if (person.group != null) setAttribute("group", person.group.trim()) + if (person.img != null) setAttribute("img", person.img.trim()) + if (person.href != null) setAttribute("href", person.href.trim()) + textContent = person.name.trim() + } + } + } + + private fun Element.appendPodcastindexLocationData(location: PodcastindexLocation?) { + if (location != null && location.name.isNotBlank()) { + appendElement("location", namespace) { + if (location.geo != null) setAttribute("geo", location.geo.toString()) + if (location.osm != null) setAttribute("osm", location.osm.toString()) + textContent = location.name + } + } } private fun Chapters.canBeWritten() = url.isNotBlank() From 46f7615e27653c5e9b2b8ce89563cdacefada7f2 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Mon, 26 Apr 2021 19:47:20 +0200 Subject: [PATCH 09/18] Add overloads of BigInteger values --- api/stalla.api | 146 +++++++++++++++++- .../builder/OpenStreetMapFeatureBuilder.kt | 10 ++ .../stalla/model/podcastindex/GeoLocation.kt | 13 +- .../podcastindex/OpenStreetMapFeature.kt | 29 ++++ .../model/podcastindex/PodcastindexEpisode.kt | 2 +- .../model/podcastindex/PodcastindexPerson.kt | 11 +- .../model/podcastindex/PodcastindexSeason.kt | 4 +- .../dev/stalla/parser/OsmFeatureParser.kt | 5 + .../fake/episode/FakeEpisodeBuilder.kt | 10 ++ .../episode/FakeEpisodePodcastindexBuilder.kt | 26 +++- .../fake/podcast/FakePodcastBuilder.kt | 5 + .../podcast/FakePodcastPodcastindexBuilder.kt | 10 +- 12 files changed, 248 insertions(+), 23 deletions(-) diff --git a/api/stalla.api b/api/stalla.api index 22ce162b..4f485d9c 100644 --- a/api/stalla.api +++ b/api/stalla.api @@ -1262,16 +1262,24 @@ public final class dev/stalla/model/podcastindex/Chapters$Factory : dev/stalla/m public final class dev/stalla/model/podcastindex/EpisodePodcastindex { public static final field Factory Ldev/stalla/model/podcastindex/EpisodePodcastindex$Factory; public fun ()V - public fun (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;)V - public synthetic fun (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;Ldev/stalla/model/podcastindex/PodcastindexSeason;Ldev/stalla/model/podcastindex/PodcastindexEpisode;)V + public synthetic fun (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;Ldev/stalla/model/podcastindex/PodcastindexSeason;Ldev/stalla/model/podcastindex/PodcastindexEpisode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public static fun builder ()Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public final fun component1 ()Ljava/util/List; public final fun component2 ()Ljava/util/List; public final fun component3 ()Ldev/stalla/model/podcastindex/Chapters; - public final fun copy (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;)Ldev/stalla/model/podcastindex/EpisodePodcastindex; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/EpisodePodcastindex;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/EpisodePodcastindex; + public final fun component4 ()Ljava/util/List; + public final fun component5 ()Ldev/stalla/model/podcastindex/PodcastindexLocation; + public final fun component6 ()Ldev/stalla/model/podcastindex/PodcastindexSeason; + public final fun component7 ()Ldev/stalla/model/podcastindex/PodcastindexEpisode; + public final fun copy (Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;Ldev/stalla/model/podcastindex/PodcastindexSeason;Ldev/stalla/model/podcastindex/PodcastindexEpisode;)Ldev/stalla/model/podcastindex/EpisodePodcastindex; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/EpisodePodcastindex;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/Chapters;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;Ldev/stalla/model/podcastindex/PodcastindexSeason;Ldev/stalla/model/podcastindex/PodcastindexEpisode;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/EpisodePodcastindex; public fun equals (Ljava/lang/Object;)Z public final fun getChapters ()Ldev/stalla/model/podcastindex/Chapters; + public final fun getEpisode ()Ldev/stalla/model/podcastindex/PodcastindexEpisode; + public final fun getLocation ()Ldev/stalla/model/podcastindex/PodcastindexLocation; + public final fun getPersons ()Ljava/util/List; + public final fun getSeason ()Ldev/stalla/model/podcastindex/PodcastindexSeason; public final fun getSoundbites ()Ljava/util/List; public final fun getTranscripts ()Ljava/util/List; public fun hashCode ()I @@ -1303,6 +1311,33 @@ public final class dev/stalla/model/podcastindex/Funding$Factory : dev/stalla/mo public fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexFundingBuilder; } +public final class dev/stalla/model/podcastindex/GeoLocation { + public static final field Factory Ldev/stalla/model/podcastindex/GeoLocation$Factory; + public final fun getCoordA ()D + public final fun getCoordB ()D + public final fun getCoordC ()Ljava/lang/Double; + public final fun getParameters ()Ljava/util/Map; + public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation; +} + +public final class dev/stalla/model/podcastindex/GeoLocation$Factory : dev/stalla/model/TypeFactory { + public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation; + public synthetic fun of (Ljava/lang/String;)Ljava/lang/Object; +} + +public final class dev/stalla/model/podcastindex/GeoLocation$Parameter { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation$Parameter; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/GeoLocation$Parameter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/GeoLocation$Parameter; + public fun equals (Ljava/lang/Object;)Z + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/stalla/model/podcastindex/Locked { public static final field Factory Ldev/stalla/model/podcastindex/Locked$Factory; public fun (Ljava/lang/String;Z)V @@ -1323,19 +1358,54 @@ public final class dev/stalla/model/podcastindex/Locked$Factory : dev/stalla/mod public fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexLockedBuilder; } +public final class dev/stalla/model/podcastindex/OpenStreetMapFeature { + public fun (Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;)V + public final fun component1 ()Ldev/stalla/model/podcastindex/OsmType; + public final fun component2 ()I + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapFeature; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/OpenStreetMapFeature;Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/OpenStreetMapFeature; + public fun equals (Ljava/lang/Object;)Z + public final fun getOsmId ()I + public final fun getOsmRevision ()Ljava/lang/String; + public final fun getOsmType ()Ldev/stalla/model/podcastindex/OsmType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/stalla/model/podcastindex/OsmType : java/lang/Enum { + public static final field Factory Ldev/stalla/model/podcastindex/OsmType$Factory; + public static final field Node Ldev/stalla/model/podcastindex/OsmType; + public static final field Relation Ldev/stalla/model/podcastindex/OsmType; + public static final field Way Ldev/stalla/model/podcastindex/OsmType; + public final fun getType ()Ljava/lang/String; + public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; + public static fun valueOf (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; + public static fun values ()[Ldev/stalla/model/podcastindex/OsmType; +} + +public final class dev/stalla/model/podcastindex/OsmType$Factory : dev/stalla/model/TypeFactory { + public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; + public synthetic fun of (Ljava/lang/String;)Ljava/lang/Object; +} + public final class dev/stalla/model/podcastindex/PodcastPodcastindex { public static final field Factory Ldev/stalla/model/podcastindex/PodcastPodcastindex$Factory; public fun ()V - public fun (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;)V - public synthetic fun (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;)V + public synthetic fun (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public static fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; public final fun component1 ()Ldev/stalla/model/podcastindex/Locked; public final fun component2 ()Ljava/util/List; - public final fun copy (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;)Ldev/stalla/model/podcastindex/PodcastPodcastindex; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastPodcastindex;Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastPodcastindex; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ldev/stalla/model/podcastindex/PodcastindexLocation; + public final fun copy (Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;)Ldev/stalla/model/podcastindex/PodcastPodcastindex; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastPodcastindex;Ldev/stalla/model/podcastindex/Locked;Ljava/util/List;Ljava/util/List;Ldev/stalla/model/podcastindex/PodcastindexLocation;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastPodcastindex; public fun equals (Ljava/lang/Object;)Z public final fun getFunding ()Ljava/util/List; + public final fun getLocation ()Ldev/stalla/model/podcastindex/PodcastindexLocation; public final fun getLocked ()Ldev/stalla/model/podcastindex/Locked; + public final fun getPersons ()Ljava/util/List; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1345,6 +1415,66 @@ public final class dev/stalla/model/podcastindex/PodcastPodcastindex$Factory : d public fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; } +public final class dev/stalla/model/podcastindex/PodcastindexEpisode { + public fun (DLjava/lang/String;)V + public final fun component1 ()D + public final fun component2 ()Ljava/lang/String; + public final fun copy (DLjava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexEpisode; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexEpisode;DLjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexEpisode; + public fun equals (Ljava/lang/Object;)Z + public final fun getDisplay ()Ljava/lang/String; + public final fun getNumber ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/stalla/model/podcastindex/PodcastindexLocation { + public fun (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ldev/stalla/model/podcastindex/GeoLocation; + public final fun component3 ()Ldev/stalla/model/podcastindex/OpenStreetMapFeature; + public final fun copy (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;)Ldev/stalla/model/podcastindex/PodcastindexLocation; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexLocation;Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexLocation; + public fun equals (Ljava/lang/Object;)Z + public final fun getGeo ()Ldev/stalla/model/podcastindex/GeoLocation; + public final fun getName ()Ljava/lang/String; + public final fun getOsm ()Ldev/stalla/model/podcastindex/OpenStreetMapFeature; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/stalla/model/podcastindex/PodcastindexPerson { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexPerson; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexPerson;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexPerson; + public fun equals (Ljava/lang/Object;)Z + public final fun getGroup ()Ljava/lang/String; + public final fun getHref ()Ljava/lang/String; + public final fun getImg ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getRole ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/stalla/model/podcastindex/PodcastindexSeason { + public fun (DLjava/lang/String;)V + public final fun component1 ()D + public final fun component2 ()Ljava/lang/String; + public final fun copy (DLjava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexSeason; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexSeason;DLjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexSeason; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getNumber ()D + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class dev/stalla/model/podcastindex/Soundbite { public static final field Factory Ldev/stalla/model/podcastindex/Soundbite$Factory; public fun (Ldev/stalla/model/StyledDuration$SecondsAndFraction;Ldev/stalla/model/StyledDuration$SecondsAndFraction;Ljava/lang/String;)V diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt index b73a4378..430abf2a 100644 --- a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt @@ -2,6 +2,7 @@ package dev.stalla.builder import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.OpenStreetMapElementType +import dev.stalla.util.asBigIntegerOrNull import dev.stalla.util.whenNotNull import java.math.BigInteger @@ -16,12 +17,21 @@ public interface OpenStreetMapFeatureBuilder : Builder { public fun id(id: BigInteger): OpenStreetMapFeatureBuilder + public fun id(id: Int): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } + + public fun id(id: Long): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } + public fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder + public fun revision(revision: Int?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } + + public fun revision(revision: Long?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } + override fun applyFrom(prototype: OpenStreetMapFeature?): OpenStreetMapFeatureBuilder = whenNotNull(prototype) { feature -> type(feature.type) id(feature.id) revision(feature.revision) } + } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index 6001357b..2b90c299 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -61,8 +61,8 @@ public class GeoLocation private constructor( public data class Parameter(val key: String, val value: String) { override fun equals(other: Any?): Boolean { return other is Parameter && - other.key == key && - other.value.equals(value, ignoreCase = true) + other.key.equals(key, ignoreCase = true) && + other.value == value } override fun hashCode(): Int { @@ -119,15 +119,18 @@ public class GeoLocation private constructor( public fun match(pattern: String): Boolean = match(of(pattern)) private fun match(parameters1: List, parameters2: List): Boolean { - for ((patternName, patternValue) in parameters1) { - val value = parameter(patternName, parameters2) - val matches = value != null && value == patternValue + for (param1 in parameters1) { + val value2 = parameter(param1.key, parameters2) + val matches = param1.match(param1.key, value2) if (!matches) return false } return true } + private fun Parameter.match(key: String, value: String?): Boolean = + if (value == null) false else this == Parameter(key, value) + private fun matchCrs(crs1: String?, crs2: String?): Boolean { return (crs1 == null || crs1.equals(CRS_WGS84, ignoreCase = true)) && (crs2 == null || crs2.equals(CRS_WGS84, ignoreCase = true)) diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt index 18523a54..592a02e5 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt @@ -5,6 +5,7 @@ import dev.stalla.builder.validating.ValidatingOpenStreetMapFeatureBuilder import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory import dev.stalla.parser.OsmFeatureParser +import dev.stalla.util.asBigIntegerOrNull import java.math.BigInteger /** @@ -22,6 +23,34 @@ public data class OpenStreetMapFeature( val revision: BigInteger? ) { + @Suppress("Unused") + public constructor( + type: OpenStreetMapElementType, + id: Int, + revision: Int? + ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) + + @Suppress("Unused") + public constructor( + type: OpenStreetMapElementType, + id: Int, + revision: Long? + ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) + + @Suppress("Unused") + public constructor( + type: OpenStreetMapElementType, + id: Long, + revision: Long? + ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) + + @Suppress("Unused") + public constructor( + type: OpenStreetMapElementType, + id: Long, + revision: Int? + ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) + override fun toString(): String = StringBuilder().apply { append(type.type) append(id) diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt index 8e6e9009..c134eaff 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt @@ -8,7 +8,7 @@ import dev.stalla.model.BuilderFactory * TODO. * * @property number TODO. - * @property display TODO. + * @property display An alternative display name of the episode. * * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#episode * @since 1.1.0 diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt index 8f97458a..0b901bae 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt @@ -9,12 +9,13 @@ import dev.stalla.model.BuilderFactory /** * TODO. * - * @property name TODO. - * @property role TODO. - * @property group TODO. - * @property img TODO. - * @property href TODO. + * @property name The full name or alias of the person. + * @property role The role the person serves on the show or episode - this should be a reference to an official role within the [Podcast Taxonomy Project](https://podcasttaxonomy.com) list. + * @property group Should be an official group within the [Podcast Taxonomy Project](https://podcasttaxonomy.com) list. + * @property img The url of a picture or avatar of the person. + * @property href The url to a relevant resource of information about the person, such as a homepage or third-party profile platform. * + * @see https://podcasttaxonomy.com * @since 1.1.0 */ public data class PodcastindexPerson( diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt index a1140288..03f0f6ad 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt @@ -7,8 +7,8 @@ import dev.stalla.model.BuilderFactory /** * TODO. * - * @property number TODO. - * @property name TODO. + * @property number The season number. + * @property name The "name" of the season. * * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#season * @since 1.1.0 diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt index 43fab69c..39ce5daa 100644 --- a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt @@ -3,6 +3,7 @@ package dev.stalla.parser import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.OpenStreetMapElementType import dev.stalla.util.InternalAPI +import dev.stalla.util.asBigIntegerOrNull import java.lang.StringBuilder import java.math.BigInteger import kotlin.contracts.contract @@ -54,6 +55,10 @@ internal object OsmFeatureParser { private fun Char.isNoDigit(): Boolean = digitToIntOrNull() == null private fun String?.asBigIntegerOrNull(): BigInteger? { + contract { + returnsNotNull() implies (this@asBigIntegerOrNull != null) + } + if (this == null) return null return try { BigInteger(this) diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt index 81bf12f2..b8e98489 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodeBuilder.kt @@ -4,11 +4,14 @@ import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBuilder import dev.stalla.builder.episode.EpisodeEnclosureBuilder import dev.stalla.builder.episode.EpisodeGuidBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder import dev.stalla.builder.episode.EpisodePodloveSimpleChapterBuilder @@ -19,6 +22,7 @@ import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.fake.FakeHrefOnlyImageBuilder import dev.stalla.builder.fake.FakeLinkBuilder import dev.stalla.builder.fake.FakePodcastindexLocationBuilder +import dev.stalla.builder.fake.FakePodcastindexPersonBuilder import dev.stalla.builder.fake.FakeRssCategoryBuilder import dev.stalla.model.Episode import java.time.temporal.TemporalAccessor @@ -95,8 +99,14 @@ internal class FakeEpisodeBuilder : FakeBuilder(), ProvidingEpisodeBuil override fun createSoundbiteBuilder(): EpisodePodcastindexSoundbiteBuilder = FakeEpisodePodcastindexSoundbiteBuilder() + override fun createPersonBuilder(): PodcastindexPersonBuilder = FakePodcastindexPersonBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = FakePodcastindexLocationBuilder() + override fun createSeasonBuilder(): EpisodePodcastindexSeasonBuilder = FakeEpisodePodcastindexSeasonBuilder() + + override fun createEpisodeBuilder(): EpisodePodcastindexEpisodeBuilder = FakeEpisodePodcastindexEpisodeBuilder() + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is FakeEpisodeBuilder) return false diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt index d381035f..c3ea2184 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt @@ -1,8 +1,11 @@ package dev.stalla.builder.fake.episode import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.episode.EpisodePodcastindexBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.episode.EpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.episode.EpisodePodcastindexTranscriptBuilder import dev.stalla.builder.fake.FakeBuilder @@ -12,8 +15,11 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder var chaptersBuilderValue: EpisodePodcastindexChaptersBuilder? = null var locationBuilderValue: PodcastindexLocationBuilder? = null + var seasonBuilder: EpisodePodcastindexSeasonBuilder? = null + var episodeBuilder: EpisodePodcastindexEpisodeBuilder? = null val transcriptBuilders: MutableList = mutableListOf() val soundbiteBuilders: MutableList = mutableListOf() + val personBuilders: MutableList = mutableListOf() override fun chaptersBuilder(chaptersBuilder: EpisodePodcastindexChaptersBuilder): EpisodePodcastindexBuilder = apply { this.chaptersBuilderValue = chaptersBuilder @@ -27,10 +33,22 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder transcriptBuilders.add(transcriptBuilder) } + override fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): EpisodePodcastindexBuilder = apply { + personBuilders.add(personBuilder) + } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): EpisodePodcastindexBuilder = apply { this.locationBuilderValue = locationBuilder } + override fun seasonBuilder(seasonBuilder: EpisodePodcastindexSeasonBuilder): EpisodePodcastindexBuilder = apply { + this.seasonBuilder = seasonBuilder + } + + override fun episodeBuilder(episodeBuilder: EpisodePodcastindexEpisodeBuilder): EpisodePodcastindexBuilder = apply { + this.episodeBuilder = episodeBuilder + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -39,8 +57,11 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder if (chaptersBuilderValue != other.chaptersBuilderValue) return false if (locationBuilderValue != other.locationBuilderValue) return false + if (seasonBuilder != other.seasonBuilder) return false + if (episodeBuilder != other.episodeBuilder) return false if (transcriptBuilders != other.transcriptBuilders) return false if (soundbiteBuilders != other.soundbiteBuilders) return false + if (personBuilders != other.personBuilders) return false return true } @@ -48,12 +69,15 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder override fun hashCode(): Int { var result = chaptersBuilderValue?.hashCode() ?: 0 result = 31 * result + (locationBuilderValue?.hashCode() ?: 0) + result = 31 * result + (seasonBuilder?.hashCode() ?: 0) + result = 31 * result + (episodeBuilder?.hashCode() ?: 0) result = 31 * result + transcriptBuilders.hashCode() result = 31 * result + soundbiteBuilders.hashCode() + result = 31 * result + personBuilders.hashCode() return result } override fun toString(): String { - return "FakeEpisodePodcastindexBuilder(chaptersBuilderValue=$chaptersBuilderValue, locationBuilderValue=$locationBuilderValue, transcriptBuilders=$transcriptBuilders, soundbiteBuilders=$soundbiteBuilders)" + return "FakeEpisodePodcastindexBuilder(chaptersBuilderValue=$chaptersBuilderValue, locationBuilderValue=$locationBuilderValue, seasonBuilder=$seasonBuilder, episodeBuilder=$episodeBuilder, transcriptBuilders=$transcriptBuilders, soundbiteBuilders=$soundbiteBuilders, personBuilders=$personBuilders)" } } diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt index 8836c0ca..e3afcdb8 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt @@ -4,6 +4,7 @@ import dev.stalla.builder.AtomPersonBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilder import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.RssImageBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -13,6 +14,7 @@ import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.fake.FakeHrefOnlyImageBuilder import dev.stalla.builder.fake.FakeLinkBuilder import dev.stalla.builder.fake.FakePodcastindexLocationBuilder +import dev.stalla.builder.fake.FakePodcastindexPersonBuilder import dev.stalla.builder.fake.FakeRssCategoryBuilder import dev.stalla.builder.fake.FakeRssImageBuilder import dev.stalla.builder.podcast.PodcastBuilder @@ -20,6 +22,7 @@ import dev.stalla.builder.podcast.PodcastItunesOwnerBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.builder.podcast.PodcastPodcastindexLockedBuilder import dev.stalla.builder.podcast.ProvidingPodcastBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.Podcast import java.time.temporal.TemporalAccessor import java.util.Locale @@ -106,6 +109,8 @@ internal class FakePodcastBuilder : FakeBuilder(), ProvidingPodcastBuil override fun createFundingBuilder(): PodcastPodcastindexFundingBuilder = FakePodcastPodcastindexFundingBuilder() + override fun createPersonBuilder(): PodcastindexPersonBuilder = FakePodcastindexPersonBuilder() + override fun createLocationBuilder(): PodcastindexLocationBuilder = FakePodcastindexLocationBuilder() override fun equals(other: Any?): Boolean { diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt index 1785a51e..d5501165 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt @@ -1,6 +1,7 @@ package dev.stalla.builder.fake.podcast import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.fake.FakeBuilder import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder @@ -12,6 +13,7 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder var lockedBuilderValue: PodcastPodcastindexLockedBuilder? = null var locationBuilderValue: PodcastindexLocationBuilder? = null val fundingBuilders: MutableList = mutableListOf() + val personBuilders: MutableList = mutableListOf() override fun lockedBuilder(lockedBuilder: PodcastPodcastindexLockedBuilder): PodcastPodcastindexBuilder = apply { this.lockedBuilderValue = lockedBuilder @@ -21,6 +23,10 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder fundingBuilders.add(fundingBuilder) } + override fun addPersonBuilder(personBuilder: PodcastindexPersonBuilder): PodcastPodcastindexBuilder = apply { + personBuilders.add(personBuilder) + } + override fun locationBuilder(locationBuilder: PodcastindexLocationBuilder): PodcastPodcastindexBuilder = apply { this.locationBuilderValue = locationBuilder } @@ -34,6 +40,7 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder if (lockedBuilderValue != other.lockedBuilderValue) return false if (locationBuilderValue != other.locationBuilderValue) return false if (fundingBuilders != other.fundingBuilders) return false + if (personBuilders != other.personBuilders) return false return true } @@ -42,10 +49,11 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder var result = lockedBuilderValue?.hashCode() ?: 0 result = 31 * result + (locationBuilderValue?.hashCode() ?: 0) result = 31 * result + fundingBuilders.hashCode() + result = 31 * result + personBuilders.hashCode() return result } override fun toString(): String { - return "FakePodcastPodcastindexBuilder(lockedBuilderValue=$lockedBuilderValue, locationBuilderValue=$locationBuilderValue, fundingBuilders=$fundingBuilders)" + return "FakePodcastPodcastindexBuilder(lockedBuilderValue=$lockedBuilderValue, locationBuilderValue=$locationBuilderValue, fundingBuilders=$fundingBuilders, personBuilders=$personBuilders)" } } From 178a2191a9fb7b42b1b61ae1860749aed6ba6f0d Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Mon, 26 Apr 2021 22:27:09 +0200 Subject: [PATCH 10/18] Implement builder and parser test for podcastindex phase 2 --- .../builder/PodcastindexLocationBuilder.kt | 4 +- .../EpisodePodcastindexSeasonBuilder.kt | 2 +- ...idatingEpisodePodcastindexSeasonBuilder.kt | 4 +- .../stalla/model/podcastindex/GeoLocation.kt | 5 +- .../model/podcastindex/PodcastindexSeason.kt | 2 +- .../parser/namespace/PodcastindexParser.kt | 2 +- .../FakeEpisodePodcastindexSeasonBuilder.kt | 4 +- .../fake/podcast/FakePodcastBuilder.kt | 1 - ...lidatingPodcastindexLocationBuilderTest.kt | 89 +++++++++ ...ValidatingPodcastindexPersonBuilderTest.kt | 94 +++++++++ ...alidatingEpisodePodcastindexBuilderTest.kt | 186 +++++++++++++++--- ...ngEpisodePodcastindexEpisodeBuilderTest.kt | 85 ++++++++ ...ingEpisodePodcastindexSeasonBuilderTest.kt | 85 ++++++++ ...EpisodePodcastindexSoundbiteBuilderTest.kt | 2 +- ...alidatingPodcastPodcastindexBuilderTest.kt | 125 +++++++++--- .../dev/stalla/model/EpisodeFixtures.kt | 20 +- src/test/kotlin/dev/stalla/model/Fixtures.kt | 39 ++++ .../dev/stalla/model/PodcastFixtures.kt | 6 +- .../namespace/PodcastindexParserTest.kt | 60 ++++++ src/test/resources/xml/channel.xml | 7 + src/test/resources/xml/item.xml | 9 + src/test/resources/xml/rss-all-empty.xml | 14 +- 22 files changed, 782 insertions(+), 63 deletions(-) create mode 100644 src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt create mode 100644 src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt create mode 100644 src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt create mode 100644 src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt index e5e5cc73..72cb436d 100644 --- a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt @@ -16,7 +16,7 @@ public interface PodcastindexLocationBuilder : Builder { override fun applyFrom(prototype: PodcastindexLocation?): PodcastindexLocationBuilder = whenNotNull(prototype) { location -> name(location.name) - geo(GeoLocation.builder().applyFrom(location.geo).build()!!) - // TODO osm or osmBuilder ?! + geo(GeoLocation.builder().applyFrom(location.geo).build()) + osm(OpenStreetMapFeature.builder().applyFrom(location.osm).build()) } } diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt index 0dd642d0..97883d3e 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt @@ -6,7 +6,7 @@ import dev.stalla.util.whenNotNull public interface EpisodePodcastindexSeasonBuilder : Builder { - public fun number(number: Double): EpisodePodcastindexSeasonBuilder + public fun number(number: Int): EpisodePodcastindexSeasonBuilder public fun name(name: String?): EpisodePodcastindexSeasonBuilder diff --git a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt index 10c99e72..9bb5533c 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilder.kt @@ -7,10 +7,10 @@ import dev.stalla.util.InternalAPI @InternalAPI internal class ValidatingEpisodePodcastindexSeasonBuilder : EpisodePodcastindexSeasonBuilder { - private var number: Double? = null + private var number: Int? = null private var name: String? = null - override fun number(number: Double): EpisodePodcastindexSeasonBuilder = apply { this.number = number } + override fun number(number: Int): EpisodePodcastindexSeasonBuilder = apply { this.number = number } override fun name(name: String?): EpisodePodcastindexSeasonBuilder = apply { this.name = name } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index 2b90c299..e9cad2f6 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -26,7 +26,7 @@ import kotlin.math.absoluteValue * @see [RFC 5870](https://tools.ietf.org/html/rfc5870) * @since 1.1.0 */ -public class GeoLocation private constructor( +public class GeoLocation public constructor( public val coordA: Double, public val coordB: Double, public val coordC: Double? = null, @@ -35,8 +35,7 @@ public class GeoLocation private constructor( public val parameters: List = emptyList() ) { - @InternalAPI - internal constructor( + public constructor( coordA: Double, coordB: Double, coordC: Double?, diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt index 03f0f6ad..ae633219 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt @@ -14,7 +14,7 @@ import dev.stalla.model.BuilderFactory * @since 1.1.0 */ public data class PodcastindexSeason( - val number: Double, + val number: Int, val name: String? ) { /** Provides a builder for the [PodcastindexSeason] class. */ diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index edc90644..5b01bb60 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -226,7 +226,7 @@ internal object PodcastindexParser : NamespaceParser() { private fun Node.toSeasonBuilder( seasonBuilder: EpisodePodcastindexSeasonBuilder ): EpisodePodcastindexSeasonBuilder? { - val number = parseAsDouble() ?: return null + val number = parseAsInt() ?: return null val name = getAttributeByName("name")?.value.trimmedOrNullIfBlank() return seasonBuilder.number(number) diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt index f30d35bb..6c601bb3 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexSeasonBuilder.kt @@ -6,10 +6,10 @@ import dev.stalla.model.podcastindex.PodcastindexSeason internal class FakeEpisodePodcastindexSeasonBuilder : FakeBuilder(), EpisodePodcastindexSeasonBuilder { - var number: Double? = null + var number: Int? = null var name: String? = null - override fun number(number: Double): EpisodePodcastindexSeasonBuilder = apply { this.number = number } + override fun number(number: Int): EpisodePodcastindexSeasonBuilder = apply { this.number = number } override fun name(name: String?): EpisodePodcastindexSeasonBuilder = apply { this.name = name } diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt index e3afcdb8..deae77c4 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastBuilder.kt @@ -22,7 +22,6 @@ import dev.stalla.builder.podcast.PodcastItunesOwnerBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.builder.podcast.PodcastPodcastindexLockedBuilder import dev.stalla.builder.podcast.ProvidingPodcastBuilder -import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.Podcast import java.time.temporal.TemporalAccessor import java.util.Locale diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt new file mode 100644 index 00000000..f29d31ba --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt @@ -0,0 +1,89 @@ +package dev.stalla.builder.validating + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.PodcastindexLocationBuilder +import dev.stalla.model.aPodcastindexGeoLocation +import dev.stalla.model.aPodcastindexLocation +import dev.stalla.model.aPodcastindexOpenStreetMapFeature +import dev.stalla.model.podcastindex.PodcastindexLocation +import org.junit.jupiter.api.Test + +internal class ValidatingPodcastindexLocationBuilderTest { + + @Test + internal fun `should not build an Podcastindex Location with when all the fields are missing`() { + val locationBuilder = ValidatingPodcastindexLocationBuilder() + + assertAll { + assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(locationBuilder.build()).isNull() + } + } + + @Test + internal fun `should not build an Podcastindex Location with when the name field is missing`() { + val locationBuilder = ValidatingPodcastindexLocationBuilder() + .geo(aPodcastindexGeoLocation()) + + assertAll { + assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(locationBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Podcastindex Location with all the mandatory fields`() { + val locationBuilder = ValidatingPodcastindexLocationBuilder() + .name("name") + + assertAll { + assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(locationBuilder.build()).isNotNull().all { + prop(PodcastindexLocation::name).isEqualTo("name") + prop(PodcastindexLocation::geo).isNull() + prop(PodcastindexLocation::osm).isNull() + } + } + } + + @Test + internal fun `should build an Podcastindex Location with all the added entries to its fields`() { + val locationBuilder = ValidatingPodcastindexLocationBuilder() + .name("name") + .geo(aPodcastindexGeoLocation()) + .osm(aPodcastindexOpenStreetMapFeature()) + + assertAll { + assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(locationBuilder.build()).isNotNull().all { + prop(PodcastindexLocation::name).isEqualTo("name") + prop(PodcastindexLocation::geo).isEqualTo(aPodcastindexGeoLocation()) + prop(PodcastindexLocation::osm).isEqualTo(aPodcastindexOpenStreetMapFeature()) + } + } + } + + @Test + internal fun `should populate an Podcastindex Location builder with all properties from an Podcastindex Location model`() { + val location = aPodcastindexLocation() + val locationBuilder = PodcastindexLocation.builder().applyFrom(location) + + assertAll { + assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(locationBuilder.build()).isNotNull().isEqualTo(location) + } + } +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt new file mode 100644 index 00000000..cae74428 --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt @@ -0,0 +1,94 @@ +package dev.stalla.builder.validating + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.PodcastindexPersonBuilder +import dev.stalla.model.aPodcastindexPerson +import dev.stalla.model.podcastindex.PodcastindexPerson +import org.junit.jupiter.api.Test + +internal class ValidatingPodcastindexPersonBuilderTest { + + @Test + internal fun `should not build an Podcastindex Person with when all the fields are missing`() { + val personBuilder = ValidatingPodcastindexPersonBuilder() + + assertAll { + assertThat(personBuilder).prop(PodcastindexPersonBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(personBuilder.build()).isNull() + } + } + + @Test + internal fun `should not build an Podcastindex Person with when the name field is missing`() { + val personBuilder = ValidatingPodcastindexPersonBuilder() + .role("role") + + assertAll { + assertThat(personBuilder).prop(PodcastindexPersonBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(personBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Podcastindex Person with all the mandatory fields`() { + val personBuilder = ValidatingPodcastindexPersonBuilder() + .name("name") + + assertAll { + assertThat(personBuilder).prop(PodcastindexPersonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(personBuilder.build()).isNotNull().all { + prop(PodcastindexPerson::name).isEqualTo("name") + prop(PodcastindexPerson::role).isNull() + prop(PodcastindexPerson::group).isNull() + prop(PodcastindexPerson::img).isNull() + prop(PodcastindexPerson::href).isNull() + } + } + } + + @Test + internal fun `should build an Podcastindex Person with all the added entries to its fields`() { + val personBuilder = ValidatingPodcastindexPersonBuilder() + .name("name") + .role("role") + .group("group") + .img("img") + .href("href") + + assertAll { + assertThat(personBuilder).prop(PodcastindexPersonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(personBuilder.build()).isNotNull().all { + prop(PodcastindexPerson::name).isEqualTo("name") + prop(PodcastindexPerson::role).isEqualTo("role") + prop(PodcastindexPerson::group).isEqualTo("group") + prop(PodcastindexPerson::img).isEqualTo("img") + prop(PodcastindexPerson::href).isEqualTo("href") + } + } + } + + @Test + internal fun `should populate an Podcastindex Person builder with all properties from an Podcastindex Person model`() { + val person = aPodcastindexPerson() + val personBuilder = PodcastindexPerson.builder().applyFrom(person) + + assertAll { + assertThat(personBuilder).prop(PodcastindexPersonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(personBuilder.build()).isNotNull().isEqualTo(person) + } + } + +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt index 6da6ea56..e81440df 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt @@ -11,10 +11,14 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.episode.EpisodePodcastindexBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.MediaType import dev.stalla.model.StyledDuration import dev.stalla.model.anEpisodePodcastindex import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.TranscriptType import org.junit.jupiter.api.Test import java.util.Locale @@ -46,6 +50,33 @@ internal class ValidatingEpisodePodcastindexBuilderTest { .type(TranscriptType.PLAIN_TEXT) .language(Locale.ITALIAN) + private val firstExpectedPersonBuilder = ValidatingPodcastindexPersonBuilder() + .name("First name") + .role("First role") + .group("First group") + .img("First img") + .href("First href") + + private val secondExpectedPersonBuilder = ValidatingPodcastindexPersonBuilder() + .name("Second name") + .role("Second role") + .group("Second group") + .img("Second img") + .href("Second href") + + private val expectedLocationBuilder = ValidatingPodcastindexLocationBuilder() + .name("Location name") + .geo(GeoLocation.of("geo:1,2,3")) + .osm(OpenStreetMapFeature.of("R123")) + + private val expectedSeasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + .number(1) + .name("Season name") + + private val expectedEpisodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + .number(1.0) + .display("Episode display") + @Test internal fun `should not build an Episode Podcastindex with when all the fields are empty`() { val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() @@ -70,97 +101,206 @@ internal class ValidatingEpisodePodcastindexBuilderTest { } @Test - internal fun `should not build an Episode Podcastindexx when there is only a chapters builder that doesn't build`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + internal fun `should not build an Episode Podcastindex when there is only a chapters builder that doesn't build`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .chaptersBuilder(ValidatingEpisodePodcastindexChaptersBuilder()) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(episodePodcastBuilder.build()).isNull() + assertThat(episodePodcastindexBuilder.build()).isNull() } } @Test internal fun `should build an Episode Podcastindex with at least one soundbite builder`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .addSoundbiteBuilder(firstExpectedSoundbiteBuilder) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(episodePodcastBuilder.build()).isNotNull() + assertThat(episodePodcastindexBuilder.build()).isNotNull() .prop(EpisodePodcastindex::soundbites).containsExactly(firstExpectedSoundbiteBuilder.build()) } } @Test internal fun `should not build an Episode Podcastindex when there is only a soundbite builder that doesn't build`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .addSoundbiteBuilder(ValidatingEpisodePodcastindexSoundbiteBuilder()) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(episodePodcastBuilder.build()).isNull() + assertThat(episodePodcastindexBuilder.build()).isNull() } } @Test internal fun `should build an Episode Podcastindex with at least a transcript builder`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .addTranscriptBuilder(firstExpectedTranscriptBuilder) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(episodePodcastBuilder.build()).isNotNull() + assertThat(episodePodcastindexBuilder.build()).isNotNull() .prop(EpisodePodcastindex::transcripts).containsExactly(firstExpectedTranscriptBuilder.build()) } } @Test internal fun `should not build an Episode Podcastindex when there is only a transcript builder that doesn't build`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .addTranscriptBuilder(ValidatingEpisodePodcastindexTranscriptBuilder()) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(episodePodcastBuilder.build()).isNull() + assertThat(episodePodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex with at least a person builder`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .addPersonBuilder(firstExpectedPersonBuilder) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodePodcastindexBuilder.build()).isNotNull() + .prop(EpisodePodcastindex::persons).containsExactly(firstExpectedPersonBuilder.build()) + } + } + + @Test + internal fun `should not build an Episode Podcastindex when there is only a person builder that doesn't build`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .addPersonBuilder(ValidatingPodcastindexPersonBuilder()) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodePodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex with at least a location builder`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .locationBuilder(expectedLocationBuilder) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodePodcastindexBuilder.build()).isNotNull() + .prop(EpisodePodcastindex::location).isEqualTo(expectedLocationBuilder.build()) + } + } + + @Test + internal fun `should not build an Episode Podcastindex when there is only a location builder that doesn't build`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .locationBuilder(ValidatingPodcastindexLocationBuilder()) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodePodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex with at least a season builder`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .seasonBuilder(expectedSeasonBuilder) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodePodcastindexBuilder.build()).isNotNull() + .prop(EpisodePodcastindex::season).isEqualTo(expectedSeasonBuilder.build()) + } + } + + @Test + internal fun `should not build an Episode Podcastindex when there is only a season builder that doesn't build`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .seasonBuilder(ValidatingEpisodePodcastindexSeasonBuilder()) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodePodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex with at least a episode builder`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .episodeBuilder(expectedEpisodeBuilder) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodePodcastindexBuilder.build()).isNotNull() + .prop(EpisodePodcastindex::episode).isEqualTo(expectedEpisodeBuilder.build()) + } + } + + @Test + internal fun `should not build an Episode Podcastindex when there is only a episode builder that doesn't build`() { + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() + .episodeBuilder(ValidatingEpisodePodcastindexEpisodeBuilder()) + + assertAll { + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodePodcastindexBuilder.build()).isNull() } } @Test internal fun `should build an Episode Podcastindex with with all the added entries to its fields`() { - val episodePodcastBuilder = ValidatingEpisodePodcastindexBuilder() + val episodePodcastindexBuilder = ValidatingEpisodePodcastindexBuilder() .chaptersBuilder(expectedChaptersBuilder) .addSoundbiteBuilder(firstExpectedSoundbiteBuilder) .addSoundbiteBuilder(secondExpectedSoundbiteBuilder) .addTranscriptBuilder(firstExpectedTranscriptBuilder) .addTranscriptBuilder(secondExpectedTranscriptBuilder) + .addPersonBuilder(firstExpectedPersonBuilder) + .addPersonBuilder(secondExpectedPersonBuilder) + .locationBuilder(expectedLocationBuilder) + .seasonBuilder(expectedSeasonBuilder) + .episodeBuilder(expectedEpisodeBuilder) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(episodePodcastBuilder.build()).isNotNull().all { + assertThat(episodePodcastindexBuilder.build()).isNotNull().all { prop(EpisodePodcastindex::chapters).isEqualTo(expectedChaptersBuilder.build()) prop(EpisodePodcastindex::soundbites).containsExactly(firstExpectedSoundbiteBuilder.build(), secondExpectedSoundbiteBuilder.build()) prop(EpisodePodcastindex::transcripts) .containsExactly(firstExpectedTranscriptBuilder.build(), secondExpectedTranscriptBuilder.build()) + prop(EpisodePodcastindex::persons).containsExactly(firstExpectedPersonBuilder.build(), secondExpectedPersonBuilder.build()) + prop(EpisodePodcastindex::location).isEqualTo(expectedLocationBuilder.build()) + prop(EpisodePodcastindex::season).isEqualTo(expectedSeasonBuilder.build()) + prop(EpisodePodcastindex::episode).isEqualTo(expectedEpisodeBuilder.build()) } } } @Test internal fun `should populate an Episode Podcastindex builder with all properties from an Episode Podcastindex model`() { - val episodePodcast = anEpisodePodcastindex() - val episodePodcastBuilder = EpisodePodcastindex.builder().applyFrom(episodePodcast) + val episodePodcastindex = anEpisodePodcastindex() + val episodePodcastindexBuilder = EpisodePodcastindex.builder().applyFrom(episodePodcastindex) assertAll { - assertThat(episodePodcastBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(episodePodcastindexBuilder).prop(EpisodePodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(episodePodcastBuilder.build()).isNotNull().isEqualTo(episodePodcast) + assertThat(episodePodcastindexBuilder.build()).isNotNull().isEqualTo(episodePodcastindex) } } } diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt new file mode 100644 index 00000000..b5d60b6d --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt @@ -0,0 +1,85 @@ +package dev.stalla.builder.validating.episode + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.episode.EpisodePodcastindexEpisodeBuilder +import dev.stalla.model.anEpisodePodcastindexEpisode +import dev.stalla.model.podcastindex.PodcastindexEpisode +import org.junit.jupiter.api.Test + +internal class ValidatingEpisodePodcastindexEpisodeBuilderTest { + + @Test + internal fun `should not build an Episode Podcastindex Episode with when all the fields are missing`() { + val episodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + + assertAll { + assertThat(episodeBuilder).prop(EpisodePodcastindexEpisodeBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodeBuilder.build()).isNull() + } + } + + @Test + internal fun `should not build an Episode Podcastindex Episode with when the number field is missing`() { + val episodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + .display("display") + + assertAll { + assertThat(episodeBuilder).prop(EpisodePodcastindexEpisodeBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(episodeBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex Episode with all the mandatory fields`() { + val episodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + .number(1.0) + + assertAll { + assertThat(episodeBuilder).prop(EpisodePodcastindexEpisodeBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodeBuilder.build()).isNotNull().all { + prop(PodcastindexEpisode::number).isEqualTo(1.0) + prop(PodcastindexEpisode::display).isNull() + } + } + } + + @Test + internal fun `should build an Episode Podcastindex Episode with all the added entries to its fields`() { + val episodeBuilder = ValidatingEpisodePodcastindexEpisodeBuilder() + .number(1.0) + .display("display") + + assertAll { + assertThat(episodeBuilder).prop(EpisodePodcastindexEpisodeBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(episodeBuilder.build()).isNotNull().all { + prop(PodcastindexEpisode::number).isEqualTo(1.0) + prop(PodcastindexEpisode::display).isEqualTo("display") + } + } + } + + @Test + internal fun `should populate an Episode Podcastindex Episode builder with all properties from an Episode Podcastindex Episode model`() { + val podcastindexEpisode = anEpisodePodcastindexEpisode() + val podcastindexEpisodeBuilder = PodcastindexEpisode.builder().applyFrom(podcastindexEpisode) + + assertAll { + assertThat(podcastindexEpisodeBuilder).prop(EpisodePodcastindexEpisodeBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(podcastindexEpisodeBuilder.build()).isNotNull().isEqualTo(podcastindexEpisode) + } + } + +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt new file mode 100644 index 00000000..0071a2f8 --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt @@ -0,0 +1,85 @@ +package dev.stalla.builder.validating.episode + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder +import dev.stalla.model.anEpisodePodcastindexSeason +import dev.stalla.model.podcastindex.PodcastindexSeason +import org.junit.jupiter.api.Test + +internal class ValidatingEpisodePodcastindexSeasonBuilderTest { + + @Test + internal fun `should not build an Episode Podcastindex Season with when all the fields are missing`() { + val seasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + + assertAll { + assertThat(seasonBuilder).prop(EpisodePodcastindexSeasonBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(seasonBuilder.build()).isNull() + } + } + + @Test + internal fun `should not build an Episode Podcastindex Season with when the number field is missing`() { + val seasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + .name("name") + + assertAll { + assertThat(seasonBuilder).prop(EpisodePodcastindexSeasonBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(seasonBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Episode Podcastindex Season with all the mandatory fields`() { + val seasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + .number(1) + + assertAll { + assertThat(seasonBuilder).prop(EpisodePodcastindexSeasonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(seasonBuilder.build()).isNotNull().all { + prop(PodcastindexSeason::number).isEqualTo(1) + prop(PodcastindexSeason::name).isNull() + } + } + } + + @Test + internal fun `should build an Episode Podcastindex Season with all the added entries to its fields`() { + val seasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() + .number(1) + .name("name") + + assertAll { + assertThat(seasonBuilder).prop(EpisodePodcastindexSeasonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(seasonBuilder.build()).isNotNull().all { + prop(PodcastindexSeason::number).isEqualTo(1) + prop(PodcastindexSeason::name).isEqualTo("name") + } + } + } + + @Test + internal fun `should populate an Episode Podcastindex Season builder with all properties from an Episode Podcastindex Episode model`() { + val podcastindexSeason = anEpisodePodcastindexSeason() + val podcastindexseasonBuilder = PodcastindexSeason.builder().applyFrom(podcastindexSeason) + + assertAll { + assertThat(podcastindexseasonBuilder).prop(EpisodePodcastindexSeasonBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(podcastindexseasonBuilder.build()).isNotNull().isEqualTo(podcastindexSeason) + } + } + +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSoundbiteBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSoundbiteBuilderTest.kt index fb03529a..b02dc5f2 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSoundbiteBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSoundbiteBuilderTest.kt @@ -86,7 +86,7 @@ internal class ValidatingEpisodePodcastindexSoundbiteBuilderTest { } @Test - internal fun `should build an Episode Podcastindex Soundbite with with all the added entries to its fields`() { + internal fun `should build an Episode Podcastindex Soundbite with all the added entries to its fields`() { val soundbiteBuilder = ValidatingEpisodePodcastindexSoundbiteBuilder() .startTime(StyledDuration.secondsAndFraction(1)) .duration(StyledDuration.secondsAndFraction(15)) diff --git a/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt index c227cdfe..a26d4e11 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt @@ -11,7 +11,12 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.podcast.PodcastPodcastindexBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.aPodcastPodcastindex +import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.PodcastPodcastindex import org.junit.jupiter.api.Test @@ -29,92 +34,166 @@ internal class ValidatingPodcastPodcastindexBuilderTest { .url("https://example.com/donate-more") .message("Second funding") + private val firstExpectedPersonBuilder = ValidatingPodcastindexPersonBuilder() + .name("First name") + .role("First role") + .group("First group") + .img("First img") + .href("First href") + + private val secondExpectedPersonBuilder = ValidatingPodcastindexPersonBuilder() + .name("Second name") + .role("Second role") + .group("Second group") + .img("Second img") + .href("Second href") + + private val expectedLocationBuilder = ValidatingPodcastindexLocationBuilder() + .name("Location name") + .geo(GeoLocation.of("geo:1,2,3")) + .osm(OpenStreetMapFeature.of("R123")) + @Test internal fun `should not build a Podcast Podcastindex with when all the fields are empty`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(podcastPodcastBuilder.build()).isNull() + assertThat(podcastPodcastindexBuilder.build()).isNull() } } @Test internal fun `should build a Podcast Podcastindex with at least a locked builder`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() .lockedBuilder(expectedLockedBuilder) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(podcastPodcastBuilder.build()).isNotNull().prop(PodcastPodcastindex::locked).isEqualTo(expectedLockedBuilder.build()) + assertThat(podcastPodcastindexBuilder.build()).isNotNull().prop(PodcastPodcastindex::locked).isEqualTo(expectedLockedBuilder.build()) } } @Test internal fun `should not build a Podcast Podcastindex when there is only a locked builder that doesn't build`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() .lockedBuilder(ValidatingPodcastPodcastindexLockedBuilder()) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(podcastPodcastBuilder.build()).isNull() + assertThat(podcastPodcastindexBuilder.build()).isNull() } } @Test - internal fun `should build a Podcast Podcastindext with at least one funding builder`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + internal fun `should build a Podcast Podcastindex with at least one funding builder`() { + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() .addFundingBuilder(firstExpectedFundingBuilder) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(podcastPodcastBuilder.build()).isNotNull() + assertThat(podcastPodcastindexBuilder.build()).isNotNull() .prop(PodcastPodcastindex::funding).containsExactly(firstExpectedFundingBuilder.build()) } } @Test internal fun `should not build a Podcast Podcastindex when there is only a funding builder that doesn't build`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() .addFundingBuilder(ValidatingPodcastPodcastindexFundingBuilder()) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(podcastPodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Podcast Podcastindex with at least a person builder`() { + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() + .addPersonBuilder(firstExpectedPersonBuilder) + + assertAll { + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(podcastPodcastindexBuilder.build()).isNotNull() + .prop(PodcastPodcastindex::persons).containsExactly(firstExpectedPersonBuilder.build()) + } + } + + @Test + internal fun `should not build an Podcast Podcastindex when there is only a person builder that doesn't build`() { + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() + .addPersonBuilder(ValidatingPodcastindexPersonBuilder()) + + assertAll { + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(podcastPodcastindexBuilder.build()).isNull() + } + } + + @Test + internal fun `should build an Podcast Podcastindex with at least a location builder`() { + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() + .locationBuilder(expectedLocationBuilder) + + assertAll { + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(podcastPodcastindexBuilder.build()).isNotNull() + .prop(PodcastPodcastindex::location).isEqualTo(expectedLocationBuilder.build()) + } + } + + @Test + internal fun `should not build an Podcast Podcastindex when there is only a location builder that doesn't build`() { + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() + .locationBuilder(ValidatingPodcastindexLocationBuilder()) + + assertAll { + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isFalse() - assertThat(podcastPodcastBuilder.build()).isNull() + assertThat(podcastPodcastindexBuilder.build()).isNull() } } @Test internal fun `should build a Podcast Podcastindex with with all the added entries to its fields`() { - val podcastPodcastBuilder = ValidatingPodcastPodcastindexBuilder() + val podcastPodcastindexBuilder = ValidatingPodcastPodcastindexBuilder() .lockedBuilder(expectedLockedBuilder) .addFundingBuilder(firstExpectedFundingBuilder) .addFundingBuilder(secondExpectedFundingBuilder) + .addPersonBuilder(firstExpectedPersonBuilder) + .addPersonBuilder(secondExpectedPersonBuilder) + .locationBuilder(expectedLocationBuilder) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(podcastPodcastBuilder.build()).isNotNull().all { + assertThat(podcastPodcastindexBuilder.build()).isNotNull().all { prop(PodcastPodcastindex::locked).isEqualTo(expectedLockedBuilder.build()) prop(PodcastPodcastindex::funding).containsExactly(firstExpectedFundingBuilder.build(), secondExpectedFundingBuilder.build()) + prop(PodcastPodcastindex::persons).containsExactly(firstExpectedPersonBuilder.build(), secondExpectedPersonBuilder.build()) + prop(PodcastPodcastindex::location).isEqualTo(expectedLocationBuilder.build()) } } } @Test internal fun `should populate a Podcast Podcastindex builder with all properties from an Podcast Podcastindex model`() { - val podcastPodcast = aPodcastPodcastindex() - val podcastPodcastBuilder = PodcastPodcastindex.builder().applyFrom(podcastPodcast) + val podcastPodcastindex = aPodcastPodcastindex() + val podcastPodcastindexBuilder = PodcastPodcastindex.builder().applyFrom(podcastPodcastindex) assertAll { - assertThat(podcastPodcastBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() + assertThat(podcastPodcastindexBuilder).prop(PodcastPodcastindexBuilder::hasEnoughDataToBuild).isTrue() - assertThat(podcastPodcastBuilder.build()).isNotNull().isEqualTo(podcastPodcast) + assertThat(podcastPodcastindexBuilder.build()).isNotNull().isEqualTo(podcastPodcastindex) } } } diff --git a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt index 52a11b86..c192573f 100644 --- a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt @@ -12,6 +12,10 @@ import dev.stalla.model.itunes.EpisodeItunes import dev.stalla.model.itunes.EpisodeType import dev.stalla.model.podcastindex.Chapters import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.PodcastindexEpisode +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexPerson +import dev.stalla.model.podcastindex.PodcastindexSeason import dev.stalla.model.podcastindex.Soundbite import dev.stalla.model.podcastindex.Transcript import dev.stalla.model.podcastindex.TranscriptType @@ -134,7 +138,9 @@ internal fun anEpisodeBitlove( internal fun anEpisodePodcastindex( transcripts: List = listOf(anEpisodePodcastindexTranscript()), soundbites: List = listOf(anEpisodePodcastindexSoundbite()), - chapters: Chapters? = anEpisodePodcastindexChapters() + chapters: Chapters? = anEpisodePodcastindexChapters(), + persons: List = listOf(aPodcastindexPerson()), + location: PodcastindexLocation? = aPodcastindexLocation() ) = EpisodePodcastindex(transcripts, soundbites, chapters) @JvmOverloads @@ -157,3 +163,15 @@ internal fun anEpisodePodcastindexChapters( url: String = "episode podcastindex chapters url", type: MediaType = MediaType.JSON_CHAPTERS ) = Chapters(url, type) + +@JvmOverloads +internal fun anEpisodePodcastindexSeason( + number: Int = 1, + name: String = "episode podcastindex season name" +) = PodcastindexSeason(number, name) + +@JvmOverloads +internal fun anEpisodePodcastindexEpisode( + number: Double = 1.0, + display: String = "episode podcastindex episode display" +) = PodcastindexEpisode(number, display) diff --git a/src/test/kotlin/dev/stalla/model/Fixtures.kt b/src/test/kotlin/dev/stalla/model/Fixtures.kt index 143f4de9..83a861a2 100644 --- a/src/test/kotlin/dev/stalla/model/Fixtures.kt +++ b/src/test/kotlin/dev/stalla/model/Fixtures.kt @@ -5,12 +5,18 @@ import dev.stalla.model.atom.AtomPerson import dev.stalla.model.atom.Link import dev.stalla.model.googleplay.GoogleplayCategory import dev.stalla.model.itunes.ItunesCategory +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapElementType +import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.model.rss.RssCategory import dev.stalla.model.rss.RssImage import dev.stalla.staticPropertiesByType import org.junit.jupiter.params.provider.ArgumentsProvider import org.reflections.Reflections +import java.math.BigInteger @JvmOverloads internal fun anRssImage( @@ -61,6 +67,39 @@ internal fun aGoogleplayCategory( category: GoogleplayCategory = GoogleplayCategory.NEWS_AND_POLITICS ) = category +@JvmOverloads +internal fun aPodcastindexGeoLocation( + coordA: Double = 48.20849, + coordB: Double = 16.37208, + coordC: Double? = 5.0, + crs: String? = GeoLocation.CRS_WGS84, + uncertainty: Double? = 10.0, + parameters: List = emptyList() +) = GeoLocation(coordA, coordB, coordC, crs, uncertainty, parameters) + +@JvmOverloads +internal fun aPodcastindexOpenStreetMapFeature( + type: OpenStreetMapElementType = OpenStreetMapElementType.Relation, + id: BigInteger = BigInteger.ONE, + revision: BigInteger? = BigInteger.TWO +) = OpenStreetMapFeature(type, id, revision) + +@JvmOverloads +internal fun aPodcastindexPerson( + name: String = "podcastindex person name", + role: String? = "podcastindex person role", + group: String? = "podcastindex person group", + img: String? = "podcastindex person img", + href: String? = "podcastindex person href" +) = PodcastindexPerson(name, role, group, img, href) + +@JvmOverloads +internal fun aPodcastindexLocation( + name: String = "podcastindex location name", + geo: GeoLocation? = aPodcastindexGeoLocation(), + osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() +) = PodcastindexLocation(name, geo, osm) + internal val simpleCategoryNames = listOf( "Arts", "Business", diff --git a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt index 809f261f..4ab8fd41 100644 --- a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt @@ -15,6 +15,8 @@ import dev.stalla.model.itunes.ShowType import dev.stalla.model.podcastindex.Funding import dev.stalla.model.podcastindex.Locked import dev.stalla.model.podcastindex.PodcastPodcastindex +import dev.stalla.model.podcastindex.PodcastindexLocation +import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.rss.RssCategory import dev.stalla.model.rss.RssImage import java.time.Month @@ -127,7 +129,9 @@ internal fun aPodcastGoogleplay( @JvmOverloads internal fun aPodcastPodcastindex( locked: Locked? = aPodcastPodcastindexLocked(), - funding: List = listOf(aPodcastPodcastindexFunding()) + funding: List = listOf(aPodcastPodcastindexFunding()), + persons: List = listOf(aPodcastindexPerson()), + location: PodcastindexLocation? = aPodcastindexLocation() ) = PodcastPodcastindex(locked, funding) @JvmOverloads diff --git a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt index 0aff7e28..6b4d061f 100644 --- a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt +++ b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt @@ -8,18 +8,28 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.prop +import dev.stalla.builder.fake.FakePodcastindexLocationBuilder +import dev.stalla.builder.fake.FakePodcastindexPersonBuilder import dev.stalla.builder.fake.episode.FakeEpisodeBuilder import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexBuilder import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexChaptersBuilder +import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexSeasonBuilder import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexSoundbiteBuilder import dev.stalla.builder.fake.episode.FakeEpisodePodcastindexTranscriptBuilder import dev.stalla.builder.fake.podcast.FakePodcastBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexFundingBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexLockedBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder +import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder +import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexEpisodeBuilder +import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeasonBuilder import dev.stalla.dom.XmlRes import dev.stalla.model.MediaType import dev.stalla.model.StyledDuration +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.parser.NamespaceParserTest import org.junit.jupiter.api.Test @@ -53,6 +63,38 @@ internal class PodcastindexParserTest : NamespaceParserTest() { .language(Locale.ITALY) .rel("episode podcastindex transcript rel") + private val expectedPodcastPersonBuilder = FakePodcastindexPersonBuilder() + .name("podcast podcastindex person name") + .role("podcast podcastindex person role") + .group("podcast podcastindex person group") + .img("podcast podcastindex person img") + .href("podcast podcastindex person href") + + private val expectedEpisodePersonBuilder = FakePodcastindexPersonBuilder() + .name("episode podcastindex person name") + .role("episode podcastindex person role") + .group("episode podcastindex person group") + .img("episode podcastindex person img") + .href("episode podcastindex person href") + + private val expectedPodcastLocationBuilder = FakePodcastindexLocationBuilder() + .name("podcast podcastindex location name") + .geo(GeoLocation.of("geo:1,2,3")) + .osm(OpenStreetMapFeature.of("R123")) + + private val expectedEpisodeLocationBuilder = FakePodcastindexLocationBuilder() + .name("episode podcastindex location name") + .geo(GeoLocation.of("geo:4,5,6")) + .osm(OpenStreetMapFeature.of("W456")) + + private val expectedSeasonBuilder = FakeEpisodePodcastindexSeasonBuilder() + .number(1) + .name("episode podcastindex season name") + + private val expectedEpisodeBuilder = FakeEpisodePodcastindexEpisodeBuilder() + .number(1.0) + .display("episode podcastindex episode display") + @Test fun `should extract all Podcastindex fields from channel when present`() { val channel: Node = XmlRes("/xml/channel.xml").rootNodeByName("channel") @@ -62,6 +104,8 @@ internal class PodcastindexParserTest : NamespaceParserTest() { assertThat(builder.podcastPodcastindexBuilder, "channel.podcastindex").isNotNull().all { prop(FakePodcastPodcastindexBuilder::lockedBuilderValue).isEqualTo(expectedLockedBuilder) prop(FakePodcastPodcastindexBuilder::fundingBuilders).containsExactly(expectedFundingBuilder) + prop(FakePodcastPodcastindexBuilder::personBuilders).containsExactly(expectedPodcastPersonBuilder) + prop(FakePodcastPodcastindexBuilder::locationBuilderValue).isEqualTo(expectedPodcastLocationBuilder) } } @@ -74,6 +118,8 @@ internal class PodcastindexParserTest : NamespaceParserTest() { assertThat(builder.podcastPodcastindexBuilder, "channel.podcastindex").all { prop(FakePodcastPodcastindexBuilder::lockedBuilderValue).isNull() prop(FakePodcastPodcastindexBuilder::fundingBuilders).isEmpty() + prop(FakePodcastPodcastindexBuilder::personBuilders).isEmpty() + prop(FakePodcastPodcastindexBuilder::locationBuilderValue).isNull() } } @@ -86,6 +132,8 @@ internal class PodcastindexParserTest : NamespaceParserTest() { assertThat(builder.podcastPodcastindexBuilder, "channel.podcastindex").all { prop(FakePodcastPodcastindexBuilder::lockedBuilderValue).isNull() prop(FakePodcastPodcastindexBuilder::fundingBuilders).isEmpty() + prop(FakePodcastPodcastindexBuilder::personBuilders).isEmpty() + prop(FakePodcastPodcastindexBuilder::locationBuilderValue).isNull() } } @@ -99,6 +147,10 @@ internal class PodcastindexParserTest : NamespaceParserTest() { prop(FakeEpisodePodcastindexBuilder::chaptersBuilderValue).isEqualTo(expectedChaptersBuilder) prop(FakeEpisodePodcastindexBuilder::soundbiteBuilders).containsExactly(expectedSoundbiteBuilder) prop(FakeEpisodePodcastindexBuilder::transcriptBuilders).containsExactly(expectedTranscriptBuilder) + prop(FakeEpisodePodcastindexBuilder::personBuilders).containsExactly(expectedEpisodePersonBuilder) + prop(FakeEpisodePodcastindexBuilder::locationBuilderValue).isEqualTo(expectedEpisodeLocationBuilder) + prop(FakeEpisodePodcastindexBuilder::seasonBuilder).isEqualTo(expectedSeasonBuilder) + prop(FakeEpisodePodcastindexBuilder::episodeBuilder).isEqualTo(expectedEpisodeBuilder) } } @@ -112,6 +164,10 @@ internal class PodcastindexParserTest : NamespaceParserTest() { prop(FakeEpisodePodcastindexBuilder::chaptersBuilderValue).isNull() prop(FakeEpisodePodcastindexBuilder::soundbiteBuilders).isEmpty() prop(FakeEpisodePodcastindexBuilder::transcriptBuilders).isEmpty() + prop(FakeEpisodePodcastindexBuilder::personBuilders).isEmpty() + prop(FakeEpisodePodcastindexBuilder::locationBuilderValue).isNull() + prop(FakeEpisodePodcastindexBuilder::seasonBuilder).isNull() + prop(FakeEpisodePodcastindexBuilder::episodeBuilder).isNull() } } @@ -125,6 +181,10 @@ internal class PodcastindexParserTest : NamespaceParserTest() { prop(FakeEpisodePodcastindexBuilder::chaptersBuilderValue).isNull() prop(FakeEpisodePodcastindexBuilder::soundbiteBuilders).isEmpty() prop(FakeEpisodePodcastindexBuilder::transcriptBuilders).isEmpty() + prop(FakeEpisodePodcastindexBuilder::personBuilders).isEmpty() + prop(FakeEpisodePodcastindexBuilder::locationBuilderValue).isNull() + prop(FakeEpisodePodcastindexBuilder::seasonBuilder).isNull() + prop(FakeEpisodePodcastindexBuilder::episodeBuilder).isNull() } } diff --git a/src/test/resources/xml/channel.xml b/src/test/resources/xml/channel.xml index 544094f1..8b0bd605 100644 --- a/src/test/resources/xml/channel.xml +++ b/src/test/resources/xml/channel.xml @@ -76,4 +76,11 @@ feedpress link yes podcast podcastindex funding message + + podcast podcastindex person name + + podcast podcastindex location name diff --git a/src/test/resources/xml/item.xml b/src/test/resources/xml/item.xml index 159568d8..db36bdd5 100644 --- a/src/test/resources/xml/item.xml +++ b/src/test/resources/xml/item.xml @@ -58,4 +58,13 @@ episode podcastindex soundbite title + + episode podcastindex person name + + episode podcastindex location name + 1 + 1.0 diff --git a/src/test/resources/xml/rss-all-empty.xml b/src/test/resources/xml/rss-all-empty.xml index b6e8f9e4..0cc265fb 100644 --- a/src/test/resources/xml/rss-all-empty.xml +++ b/src/test/resources/xml/rss-all-empty.xml @@ -7,7 +7,8 @@ xmlns:fyyd="https://fyyd.de/fyyd-ns/" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" - xmlns:psc="http://podlove.org/simple-chapters"> + xmlns:psc="http://podlove.org/simple-chapters" + xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md"> @@ -67,6 +68,10 @@ + + + + @@ -113,6 +118,13 @@ + + + + + + + From b4a720dec28e1640091cab129959711c864c31c6 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Tue, 27 Apr 2021 19:10:40 +0200 Subject: [PATCH 11/18] Implement remaining tests --- .../stalla/PodcastRssParserInteropTest.java | 17 ++++++ .../EpisodePodcastindexInteropTest.java | 8 +++ .../PodcastPodcastindexInteropTest.java | 8 +++ ...lidatingPodcastindexLocationBuilderTest.kt | 4 +- ...ValidatingPodcastindexPersonBuilderTest.kt | 4 +- .../dev/stalla/model/EpisodeFixtures.kt | 26 ++++++++- src/test/kotlin/dev/stalla/model/Fixtures.kt | 16 ----- .../dev/stalla/model/PodcastFixtures.kt | 24 +++++++- .../namespace/PodcastindexParserTest.kt | 24 ++++++++ .../namespace/PodcastindexWriterTest.kt | 58 +++++++++++++++++-- src/test/resources/xml/item-invalid.xml | 2 + src/test/resources/xml/rss-full.xml | 16 +++++ src/test/resources/xml/writer-fixtures.xml | 16 +++++ 13 files changed, 193 insertions(+), 30 deletions(-) diff --git a/src/test/java/dev/stalla/PodcastRssParserInteropTest.java b/src/test/java/dev/stalla/PodcastRssParserInteropTest.java index 8118763d..716dfa2f 100644 --- a/src/test/java/dev/stalla/PodcastRssParserInteropTest.java +++ b/src/test/java/dev/stalla/PodcastRssParserInteropTest.java @@ -7,6 +7,7 @@ import dev.stalla.model.googleplay.GoogleplayCategory; import dev.stalla.model.itunes.ItunesCategory; import dev.stalla.model.podcastindex.Funding; +import dev.stalla.model.podcastindex.PodcastindexPerson; import dev.stalla.model.podcastindex.Soundbite; import dev.stalla.model.podcastindex.Transcript; import dev.stalla.model.podlove.SimpleChapter; @@ -31,6 +32,7 @@ import static dev.stalla.TestUtilKt.declaresException; import static dev.stalla.TestUtilKt.declaresNoExceptions; +import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindexPerson; import static dev.stalla.model.EpisodeFixturesKt.aPodloveSimpleChapter; import static dev.stalla.model.EpisodeFixturesKt.anEpisode; import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindexSoundbite; @@ -41,6 +43,7 @@ import static dev.stalla.model.FixturesKt.anItunesCategory; import static dev.stalla.model.FixturesKt.anRssCategory; import static dev.stalla.model.PodcastFixturesKt.aPodcastPodcastindexFunding; +import static dev.stalla.model.PodcastFixturesKt.aPodcastPodcastindexPerson; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -280,6 +283,13 @@ void shouldParsePodcastPodcastindexUnmodifiableFunding() { assertThrows(UnsupportedOperationException.class, () -> fundingList.add(aPodcastPodcastindexFunding())); } + @Test + @DisplayName("should parse to an unmodifiable list of Podcast Podcastindex persons") + void shouldParsePodcastPodcastindexUnmodifiablePerson() { + final List personList = requireNonNull(parsedPodcast.getPodcastindex().getPersons()); + assertThrows(UnsupportedOperationException.class, () -> personList.add(aPodcastPodcastindexPerson())); + } + @Test @DisplayName("should parse to an unmodifiable list of Episode Podcastindex soundbites") void shouldParseEpisodePodcastindexUnmodifiableSoundbites() { @@ -294,6 +304,13 @@ void shouldParseEpisodePodcastindexUnmodifiableTranscripts() { assertThrows(UnsupportedOperationException.class, () -> transcripts.add(anEpisodePodcastindexTranscript())); } + @Test + @DisplayName("should parse to an unmodifiable list of Episode Podcastindex persons") + void shouldParseEpisodePodcastindexUnmodifiablePerson() { + final List personList = requireNonNull(parsedEpisode.getPodcastindex().getPersons()); + assertThrows(UnsupportedOperationException.class, () -> personList.add(anEpisodePodcastindexPerson())); + } + @Test @DisplayName("should parse to an unmodifiable list of Episode Atom authors") void shouldParseEpisodetAtomUnmodifiableAuthors() { diff --git a/src/test/java/dev/stalla/model/podcastindex/EpisodePodcastindexInteropTest.java b/src/test/java/dev/stalla/model/podcastindex/EpisodePodcastindexInteropTest.java index 87191c3c..a6a32c1a 100644 --- a/src/test/java/dev/stalla/model/podcastindex/EpisodePodcastindexInteropTest.java +++ b/src/test/java/dev/stalla/model/podcastindex/EpisodePodcastindexInteropTest.java @@ -5,6 +5,7 @@ import java.util.List; +import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindexPerson; import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindex; import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindexSoundbite; import static dev.stalla.model.EpisodeFixturesKt.anEpisodePodcastindexTranscript; @@ -39,4 +40,11 @@ void shouldBuildUnmodifiableEpisodePodcastindexTranscripts() { assertThrows(UnsupportedOperationException.class, () -> transcripts.add(anEpisodePodcastindexTranscript())); } + @Test + @DisplayName("should build an unmodifiable list of Episode Podcastindex persons") + void shouldParseEpisodePodcastindexUnmodifiablePerson() { + final List personList = requireNonNull(episodePodcastindex.getPersons()); + assertThrows(UnsupportedOperationException.class, () -> personList.add(anEpisodePodcastindexPerson())); + } + } diff --git a/src/test/java/dev/stalla/model/podcastindex/PodcastPodcastindexInteropTest.java b/src/test/java/dev/stalla/model/podcastindex/PodcastPodcastindexInteropTest.java index 767bf675..b355fbcb 100644 --- a/src/test/java/dev/stalla/model/podcastindex/PodcastPodcastindexInteropTest.java +++ b/src/test/java/dev/stalla/model/podcastindex/PodcastPodcastindexInteropTest.java @@ -7,6 +7,7 @@ import static dev.stalla.model.PodcastFixturesKt.aPodcastPodcastindex; import static dev.stalla.model.PodcastFixturesKt.aPodcastPodcastindexFunding; +import static dev.stalla.model.PodcastFixturesKt.aPodcastPodcastindexPerson; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -30,4 +31,11 @@ void shouldBuildUnmodifiablePodcastPodcastindexFunding() { assertThrows(UnsupportedOperationException.class, () -> fundingList.add(aPodcastPodcastindexFunding())); } + @Test + @DisplayName("should build an unmodifiable list of Podcast Podcastindex persons") + void shouldParsePodcastPodcastindexUnmodifiablePerson() { + final List personList = requireNonNull(podcastPodcastindex.getPersons()); + assertThrows(UnsupportedOperationException.class, () -> personList.add(aPodcastPodcastindexPerson())); + } + } diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt index f29d31ba..547a2eaf 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt @@ -11,8 +11,8 @@ import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.model.aPodcastindexGeoLocation -import dev.stalla.model.aPodcastindexLocation import dev.stalla.model.aPodcastindexOpenStreetMapFeature +import dev.stalla.model.anEpisodePodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexLocation import org.junit.jupiter.api.Test @@ -77,7 +77,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { @Test internal fun `should populate an Podcastindex Location builder with all properties from an Podcastindex Location model`() { - val location = aPodcastindexLocation() + val location = anEpisodePodcastindexLocation() val locationBuilder = PodcastindexLocation.builder().applyFrom(location) assertAll { diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt index cae74428..c459393a 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt @@ -10,7 +10,7 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.PodcastindexPersonBuilder -import dev.stalla.model.aPodcastindexPerson +import dev.stalla.model.anEpisodePodcastindexPerson import dev.stalla.model.podcastindex.PodcastindexPerson import org.junit.jupiter.api.Test @@ -81,7 +81,7 @@ internal class ValidatingPodcastindexPersonBuilderTest { @Test internal fun `should populate an Podcastindex Person builder with all properties from an Podcastindex Person model`() { - val person = aPodcastindexPerson() + val person = anEpisodePodcastindexPerson() val personBuilder = PodcastindexPerson.builder().applyFrom(person) assertAll { diff --git a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt index c192573f..189f9b06 100644 --- a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt @@ -12,6 +12,8 @@ import dev.stalla.model.itunes.EpisodeItunes import dev.stalla.model.itunes.EpisodeType import dev.stalla.model.podcastindex.Chapters import dev.stalla.model.podcastindex.EpisodePodcastindex +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.PodcastindexEpisode import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexPerson @@ -139,9 +141,11 @@ internal fun anEpisodePodcastindex( transcripts: List = listOf(anEpisodePodcastindexTranscript()), soundbites: List = listOf(anEpisodePodcastindexSoundbite()), chapters: Chapters? = anEpisodePodcastindexChapters(), - persons: List = listOf(aPodcastindexPerson()), - location: PodcastindexLocation? = aPodcastindexLocation() -) = EpisodePodcastindex(transcripts, soundbites, chapters) + persons: List = listOf(anEpisodePodcastindexPerson()), + location: PodcastindexLocation? = anEpisodePodcastindexLocation(), + season: PodcastindexSeason? = anEpisodePodcastindexSeason(), + episode: PodcastindexEpisode? = anEpisodePodcastindexEpisode() +) = EpisodePodcastindex(transcripts, soundbites, chapters, persons, location, season, episode) @JvmOverloads internal fun anEpisodePodcastindexTranscript( @@ -164,6 +168,22 @@ internal fun anEpisodePodcastindexChapters( type: MediaType = MediaType.JSON_CHAPTERS ) = Chapters(url, type) +@JvmOverloads +internal fun anEpisodePodcastindexPerson( + name: String = "episode podcastindex person name", + role: String? = "episode podcastindex person role", + group: String? = "episode podcastindex person group", + img: String? = "episode podcastindex person img", + href: String? = "episode podcastindex person href" +) = PodcastindexPerson(name, role, group, img, href) + +@JvmOverloads +internal fun anEpisodePodcastindexLocation( + name: String = "episode podcastindex location name", + geo: GeoLocation? = aPodcastindexGeoLocation(), + osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() +) = PodcastindexLocation(name, geo, osm) + @JvmOverloads internal fun anEpisodePodcastindexSeason( number: Int = 1, diff --git a/src/test/kotlin/dev/stalla/model/Fixtures.kt b/src/test/kotlin/dev/stalla/model/Fixtures.kt index 83a861a2..fea8a25a 100644 --- a/src/test/kotlin/dev/stalla/model/Fixtures.kt +++ b/src/test/kotlin/dev/stalla/model/Fixtures.kt @@ -84,22 +84,6 @@ internal fun aPodcastindexOpenStreetMapFeature( revision: BigInteger? = BigInteger.TWO ) = OpenStreetMapFeature(type, id, revision) -@JvmOverloads -internal fun aPodcastindexPerson( - name: String = "podcastindex person name", - role: String? = "podcastindex person role", - group: String? = "podcastindex person group", - img: String? = "podcastindex person img", - href: String? = "podcastindex person href" -) = PodcastindexPerson(name, role, group, img, href) - -@JvmOverloads -internal fun aPodcastindexLocation( - name: String = "podcastindex location name", - geo: GeoLocation? = aPodcastindexGeoLocation(), - osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() -) = PodcastindexLocation(name, geo, osm) - internal val simpleCategoryNames = listOf( "Arts", "Business", diff --git a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt index 4ab8fd41..21aa31dd 100644 --- a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt @@ -13,7 +13,9 @@ import dev.stalla.model.itunes.ItunesOwner import dev.stalla.model.itunes.PodcastItunes import dev.stalla.model.itunes.ShowType import dev.stalla.model.podcastindex.Funding +import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.Locked +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.PodcastPodcastindex import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexPerson @@ -130,9 +132,9 @@ internal fun aPodcastGoogleplay( internal fun aPodcastPodcastindex( locked: Locked? = aPodcastPodcastindexLocked(), funding: List = listOf(aPodcastPodcastindexFunding()), - persons: List = listOf(aPodcastindexPerson()), - location: PodcastindexLocation? = aPodcastindexLocation() -) = PodcastPodcastindex(locked, funding) + persons: List = listOf(aPodcastPodcastindexPerson()), + location: PodcastindexLocation? = aPodcastPodcastindexLocation() +) = PodcastPodcastindex(locked, funding, persons, location) @JvmOverloads internal fun aPodcastPodcastindexLocked( @@ -145,3 +147,19 @@ internal fun aPodcastPodcastindexFunding( url: String = "podcast podcastindex funding url", message: String = "podcast podcastindex funding message" ) = Funding(url, message) + +@JvmOverloads +internal fun aPodcastPodcastindexPerson( + name: String = "podcast podcastindex person name", + role: String? = "podcast podcastindex person role", + group: String? = "podcast podcastindex person group", + img: String? = "podcast podcastindex person img", + href: String? = "podcast podcastindex person href" +) = PodcastindexPerson(name, role, group, img, href) + +@JvmOverloads +internal fun aPodcastPodcastindexLocation( + name: String = "podcast podcastindex location name", + geo: GeoLocation? = aPodcastindexGeoLocation(), + osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() +) = PodcastindexLocation(name, geo, osm) diff --git a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt index 6b4d061f..fdb9f877 100644 --- a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt +++ b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt @@ -223,4 +223,28 @@ internal class PodcastindexParserTest : NamespaceParserTest() { prop(FakeEpisodePodcastindexBuilder::transcriptBuilders).isEmpty() } } + + @Test + internal fun `should not extract Podcastindex Season tags from the item when the types are invalid`() { + val item: Node = XmlRes("/xml/item-invalid.xml").rootNodeByName("item") + + val builder = FakeEpisodeBuilder() + item.parseItemChildNodes(builder) + + assertThat(builder.podcastindexBuilder, "item.podcastindex").all { + prop(FakeEpisodePodcastindexBuilder::seasonBuilder).isNull() + } + } + + @Test + internal fun `should not extract Podcastindex Epsiode tags from the item when the types are invalid`() { + val item: Node = XmlRes("/xml/item-invalid.xml").rootNodeByName("item") + + val builder = FakeEpisodeBuilder() + item.parseItemChildNodes(builder) + + assertThat(builder.podcastindexBuilder, "item.podcastindex").all { + prop(FakeEpisodePodcastindexBuilder::episodeBuilder).isNull() + } + } } diff --git a/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt b/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt index eac36541..bd3db32a 100644 --- a/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt +++ b/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt @@ -7,12 +7,16 @@ import dev.stalla.model.StyledDuration import dev.stalla.model.aPodcast import dev.stalla.model.aPodcastPodcastindex import dev.stalla.model.aPodcastPodcastindexFunding +import dev.stalla.model.aPodcastPodcastindexLocation import dev.stalla.model.aPodcastPodcastindexLocked +import dev.stalla.model.aPodcastPodcastindexPerson import dev.stalla.model.anEpisode import dev.stalla.model.anEpisodePodcastindex import dev.stalla.model.anEpisodePodcastindexChapters import dev.stalla.model.anEpisodePodcastindexSoundbite import dev.stalla.model.anEpisodePodcastindexTranscript +import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.OpenStreetMapFeature import org.junit.jupiter.api.Test internal class PodcastindexWriterTest : NamespaceWriterTest() { @@ -30,6 +34,14 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val diff = element.diffFromExpected("/rss/channel/podcast:funding") assertThat(diff).hasNoDifferences() } + writePodcastData("person") { element -> + val diff = element.diffFromExpected("/rss/channel/podcast:person") + assertThat(diff).hasNoDifferences() + } + writePodcastData("location") { element -> + val diff = element.diffFromExpected("/rss/channel/podcast:location") + assertThat(diff).hasNoDifferences() + } } } @@ -39,6 +51,8 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { assertAll { assertTagIsNotWrittenToPodcast(podcast, "locked") assertTagIsNotWrittenToPodcast(podcast, "funding") + assertTagIsNotWrittenToPodcast(podcast, "person") + assertTagIsNotWrittenToPodcast(podcast, "location") } } @@ -47,12 +61,16 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val podcast = aPodcast( podcastindex = aPodcastPodcastindex( locked = aPodcastPodcastindexLocked(" "), - funding = listOf(aPodcastPodcastindexFunding(" ", " ")) + funding = listOf(aPodcastPodcastindexFunding(" ", " ")), + persons = listOf(aPodcastPodcastindexPerson(" ", " ", " ", " ", " ")), + location = aPodcastPodcastindexLocation(" ") ) ) assertAll { assertTagIsNotWrittenToPodcast(podcast, "locked") assertTagIsNotWrittenToPodcast(podcast, "funding") + assertTagIsNotWrittenToPodcast(podcast, "person") + assertTagIsNotWrittenToPodcast(podcast, "location") } } @@ -61,12 +79,16 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val podcast = aPodcast( podcastindex = aPodcastPodcastindex( locked = aPodcastPodcastindexLocked(""), - funding = listOf(aPodcastPodcastindexFunding("", "")) + funding = listOf(aPodcastPodcastindexFunding("", "")), + persons = listOf(aPodcastPodcastindexPerson("", "", "", "", "")), + location = aPodcastPodcastindexLocation("") ) ) assertAll { assertTagIsNotWrittenToPodcast(podcast, "locked") assertTagIsNotWrittenToPodcast(podcast, "funding") + assertTagIsNotWrittenToPodcast(podcast, "person") + assertTagIsNotWrittenToPodcast(podcast, "location") } } @@ -85,6 +107,22 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val diff = element.diffFromExpected("/rss/channel/item[1]/podcast:chapters") assertThat(diff).hasNoDifferences() } + writeEpisodeData("person") { element -> + val diff = element.diffFromExpected("/rss/channel/item[1]/podcast:person") + assertThat(diff).hasNoDifferences() + } + writeEpisodeData("location") { element -> + val diff = element.diffFromExpected("/rss/channel/item[1]/podcast:location") + assertThat(diff).hasNoDifferences() + } + writeEpisodeData("season") { element -> + val diff = element.diffFromExpected("/rss/channel/item[1]/podcast:season") + assertThat(diff).hasNoDifferences() + } + writeEpisodeData("episode") { element -> + val diff = element.diffFromExpected("/rss/channel/item[1]/podcast:episode") + assertThat(diff).hasNoDifferences() + } } } @@ -95,6 +133,10 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { assertTagIsNotWrittenToEpisode(episode, "transcript") assertTagIsNotWrittenToEpisode(episode, "soundbite") assertTagIsNotWrittenToEpisode(episode, "chapters") + assertTagIsNotWrittenToEpisode(episode, "person") + assertTagIsNotWrittenToEpisode(episode, "location") + assertTagIsNotWrittenToEpisode(episode, "season") + assertTagIsNotWrittenToEpisode(episode, "episode") } } @@ -103,12 +145,16 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val episode = anEpisode( podcastindex = anEpisodePodcastindex( transcripts = listOf(anEpisodePodcastindexTranscript(" ")), - chapters = anEpisodePodcastindexChapters(" ") + chapters = anEpisodePodcastindexChapters(" "), + persons = listOf(aPodcastPodcastindexPerson(" ", " ", " ", " ", " ")), + location = aPodcastPodcastindexLocation(" ") ) ) assertAll { assertTagIsNotWrittenToEpisode(episode, "transcript") assertTagIsNotWrittenToEpisode(episode, "chapters") + assertTagIsNotWrittenToEpisode(episode, "person") + assertTagIsNotWrittenToEpisode(episode, "location") } } @@ -117,12 +163,16 @@ internal class PodcastindexWriterTest : NamespaceWriterTest() { val episode = anEpisode( podcastindex = anEpisodePodcastindex( transcripts = listOf(anEpisodePodcastindexTranscript("")), - chapters = anEpisodePodcastindexChapters("") + chapters = anEpisodePodcastindexChapters(""), + persons = listOf(aPodcastPodcastindexPerson("", "", "", "", "")), + location = aPodcastPodcastindexLocation("") ) ) assertAll { assertTagIsNotWrittenToEpisode(episode, "transcript") assertTagIsNotWrittenToEpisode(episode, "chapters") + assertTagIsNotWrittenToEpisode(episode, "person") + assertTagIsNotWrittenToEpisode(episode, "location") } } diff --git a/src/test/resources/xml/item-invalid.xml b/src/test/resources/xml/item-invalid.xml index 63324b06..39dbc003 100644 --- a/src/test/resources/xml/item-invalid.xml +++ b/src/test/resources/xml/item-invalid.xml @@ -19,4 +19,6 @@ I'm a soundbite I'm a soundbite + Not an integer + Not a decimal diff --git a/src/test/resources/xml/rss-full.xml b/src/test/resources/xml/rss-full.xml index 7b61ab9f..bb5f68fb 100644 --- a/src/test/resources/xml/rss-full.xml +++ b/src/test/resources/xml/rss-full.xml @@ -78,6 +78,13 @@ feedpress link yes podcast podcastindex funding message + + podcast podcastindex person name + + podcast podcastindex location name episode podcastindex soundbite title + + episode podcastindex person name + + episode podcastindex location name + 1 + 1.0 diff --git a/src/test/resources/xml/writer-fixtures.xml b/src/test/resources/xml/writer-fixtures.xml index 860921d5..c07ae747 100644 --- a/src/test/resources/xml/writer-fixtures.xml +++ b/src/test/resources/xml/writer-fixtures.xml @@ -76,6 +76,13 @@ podcast googleplay newFeedUrl yes podcast podcastindex funding message + + podcast podcastindex person name + + podcast podcastindex location name episode title episode link @@ -123,6 +130,15 @@ episode podcastindex soundbite title + + episode podcastindex person name + + episode podcastindex location name + 1 + 1.0 From 399cb5a97739b25b2fb6aea43706430407ece908 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Tue, 27 Apr 2021 22:01:00 +0200 Subject: [PATCH 12/18] Sort out detekt results, add comments --- build.gradle.kts | 1 + .../dev/stalla/builder/GeoLocationBuilder.kt | 17 ++++- .../builder/OpenStreetMapFeatureBuilder.kt | 7 ++ .../builder/PodcastindexLocationBuilder.kt | 8 ++ .../builder/PodcastindexPersonBuilder.kt | 10 +++ .../EpisodePodcastindexEpisodeBuilder.kt | 7 ++ .../EpisodePodcastindexSeasonBuilder.kt | 7 ++ .../episode/ProvidingEpisodeBuilder.kt | 1 - .../podcast/PodcastPodcastindexBuilder.kt | 2 - .../podcast/ProvidingPodcastBuilder.kt | 1 - .../ValidatingGeoLocationBuilder.kt | 24 +++--- .../stalla/model/podcastindex/GeoLocation.kt | 73 +++++++++++++------ .../model/podcastindex/PodcastindexPerson.kt | 5 +- .../kotlin/dev/stalla/parser/GeoUriParser.kt | 20 +++-- .../dev/stalla/parser/OsmFeatureParser.kt | 5 +- .../parser/namespace/PodcastindexParser.kt | 4 +- .../dev/stalla/util/CollectionsExtensions.kt | 2 - .../writer/namespace/PodcastindexWriter.kt | 1 + .../dev/stalla/model/GeoLocationTest.kt | 4 +- 19 files changed, 135 insertions(+), 64 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 623df204..546e9e5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -174,6 +174,7 @@ tasks { kotlinOptions { jvmTarget = "1.8" freeCompilerArgs = listOf( + "-Xopt-in=kotlin.ExperimentalStdlibApi", "-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlin.contracts.ExperimentalContracts", "-Xopt-in=dev.stalla.util.InternalAPI", diff --git a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt index a06e417d..71205d02 100644 --- a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt @@ -10,32 +10,45 @@ import dev.stalla.util.whenNotNull */ public interface GeoLocationBuilder : Builder { + /** Set the coord value. */ public fun coordA(coordA: Double): GeoLocationBuilder + /** Set the coord value. */ public fun coordB(coordB: Double): GeoLocationBuilder + /** Set the coord value. */ public fun coordC(coordC: Double?): GeoLocationBuilder + /** Set the crs value. */ public fun crs(crs: String?): GeoLocationBuilder + /** Set the uncertainty value. */ public fun uncertainty(uncertainty: Double?): GeoLocationBuilder - public fun addParameter(name: String, value: String): GeoLocationBuilder + /** Adds a [GeoLocation.Parameter] based on [key] and [value] to the list of parameters. */ + public fun addParameter(key: String, value: String): GeoLocationBuilder + /** Adds a [GeoLocation.Parameter] to the list of parameters. */ public fun addParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = apply { addParameter(parameter.key, parameter.value) } + /** Adds all [GeoLocation.Parameter] to the list of parameters. */ public fun addAllParameters(parameters: List): GeoLocationBuilder = apply { parameters.forEach(::addParameter) } - public fun removeParameter(name: String): GeoLocationBuilder + /** Removes the parameter with [key] from the list of parameters. */ + public fun removeParameter(key: String): GeoLocationBuilder + /** Removes [parameter] from the list of parameters. */ public fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder + /** Returns `true` if the coordA property is set. */ public fun hasCoordA(): Boolean + /** Returns `true` if the coordB property is set. */ public fun hasCoordB(): Boolean + /** Returns `true` if the coordC property is set. */ public fun hasCoordC(): Boolean override fun applyFrom(prototype: GeoLocation?): GeoLocationBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt index 430abf2a..8b77a50c 100644 --- a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt @@ -13,18 +13,25 @@ import java.math.BigInteger */ public interface OpenStreetMapFeatureBuilder : Builder { + /** Set the type value. */ public fun type(type: OpenStreetMapElementType): OpenStreetMapFeatureBuilder + /** Set the id value. */ public fun id(id: BigInteger): OpenStreetMapFeatureBuilder + /** Set the id value. */ public fun id(id: Int): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } + /** Set the id value. */ public fun id(id: Long): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } + /** Set the revision value. */ public fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder + /** Set the revision value. */ public fun revision(revision: Int?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } + /** Set the revision value. */ public fun revision(revision: Long?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } override fun applyFrom(prototype: OpenStreetMapFeature?): OpenStreetMapFeatureBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt index 72cb436d..8f7a5721 100644 --- a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt @@ -5,12 +5,20 @@ import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.util.whenNotNull +/** + * Builder for constructing [PodcastindexLocation] instances. + * + * @since 1.1.0 + */ public interface PodcastindexLocationBuilder : Builder { + /** Set the name value. */ public fun name(name: String): PodcastindexLocationBuilder + /** Set the geo value. */ public fun geo(geo: GeoLocation?): PodcastindexLocationBuilder + /** Set the osm value. */ public fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder override fun applyFrom(prototype: PodcastindexLocation?): PodcastindexLocationBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt index 54c80c1d..e80a958a 100644 --- a/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexPersonBuilder.kt @@ -3,16 +3,26 @@ package dev.stalla.builder import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.util.whenNotNull +/** + * Builder for constructing [PodcastindexPerson] instances. + * + * @since 1.1.0 + */ public interface PodcastindexPersonBuilder : Builder { + /** Set the name value. */ public fun name(name: String): PodcastindexPersonBuilder + /** Set the role value. */ public fun role(role: String?): PodcastindexPersonBuilder + /** Set the group value. */ public fun group(group: String?): PodcastindexPersonBuilder + /** Set the img value. */ public fun img(img: String?): PodcastindexPersonBuilder + /** Set the href value. */ public fun href(href: String?): PodcastindexPersonBuilder override fun applyFrom(prototype: PodcastindexPerson?): PodcastindexPersonBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt index f7977895..f53a27a3 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder.kt @@ -4,10 +4,17 @@ import dev.stalla.builder.Builder import dev.stalla.model.podcastindex.PodcastindexEpisode import dev.stalla.util.whenNotNull +/** + * Builder for constructing [PodcastindexEpisode] instances. + * + * @since 1.1.0 + */ public interface EpisodePodcastindexEpisodeBuilder : Builder { + /** Set the number value. */ public fun number(number: Double): EpisodePodcastindexEpisodeBuilder + /** Set the display value. */ public fun display(display: String?): EpisodePodcastindexEpisodeBuilder override fun applyFrom(prototype: PodcastindexEpisode?): EpisodePodcastindexEpisodeBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt index 97883d3e..b8407c85 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder.kt @@ -4,10 +4,17 @@ import dev.stalla.builder.Builder import dev.stalla.model.podcastindex.PodcastindexSeason import dev.stalla.util.whenNotNull +/** + * Builder for constructing [PodcastindexSeason] instances. + * + * @since 1.1.0 + */ public interface EpisodePodcastindexSeasonBuilder : Builder { + /** Set the number value. */ public fun number(number: Int): EpisodePodcastindexSeasonBuilder + /** Set the name value. */ public fun name(name: String?): EpisodePodcastindexSeasonBuilder override fun applyFrom(prototype: PodcastindexSeason?): EpisodePodcastindexSeasonBuilder = diff --git a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt index ea4531f3..baaea154 100644 --- a/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/episode/ProvidingEpisodeBuilder.kt @@ -1,7 +1,6 @@ package dev.stalla.builder.episode import dev.stalla.builder.AtomPersonBuilderProvider -import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider import dev.stalla.builder.PodcastindexLocationBuilder diff --git a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt index 761db57f..018d9a7f 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/PodcastPodcastindexBuilder.kt @@ -1,10 +1,8 @@ package dev.stalla.builder.podcast import dev.stalla.builder.Builder -import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.PodcastindexPersonBuilder -import dev.stalla.model.podcastindex.GeoLocation import dev.stalla.model.podcastindex.Locked import dev.stalla.model.podcastindex.PodcastPodcastindex import dev.stalla.model.podcastindex.PodcastindexLocation diff --git a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt index 08d30ee0..7cf95a45 100644 --- a/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/podcast/ProvidingPodcastBuilder.kt @@ -1,7 +1,6 @@ package dev.stalla.builder.podcast import dev.stalla.builder.AtomPersonBuilderProvider -import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.HrefOnlyImageBuilder import dev.stalla.builder.LinkBuilderProvider import dev.stalla.builder.PodcastindexLocationBuilder diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt index 76c6c1bb..6089001c 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -2,10 +2,12 @@ package dev.stalla.builder.validating import dev.stalla.builder.GeoLocationBuilder import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.GeoLocation.Factory.PARAM_CRS -import dev.stalla.model.podcastindex.GeoLocation.Factory.PARAM_UNCERTAINTY +import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_PARAM +import dev.stalla.model.podcastindex.GeoLocation.Factory.UNCERTAINTY_PARAM +import dev.stalla.util.InternalAPI -public class ValidatingGeoLocationBuilder : GeoLocationBuilder { +@InternalAPI +internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { private var coordA: Double? = null private var coordB: Double? = null @@ -24,12 +26,12 @@ public class ValidatingGeoLocationBuilder : GeoLocationBuilder { override fun uncertainty(uncertainty: Double?): GeoLocationBuilder = apply { this.uncertainty = uncertainty } - override fun addParameter(name: String, value: String): GeoLocationBuilder = apply { - if (PARAM_CRS.equals(name, ignoreCase = true)) { + override fun addParameter(key: String, value: String): GeoLocationBuilder = apply { + if (CRS_PARAM.equals(key, ignoreCase = true)) { crs(value) return@apply } - if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { + if (UNCERTAINTY_PARAM.equals(key, ignoreCase = true)) { try { uncertainty(value.toDouble()) return@apply @@ -37,17 +39,17 @@ public class ValidatingGeoLocationBuilder : GeoLocationBuilder { // if it can't be parsed, then treat it as an ordinary parameter } } - parameters[name] = value + parameters[key] = value } - override fun removeParameter(name: String): GeoLocationBuilder = apply { - if (PARAM_CRS.equals(name, ignoreCase = true)) { + override fun removeParameter(key: String): GeoLocationBuilder = apply { + if (CRS_PARAM.equals(key, ignoreCase = true)) { crs(null) } - if (PARAM_UNCERTAINTY.equals(name, ignoreCase = true)) { + if (UNCERTAINTY_PARAM.equals(key, ignoreCase = true)) { uncertainty(null) } - parameters.remove(name) + parameters.remove(key) } override fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index e9cad2f6..53106895 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -6,7 +6,6 @@ import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_WGS84 import dev.stalla.parser.GeoUriParser -import dev.stalla.util.InternalAPI import dev.stalla.util.asUnmodifiable import dev.stalla.util.containsMediaTypeSeparatorSymbol import java.util.Locale @@ -14,13 +13,17 @@ import kotlin.contracts.contract import kotlin.math.absoluteValue /** - * TODO. + * Represents a Geographic Location ('geo' URI) as defined by [RFC 5870](https://tools.ietf.org/html/rfc5870). * - * @property coordA Latitude. - * @property coordB Longitude. - * @property coordC Altitude. + * Note that this class only ensures syntactically valid geo URI constructs, + * but does not ensure that the coordinate values are valid within the coordinate + * reference system ([crs]). + * + * @property coordA The latitude value. + * @property coordB The longitude value. + * @property coordC The altitude value. * @property crs The coordinate reference system - defaults to ([CRS_WGS84]) if not set. - * @property uncertainty TODO. + * @property uncertainty The amount of uncertainty in the location as a value in meters. * @property parameters TODO. * * @see [RFC 5870](https://tools.ietf.org/html/rfc5870) @@ -52,10 +55,10 @@ public class GeoLocation public constructor( ) /** - * TODO. + * Represents a single value parameter. * - * @property key TODO. - * @property value TODO. + * @property key The key of parameter. + * @property value The value of parameter. */ public data class Parameter(val key: String, val value: String) { override fun equals(other: Any?): Boolean { @@ -98,12 +101,24 @@ public class GeoLocation public constructor( if (pattern == null) return false if (this == pattern) return true - if (coordA.absoluteValue == 90.0 && matchCrs(crs, pattern.crs)) { - // Special "poles" rule for WGS-84 applies - longitude is to be ignored - return coordA == pattern.coordA && - coordC == pattern.coordC && - uncertainty == pattern.uncertainty && - match(parameters, pattern.parameters) + + if (crs.isWGS84() && pattern.crs.isWGS84()) { + if (coordA.isPoleWGS84() || pattern.coordA.isPoleWGS84()) { + // Special "poles" rule for WGS-84 applies - longitude is to be ignored + return coordA == pattern.coordA && + coordC == pattern.coordC && + uncertainty == pattern.uncertainty && + match(parameters, pattern.parameters) + } + + if (coordB.isDateLineWGS84() && pattern.coordB.isDateLineWGS84()) { + // Special "date line" rule for WGS-84 applies - longitude 180 degrees == -180 degrees + return coordA == pattern.coordA && + coordC == pattern.coordC && + crs.equals(pattern.crs, ignoreCase = true) && + uncertainty == pattern.uncertainty && + match(parameters, pattern.parameters) + } } return coordA == pattern.coordA && @@ -148,11 +163,25 @@ public class GeoLocation public constructor( } } + private fun String?.isWGS84(): Boolean = this == null || this.equals(CRS_WGS84, ignoreCase = true) + + private fun Double.isPoleWGS84(): Boolean = this.absoluteValue == MAX_LATITUDE + + private fun Double.isDateLineWGS84(): Boolean = this.absoluteValue == MAX_LONGITUDE + + private fun Double.equalDateLineWGS84(other: Double): Boolean { + return if (this.isDateLineWGS84() && other.isDateLineWGS84()) { + true + } else { + this == other + } + } + override fun toString(): String = StringBuilder().apply { append("geo:$coordA,$coordB") if (coordC != null) append(",$coordC") - if (crs != null) append(";$PARAM_CRS=$crs") - if (uncertainty != null) append(";$PARAM_UNCERTAINTY=$uncertainty") + if (crs != null) append(";$CRS_PARAM=$crs") + if (uncertainty != null) append(";$UNCERTAINTY_PARAM=$uncertainty") for ((key, value) in parameters) append(";$key=$value") }.toString() @@ -182,16 +211,16 @@ public class GeoLocation public constructor( return result } - /** - * TODO. - */ + /** Provides a builder and a factory for the [GeoLocation] class. */ public companion object Factory : BuilderFactory, TypeFactory { /** The World Geodetic System 1984 coordinate reference system used by GPS. */ public const val CRS_WGS84: String = "WGS84" - internal const val PARAM_CRS = "crs" - internal const val PARAM_UNCERTAINTY = "u" + internal const val CRS_PARAM = "crs" + internal const val UNCERTAINTY_PARAM = "u" + internal const val MAX_LATITUDE = 90.0 + internal const val MAX_LONGITUDE = 180.0 /** Returns a builder implementation for building [GeoLocation] model instances. */ @JvmStatic diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt index 0b901bae..cb707c35 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt @@ -1,16 +1,15 @@ package dev.stalla.model.podcastindex import dev.stalla.builder.PodcastindexPersonBuilder -import dev.stalla.builder.episode.EpisodePodcastindexSeasonBuilder import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder -import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeasonBuilder import dev.stalla.model.BuilderFactory /** * TODO. * * @property name The full name or alias of the person. - * @property role The role the person serves on the show or episode - this should be a reference to an official role within the [Podcast Taxonomy Project](https://podcasttaxonomy.com) list. + * @property role The role the person serves on the show or episode - this should be a reference to an + * official role within the [Podcast Taxonomy Project](https://podcasttaxonomy.com) list. * @property group Should be an official group within the [Podcast Taxonomy Project](https://podcasttaxonomy.com) list. * @property img The url of a picture or avatar of the person. * @property href The url to a relevant resource of information about the person, such as a homepage or third-party profile platform. diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt index 980acac4..6995dc7f 100644 --- a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt @@ -77,33 +77,31 @@ internal object GeoUriParser { } private fun GeoLocationBuilder.handleEndOfCoordinate(buffer: StringBuilder) { - val s: String = buffer.getAndClear() + val symbol: String = buffer.getAndClear() + val coordinate = symbol.asDoubleOrNull() ?: return if (!hasCoordA()) { - val a = s.asDoubleOrNull() ?: return - coordA(a) + coordA(coordinate) return } if (!hasCoordB()) { - val b = s.asDoubleOrNull() ?: return - coordB(b) + coordB(coordinate) return } if (!hasCoordC()) { - val c = s.asDoubleOrNull() ?: return - coordC(c) + coordC(coordinate) return } } private fun GeoLocationBuilder.handleEndOfParameter(buffer: StringBuilder, paramName: String?) { - val s = buffer.getAndClear() + val symbol = buffer.getAndClear() if (paramName == null) { - if (s.isNotEmpty()) { - addParameterDecodeValue(s, "") + if (symbol.isNotEmpty()) { + addParameterDecodeValue(symbol, "") } return } - addParameterDecodeValue(paramName, s) + addParameterDecodeValue(paramName, symbol) } private fun GeoLocationBuilder.addParameterDecodeValue(name: String, value: String?) { diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt index 39ce5daa..8d87068f 100644 --- a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt @@ -1,10 +1,8 @@ package dev.stalla.parser -import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.model.podcastindex.OpenStreetMapElementType +import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.util.InternalAPI -import dev.stalla.util.asBigIntegerOrNull -import java.lang.StringBuilder import java.math.BigInteger import kotlin.contracts.contract @@ -51,7 +49,6 @@ internal object OsmFeatureParser { return builder.build() } - @OptIn(ExperimentalStdlibApi::class) private fun Char.isNoDigit(): Boolean = digitToIntOrNull() == null private fun String?.asBigIntegerOrNull(): BigInteger? { diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index 5b01bb60..207d40b6 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -1,6 +1,5 @@ package dev.stalla.parser.namespace -import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.episode.EpisodePodcastindexChaptersBuilder @@ -20,8 +19,6 @@ import dev.stalla.dom.parseAsMediaTypeOrNull import dev.stalla.dom.textAsBooleanOrNull import dev.stalla.dom.textOrNull import dev.stalla.model.StyledDuration -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.parser.GeoUriParser import dev.stalla.parser.NamespaceParser @@ -73,6 +70,7 @@ internal object PodcastindexParser : NamespaceParser() { } } + @Suppress("ComplexMethod") override fun Node.parseItemData(builder: ProvidingEpisodeBuilder) { when (localName) { "chapters" -> { diff --git a/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt b/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt index 3b06c363..fea0ad7e 100644 --- a/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt +++ b/src/main/kotlin/dev/stalla/util/CollectionsExtensions.kt @@ -1,6 +1,5 @@ package dev.stalla.util -import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.builder.PodcastindexPersonBuilder import dev.stalla.builder.RssCategoryBuilder import dev.stalla.builder.episode.EpisodeBuilder @@ -11,7 +10,6 @@ import dev.stalla.builder.episode.EpisodePodloveSimpleChapterBuilder import dev.stalla.builder.podcast.PodcastPodcastindexFundingBuilder import dev.stalla.model.Episode import dev.stalla.model.podcastindex.Funding -import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.Soundbite import dev.stalla.model.podcastindex.Transcript diff --git a/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt b/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt index 3e0c95ac..def46f37 100644 --- a/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt +++ b/src/main/kotlin/dev/stalla/writer/namespace/PodcastindexWriter.kt @@ -49,6 +49,7 @@ internal object PodcastindexWriter : NamespaceWriter() { appendPodcastindexLocationData(podcastindex.location) } + @Suppress("ComplexMethod") override fun Element.appendEpisodeData(episode: Episode) { val podcastindex = episode.podcastindex ?: return diff --git a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt index 626a0408..a806d5e7 100644 --- a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt +++ b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt @@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test class GeoLocationTest { @Test - fun `should parse a Geo URI with A and B correctly`() { + fun `should parse a Geo URI with latitude and longitude correctly`() { assertThat(GeoLocation.of("geo:37.786971,-122.399677")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(37.786971) prop(GeoLocation::coordB).isEqualTo(-122.399677) @@ -29,7 +29,7 @@ class GeoLocationTest { } @Test - fun `should parse a Geo URI with A and B and C correctly`() { + fun `should parse a Geo URI with latitude and longitude and altitude correctly`() { assertThat(GeoLocation.of("geo:40.714623,-74.006605,1.1")).isNotNull().all { prop(GeoLocation::coordA).isEqualTo(40.714623) prop(GeoLocation::coordB).isEqualTo(-74.006605) From 92ce63d3b83c85bf48e7db130bc6f0b386bbfd56 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Tue, 27 Apr 2021 23:07:31 +0200 Subject: [PATCH 13/18] More special RFC 5870 case handling --- .../dev/stalla/builder/GeoLocationBuilder.kt | 24 +-- .../ValidatingGeoLocationBuilder.kt | 26 ++-- .../stalla/model/podcastindex/GeoLocation.kt | 65 ++++----- .../kotlin/dev/stalla/parser/GeoUriParser.kt | 16 +- .../dev/stalla/model/GeoLocationTest.kt | 138 +++++++++++++----- 5 files changed, 167 insertions(+), 102 deletions(-) diff --git a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt index 71205d02..3d65ae15 100644 --- a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt @@ -10,14 +10,14 @@ import dev.stalla.util.whenNotNull */ public interface GeoLocationBuilder : Builder { - /** Set the coord value. */ - public fun coordA(coordA: Double): GeoLocationBuilder + /** Set the latitude value. */ + public fun latitude(latitude: Double): GeoLocationBuilder - /** Set the coord value. */ - public fun coordB(coordB: Double): GeoLocationBuilder + /** Set the longitude value. */ + public fun longitude(longitude: Double): GeoLocationBuilder - /** Set the coord value. */ - public fun coordC(coordC: Double?): GeoLocationBuilder + /** Set the altitude value. */ + public fun altitude(altitude: Double?): GeoLocationBuilder /** Set the crs value. */ public fun crs(crs: String?): GeoLocationBuilder @@ -43,19 +43,19 @@ public interface GeoLocationBuilder : Builder { public fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder /** Returns `true` if the coordA property is set. */ - public fun hasCoordA(): Boolean + public fun hasLatitude(): Boolean /** Returns `true` if the coordB property is set. */ - public fun hasCoordB(): Boolean + public fun hasLongitude(): Boolean /** Returns `true` if the coordC property is set. */ - public fun hasCoordC(): Boolean + public fun hasAltitude(): Boolean override fun applyFrom(prototype: GeoLocation?): GeoLocationBuilder = whenNotNull(prototype) { location -> - coordA(location.coordA) - coordB(location.coordB) - coordC(location.coordC) + latitude(location.latitude) + longitude(location.longitude) + altitude(location.altitude) crs(location.crs) uncertainty(location.uncertainty) addAllParameters(location.parameters) diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt index 6089001c..af37ce7f 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -9,18 +9,18 @@ import dev.stalla.util.InternalAPI @InternalAPI internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { - private var coordA: Double? = null - private var coordB: Double? = null - private var coordC: Double? = null + private var latitude: Double? = null + private var longitude: Double? = null + private var altitude: Double? = null private var crs: String? = null private var uncertainty: Double? = null private var parameters: MutableMap = mutableMapOf() - override fun coordA(coordA: Double): GeoLocationBuilder = apply { this.coordA = coordA } + override fun latitude(latitude: Double): GeoLocationBuilder = apply { this.latitude = latitude } - override fun coordB(coordB: Double): GeoLocationBuilder = apply { this.coordB = coordB } + override fun longitude(longitude: Double): GeoLocationBuilder = apply { this.longitude = longitude } - override fun coordC(coordC: Double?): GeoLocationBuilder = apply { this.coordC = coordC } + override fun altitude(altitude: Double?): GeoLocationBuilder = apply { this.altitude = altitude } override fun crs(crs: String?): GeoLocationBuilder = apply { this.crs = crs } @@ -55,22 +55,22 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { override fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = removeParameter(parameter.key) - override fun hasCoordA(): Boolean = coordA != null + override fun hasLatitude(): Boolean = latitude != null - override fun hasCoordB(): Boolean = coordB != null + override fun hasLongitude(): Boolean = longitude != null - override fun hasCoordC(): Boolean = coordC != null + override fun hasAltitude(): Boolean = altitude != null override val hasEnoughDataToBuild: Boolean - get() = coordA != null && coordB != null + get() = latitude != null && longitude != null override fun build(): GeoLocation? { if (!hasEnoughDataToBuild) return null return GeoLocation( - coordA = coordA ?: return null, - coordB = coordB ?: return null, - coordC = coordC, + latitude = latitude ?: return null, + longitude = longitude ?: return null, + altitude = altitude, crs = crs, uncertainty = uncertainty, parameters = parameters // this secondary constructor will apply .asUnmodifiable() diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt index 53106895..317f82de 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt @@ -17,11 +17,11 @@ import kotlin.math.absoluteValue * * Note that this class only ensures syntactically valid geo URI constructs, * but does not ensure that the coordinate values are valid within the coordinate - * reference system ([crs]). + * reference system ([crs]), besides the comparison rules that apply for WGS-84. * - * @property coordA The latitude value. - * @property coordB The longitude value. - * @property coordC The altitude value. + * @property latitude The latitude value. + * @property longitude The longitude value. + * @property altitude The altitude value. * @property crs The coordinate reference system - defaults to ([CRS_WGS84]) if not set. * @property uncertainty The amount of uncertainty in the location as a value in meters. * @property parameters TODO. @@ -30,25 +30,25 @@ import kotlin.math.absoluteValue * @since 1.1.0 */ public class GeoLocation public constructor( - public val coordA: Double, - public val coordB: Double, - public val coordC: Double? = null, + public val latitude: Double, + public val longitude: Double, + public val altitude: Double? = null, public val crs: String? = null, public val uncertainty: Double? = null, public val parameters: List = emptyList() ) { public constructor( - coordA: Double, - coordB: Double, - coordC: Double?, + latitude: Double, + longitude: Double, + altitude: Double?, crs: String?, uncertainty: Double?, parameters: Map ) : this( - coordA = coordA, - coordB = coordB, - coordC = coordC, + latitude = latitude, + longitude = longitude, + altitude = altitude, crs = crs, uncertainty = uncertainty, parameters = parameters.map { Parameter(it.key, it.value) }.asUnmodifiable() @@ -87,11 +87,11 @@ public class GeoLocation public constructor( if (value.isBlank()) return this if (hasParameter(key, value)) return this - return GeoLocation(coordA, coordB, coordC, crs, uncertainty, parameters + Parameter(key, value)) + return GeoLocation(latitude, longitude, altitude, crs, uncertainty, parameters + Parameter(key, value)) } /** Creates a copy of `this` type without any parameters.*/ - public fun withoutParameters(): GeoLocation = GeoLocation(coordA, coordB, coordC) + public fun withoutParameters(): GeoLocation = GeoLocation(latitude, longitude, altitude) /** Checks if `this` type matches a [pattern] type taking parameters into account. */ public fun match(pattern: GeoLocation?): Boolean { @@ -103,27 +103,26 @@ public class GeoLocation public constructor( if (this == pattern) return true if (crs.isWGS84() && pattern.crs.isWGS84()) { - if (coordA.isPoleWGS84() || pattern.coordA.isPoleWGS84()) { + if (latitude.isPoleWGS84() || pattern.latitude.isPoleWGS84()) { // Special "poles" rule for WGS-84 applies - longitude is to be ignored - return coordA == pattern.coordA && - coordC == pattern.coordC && + return latitude == pattern.latitude && + altitude == pattern.altitude && uncertainty == pattern.uncertainty && match(parameters, pattern.parameters) } - if (coordB.isDateLineWGS84() && pattern.coordB.isDateLineWGS84()) { + if (longitude.isDateLineWGS84() && pattern.longitude.isDateLineWGS84()) { // Special "date line" rule for WGS-84 applies - longitude 180 degrees == -180 degrees - return coordA == pattern.coordA && - coordC == pattern.coordC && - crs.equals(pattern.crs, ignoreCase = true) && + return latitude == pattern.latitude && + altitude == pattern.altitude && uncertainty == pattern.uncertainty && match(parameters, pattern.parameters) } } - return coordA == pattern.coordA && - coordB == pattern.coordB && - coordC == pattern.coordC && + return latitude == pattern.latitude && + longitude == pattern.longitude && + altitude == pattern.altitude && crs.equals(pattern.crs, ignoreCase = true) && uncertainty == pattern.uncertainty && match(parameters, pattern.parameters) @@ -178,8 +177,8 @@ public class GeoLocation public constructor( } override fun toString(): String = StringBuilder().apply { - append("geo:$coordA,$coordB") - if (coordC != null) append(",$coordC") + append("geo:$latitude,$longitude") + if (altitude != null) append(",$altitude") if (crs != null) append(";$CRS_PARAM=$crs") if (uncertainty != null) append(";$UNCERTAINTY_PARAM=$uncertainty") for ((key, value) in parameters) append(";$key=$value") @@ -191,9 +190,9 @@ public class GeoLocation public constructor( other as GeoLocation - if (coordA != other.coordA) return false - if (coordB != other.coordB) return false - if (coordC != other.coordC) return false + if (latitude != other.latitude) return false + if (longitude != other.longitude) return false + if (altitude != other.altitude) return false if (crs != other.crs) return false if (uncertainty != other.uncertainty) return false if (parameters != other.parameters) return false @@ -202,9 +201,9 @@ public class GeoLocation public constructor( } override fun hashCode(): Int { - var result = coordA.hashCode() - result = 31 * result + coordB.hashCode() - result = 31 * result + (coordC?.hashCode() ?: 0) + var result = latitude.hashCode() + result = 31 * result + longitude.hashCode() + result = 31 * result + (altitude?.hashCode() ?: 0) result = 31 * result + (crs?.hashCode() ?: 0) result = 31 * result + (uncertainty?.hashCode() ?: 0) result = 31 * result + parameters.hashCode() diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt index 6995dc7f..e9687a67 100644 --- a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt @@ -50,7 +50,7 @@ internal object GeoUriParser { paramName = null } else { builder.handleEndOfCoordinate(buffer) - if (!builder.hasCoordB()) return null + if (!builder.hasLongitude()) return null coordinatesDone = true } continue @@ -65,7 +65,7 @@ internal object GeoUriParser { builder.handleEndOfParameter(buffer, paramName) } else { builder.handleEndOfCoordinate(buffer) - if (!builder.hasCoordB()) return null + if (!builder.hasLongitude()) return null } return builder.build() } @@ -79,16 +79,16 @@ internal object GeoUriParser { private fun GeoLocationBuilder.handleEndOfCoordinate(buffer: StringBuilder) { val symbol: String = buffer.getAndClear() val coordinate = symbol.asDoubleOrNull() ?: return - if (!hasCoordA()) { - coordA(coordinate) + if (!hasLatitude()) { + latitude(coordinate) return } - if (!hasCoordB()) { - coordB(coordinate) + if (!hasLongitude()) { + longitude(coordinate) return } - if (!hasCoordC()) { - coordC(coordinate) + if (!hasAltitude()) { + altitude(coordinate) return } } diff --git a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt index a806d5e7..e9c028f2 100644 --- a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt +++ b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt @@ -19,9 +19,9 @@ class GeoLocationTest { @Test fun `should parse a Geo URI with latitude and longitude correctly`() { assertThat(GeoLocation.of("geo:37.786971,-122.399677")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(37.786971) - prop(GeoLocation::coordB).isEqualTo(-122.399677) - prop(GeoLocation::coordC).isNull() + prop(GeoLocation::latitude).isEqualTo(37.786971) + prop(GeoLocation::longitude).isEqualTo(-122.399677) + prop(GeoLocation::altitude).isNull() prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).isEmpty() @@ -31,9 +31,9 @@ class GeoLocationTest { @Test fun `should parse a Geo URI with latitude and longitude and altitude correctly`() { assertThat(GeoLocation.of("geo:40.714623,-74.006605,1.1")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(40.714623) - prop(GeoLocation::coordB).isEqualTo(-74.006605) - prop(GeoLocation::coordC).isEqualTo(1.1) + prop(GeoLocation::latitude).isEqualTo(40.714623) + prop(GeoLocation::longitude).isEqualTo(-74.006605) + prop(GeoLocation::altitude).isEqualTo(1.1) prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).isEmpty() @@ -43,9 +43,9 @@ class GeoLocationTest { @Test fun `test 2`() { assertThat(GeoLocation.of("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(48.198634) - prop(GeoLocation::coordB).isEqualTo(16.371648) - prop(GeoLocation::coordC).isNull() + prop(GeoLocation::latitude).isEqualTo(48.198634) + prop(GeoLocation::longitude).isEqualTo(16.371648) + prop(GeoLocation::altitude).isNull() prop(GeoLocation::crs).isEqualTo("wgs84") prop(GeoLocation::uncertainty).isEqualTo(40.0) prop(GeoLocation::parameters).isEmpty() @@ -55,9 +55,9 @@ class GeoLocationTest { @Test fun `parse all`() { assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isEqualTo("wgs84") prop(GeoLocation::uncertainty).isEqualTo(12.0) prop(GeoLocation::parameters).hasSize(1) @@ -68,9 +68,9 @@ class GeoLocationTest { @Test fun `parse no params`() { assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isEqualTo("wgs84") prop(GeoLocation::uncertainty).isEqualTo(12.0) prop(GeoLocation::parameters).isEmpty() @@ -80,9 +80,9 @@ class GeoLocationTest { @Test fun `parse no params or u`() { assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isEqualTo("wgs84") prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).isEmpty() @@ -92,9 +92,9 @@ class GeoLocationTest { @Test fun `parse no params or crs`() { assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isEqualTo(12.0) prop(GeoLocation::parameters).isEmpty() @@ -104,9 +104,9 @@ class GeoLocationTest { @Test fun `parse no params or u or crs`() { assertThat(GeoLocation.of("geo:12.34,56.78,-21.43")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).isEmpty() @@ -116,9 +116,9 @@ class GeoLocationTest { @Test fun `parse no params or u or crs or coordC`() { assertThat(GeoLocation.of("geo:12.34,56.78")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isNull() + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isNull() prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).isEmpty() @@ -128,9 +128,9 @@ class GeoLocationTest { @Test fun `parse invalid uncertainty`() { assertThat(GeoLocation.of("geo:12.34,56.78;u=invalid")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isNull() + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isNull() prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).hasSize(1) @@ -156,9 +156,9 @@ class GeoLocationTest { @Test fun `parse decode special chars in param value`() { assertThat(GeoLocation.of("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(56.78) - prop(GeoLocation::coordC).isNull() + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(56.78) + prop(GeoLocation::altitude).isNull() prop(GeoLocation::crs).isNull() prop(GeoLocation::uncertainty).isNull() prop(GeoLocation::parameters).hasSize(1) @@ -169,9 +169,9 @@ class GeoLocationTest { @Test fun `multiple params`() { assertThat(GeoLocation.of("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { - prop(GeoLocation::coordA).isEqualTo(12.34) - prop(GeoLocation::coordB).isEqualTo(45.67) - prop(GeoLocation::coordC).isEqualTo(-21.43) + prop(GeoLocation::latitude).isEqualTo(12.34) + prop(GeoLocation::longitude).isEqualTo(45.67) + prop(GeoLocation::altitude).isEqualTo(-21.43) prop(GeoLocation::crs).isEqualTo("theCrs") prop(GeoLocation::uncertainty).isEqualTo(12.0) prop(GeoLocation::parameters).hasSize(2) @@ -191,6 +191,72 @@ class GeoLocationTest { } } + @Test + fun `should no match two Geo URIs in WGS-84 special pole case if the latitude is different`() { + val geoLocation1 = GeoLocation.of("geo:90,10") + val geoLocation2 = GeoLocation.of("geo:45,20") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } + + @Test + fun `should no match two Geo URIs in WGS-84 special pole case if the latitude has a different sign`() { + val geoLocation1 = GeoLocation.of("geo:90,10") + val geoLocation2 = GeoLocation.of("geo:-90,10") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } + + @Test + fun `should match two Geo URIs in WGS-84 special pole case by ignoring the longitude`() { + val geoLocation1 = GeoLocation.of("geo:90,5") + val geoLocation2 = GeoLocation.of("geo:90,10") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special pole case`() { + val geoLocation1 = GeoLocation.of("geo:90,5") + val geoLocation2 = GeoLocation.of("geo:90,10;crs=WGS84") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should match two Geo URIs in WGS-84 special date line case if the longitude has a different sign`() { + val geoLocation1 = GeoLocation.of("geo:10,180") + val geoLocation2 = GeoLocation.of("geo:10,-180") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special date line case`() { + val geoLocation1 = GeoLocation.of("geo:10,180") + val geoLocation2 = GeoLocation.of("geo:10,-180;crs=WGS84") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + @Test fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { val geoLocation1 = GeoLocation.of("geo:66,30;u=6.500;FOo=this%2dthat") From 731ef51e99e7a28c9c8a082b18fdf4e5670ccda6 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Wed, 28 Apr 2021 20:07:42 +0200 Subject: [PATCH 14/18] Comments and refactoring --- .../dev/stalla/builder/GeoLocationBuilder.kt | 20 +- .../builder/OpenStreetMapElementBuilder.kt | 29 ++ .../builder/OpenStreetMapFeatureBuilder.kt | 44 --- .../builder/PodcastindexLocationBuilder.kt | 12 +- .../ValidatingGeoLocationBuilder.kt | 13 +- .../ValidatingOpenStreetMapElementBuilder.kt | 33 ++ .../ValidatingOpenStreetMapFeatureBuilder.kt | 35 -- .../ValidatingPodcastindexLocationBuilder.kt | 12 +- .../{GeoLocation.kt => GeographicLocation.kt} | 89 ++--- .../podcastindex/OpenStreetMapElement.kt | 46 +++ .../podcastindex/OpenStreetMapElementType.kt | 20 +- .../podcastindex/OpenStreetMapFeature.kt | 72 ----- .../model/podcastindex/PodcastindexEpisode.kt | 7 +- .../podcastindex/PodcastindexLocation.kt | 14 +- .../model/podcastindex/PodcastindexPerson.kt | 7 +- .../model/podcastindex/PodcastindexSeason.kt | 7 +- ...iParser.kt => GeographicLocationParser.kt} | 10 +- ...arser.kt => OpenStreetMapElementParser.kt} | 33 +- .../parser/namespace/PodcastindexParser.kt | 8 +- .../dev/stalla/util/NumberExtensions.kt | 24 -- src/test/kotlin/dev/stalla/Assertions.kt | 6 +- .../fake/FakePodcastindexLocationBuilder.kt | 12 +- .../episode/FakeEpisodePodcastindexBuilder.kt | 4 +- .../podcast/FakePodcastPodcastindexBuilder.kt | 3 +- ...lidatingPodcastindexLocationBuilderTest.kt | 6 +- ...ValidatingPodcastindexPersonBuilderTest.kt | 1 - ...alidatingEpisodePodcastindexBuilderTest.kt | 8 +- ...ngEpisodePodcastindexEpisodeBuilderTest.kt | 1 - ...ingEpisodePodcastindexSeasonBuilderTest.kt | 1 - ...alidatingPodcastPodcastindexBuilderTest.kt | 9 +- .../dev/stalla/model/EpisodeFixtures.kt | 8 +- src/test/kotlin/dev/stalla/model/Fixtures.kt | 21 +- .../dev/stalla/model/GeoLocationTest.kt | 304 ------------------ .../stalla/model/OpenStreetMapFeatureTest.kt | 78 ----- .../dev/stalla/model/PodcastFixtures.kt | 8 +- .../podcastindex/GeographicLocationTest.kt | 302 +++++++++++++++++ .../podcastindex/OpenStreetMapElementTest.kt | 74 +++++ .../namespace/PodcastindexParserTest.kt | 16 +- .../namespace/PodcastindexWriterTest.kt | 2 - 39 files changed, 671 insertions(+), 728 deletions(-) create mode 100644 src/main/kotlin/dev/stalla/builder/OpenStreetMapElementBuilder.kt delete mode 100644 src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt create mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilder.kt delete mode 100644 src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt rename src/main/kotlin/dev/stalla/model/podcastindex/{GeoLocation.kt => GeographicLocation.kt} (70%) create mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElement.kt delete mode 100644 src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt rename src/main/kotlin/dev/stalla/parser/{GeoUriParser.kt => GeographicLocationParser.kt} (92%) rename src/main/kotlin/dev/stalla/parser/{OsmFeatureParser.kt => OpenStreetMapElementParser.kt} (61%) delete mode 100644 src/main/kotlin/dev/stalla/util/NumberExtensions.kt delete mode 100644 src/test/kotlin/dev/stalla/model/GeoLocationTest.kt delete mode 100644 src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt create mode 100644 src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt create mode 100644 src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt diff --git a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt index 3d65ae15..362a8849 100644 --- a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt @@ -1,14 +1,14 @@ package dev.stalla.builder -import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.util.whenNotNull /** - * Builder for constructing [GeoLocation] instances. + * Builder for constructing [GeographicLocation] instances. * * @since 1.1.0 */ -public interface GeoLocationBuilder : Builder { +public interface GeoLocationBuilder : Builder { /** Set the latitude value. */ public fun latitude(latitude: Double): GeoLocationBuilder @@ -25,22 +25,22 @@ public interface GeoLocationBuilder : Builder { /** Set the uncertainty value. */ public fun uncertainty(uncertainty: Double?): GeoLocationBuilder - /** Adds a [GeoLocation.Parameter] based on [key] and [value] to the list of parameters. */ + /** Adds a [GeographicLocation.Parameter] based on [key] and [value] to the list of parameters. */ public fun addParameter(key: String, value: String): GeoLocationBuilder - /** Adds a [GeoLocation.Parameter] to the list of parameters. */ - public fun addParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = + /** Adds a [GeographicLocation.Parameter] to the list of parameters. */ + public fun addParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder = apply { addParameter(parameter.key, parameter.value) } - /** Adds all [GeoLocation.Parameter] to the list of parameters. */ - public fun addAllParameters(parameters: List): GeoLocationBuilder = + /** Adds all [GeographicLocation.Parameter] to the list of parameters. */ + public fun addAllParameters(parameters: List): GeoLocationBuilder = apply { parameters.forEach(::addParameter) } /** Removes the parameter with [key] from the list of parameters. */ public fun removeParameter(key: String): GeoLocationBuilder /** Removes [parameter] from the list of parameters. */ - public fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder + public fun removeParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder /** Returns `true` if the coordA property is set. */ public fun hasLatitude(): Boolean @@ -51,7 +51,7 @@ public interface GeoLocationBuilder : Builder { /** Returns `true` if the coordC property is set. */ public fun hasAltitude(): Boolean - override fun applyFrom(prototype: GeoLocation?): GeoLocationBuilder = + override fun applyFrom(prototype: GeographicLocation?): GeoLocationBuilder = whenNotNull(prototype) { location -> latitude(location.latitude) longitude(location.longitude) diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapElementBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapElementBuilder.kt new file mode 100644 index 00000000..4eaa932e --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/OpenStreetMapElementBuilder.kt @@ -0,0 +1,29 @@ +package dev.stalla.builder + +import dev.stalla.model.podcastindex.OpenStreetMapElement +import dev.stalla.model.podcastindex.OpenStreetMapElementType +import dev.stalla.util.whenNotNull + +/** + * Builder for constructing [OpenStreetMapElement] instances. + * + * @since 1.1.0 + */ +public interface OpenStreetMapElementBuilder : Builder { + + /** Set the type value. */ + public fun type(type: OpenStreetMapElementType): OpenStreetMapElementBuilder + + /** Set the id value. */ + public fun id(id: Long): OpenStreetMapElementBuilder = apply { id(id) } + + /** Set the revision value. */ + public fun revision(revision: Int?): OpenStreetMapElementBuilder = apply { revision(revision) } + + override fun applyFrom(prototype: OpenStreetMapElement?): OpenStreetMapElementBuilder = + whenNotNull(prototype) { feature -> + type(feature.type) + id(feature.id) + revision(feature.revision) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt deleted file mode 100644 index 8b77a50c..00000000 --- a/src/main/kotlin/dev/stalla/builder/OpenStreetMapFeatureBuilder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package dev.stalla.builder - -import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OpenStreetMapElementType -import dev.stalla.util.asBigIntegerOrNull -import dev.stalla.util.whenNotNull -import java.math.BigInteger - -/** - * Builder for constructing [OpenStreetMapFeature] instances. - * - * @since 1.1.0 - */ -public interface OpenStreetMapFeatureBuilder : Builder { - - /** Set the type value. */ - public fun type(type: OpenStreetMapElementType): OpenStreetMapFeatureBuilder - - /** Set the id value. */ - public fun id(id: BigInteger): OpenStreetMapFeatureBuilder - - /** Set the id value. */ - public fun id(id: Int): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } - - /** Set the id value. */ - public fun id(id: Long): OpenStreetMapFeatureBuilder = apply { id(id.toBigInteger()) } - - /** Set the revision value. */ - public fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder - - /** Set the revision value. */ - public fun revision(revision: Int?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } - - /** Set the revision value. */ - public fun revision(revision: Long?): OpenStreetMapFeatureBuilder = apply { revision(revision.asBigIntegerOrNull()) } - - override fun applyFrom(prototype: OpenStreetMapFeature?): OpenStreetMapFeatureBuilder = - whenNotNull(prototype) { feature -> - type(feature.type) - id(feature.id) - revision(feature.revision) - } - -} diff --git a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt index 8f7a5721..d380ee64 100644 --- a/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/PodcastindexLocationBuilder.kt @@ -1,7 +1,7 @@ package dev.stalla.builder -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.util.whenNotNull @@ -16,15 +16,15 @@ public interface PodcastindexLocationBuilder : Builder { public fun name(name: String): PodcastindexLocationBuilder /** Set the geo value. */ - public fun geo(geo: GeoLocation?): PodcastindexLocationBuilder + public fun geo(geo: GeographicLocation?): PodcastindexLocationBuilder /** Set the osm value. */ - public fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder + public fun osm(osm: OpenStreetMapElement?): PodcastindexLocationBuilder override fun applyFrom(prototype: PodcastindexLocation?): PodcastindexLocationBuilder = whenNotNull(prototype) { location -> name(location.name) - geo(GeoLocation.builder().applyFrom(location.geo).build()) - osm(OpenStreetMapFeature.builder().applyFrom(location.osm).build()) + geo(GeographicLocation.builder().applyFrom(location.geo).build()) + osm(OpenStreetMapElement.builder().applyFrom(location.osm).build()) } } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt index af37ce7f..626a9d42 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt @@ -1,9 +1,9 @@ package dev.stalla.builder.validating import dev.stalla.builder.GeoLocationBuilder -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_PARAM -import dev.stalla.model.podcastindex.GeoLocation.Factory.UNCERTAINTY_PARAM +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.GeographicLocation.Factory.CRS_PARAM +import dev.stalla.model.podcastindex.GeographicLocation.Factory.UNCERTAINTY_PARAM import dev.stalla.util.InternalAPI @InternalAPI @@ -52,7 +52,7 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { parameters.remove(key) } - override fun removeParameter(parameter: GeoLocation.Parameter): GeoLocationBuilder = + override fun removeParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder = removeParameter(parameter.key) override fun hasLatitude(): Boolean = latitude != null @@ -64,10 +64,10 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { override val hasEnoughDataToBuild: Boolean get() = latitude != null && longitude != null - override fun build(): GeoLocation? { + override fun build(): GeographicLocation? { if (!hasEnoughDataToBuild) return null - return GeoLocation( + return GeographicLocation( latitude = latitude ?: return null, longitude = longitude ?: return null, altitude = altitude, @@ -76,5 +76,4 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { parameters = parameters // this secondary constructor will apply .asUnmodifiable() ) } - } diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilder.kt new file mode 100644 index 00000000..145d0256 --- /dev/null +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilder.kt @@ -0,0 +1,33 @@ +package dev.stalla.builder.validating + +import dev.stalla.builder.OpenStreetMapElementBuilder +import dev.stalla.model.podcastindex.OpenStreetMapElement +import dev.stalla.model.podcastindex.OpenStreetMapElementType +import dev.stalla.util.InternalAPI + +@InternalAPI +internal class ValidatingOpenStreetMapElementBuilder : OpenStreetMapElementBuilder { + + private lateinit var typeValue: OpenStreetMapElementType + private var id: Long? = null + private var revision: Int? = null + + override fun type(type: OpenStreetMapElementType): OpenStreetMapElementBuilder = apply { this.typeValue = type } + + override fun id(id: Long): OpenStreetMapElementBuilder = apply { this.id = id } + + override fun revision(revision: Int?): OpenStreetMapElementBuilder = apply { this.revision = revision } + + override val hasEnoughDataToBuild: Boolean + get() = ::typeValue.isInitialized && id != null + + override fun build(): OpenStreetMapElement? { + if (!hasEnoughDataToBuild) return null + + return OpenStreetMapElement( + type = typeValue, + id = id ?: return null, + revision = revision + ) + } +} diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt deleted file mode 100644 index c0e0755b..00000000 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapFeatureBuilder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.stalla.builder.validating - -import dev.stalla.builder.OpenStreetMapFeatureBuilder -import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OpenStreetMapElementType -import dev.stalla.util.InternalAPI -import java.math.BigInteger - -@InternalAPI -internal class ValidatingOpenStreetMapFeatureBuilder : OpenStreetMapFeatureBuilder { - - private lateinit var typeValue: OpenStreetMapElementType - private lateinit var idValue: BigInteger - private var revision: BigInteger? = null - - override fun type(type: OpenStreetMapElementType): OpenStreetMapFeatureBuilder = apply { this.typeValue = type } - - override fun id(id: BigInteger): OpenStreetMapFeatureBuilder = apply { this.idValue = id } - - override fun revision(revision: BigInteger?): OpenStreetMapFeatureBuilder = apply { this.revision = revision } - - override val hasEnoughDataToBuild: Boolean - get() = ::typeValue.isInitialized && ::idValue.isInitialized - - override fun build(): OpenStreetMapFeature? { - if (!hasEnoughDataToBuild) return null - - return OpenStreetMapFeature( - type = typeValue, - id = idValue, - revision = revision - ) - } - -} diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt index 323b0b94..be0f27c0 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilder.kt @@ -1,8 +1,8 @@ package dev.stalla.builder.validating import dev.stalla.builder.PodcastindexLocationBuilder -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.util.InternalAPI @@ -10,14 +10,14 @@ import dev.stalla.util.InternalAPI internal class ValidatingPodcastindexLocationBuilder : PodcastindexLocationBuilder { private lateinit var nameValue: String - private var geo: GeoLocation? = null - private var osm: OpenStreetMapFeature? = null + private var geo: GeographicLocation? = null + private var osm: OpenStreetMapElement? = null override fun name(name: String): PodcastindexLocationBuilder = apply { this.nameValue = name } - override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } + override fun geo(geo: GeographicLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } - override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osm = osm } + override fun osm(osm: OpenStreetMapElement?): PodcastindexLocationBuilder = apply { this.osm = osm } override val hasEnoughDataToBuild: Boolean get() = ::nameValue.isInitialized diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt similarity index 70% rename from src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt rename to src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt index 317f82de..bcc17c34 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeoLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt @@ -4,8 +4,9 @@ import dev.stalla.builder.GeoLocationBuilder import dev.stalla.builder.validating.ValidatingGeoLocationBuilder import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory -import dev.stalla.model.podcastindex.GeoLocation.Factory.CRS_WGS84 -import dev.stalla.parser.GeoUriParser +import dev.stalla.model.podcastindex.GeographicLocation.Factory.CRS_WGS84 +import dev.stalla.model.podcastindex.GeographicLocation.Parameter +import dev.stalla.parser.GeographicLocationParser import dev.stalla.util.asUnmodifiable import dev.stalla.util.containsMediaTypeSeparatorSymbol import java.util.Locale @@ -19,17 +20,22 @@ import kotlin.math.absoluteValue * but does not ensure that the coordinate values are valid within the coordinate * reference system ([crs]), besides the comparison rules that apply for WGS-84. * + * Direct instantiation in Java is discouraged. Use the [builder][GeographicLocation.Factory.builder] method + * to obtain a [GeographicLocation] instance for expressive construction instead. + * + * Use the [of][GeographicLocation.Factory.of] method to obtain an instance from a string pattern. + * * @property latitude The latitude value. * @property longitude The longitude value. * @property altitude The altitude value. * @property crs The coordinate reference system - defaults to ([CRS_WGS84]) if not set. * @property uncertainty The amount of uncertainty in the location as a value in meters. - * @property parameters TODO. + * @property parameters The list of geo URI parameters as [Parameter]. * * @see [RFC 5870](https://tools.ietf.org/html/rfc5870) * @since 1.1.0 */ -public class GeoLocation public constructor( +public class GeographicLocation public constructor( public val latitude: Double, public val longitude: Double, public val altitude: Double? = null, @@ -38,6 +44,16 @@ public class GeoLocation public constructor( public val parameters: List = emptyList() ) { + /** + * Instantiates a new geographic location. + * + * @property latitude The latitude value. + * @property longitude The longitude value. + * @property altitude The altitude value. + * @property crs The coordinate reference system - defaults to ([CRS_WGS84]) if not set. + * @property uncertainty The amount of uncertainty in the location as a value in meters. + * @property parameters The list of geo URI parameters as key/value pairs. + */ public constructor( latitude: Double, longitude: Double, @@ -82,19 +98,19 @@ public class GeoLocation public constructor( parameters.firstOrNull { it.key.equals(key, ignoreCase = true) }?.value /** Creates a copy of `this` type with an added parameter of [key] and [value]. */ - public fun withParameter(key: String, value: String): GeoLocation { + public fun withParameter(key: String, value: String): GeographicLocation { if (key.isBlank() || key.containsMediaTypeSeparatorSymbol()) return this if (value.isBlank()) return this if (hasParameter(key, value)) return this - return GeoLocation(latitude, longitude, altitude, crs, uncertainty, parameters + Parameter(key, value)) + return GeographicLocation(latitude, longitude, altitude, crs, uncertainty, parameters + Parameter(key, value)) } /** Creates a copy of `this` type without any parameters.*/ - public fun withoutParameters(): GeoLocation = GeoLocation(latitude, longitude, altitude) + public fun withoutParameters(): GeographicLocation = GeographicLocation(latitude, longitude, altitude) /** Checks if `this` type matches a [pattern] type taking parameters into account. */ - public fun match(pattern: GeoLocation?): Boolean { + public fun match(pattern: GeographicLocation?): Boolean { contract { returns(true) implies (pattern != null) } @@ -104,19 +120,13 @@ public class GeoLocation public constructor( if (crs.isWGS84() && pattern.crs.isWGS84()) { if (latitude.isPoleWGS84() || pattern.latitude.isPoleWGS84()) { - // Special "poles" rule for WGS-84 applies - longitude is to be ignored - return latitude == pattern.latitude && - altitude == pattern.altitude && - uncertainty == pattern.uncertainty && - match(parameters, pattern.parameters) + // Special "poles" rule for WGS-84 applies + return isEqualPoleWGS84(pattern) } if (longitude.isDateLineWGS84() && pattern.longitude.isDateLineWGS84()) { - // Special "date line" rule for WGS-84 applies - longitude 180 degrees == -180 degrees - return latitude == pattern.latitude && - altitude == pattern.altitude && - uncertainty == pattern.uncertainty && - match(parameters, pattern.parameters) + // Special "date line" rule for WGS-84 applies + return isEqualDateLineWGS84(pattern) } } @@ -144,11 +154,6 @@ public class GeoLocation public constructor( private fun Parameter.match(key: String, value: String?): Boolean = if (value == null) false else this == Parameter(key, value) - private fun matchCrs(crs1: String?, crs2: String?): Boolean { - return (crs1 == null || crs1.equals(CRS_WGS84, ignoreCase = true)) - && (crs2 == null || crs2.equals(CRS_WGS84, ignoreCase = true)) - } - private fun parameter(key: String, elements: List): String? = elements.firstOrNull { it.key.equals(key, ignoreCase = true) }?.value @@ -162,18 +167,31 @@ public class GeoLocation public constructor( } } - private fun String?.isWGS84(): Boolean = this == null || this.equals(CRS_WGS84, ignoreCase = true) + private fun String?.isWGS84(): Boolean { + contract { + returns(false) implies(this@isWGS84 != null) + } + return this == null || this.equals(CRS_WGS84, ignoreCase = true) + } private fun Double.isPoleWGS84(): Boolean = this.absoluteValue == MAX_LATITUDE private fun Double.isDateLineWGS84(): Boolean = this.absoluteValue == MAX_LONGITUDE - private fun Double.equalDateLineWGS84(other: Double): Boolean { - return if (this.isDateLineWGS84() && other.isDateLineWGS84()) { - true - } else { - this == other - } + // In WGS-84's special pole case, longitude is to be ignored + private fun isEqualPoleWGS84(other: GeographicLocation): Boolean { + return latitude == other.latitude && + altitude == other.altitude && + uncertainty == other.uncertainty && + match(parameters, other.parameters) + } + + // In WGS-84's special date line case, longitude 180 degrees == -180 degrees + private fun isEqualDateLineWGS84(other: GeographicLocation): Boolean { + return latitude == other.latitude && + altitude == other.altitude && + uncertainty == other.uncertainty && + match(parameters, other.parameters) } override fun toString(): String = StringBuilder().apply { @@ -188,7 +206,7 @@ public class GeoLocation public constructor( if (this === other) return true if (javaClass != other?.javaClass) return false - other as GeoLocation + other as GeographicLocation if (latitude != other.latitude) return false if (longitude != other.longitude) return false @@ -210,8 +228,8 @@ public class GeoLocation public constructor( return result } - /** Provides a builder and a factory for the [GeoLocation] class. */ - public companion object Factory : BuilderFactory, TypeFactory { + /** Provides a builder and a factory for the [GeographicLocation] class. */ + public companion object Factory : BuilderFactory, TypeFactory { /** The World Geodetic System 1984 coordinate reference system used by GPS. */ public const val CRS_WGS84: String = "WGS84" @@ -221,12 +239,11 @@ public class GeoLocation public constructor( internal const val MAX_LATITUDE = 90.0 internal const val MAX_LONGITUDE = 180.0 - /** Returns a builder implementation for building [GeoLocation] model instances. */ + /** Returns a builder implementation for building [GeographicLocation] model instances. */ @JvmStatic override fun builder(): GeoLocationBuilder = ValidatingGeoLocationBuilder() @JvmStatic - override fun of(rawValue: String?): GeoLocation? = rawValue?.let { value -> GeoUriParser.parse(value) } - + override fun of(rawValue: String?): GeographicLocation? = rawValue?.let { value -> GeographicLocationParser.parse(value) } } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElement.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElement.kt new file mode 100644 index 00000000..68f59347 --- /dev/null +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElement.kt @@ -0,0 +1,46 @@ +package dev.stalla.model.podcastindex + +import dev.stalla.builder.OpenStreetMapElementBuilder +import dev.stalla.builder.validating.ValidatingOpenStreetMapElementBuilder +import dev.stalla.model.BuilderFactory +import dev.stalla.model.TypeFactory +import dev.stalla.parser.OpenStreetMapElementParser + +/** + * Represents an [OpenStreetMap element](https://wiki.openstreetmap.org/wiki/Elements). + * + * Direct instantiation in Java is discouraged. Use the [builder][OpenStreetMapElement.Factory.builder] + * method to obtain a [OpenStreetMapElement] instance for expressive construction instead. + * + * Use the [of][OpenStreetMapElement.Factory.of] method to obtain an instance from a string pattern. + * + * @property type A one-character description of the type of OSM point. One of the supported [OpenStreetMapElementType]s. + * @property id The ID of the OpenStreetMap element. + * @property revision An optional revision ID - the "changeset" of the element. + * + * @see [wiki.openstreetmap.org/wiki/Elements](https://wiki.openstreetmap.org/wiki/Elements) + * @since 1.1.0 + */ +public data class OpenStreetMapElement( + val type: OpenStreetMapElementType, + val id: Long, + val revision: Int? +) { + + override fun toString(): String = StringBuilder().apply { + append(type.type) + append(id) + if (revision != null) append("#$revision") + }.toString() + + /** Provides a builder and a factory for the [OpenStreetMapElement] class. */ + public companion object Factory : BuilderFactory, TypeFactory { + + /** Returns a builder implementation for building [OpenStreetMapElement] model instances. */ + @JvmStatic + override fun builder(): OpenStreetMapElementBuilder = ValidatingOpenStreetMapElementBuilder() + + @JvmStatic + override fun of(rawValue: String?): OpenStreetMapElement? = rawValue?.let { value -> OpenStreetMapElementParser.parse(value) } + } +} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt index dfcf9b10..44e92b46 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementType.kt @@ -3,22 +3,30 @@ package dev.stalla.model.podcastindex import dev.stalla.model.TypeFactory /** - * TODO. + * Supported element types of [OpenStreeMap elements](https://wiki.openstreetmap.org/wiki/Elements). * - * @property type TODO. + * Use the [of][OpenStreetMapElementType.Factory.of] method to obtain an instance from a string pattern. * - * @see https://wiki.openstreetmap.org/wiki/Elements + * @property type The string representation of the enum instance. + * + * @see OpenStreetMapElementType.Factory Provides a factory method for type instances. + * @see [wiki.openstreetmap.org/wiki/Elements](https://wiki.openstreetmap.org/wiki/Elements) * @since 1.1.0 */ public enum class OpenStreetMapElementType(public val type: String) { + + /** Type describing an OpenStreetMap node element. */ Node("N"), + + /** Type describing an OpenStreetMap way element. */ Way("W"), + + /** Type describing an OpenStreetMap relation element. */ Relation("R"); - /** - * TODO. - */ + /** Gets an instance of [OpenStreetMapElementType] from a raw value. */ public companion object Factory : TypeFactory { + @JvmStatic override fun of(rawValue: String?): OpenStreetMapElementType? = rawValue?.let { values().find { t -> t.type.equals(it, ignoreCase = true) } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt b/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt deleted file mode 100644 index 592a02e5..00000000 --- a/src/main/kotlin/dev/stalla/model/podcastindex/OpenStreetMapFeature.kt +++ /dev/null @@ -1,72 +0,0 @@ -package dev.stalla.model.podcastindex - -import dev.stalla.builder.OpenStreetMapFeatureBuilder -import dev.stalla.builder.validating.ValidatingOpenStreetMapFeatureBuilder -import dev.stalla.model.BuilderFactory -import dev.stalla.model.TypeFactory -import dev.stalla.parser.OsmFeatureParser -import dev.stalla.util.asBigIntegerOrNull -import java.math.BigInteger - -/** - * TODO. - * - * @property type A one-character description of the type of OSM point. One of the supported [OpenStreetMapElementType]s. - * @property id The ID of the OpenStreetMap feature that is described. - * @property revision An optional revision ID for an OSM object, preceded by a hash. - * - * @since 1.1.0 - */ -public data class OpenStreetMapFeature( - val type: OpenStreetMapElementType, - val id: BigInteger, - val revision: BigInteger? -) { - - @Suppress("Unused") - public constructor( - type: OpenStreetMapElementType, - id: Int, - revision: Int? - ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) - - @Suppress("Unused") - public constructor( - type: OpenStreetMapElementType, - id: Int, - revision: Long? - ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) - - @Suppress("Unused") - public constructor( - type: OpenStreetMapElementType, - id: Long, - revision: Long? - ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) - - @Suppress("Unused") - public constructor( - type: OpenStreetMapElementType, - id: Long, - revision: Int? - ): this(type, id.toBigInteger(), revision.asBigIntegerOrNull()) - - override fun toString(): String = StringBuilder().apply { - append(type.type) - append(id) - if (revision != null) append("#$revision") - }.toString() - - /** - * TODO. - */ - public companion object Factory : BuilderFactory, TypeFactory { - - /** Returns a builder implementation for building [OpenStreetMapFeature] model instances. */ - @JvmStatic - override fun builder(): OpenStreetMapFeatureBuilder = ValidatingOpenStreetMapFeatureBuilder() - - @JvmStatic - override fun of(rawValue: String?): OpenStreetMapFeature? = rawValue?.let { value -> OsmFeatureParser.parse(value) } - } -} diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt index c134eaff..4d464afd 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexEpisode.kt @@ -5,9 +5,12 @@ import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexEpisod import dev.stalla.model.BuilderFactory /** - * TODO. + * The episode information for the podcast episode. * - * @property number TODO. + * Direct instantiation in Java is discouraged. Use the [builder][PodcastindexEpisode.Factory.builder] + * method to obtain a [PodcastindexEpisode] instance for expressive construction instead. + * + * @property number The episode's number. * @property display An alternative display name of the episode. * * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#episode diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt index 746cc3a1..1aa3fef9 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexLocation.kt @@ -5,18 +5,22 @@ import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder import dev.stalla.model.BuilderFactory /** - * TODO. + * The location information of editorial focus for a podcast's or episode's content + * (i.e. "what place is this podcast/episode about?"). + * + * Direct instantiation in Java is discouraged. Use the [builder][PodcastindexLocation.Factory.builder] + * method to obtain a [PodcastindexLocation] instance for expressive construction instead. * * @property name Human-readable place name. - * @property geo TODO. - * @property osm TODO. + * @property geo The geographic location of this place, based on a "geo" notation URI. + * @property osm The Open Street Map identifier of this place, given using the OSM notation. * * @since 1.1.0 */ public data class PodcastindexLocation( val name: String, - val geo: GeoLocation?, - val osm: OpenStreetMapFeature? + val geo: GeographicLocation?, + val osm: OpenStreetMapElement? ) { /** Provides a builder for the [PodcastindexLocation] class. */ public companion object Factory : BuilderFactory { diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt index cb707c35..abfb3c48 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexPerson.kt @@ -5,7 +5,12 @@ import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.BuilderFactory /** - * TODO. + * The information for a person of interest to the podcast or episode. Intended to identify + * people like hosts, co-hosts and guests. It is recommented to use [role] and [group] values + * based on the [Podcast Taxonomy Project](https://podcasttaxonomy.com) + * + * Direct instantiation in Java is discouraged. Use the [builder][PodcastindexPerson.Factory.builder] + * method to obtain a [PodcastindexPerson] instance for expressive construction instead. * * @property name The full name or alias of the person. * @property role The role the person serves on the show or episode - this should be a reference to an diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt index ae633219..a4781910 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/PodcastindexSeason.kt @@ -5,9 +5,12 @@ import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeason import dev.stalla.model.BuilderFactory /** - * TODO. + * The season information for the podcast episode. * - * @property number The season number. + * Direct instantiation in Java is discouraged. Use the [builder][PodcastindexSeason.Factory.builder] + * method to obtain a [PodcastindexSeason] instance for expressive construction instead. + * + * @property number The season's number. * @property name The "name" of the season. * * @see https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#season diff --git a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt b/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt similarity index 92% rename from src/main/kotlin/dev/stalla/parser/GeoUriParser.kt rename to src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt index e9687a67..4c9bd258 100644 --- a/src/main/kotlin/dev/stalla/parser/GeoUriParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt @@ -1,13 +1,13 @@ package dev.stalla.parser import dev.stalla.builder.GeoLocationBuilder -import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.util.InternalAPI import java.util.regex.Pattern import kotlin.contracts.contract /** - * Parser implementation for [GeoLocation] values, as defined in [RFC 5870](https://tools.ietf.org/html/rfc5870). + * Parser implementation for [GeographicLocation] values, as defined in [RFC 5870](https://tools.ietf.org/html/rfc5870). * * The parsing logic is inspired by the * [GeoUri](https://github.com/mangstadt/ez-vcard/blob/master/src/main/java/ezvcard/util/GeoUri.java) @@ -15,13 +15,13 @@ import kotlin.contracts.contract * Special thanks to the ez-vcard contributors. */ @InternalAPI -internal object GeoUriParser { +internal object GeographicLocationParser { private val hexPattern: Pattern = Pattern.compile("(?i)%([0-9a-f]{2})") @InternalAPI @Suppress("ComplexMethod", "NestedBlockDepth") - internal fun parse(value: String?): GeoLocation? { + internal fun parse(value: String?): GeographicLocation? { contract { returnsNotNull() implies (value != null) } @@ -34,7 +34,7 @@ internal object GeoUriParser { // not a geo URI return null } - val builder = GeoLocation.builder() + val builder = GeographicLocation.builder() val buffer = StringBuilder() var paramName: String? = null var coordinatesDone = false diff --git a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt b/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt similarity index 61% rename from src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt rename to src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt index 8d87068f..98ec8b16 100644 --- a/src/main/kotlin/dev/stalla/parser/OsmFeatureParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt @@ -1,27 +1,26 @@ package dev.stalla.parser +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.OpenStreetMapElementType -import dev.stalla.model.podcastindex.OpenStreetMapFeature import dev.stalla.util.InternalAPI -import java.math.BigInteger import kotlin.contracts.contract @InternalAPI -internal object OsmFeatureParser { +internal object OpenStreetMapElementParser { private enum class ParsingMode { Type, Id, Revision } @InternalAPI - internal fun parse(value: String?): OpenStreetMapFeature? { + internal fun parse(value: String?): OpenStreetMapElement? { contract { returnsNotNull() implies (value != null) } if (value == null) return null - val builder = OpenStreetMapFeature.builder() + val builder = OpenStreetMapElement.builder() val idBuffer = StringBuilder() val revisionBuffer = StringBuilder() var mode = ParsingMode.Type @@ -42,28 +41,16 @@ internal object OsmFeatureParser { } } - val idValue = idBuffer.stringOrNullIfEmpty().asBigIntegerOrNull() ?: return null - builder.id(idValue) - builder.revision(revisionBuffer.stringOrNullIfEmpty().asBigIntegerOrNull()) + val id = idBuffer.stringOrNullIfEmpty()?.toLong() ?: return null + val revision = revisionBuffer.stringOrNullIfEmpty()?.toInt() - return builder.build() + return builder + .id(id) + .revision(revision) + .build() } private fun Char.isNoDigit(): Boolean = digitToIntOrNull() == null - private fun String?.asBigIntegerOrNull(): BigInteger? { - contract { - returnsNotNull() implies (this@asBigIntegerOrNull != null) - } - - if (this == null) return null - return try { - BigInteger(this) - } catch (e: NumberFormatException) { - null - } - } - private fun StringBuilder.stringOrNullIfEmpty(): String? = if (isNotEmpty()) toString() else null - } diff --git a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt index 207d40b6..23c31009 100644 --- a/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt +++ b/src/main/kotlin/dev/stalla/parser/namespace/PodcastindexParser.kt @@ -20,9 +20,9 @@ import dev.stalla.dom.textAsBooleanOrNull import dev.stalla.dom.textOrNull import dev.stalla.model.StyledDuration import dev.stalla.model.podcastindex.TranscriptType -import dev.stalla.parser.GeoUriParser +import dev.stalla.parser.GeographicLocationParser import dev.stalla.parser.NamespaceParser -import dev.stalla.parser.OsmFeatureParser +import dev.stalla.parser.OpenStreetMapElementParser import dev.stalla.util.FeedNamespace import dev.stalla.util.InternalAPI import dev.stalla.util.allNotNull @@ -201,8 +201,8 @@ internal object PodcastindexParser : NamespaceParser() { val osmValue = getAttributeByName("osm")?.value.trimmedOrNullIfBlank() return locationBuilder.name(name) - .geo(GeoUriParser.parse(geoValue)) - .osm(OsmFeatureParser.parse(osmValue)) + .geo(GeographicLocationParser.parse(geoValue)) + .osm(OpenStreetMapElementParser.parse(osmValue)) } private fun Node.toPersonBuilder( diff --git a/src/main/kotlin/dev/stalla/util/NumberExtensions.kt b/src/main/kotlin/dev/stalla/util/NumberExtensions.kt deleted file mode 100644 index c1411fbb..00000000 --- a/src/main/kotlin/dev/stalla/util/NumberExtensions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.stalla.util - -import java.math.BigInteger -import kotlin.contracts.contract - -@InternalAPI -internal fun Int?.asBigIntegerOrNull(): BigInteger? { - contract { - returnsNotNull() implies (this@asBigIntegerOrNull != null) - } - - if (this == null) return null - return BigInteger.valueOf(this.toLong()) -} - -@InternalAPI -internal fun Long?.asBigIntegerOrNull(): BigInteger? { - contract { - returnsNotNull() implies (this@asBigIntegerOrNull != null) - } - - if (this == null) return null - return BigInteger.valueOf(this) -} diff --git a/src/test/kotlin/dev/stalla/Assertions.kt b/src/test/kotlin/dev/stalla/Assertions.kt index 49375e17..893ff5f0 100644 --- a/src/test/kotlin/dev/stalla/Assertions.kt +++ b/src/test/kotlin/dev/stalla/Assertions.kt @@ -7,7 +7,7 @@ import dev.stalla.builder.Builder import dev.stalla.dom.asListOfNodes import dev.stalla.dom.asString import dev.stalla.model.MediaType -import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.util.FeedNamespace import dev.stalla.util.FeedNamespace.Companion.matches import org.w3c.dom.Attr @@ -222,8 +222,8 @@ internal fun Assert.doesNotMatchSymmetrically(expected: MediaType?) = } } -/** Asserts that [GeoLocation.match] matches the expected value. */ -internal fun Assert.matchPattern(expected: GeoLocation) = given { geoLocation -> +/** Asserts that [GeographicLocation.match] matches the expected value. */ +internal fun Assert.matchPattern(expected: GeographicLocation) = given { geoLocation -> if (geoLocation.match(expected)) return@given expected( "to be: '$expected' but was: '$geoLocation'", diff --git a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt index 0332aad9..c790493e 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/FakePodcastindexLocationBuilder.kt @@ -1,21 +1,21 @@ package dev.stalla.builder.fake import dev.stalla.builder.PodcastindexLocationBuilder -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastindexLocation internal class FakePodcastindexLocationBuilder : FakeBuilder(), PodcastindexLocationBuilder { var name: String? = null - var geo: GeoLocation? = null - var osm: OpenStreetMapFeature? = null + var geo: GeographicLocation? = null + var osm: OpenStreetMapElement? = null override fun name(name: String): PodcastindexLocationBuilder = apply { this.name = name } - override fun geo(geo: GeoLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } + override fun geo(geo: GeographicLocation?): PodcastindexLocationBuilder = apply { this.geo = geo } - override fun osm(osm: OpenStreetMapFeature?): PodcastindexLocationBuilder = apply { this.osm = osm } + override fun osm(osm: OpenStreetMapElement?): PodcastindexLocationBuilder = apply { this.osm = osm } override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt index c3ea2184..6fd6217c 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/episode/FakeEpisodePodcastindexBuilder.kt @@ -78,6 +78,8 @@ internal class FakeEpisodePodcastindexBuilder : FakeBuilder } override fun toString(): String { - return "FakeEpisodePodcastindexBuilder(chaptersBuilderValue=$chaptersBuilderValue, locationBuilderValue=$locationBuilderValue, seasonBuilder=$seasonBuilder, episodeBuilder=$episodeBuilder, transcriptBuilders=$transcriptBuilders, soundbiteBuilders=$soundbiteBuilders, personBuilders=$personBuilders)" + return "FakeEpisodePodcastindexBuilder(chaptersBuilderValue=$chaptersBuilderValue, locationBuilderValue=$locationBuilderValue, " + + "seasonBuilder=$seasonBuilder, episodeBuilder=$episodeBuilder, transcriptBuilders=$transcriptBuilders, " + + "soundbiteBuilders=$soundbiteBuilders, personBuilders=$personBuilders)" } } diff --git a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt index d5501165..6304c885 100644 --- a/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt +++ b/src/test/kotlin/dev/stalla/builder/fake/podcast/FakePodcastPodcastindexBuilder.kt @@ -54,6 +54,7 @@ internal class FakePodcastPodcastindexBuilder : FakeBuilder } override fun toString(): String { - return "FakePodcastPodcastindexBuilder(lockedBuilderValue=$lockedBuilderValue, locationBuilderValue=$locationBuilderValue, fundingBuilders=$fundingBuilders, personBuilders=$personBuilders)" + return "FakePodcastPodcastindexBuilder(lockedBuilderValue=$lockedBuilderValue, locationBuilderValue=$locationBuilderValue, " + + "fundingBuilders=$fundingBuilders, personBuilders=$personBuilders)" } } diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt index 547a2eaf..3714bf1e 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt @@ -11,7 +11,7 @@ import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.PodcastindexLocationBuilder import dev.stalla.model.aPodcastindexGeoLocation -import dev.stalla.model.aPodcastindexOpenStreetMapFeature +import dev.stalla.model.aPodcastindexOpenStreetMapElement import dev.stalla.model.anEpisodePodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexLocation import org.junit.jupiter.api.Test @@ -62,7 +62,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { val locationBuilder = ValidatingPodcastindexLocationBuilder() .name("name") .geo(aPodcastindexGeoLocation()) - .osm(aPodcastindexOpenStreetMapFeature()) + .osm(aPodcastindexOpenStreetMapElement()) assertAll { assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isTrue() @@ -70,7 +70,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { assertThat(locationBuilder.build()).isNotNull().all { prop(PodcastindexLocation::name).isEqualTo("name") prop(PodcastindexLocation::geo).isEqualTo(aPodcastindexGeoLocation()) - prop(PodcastindexLocation::osm).isEqualTo(aPodcastindexOpenStreetMapFeature()) + prop(PodcastindexLocation::osm).isEqualTo(aPodcastindexOpenStreetMapElement()) } } } diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt index c459393a..996500fb 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexPersonBuilderTest.kt @@ -90,5 +90,4 @@ internal class ValidatingPodcastindexPersonBuilderTest { assertThat(personBuilder.build()).isNotNull().isEqualTo(person) } } - } diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt index e81440df..ca20c23c 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexBuilderTest.kt @@ -17,8 +17,8 @@ import dev.stalla.model.MediaType import dev.stalla.model.StyledDuration import dev.stalla.model.anEpisodePodcastindex import dev.stalla.model.podcastindex.EpisodePodcastindex -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.TranscriptType import org.junit.jupiter.api.Test import java.util.Locale @@ -66,8 +66,8 @@ internal class ValidatingEpisodePodcastindexBuilderTest { private val expectedLocationBuilder = ValidatingPodcastindexLocationBuilder() .name("Location name") - .geo(GeoLocation.of("geo:1,2,3")) - .osm(OpenStreetMapFeature.of("R123")) + .geo(GeographicLocation.of("geo:1,2,3")) + .osm(OpenStreetMapElement.of("R123")) private val expectedSeasonBuilder = ValidatingEpisodePodcastindexSeasonBuilder() .number(1) diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt index b5d60b6d..5d309138 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexEpisodeBuilderTest.kt @@ -81,5 +81,4 @@ internal class ValidatingEpisodePodcastindexEpisodeBuilderTest { assertThat(podcastindexEpisodeBuilder.build()).isNotNull().isEqualTo(podcastindexEpisode) } } - } diff --git a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt index 0071a2f8..e67c1ed8 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/episode/ValidatingEpisodePodcastindexSeasonBuilderTest.kt @@ -81,5 +81,4 @@ internal class ValidatingEpisodePodcastindexSeasonBuilderTest { assertThat(podcastindexseasonBuilder.build()).isNotNull().isEqualTo(podcastindexSeason) } } - } diff --git a/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt index a26d4e11..f6019332 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/podcast/ValidatingPodcastPodcastindexBuilderTest.kt @@ -14,9 +14,8 @@ import dev.stalla.builder.podcast.PodcastPodcastindexBuilder import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder import dev.stalla.model.aPodcastPodcastindex -import dev.stalla.model.podcastindex.EpisodePodcastindex -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastPodcastindex import org.junit.jupiter.api.Test @@ -50,8 +49,8 @@ internal class ValidatingPodcastPodcastindexBuilderTest { private val expectedLocationBuilder = ValidatingPodcastindexLocationBuilder() .name("Location name") - .geo(GeoLocation.of("geo:1,2,3")) - .osm(OpenStreetMapFeature.of("R123")) + .geo(GeographicLocation.of("geo:1,2,3")) + .osm(OpenStreetMapElement.of("R123")) @Test internal fun `should not build a Podcast Podcastindex with when all the fields are empty`() { diff --git a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt index 189f9b06..cbeb2818 100644 --- a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt @@ -12,8 +12,8 @@ import dev.stalla.model.itunes.EpisodeItunes import dev.stalla.model.itunes.EpisodeType import dev.stalla.model.podcastindex.Chapters import dev.stalla.model.podcastindex.EpisodePodcastindex -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastindexEpisode import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexPerson @@ -180,8 +180,8 @@ internal fun anEpisodePodcastindexPerson( @JvmOverloads internal fun anEpisodePodcastindexLocation( name: String = "episode podcastindex location name", - geo: GeoLocation? = aPodcastindexGeoLocation(), - osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() + geo: GeographicLocation? = aPodcastindexGeoLocation(), + osm: OpenStreetMapElement? = aPodcastindexOpenStreetMapElement() ) = PodcastindexLocation(name, geo, osm) @JvmOverloads diff --git a/src/test/kotlin/dev/stalla/model/Fixtures.kt b/src/test/kotlin/dev/stalla/model/Fixtures.kt index fea8a25a..3f7a0c92 100644 --- a/src/test/kotlin/dev/stalla/model/Fixtures.kt +++ b/src/test/kotlin/dev/stalla/model/Fixtures.kt @@ -5,18 +5,15 @@ import dev.stalla.model.atom.AtomPerson import dev.stalla.model.atom.Link import dev.stalla.model.googleplay.GoogleplayCategory import dev.stalla.model.itunes.ItunesCategory -import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.OpenStreetMapElementType -import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.PodcastindexLocation -import dev.stalla.model.podcastindex.PodcastindexPerson import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.model.rss.RssCategory import dev.stalla.model.rss.RssImage import dev.stalla.staticPropertiesByType import org.junit.jupiter.params.provider.ArgumentsProvider import org.reflections.Reflections -import java.math.BigInteger @JvmOverloads internal fun anRssImage( @@ -72,17 +69,17 @@ internal fun aPodcastindexGeoLocation( coordA: Double = 48.20849, coordB: Double = 16.37208, coordC: Double? = 5.0, - crs: String? = GeoLocation.CRS_WGS84, + crs: String? = GeographicLocation.CRS_WGS84, uncertainty: Double? = 10.0, - parameters: List = emptyList() -) = GeoLocation(coordA, coordB, coordC, crs, uncertainty, parameters) + parameters: List = emptyList() +) = GeographicLocation(coordA, coordB, coordC, crs, uncertainty, parameters) @JvmOverloads -internal fun aPodcastindexOpenStreetMapFeature( +internal fun aPodcastindexOpenStreetMapElement( type: OpenStreetMapElementType = OpenStreetMapElementType.Relation, - id: BigInteger = BigInteger.ONE, - revision: BigInteger? = BigInteger.TWO -) = OpenStreetMapFeature(type, id, revision) + id: Long = 1, + revision: Int? = 2 +) = OpenStreetMapElement(type, id, revision) internal val simpleCategoryNames = listOf( "Arts", diff --git a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt b/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt deleted file mode 100644 index e9c028f2..00000000 --- a/src/test/kotlin/dev/stalla/model/GeoLocationTest.kt +++ /dev/null @@ -1,304 +0,0 @@ -package dev.stalla.model - -import assertk.all -import assertk.assertAll -import assertk.assertThat -import assertk.assertions.hasSize -import assertk.assertions.isEmpty -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isNotNull -import assertk.assertions.isNull -import assertk.assertions.isTrue -import assertk.assertions.prop -import dev.stalla.model.podcastindex.GeoLocation -import org.junit.jupiter.api.Test - -class GeoLocationTest { - - @Test - fun `should parse a Geo URI with latitude and longitude correctly`() { - assertThat(GeoLocation.of("geo:37.786971,-122.399677")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(37.786971) - prop(GeoLocation::longitude).isEqualTo(-122.399677) - prop(GeoLocation::altitude).isNull() - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `should parse a Geo URI with latitude and longitude and altitude correctly`() { - assertThat(GeoLocation.of("geo:40.714623,-74.006605,1.1")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(40.714623) - prop(GeoLocation::longitude).isEqualTo(-74.006605) - prop(GeoLocation::altitude).isEqualTo(1.1) - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `test 2`() { - assertThat(GeoLocation.of("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(48.198634) - prop(GeoLocation::longitude).isEqualTo(16.371648) - prop(GeoLocation::altitude).isNull() - prop(GeoLocation::crs).isEqualTo("wgs84") - prop(GeoLocation::uncertainty).isEqualTo(40.0) - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse all`() { - assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isEqualTo("wgs84") - prop(GeoLocation::uncertainty).isEqualTo(12.0) - prop(GeoLocation::parameters).hasSize(1) - prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") - } - } - - @Test - fun `parse no params`() { - assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isEqualTo("wgs84") - prop(GeoLocation::uncertainty).isEqualTo(12.0) - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse no params or u`() { - assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isEqualTo("wgs84") - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse no params or crs`() { - assertThat(GeoLocation.of("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isEqualTo(12.0) - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse no params or u or crs`() { - assertThat(GeoLocation.of("geo:12.34,56.78,-21.43")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse no params or u or crs or coordC`() { - assertThat(GeoLocation.of("geo:12.34,56.78")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isNull() - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).isEmpty() - } - } - - @Test - fun `parse invalid uncertainty`() { - assertThat(GeoLocation.of("geo:12.34,56.78;u=invalid")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isNull() - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).hasSize(1) - prop("parameter") { GeoLocation::parameter.call(it, "u") }.isNotNull().isEqualTo("invalid") - } - } - - @Test - fun `parse no params or u or crs or coordsC or coordB`() { - assertThat(GeoLocation.of("geo:12.34")).isNull() - } - - @Test - fun `parse no params or u or crs or coordsC or coordB or coordA`() { - assertThat(GeoLocation.of("geo:")).isNull() - } - - @Test - fun `parse not geo uri(`() { - assertThat(GeoLocation.of("https://stalla.dev")).isNull() - } - - @Test - fun `parse decode special chars in param value`() { - assertThat(GeoLocation.of("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(56.78) - prop(GeoLocation::altitude).isNull() - prop(GeoLocation::crs).isNull() - prop(GeoLocation::uncertainty).isNull() - prop(GeoLocation::parameters).hasSize(1) - prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("with = special & chars") - } - } - - @Test - fun `multiple params`() { - assertThat(GeoLocation.of("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { - prop(GeoLocation::latitude).isEqualTo(12.34) - prop(GeoLocation::longitude).isEqualTo(45.67) - prop(GeoLocation::altitude).isEqualTo(-21.43) - prop(GeoLocation::crs).isEqualTo("theCrs") - prop(GeoLocation::uncertainty).isEqualTo(12.0) - prop(GeoLocation::parameters).hasSize(2) - prop("parameter") { GeoLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") - prop("parameter") { GeoLocation::parameter.call(it, "param2") }.isNotNull().isEqualTo("value2") - } - } - - @Test - fun `WGS84 pole rule`() { - val geoLocation1 = GeoLocation.of("geo:90,-22.43;crs=WGS84") - val geoLocation2 = GeoLocation.of("geo:90,46;crs=WGS84") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `should no match two Geo URIs in WGS-84 special pole case if the latitude is different`() { - val geoLocation1 = GeoLocation.of("geo:90,10") - val geoLocation2 = GeoLocation.of("geo:45,20") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() - } - } - - @Test - fun `should no match two Geo URIs in WGS-84 special pole case if the latitude has a different sign`() { - val geoLocation1 = GeoLocation.of("geo:90,10") - val geoLocation2 = GeoLocation.of("geo:-90,10") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() - } - } - - @Test - fun `should match two Geo URIs in WGS-84 special pole case by ignoring the longitude`() { - val geoLocation1 = GeoLocation.of("geo:90,5") - val geoLocation2 = GeoLocation.of("geo:90,10") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special pole case`() { - val geoLocation1 = GeoLocation.of("geo:90,5") - val geoLocation2 = GeoLocation.of("geo:90,10;crs=WGS84") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `should match two Geo URIs in WGS-84 special date line case if the longitude has a different sign`() { - val geoLocation1 = GeoLocation.of("geo:10,180") - val geoLocation2 = GeoLocation.of("geo:10,-180") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special date line case`() { - val geoLocation1 = GeoLocation.of("geo:10,180") - val geoLocation2 = GeoLocation.of("geo:10,-180;crs=WGS84") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { - val geoLocation1 = GeoLocation.of("geo:66,30;u=6.500;FOo=this%2dthat") - val geoLocation2 = GeoLocation.of("geo:66.0,30;u=6.5;foo=this-that") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `parameter order is insignificant`() { - val geoLocation1 = GeoLocation.of("geo:47,11;foo=blue;bar=white") - val geoLocation2 = GeoLocation.of("geo:47,11;bar=white;foo=blue") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `parameter keys are case-insensitive`() { - val geoLocation1 = GeoLocation.of("geo:22,0;bar=blue") - val geoLocation2 = GeoLocation.of("geo:22,0;BAR=blue") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() - } - } - - @Test - fun `parameter values are case-sensitive`() { - val geoLocation1 = GeoLocation.of("geo:22,0;bar=BLUE") - val geoLocation2 = GeoLocation.of("geo:22,0;bar=blue") - assertAll { - assertThat(geoLocation1).isNotNull() - assertThat(geoLocation2).isNotNull() - assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() - } - } - -} diff --git a/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt b/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt deleted file mode 100644 index 02a6e15f..00000000 --- a/src/test/kotlin/dev/stalla/model/OpenStreetMapFeatureTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package dev.stalla.model - -import assertk.all -import assertk.assertThat -import assertk.assertions.isEqualTo -import assertk.assertions.isNotNull -import assertk.assertions.isNull -import assertk.assertions.prop -import dev.stalla.model.podcastindex.OpenStreetMapFeature -import dev.stalla.model.podcastindex.OpenStreetMapElementType -import org.junit.jupiter.api.Test -import java.math.BigInteger - -class OpenStreetMapFeatureTest { - - @Test - fun `test 1`() { - assertThat(OpenStreetMapFeature.of("R148838")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Relation) - prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("148838")) - prop(OpenStreetMapFeature::revision).isNull() - } - } - - @Test - fun `test 2`() { - assertThat(OpenStreetMapFeature.of("W5013364")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Way) - prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("5013364")) - prop(OpenStreetMapFeature::revision).isNull() - } - } - - @Test - fun `test 3`() { - assertThat(OpenStreetMapFeature.of("R7444#188")).isNotNull().all { - prop(OpenStreetMapFeature::type).isEqualTo(OpenStreetMapElementType.Relation) - prop(OpenStreetMapFeature::id).isEqualTo(BigInteger("7444")) - prop(OpenStreetMapFeature::revision).isEqualTo(BigInteger("188")) - } - } - - @Test - fun `test 3_5`() { - assertThat(OpenStreetMapFeature.of("X")).isNull() - } - - @Test - fun `test 4`() { - assertThat(OpenStreetMapFeature.of("X12345")).isNull() - } - - @Test - fun `test 5`() { - assertThat(OpenStreetMapFeature.of("R")).isNull() - } - - @Test - fun `test 6`() { - assertThat(OpenStreetMapFeature.of("R#188")).isNull() - } - - @Test - fun `test 7`() { - assertThat(OpenStreetMapFeature.of("Rabc")).isNull() - } - - @Test - fun `test 8`() { - assertThat(OpenStreetMapFeature.of("Rabc")).isNull() - } - - @Test - fun `test 9`() { - assertThat(OpenStreetMapFeature.of("Rabc#123")).isNull() - } - -} diff --git a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt index 21aa31dd..706d1dd5 100644 --- a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt @@ -13,9 +13,9 @@ import dev.stalla.model.itunes.ItunesOwner import dev.stalla.model.itunes.PodcastItunes import dev.stalla.model.itunes.ShowType import dev.stalla.model.podcastindex.Funding -import dev.stalla.model.podcastindex.GeoLocation +import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.model.podcastindex.Locked -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.PodcastPodcastindex import dev.stalla.model.podcastindex.PodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexPerson @@ -160,6 +160,6 @@ internal fun aPodcastPodcastindexPerson( @JvmOverloads internal fun aPodcastPodcastindexLocation( name: String = "podcast podcastindex location name", - geo: GeoLocation? = aPodcastindexGeoLocation(), - osm: OpenStreetMapFeature? = aPodcastindexOpenStreetMapFeature() + geo: GeographicLocation? = aPodcastindexGeoLocation(), + osm: OpenStreetMapElement? = aPodcastindexOpenStreetMapElement() ) = PodcastindexLocation(name, geo, osm) diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt new file mode 100644 index 00000000..7cf04cdd --- /dev/null +++ b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt @@ -0,0 +1,302 @@ +package dev.stalla.model.podcastindex + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.hasSize +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import org.junit.jupiter.api.Test + +class GeographicLocationTest { + + @Test + fun `should parse a Geo URI with latitude and longitude correctly`() { + assertThat(GeographicLocation.of("geo:37.786971,-122.399677")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(37.786971) + prop(GeographicLocation::longitude).isEqualTo(-122.399677) + prop(GeographicLocation::altitude).isNull() + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `should parse a Geo URI with latitude and longitude and altitude correctly`() { + assertThat(GeographicLocation.of("geo:40.714623,-74.006605,1.1")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(40.714623) + prop(GeographicLocation::longitude).isEqualTo(-74.006605) + prop(GeographicLocation::altitude).isEqualTo(1.1) + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `test 2`() { + assertThat(GeographicLocation.of("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(48.198634) + prop(GeographicLocation::longitude).isEqualTo(16.371648) + prop(GeographicLocation::altitude).isNull() + prop(GeographicLocation::crs).isEqualTo("wgs84") + prop(GeographicLocation::uncertainty).isEqualTo(40.0) + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse all`() { + assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isEqualTo("wgs84") + prop(GeographicLocation::uncertainty).isEqualTo(12.0) + prop(GeographicLocation::parameters).hasSize(1) + prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") + } + } + + @Test + fun `parse no params`() { + assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isEqualTo("wgs84") + prop(GeographicLocation::uncertainty).isEqualTo(12.0) + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u`() { + assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isEqualTo("wgs84") + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or crs`() { + assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isEqualTo(12.0) + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u or crs`() { + assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse no params or u or crs or coordC`() { + assertThat(GeographicLocation.of("geo:12.34,56.78")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isNull() + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).isEmpty() + } + } + + @Test + fun `parse invalid uncertainty`() { + assertThat(GeographicLocation.of("geo:12.34,56.78;u=invalid")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isNull() + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).hasSize(1) + prop("parameter") { GeographicLocation::parameter.call(it, "u") }.isNotNull().isEqualTo("invalid") + } + } + + @Test + fun `parse no params or u or crs or coordsC or coordB`() { + assertThat(GeographicLocation.of("geo:12.34")).isNull() + } + + @Test + fun `parse no params or u or crs or coordsC or coordB or coordA`() { + assertThat(GeographicLocation.of("geo:")).isNull() + } + + @Test + fun `parse not geo uri(`() { + assertThat(GeographicLocation.of("https://stalla.dev")).isNull() + } + + @Test + fun `parse decode special chars in param value`() { + assertThat(GeographicLocation.of("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(56.78) + prop(GeographicLocation::altitude).isNull() + prop(GeographicLocation::crs).isNull() + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).hasSize(1) + prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("with = special & chars") + } + } + + @Test + fun `multiple params`() { + assertThat(GeographicLocation.of("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(12.34) + prop(GeographicLocation::longitude).isEqualTo(45.67) + prop(GeographicLocation::altitude).isEqualTo(-21.43) + prop(GeographicLocation::crs).isEqualTo("theCrs") + prop(GeographicLocation::uncertainty).isEqualTo(12.0) + prop(GeographicLocation::parameters).hasSize(2) + prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") + prop("parameter") { GeographicLocation::parameter.call(it, "param2") }.isNotNull().isEqualTo("value2") + } + } + + @Test + fun `WGS84 pole rule`() { + val geoLocation1 = GeographicLocation.of("geo:90,-22.43;crs=WGS84") + val geoLocation2 = GeographicLocation.of("geo:90,46;crs=WGS84") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should no match two Geo URIs in WGS-84 special pole case if the latitude is different`() { + val geoLocation1 = GeographicLocation.of("geo:90,10") + val geoLocation2 = GeographicLocation.of("geo:45,20") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } + + @Test + fun `should no match two Geo URIs in WGS-84 special pole case if the latitude has a different sign`() { + val geoLocation1 = GeographicLocation.of("geo:90,10") + val geoLocation2 = GeographicLocation.of("geo:-90,10") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } + + @Test + fun `should match two Geo URIs in WGS-84 special pole case by ignoring the longitude`() { + val geoLocation1 = GeographicLocation.of("geo:90,5") + val geoLocation2 = GeographicLocation.of("geo:90,10") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special pole case`() { + val geoLocation1 = GeographicLocation.of("geo:90,5") + val geoLocation2 = GeographicLocation.of("geo:90,10;crs=WGS84") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should match two Geo URIs in WGS-84 special date line case if the longitude has a different sign`() { + val geoLocation1 = GeographicLocation.of("geo:10,180") + val geoLocation2 = GeographicLocation.of("geo:10,-180") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `should interprete a missing CRS value as WGS-84 when matching two Geo URIs with WGS-84 special date line case`() { + val geoLocation1 = GeographicLocation.of("geo:10,180") + val geoLocation2 = GeographicLocation.of("geo:10,-180;crs=WGS84") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { + val geoLocation1 = GeographicLocation.of("geo:66,30;u=6.500;FOo=this%2dthat") + val geoLocation2 = GeographicLocation.of("geo:66.0,30;u=6.5;foo=this-that") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter order is insignificant`() { + val geoLocation1 = GeographicLocation.of("geo:47,11;foo=blue;bar=white") + val geoLocation2 = GeographicLocation.of("geo:47,11;bar=white;foo=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter keys are case-insensitive`() { + val geoLocation1 = GeographicLocation.of("geo:22,0;bar=blue") + val geoLocation2 = GeographicLocation.of("geo:22,0;BAR=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isTrue() + } + } + + @Test + fun `parameter values are case-sensitive`() { + val geoLocation1 = GeographicLocation.of("geo:22,0;bar=BLUE") + val geoLocation2 = GeographicLocation.of("geo:22,0;bar=blue") + assertAll { + assertThat(geoLocation1).isNotNull() + assertThat(geoLocation2).isNotNull() + assertThat(geoLocation1!!.match(geoLocation2!!)).isFalse() + } + } +} diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt new file mode 100644 index 00000000..b02db8a6 --- /dev/null +++ b/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt @@ -0,0 +1,74 @@ +package dev.stalla.model.podcastindex + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.prop +import org.junit.jupiter.api.Test + +class OpenStreetMapElementTest { + + @Test + fun `test 1`() { + assertThat(OpenStreetMapElement.of("R148838")).isNotNull().all { + prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Relation) + prop(OpenStreetMapElement::id).isEqualTo(148838) + prop(OpenStreetMapElement::revision).isNull() + } + } + + @Test + fun `test 2`() { + assertThat(OpenStreetMapElement.of("W5013364")).isNotNull().all { + prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Way) + prop(OpenStreetMapElement::id).isEqualTo(5013364) + prop(OpenStreetMapElement::revision).isNull() + } + } + + @Test + fun `test 3`() { + assertThat(OpenStreetMapElement.of("R7444#188")).isNotNull().all { + prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Relation) + prop(OpenStreetMapElement::id).isEqualTo(7444) + prop(OpenStreetMapElement::revision).isEqualTo(188) + } + } + + @Test + fun `test 3_5`() { + assertThat(OpenStreetMapElement.of("X")).isNull() + } + + @Test + fun `test 4`() { + assertThat(OpenStreetMapElement.of("X12345")).isNull() + } + + @Test + fun `test 5`() { + assertThat(OpenStreetMapElement.of("R")).isNull() + } + + @Test + fun `test 6`() { + assertThat(OpenStreetMapElement.of("R#188")).isNull() + } + + @Test + fun `test 7`() { + assertThat(OpenStreetMapElement.of("Rabc")).isNull() + } + + @Test + fun `test 8`() { + assertThat(OpenStreetMapElement.of("Rabc")).isNull() + } + + @Test + fun `test 9`() { + assertThat(OpenStreetMapElement.of("Rabc#123")).isNull() + } +} diff --git a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt index fdb9f877..ac7c96bc 100644 --- a/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt +++ b/src/test/kotlin/dev/stalla/parser/namespace/PodcastindexParserTest.kt @@ -21,15 +21,11 @@ import dev.stalla.builder.fake.podcast.FakePodcastBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexFundingBuilder import dev.stalla.builder.fake.podcast.FakePodcastPodcastindexLockedBuilder -import dev.stalla.builder.validating.ValidatingPodcastindexLocationBuilder -import dev.stalla.builder.validating.ValidatingPodcastindexPersonBuilder -import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexEpisodeBuilder -import dev.stalla.builder.validating.episode.ValidatingEpisodePodcastindexSeasonBuilder import dev.stalla.dom.XmlRes import dev.stalla.model.MediaType import dev.stalla.model.StyledDuration -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature +import dev.stalla.model.podcastindex.GeographicLocation +import dev.stalla.model.podcastindex.OpenStreetMapElement import dev.stalla.model.podcastindex.TranscriptType import dev.stalla.parser.NamespaceParserTest import org.junit.jupiter.api.Test @@ -79,13 +75,13 @@ internal class PodcastindexParserTest : NamespaceParserTest() { private val expectedPodcastLocationBuilder = FakePodcastindexLocationBuilder() .name("podcast podcastindex location name") - .geo(GeoLocation.of("geo:1,2,3")) - .osm(OpenStreetMapFeature.of("R123")) + .geo(GeographicLocation.of("geo:1,2,3")) + .osm(OpenStreetMapElement.of("R123")) private val expectedEpisodeLocationBuilder = FakePodcastindexLocationBuilder() .name("episode podcastindex location name") - .geo(GeoLocation.of("geo:4,5,6")) - .osm(OpenStreetMapFeature.of("W456")) + .geo(GeographicLocation.of("geo:4,5,6")) + .osm(OpenStreetMapElement.of("W456")) private val expectedSeasonBuilder = FakeEpisodePodcastindexSeasonBuilder() .number(1) diff --git a/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt b/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt index bd3db32a..ad174c3b 100644 --- a/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt +++ b/src/test/kotlin/dev/stalla/writer/namespace/PodcastindexWriterTest.kt @@ -15,8 +15,6 @@ import dev.stalla.model.anEpisodePodcastindex import dev.stalla.model.anEpisodePodcastindexChapters import dev.stalla.model.anEpisodePodcastindexSoundbite import dev.stalla.model.anEpisodePodcastindexTranscript -import dev.stalla.model.podcastindex.GeoLocation -import dev.stalla.model.podcastindex.OpenStreetMapFeature import org.junit.jupiter.api.Test internal class PodcastindexWriterTest : NamespaceWriterTest() { From 293da60bde7bc7046aa7d15846b24b4042b090ca Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Wed, 28 Apr 2021 21:35:41 +0200 Subject: [PATCH 15/18] Improve code and tests --- .../model/podcastindex/GeographicLocation.kt | 54 +++++++-------- .../parser/OpenStreetMapElementParser.kt | 5 +- .../podcastindex/GeographicLocationTest.kt | 69 +++++++------------ .../podcastindex/OpenStreetMapElementTest.kt | 33 +++++---- 4 files changed, 76 insertions(+), 85 deletions(-) diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt index bcc17c34..53fa3074 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt @@ -115,27 +115,19 @@ public class GeographicLocation public constructor( returns(true) implies (pattern != null) } - if (pattern == null) return false - if (this == pattern) return true - - if (crs.isWGS84() && pattern.crs.isWGS84()) { - if (latitude.isPoleWGS84() || pattern.latitude.isPoleWGS84()) { - // Special "poles" rule for WGS-84 applies - return isEqualPoleWGS84(pattern) - } - - if (longitude.isDateLineWGS84() && pattern.longitude.isDateLineWGS84()) { - // Special "date line" rule for WGS-84 applies - return isEqualDateLineWGS84(pattern) - } + return when { + pattern == null -> false + this == pattern -> true + matchPoleWGS84(pattern) -> true + matchDateLineWGS84(pattern) -> true + else -> + latitude == pattern.latitude && + longitude == pattern.longitude && + altitude == pattern.altitude && + crs.equals(pattern.crs, ignoreCase = true) && + uncertainty == pattern.uncertainty && + match(parameters, pattern.parameters) } - - return latitude == pattern.latitude && - longitude == pattern.longitude && - altitude == pattern.altitude && - crs.equals(pattern.crs, ignoreCase = true) && - uncertainty == pattern.uncertainty && - match(parameters, pattern.parameters) } /** Checks if `this` type matches a [pattern] type taking parameters into account. */ @@ -178,21 +170,27 @@ public class GeographicLocation public constructor( private fun Double.isDateLineWGS84(): Boolean = this.absoluteValue == MAX_LONGITUDE - // In WGS-84's special pole case, longitude is to be ignored - private fun isEqualPoleWGS84(other: GeographicLocation): Boolean { - return latitude == other.latitude && + private fun isWGS84(other: GeographicLocation): Boolean = + crs.isWGS84() && other.crs.isWGS84() + + // In WGS-84's special "poles" rule, longitude is to be ignored + private fun matchPoleWGS84(other: GeographicLocation): Boolean = + isWGS84(other) && + (latitude.isPoleWGS84() || other.latitude.isPoleWGS84()) && + latitude == other.latitude && altitude == other.altitude && uncertainty == other.uncertainty && match(parameters, other.parameters) - } - // In WGS-84's special date line case, longitude 180 degrees == -180 degrees - private fun isEqualDateLineWGS84(other: GeographicLocation): Boolean { - return latitude == other.latitude && + // In WGS-84's special "date line" rule, longitude 180 degrees == -180 degrees + private fun matchDateLineWGS84(other: GeographicLocation): Boolean = + isWGS84(other) && + longitude.isDateLineWGS84() && + other.longitude.isDateLineWGS84() && + latitude == other.latitude && altitude == other.altitude && uncertainty == other.uncertainty && match(parameters, other.parameters) - } override fun toString(): String = StringBuilder().apply { append("geo:$latitude,$longitude") diff --git a/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt b/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt index 98ec8b16..9bc2eb5c 100644 --- a/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt +++ b/src/main/kotlin/dev/stalla/parser/OpenStreetMapElementParser.kt @@ -37,7 +37,10 @@ internal object OpenStreetMapElementParser { c.isNoDigit() -> return null else -> idBuffer.append(c) } - ParsingMode.Revision -> revisionBuffer.append(c) + ParsingMode.Revision -> when { + c.isNoDigit() -> return null + else -> revisionBuffer.append(c) + } } } diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt index 7cf04cdd..533c4dba 100644 --- a/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt +++ b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt @@ -40,7 +40,7 @@ class GeographicLocationTest { } @Test - fun `test 2`() { + fun `should parse a Geo URI with crs and uncertainty correctly`() { assertThat(GeographicLocation.of("geo:48.198634,16.371648;crs=wgs84;u=40")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(48.198634) prop(GeographicLocation::longitude).isEqualTo(16.371648) @@ -52,7 +52,7 @@ class GeographicLocationTest { } @Test - fun `parse all`() { + fun `should parse a Geo URI with crs and uncertainty and a generic parameter correctly`() { assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12;param=value")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -65,7 +65,7 @@ class GeographicLocationTest { } @Test - fun `parse no params`() { + fun `should parse a Geo URI with crs and uncertainty and not match them as parameters`() { assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84;u=12")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -77,7 +77,7 @@ class GeographicLocationTest { } @Test - fun `parse no params or u`() { + fun `should parse a Geo URI with crs and not match uncertainty and parameters correctly`() { assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;crs=wgs84")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -89,7 +89,7 @@ class GeographicLocationTest { } @Test - fun `parse no params or crs`() { + fun `should parse a Geo URI with uncertainty and not match crs and parameters correctly`() { assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43;u=12")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -101,31 +101,7 @@ class GeographicLocationTest { } @Test - fun `parse no params or u or crs`() { - assertThat(GeographicLocation.of("geo:12.34,56.78,-21.43")).isNotNull().all { - prop(GeographicLocation::latitude).isEqualTo(12.34) - prop(GeographicLocation::longitude).isEqualTo(56.78) - prop(GeographicLocation::altitude).isEqualTo(-21.43) - prop(GeographicLocation::crs).isNull() - prop(GeographicLocation::uncertainty).isNull() - prop(GeographicLocation::parameters).isEmpty() - } - } - - @Test - fun `parse no params or u or crs or coordC`() { - assertThat(GeographicLocation.of("geo:12.34,56.78")).isNotNull().all { - prop(GeographicLocation::latitude).isEqualTo(12.34) - prop(GeographicLocation::longitude).isEqualTo(56.78) - prop(GeographicLocation::altitude).isNull() - prop(GeographicLocation::crs).isNull() - prop(GeographicLocation::uncertainty).isNull() - prop(GeographicLocation::parameters).isEmpty() - } - } - - @Test - fun `parse invalid uncertainty`() { + fun `should parse a Geo URI with invalid uncertainty and not match as uncertainty but as a parameter correctly`() { assertThat(GeographicLocation.of("geo:12.34,56.78;u=invalid")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -138,22 +114,27 @@ class GeographicLocationTest { } @Test - fun `parse no params or u or crs or coordsC or coordB`() { + fun `should not parse a Geo URI with only a latitude`() { assertThat(GeographicLocation.of("geo:12.34")).isNull() } @Test - fun `parse no params or u or crs or coordsC or coordB or coordA`() { + fun `should not parse a Geo URI when there is only the scheme`() { assertThat(GeographicLocation.of("geo:")).isNull() } @Test - fun `parse not geo uri(`() { + fun `should not parse an URI that is not a Geo URI`() { assertThat(GeographicLocation.of("https://stalla.dev")).isNull() } @Test - fun `parse decode special chars in param value`() { + fun `should not parse a Geo URI when the scheme is missing`() { + assertThat(GeographicLocation.of("48.198634,16.371648;crs=wgs84;u=40")).isNull() + } + + @Test + fun `should parse a Geo URI and decode special chars in param value`() { assertThat(GeographicLocation.of("geo:12.34,56.78;param=with%20%3d%20special%20&%20chars")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(56.78) @@ -166,7 +147,7 @@ class GeographicLocationTest { } @Test - fun `multiple params`() { + fun `should parse a Geo URI and match multiple parameters correctly`() { assertThat(GeographicLocation.of("geo:12.34,45.67,-21.43;crs=theCrs;u=12.0;param=value;param2=value2")).isNotNull().all { prop(GeographicLocation::latitude).isEqualTo(12.34) prop(GeographicLocation::longitude).isEqualTo(45.67) @@ -180,7 +161,7 @@ class GeographicLocationTest { } @Test - fun `WGS84 pole rule`() { + fun `should match Geo URIs when WGS-84 special pole case applies correctly`() { val geoLocation1 = GeographicLocation.of("geo:90,-22.43;crs=WGS84") val geoLocation2 = GeographicLocation.of("geo:90,46;crs=WGS84") assertAll { @@ -191,7 +172,7 @@ class GeographicLocationTest { } @Test - fun `should no match two Geo URIs in WGS-84 special pole case if the latitude is different`() { + fun `should not match Geo URIs in WGS-84 special pole case if the latitude is different`() { val geoLocation1 = GeographicLocation.of("geo:90,10") val geoLocation2 = GeographicLocation.of("geo:45,20") assertAll { @@ -202,7 +183,7 @@ class GeographicLocationTest { } @Test - fun `should no match two Geo URIs in WGS-84 special pole case if the latitude has a different sign`() { + fun `should no match Geo URIs in WGS-84 special pole case if the latitude has a different sign`() { val geoLocation1 = GeographicLocation.of("geo:90,10") val geoLocation2 = GeographicLocation.of("geo:-90,10") assertAll { @@ -213,7 +194,7 @@ class GeographicLocationTest { } @Test - fun `should match two Geo URIs in WGS-84 special pole case by ignoring the longitude`() { + fun `should match Geo URIs in WGS-84 special pole case by ignoring the longitude`() { val geoLocation1 = GeographicLocation.of("geo:90,5") val geoLocation2 = GeographicLocation.of("geo:90,10") assertAll { @@ -235,7 +216,7 @@ class GeographicLocationTest { } @Test - fun `should match two Geo URIs in WGS-84 special date line case if the longitude has a different sign`() { + fun `should match Geo URIs in WGS-84 special date line case if the longitude has a different sign`() { val geoLocation1 = GeographicLocation.of("geo:10,180") val geoLocation2 = GeographicLocation.of("geo:10,-180") assertAll { @@ -257,7 +238,7 @@ class GeographicLocationTest { } @Test - fun `parameters bitwise identical after percent-decoding parameter names are case insensitive`() { + fun `should match Geo URIs parameters bitwise identical after percent-decoding parameter names are case insensitive`() { val geoLocation1 = GeographicLocation.of("geo:66,30;u=6.500;FOo=this%2dthat") val geoLocation2 = GeographicLocation.of("geo:66.0,30;u=6.5;foo=this-that") assertAll { @@ -268,7 +249,7 @@ class GeographicLocationTest { } @Test - fun `parameter order is insignificant`() { + fun `should match Geo URIs with parameter order being insignificant`() { val geoLocation1 = GeographicLocation.of("geo:47,11;foo=blue;bar=white") val geoLocation2 = GeographicLocation.of("geo:47,11;bar=white;foo=blue") assertAll { @@ -279,7 +260,7 @@ class GeographicLocationTest { } @Test - fun `parameter keys are case-insensitive`() { + fun `should match Geo URIs with parameter keys being case-insensitive`() { val geoLocation1 = GeographicLocation.of("geo:22,0;bar=blue") val geoLocation2 = GeographicLocation.of("geo:22,0;BAR=blue") assertAll { @@ -290,7 +271,7 @@ class GeographicLocationTest { } @Test - fun `parameter values are case-sensitive`() { + fun `should match Geo URIs with parameter values being case-sensitive`() { val geoLocation1 = GeographicLocation.of("geo:22,0;bar=BLUE") val geoLocation2 = GeographicLocation.of("geo:22,0;bar=blue") assertAll { diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt index b02db8a6..b3e98331 100644 --- a/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt +++ b/src/test/kotlin/dev/stalla/model/podcastindex/OpenStreetMapElementTest.kt @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test class OpenStreetMapElementTest { @Test - fun `test 1`() { + fun `should parse an OpenStreetMap element with Relation type correctly`() { assertThat(OpenStreetMapElement.of("R148838")).isNotNull().all { prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Relation) prop(OpenStreetMapElement::id).isEqualTo(148838) @@ -20,7 +20,7 @@ class OpenStreetMapElementTest { } @Test - fun `test 2`() { + fun `should parse an OpenStreetMap element with Way type correctly`() { assertThat(OpenStreetMapElement.of("W5013364")).isNotNull().all { prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Way) prop(OpenStreetMapElement::id).isEqualTo(5013364) @@ -29,7 +29,16 @@ class OpenStreetMapElementTest { } @Test - fun `test 3`() { + fun `should parse an OpenStreetMap element with Node type correctly`() { + assertThat(OpenStreetMapElement.of("N45678")).isNotNull().all { + prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Node) + prop(OpenStreetMapElement::id).isEqualTo(45678) + prop(OpenStreetMapElement::revision).isNull() + } + } + + @Test + fun `should parse an OpenStreetMap element with revision correctly`() { assertThat(OpenStreetMapElement.of("R7444#188")).isNotNull().all { prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Relation) prop(OpenStreetMapElement::id).isEqualTo(7444) @@ -38,37 +47,37 @@ class OpenStreetMapElementTest { } @Test - fun `test 3_5`() { + fun `should not parse an OpenStreetMap element when there is only an invalid type`() { assertThat(OpenStreetMapElement.of("X")).isNull() } @Test - fun `test 4`() { + fun `should not parse an OpenStreetMap element when the type is invalid`() { assertThat(OpenStreetMapElement.of("X12345")).isNull() } @Test - fun `test 5`() { + fun `should not parse an OpenStreetMap element when there is only a type`() { assertThat(OpenStreetMapElement.of("R")).isNull() } @Test - fun `test 6`() { + fun `should not parse an OpenStreetMap element when there is a type and revision but the ID is missing`() { assertThat(OpenStreetMapElement.of("R#188")).isNull() } @Test - fun `test 7`() { + fun `should not parse an OpenStreetMap element when the ID is not numeric`() { assertThat(OpenStreetMapElement.of("Rabc")).isNull() } @Test - fun `test 8`() { - assertThat(OpenStreetMapElement.of("Rabc")).isNull() + fun `should not parse an OpenStreetMap element with revision when the ID is not numeric`() { + assertThat(OpenStreetMapElement.of("Rabc#123")).isNull() } @Test - fun `test 9`() { - assertThat(OpenStreetMapElement.of("Rabc#123")).isNull() + fun `should not parse an OpenStreetMap element when the revison is not numeric`() { + assertThat(OpenStreetMapElement.of("R123#abc")).isNull() } } From 1d9cd3921c4f8b14c2a990f7f8a8fd6240dfc297 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Wed, 28 Apr 2021 21:43:17 +0200 Subject: [PATCH 16/18] Rename for consistency --- ...uilder.kt => GeographicLocationBuilder.kt} | 24 +++++++-------- ...=> ValidatingGeographicLocationBuilder.kt} | 30 ++++++++----------- .../model/podcastindex/GeographicLocation.kt | 8 ++--- .../stalla/parser/GeographicLocationParser.kt | 8 ++--- 4 files changed, 33 insertions(+), 37 deletions(-) rename src/main/kotlin/dev/stalla/builder/{GeoLocationBuilder.kt => GeographicLocationBuilder.kt} (69%) rename src/main/kotlin/dev/stalla/builder/validating/{ValidatingGeoLocationBuilder.kt => ValidatingGeographicLocationBuilder.kt} (61%) diff --git a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/GeographicLocationBuilder.kt similarity index 69% rename from src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt rename to src/main/kotlin/dev/stalla/builder/GeographicLocationBuilder.kt index 362a8849..b4ab395f 100644 --- a/src/main/kotlin/dev/stalla/builder/GeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/GeographicLocationBuilder.kt @@ -8,39 +8,39 @@ import dev.stalla.util.whenNotNull * * @since 1.1.0 */ -public interface GeoLocationBuilder : Builder { +public interface GeographicLocationBuilder : Builder { /** Set the latitude value. */ - public fun latitude(latitude: Double): GeoLocationBuilder + public fun latitude(latitude: Double): GeographicLocationBuilder /** Set the longitude value. */ - public fun longitude(longitude: Double): GeoLocationBuilder + public fun longitude(longitude: Double): GeographicLocationBuilder /** Set the altitude value. */ - public fun altitude(altitude: Double?): GeoLocationBuilder + public fun altitude(altitude: Double?): GeographicLocationBuilder /** Set the crs value. */ - public fun crs(crs: String?): GeoLocationBuilder + public fun crs(crs: String?): GeographicLocationBuilder /** Set the uncertainty value. */ - public fun uncertainty(uncertainty: Double?): GeoLocationBuilder + public fun uncertainty(uncertainty: Double?): GeographicLocationBuilder /** Adds a [GeographicLocation.Parameter] based on [key] and [value] to the list of parameters. */ - public fun addParameter(key: String, value: String): GeoLocationBuilder + public fun addParameter(key: String, value: String): GeographicLocationBuilder /** Adds a [GeographicLocation.Parameter] to the list of parameters. */ - public fun addParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder = + public fun addParameter(parameter: GeographicLocation.Parameter): GeographicLocationBuilder = apply { addParameter(parameter.key, parameter.value) } /** Adds all [GeographicLocation.Parameter] to the list of parameters. */ - public fun addAllParameters(parameters: List): GeoLocationBuilder = + public fun addAllParameters(parameters: List): GeographicLocationBuilder = apply { parameters.forEach(::addParameter) } /** Removes the parameter with [key] from the list of parameters. */ - public fun removeParameter(key: String): GeoLocationBuilder + public fun removeParameter(key: String): GeographicLocationBuilder /** Removes [parameter] from the list of parameters. */ - public fun removeParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder + public fun removeParameter(parameter: GeographicLocation.Parameter): GeographicLocationBuilder /** Returns `true` if the coordA property is set. */ public fun hasLatitude(): Boolean @@ -51,7 +51,7 @@ public interface GeoLocationBuilder : Builder { /** Returns `true` if the coordC property is set. */ public fun hasAltitude(): Boolean - override fun applyFrom(prototype: GeographicLocation?): GeoLocationBuilder = + override fun applyFrom(prototype: GeographicLocation?): GeographicLocationBuilder = whenNotNull(prototype) { location -> latitude(location.latitude) longitude(location.longitude) diff --git a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilder.kt similarity index 61% rename from src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt rename to src/main/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilder.kt index 626a9d42..0c70a539 100644 --- a/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeoLocationBuilder.kt +++ b/src/main/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilder.kt @@ -1,13 +1,13 @@ package dev.stalla.builder.validating -import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.builder.GeographicLocationBuilder import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.model.podcastindex.GeographicLocation.Factory.CRS_PARAM import dev.stalla.model.podcastindex.GeographicLocation.Factory.UNCERTAINTY_PARAM import dev.stalla.util.InternalAPI @InternalAPI -internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { +internal class ValidatingGeographicLocationBuilder : GeographicLocationBuilder { private var latitude: Double? = null private var longitude: Double? = null @@ -16,17 +16,17 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { private var uncertainty: Double? = null private var parameters: MutableMap = mutableMapOf() - override fun latitude(latitude: Double): GeoLocationBuilder = apply { this.latitude = latitude } + override fun latitude(latitude: Double): GeographicLocationBuilder = apply { this.latitude = latitude } - override fun longitude(longitude: Double): GeoLocationBuilder = apply { this.longitude = longitude } + override fun longitude(longitude: Double): GeographicLocationBuilder = apply { this.longitude = longitude } - override fun altitude(altitude: Double?): GeoLocationBuilder = apply { this.altitude = altitude } + override fun altitude(altitude: Double?): GeographicLocationBuilder = apply { this.altitude = altitude } - override fun crs(crs: String?): GeoLocationBuilder = apply { this.crs = crs } + override fun crs(crs: String?): GeographicLocationBuilder = apply { this.crs = crs } - override fun uncertainty(uncertainty: Double?): GeoLocationBuilder = apply { this.uncertainty = uncertainty } + override fun uncertainty(uncertainty: Double?): GeographicLocationBuilder = apply { this.uncertainty = uncertainty } - override fun addParameter(key: String, value: String): GeoLocationBuilder = apply { + override fun addParameter(key: String, value: String): GeographicLocationBuilder = apply { if (CRS_PARAM.equals(key, ignoreCase = true)) { crs(value) return@apply @@ -42,17 +42,13 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { parameters[key] = value } - override fun removeParameter(key: String): GeoLocationBuilder = apply { - if (CRS_PARAM.equals(key, ignoreCase = true)) { - crs(null) - } - if (UNCERTAINTY_PARAM.equals(key, ignoreCase = true)) { - uncertainty(null) - } + override fun removeParameter(key: String): GeographicLocationBuilder = apply { + if (CRS_PARAM.equals(key, ignoreCase = true)) crs(null) + if (UNCERTAINTY_PARAM.equals(key, ignoreCase = true)) uncertainty(null) parameters.remove(key) } - override fun removeParameter(parameter: GeographicLocation.Parameter): GeoLocationBuilder = + override fun removeParameter(parameter: GeographicLocation.Parameter): GeographicLocationBuilder = removeParameter(parameter.key) override fun hasLatitude(): Boolean = latitude != null @@ -73,7 +69,7 @@ internal class ValidatingGeoLocationBuilder : GeoLocationBuilder { altitude = altitude, crs = crs, uncertainty = uncertainty, - parameters = parameters // this secondary constructor will apply .asUnmodifiable() + parameters = parameters // secondary constructor will apply .asUnmodifiable() ) } } diff --git a/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt index 53fa3074..c66e5912 100644 --- a/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt +++ b/src/main/kotlin/dev/stalla/model/podcastindex/GeographicLocation.kt @@ -1,7 +1,7 @@ package dev.stalla.model.podcastindex -import dev.stalla.builder.GeoLocationBuilder -import dev.stalla.builder.validating.ValidatingGeoLocationBuilder +import dev.stalla.builder.GeographicLocationBuilder +import dev.stalla.builder.validating.ValidatingGeographicLocationBuilder import dev.stalla.model.BuilderFactory import dev.stalla.model.TypeFactory import dev.stalla.model.podcastindex.GeographicLocation.Factory.CRS_WGS84 @@ -227,7 +227,7 @@ public class GeographicLocation public constructor( } /** Provides a builder and a factory for the [GeographicLocation] class. */ - public companion object Factory : BuilderFactory, TypeFactory { + public companion object Factory : BuilderFactory, TypeFactory { /** The World Geodetic System 1984 coordinate reference system used by GPS. */ public const val CRS_WGS84: String = "WGS84" @@ -239,7 +239,7 @@ public class GeographicLocation public constructor( /** Returns a builder implementation for building [GeographicLocation] model instances. */ @JvmStatic - override fun builder(): GeoLocationBuilder = ValidatingGeoLocationBuilder() + override fun builder(): GeographicLocationBuilder = ValidatingGeographicLocationBuilder() @JvmStatic override fun of(rawValue: String?): GeographicLocation? = rawValue?.let { value -> GeographicLocationParser.parse(value) } diff --git a/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt b/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt index 4c9bd258..e827552e 100644 --- a/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt +++ b/src/main/kotlin/dev/stalla/parser/GeographicLocationParser.kt @@ -1,6 +1,6 @@ package dev.stalla.parser -import dev.stalla.builder.GeoLocationBuilder +import dev.stalla.builder.GeographicLocationBuilder import dev.stalla.model.podcastindex.GeographicLocation import dev.stalla.util.InternalAPI import java.util.regex.Pattern @@ -76,7 +76,7 @@ internal object GeographicLocationParser { null } - private fun GeoLocationBuilder.handleEndOfCoordinate(buffer: StringBuilder) { + private fun GeographicLocationBuilder.handleEndOfCoordinate(buffer: StringBuilder) { val symbol: String = buffer.getAndClear() val coordinate = symbol.asDoubleOrNull() ?: return if (!hasLatitude()) { @@ -93,7 +93,7 @@ internal object GeographicLocationParser { } } - private fun GeoLocationBuilder.handleEndOfParameter(buffer: StringBuilder, paramName: String?) { + private fun GeographicLocationBuilder.handleEndOfParameter(buffer: StringBuilder, paramName: String?) { val symbol = buffer.getAndClear() if (paramName == null) { if (symbol.isNotEmpty()) { @@ -104,7 +104,7 @@ internal object GeographicLocationParser { addParameterDecodeValue(paramName, symbol) } - private fun GeoLocationBuilder.addParameterDecodeValue(name: String, value: String?) { + private fun GeographicLocationBuilder.addParameterDecodeValue(name: String, value: String?) { if (value == null) { removeParameter(name) return From 3027242a337e953c5d5ae4ebca4911f90adf5d96 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Wed, 28 Apr 2021 22:10:41 +0200 Subject: [PATCH 17/18] Add builder tests for GeographicLocation and OpenStreetMapElement --- ...ValidatingGeographicLocationBuilderTest.kt | 57 ++++++++++++++++++ ...lidatingOpenStreetMapElementBuilderTest.kt | 58 +++++++++++++++++++ ...lidatingPodcastindexLocationBuilderTest.kt | 8 +-- .../dev/stalla/model/EpisodeFixtures.kt | 2 +- src/test/kotlin/dev/stalla/model/Fixtures.kt | 2 +- .../dev/stalla/model/PodcastFixtures.kt | 2 +- 6 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt create mode 100644 src/test/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilderTest.kt diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt new file mode 100644 index 00000000..c6b9657c --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt @@ -0,0 +1,57 @@ +package dev.stalla.builder.validating + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.GeographicLocationBuilder +import dev.stalla.model.aPodcastindexGeographicLocation +import dev.stalla.model.podcastindex.GeographicLocation +import org.junit.jupiter.api.Test + +internal class ValidatingGeographicLocationBuilderTest { + + @Test + internal fun `should not build a GeographicLocation when the mandatory fields are missing`() { + val geoBuilder = ValidatingGeographicLocationBuilder() + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(geoBuilder.build()).isNull() + } + } + + @Test + internal fun `should build a GeographicLocation with all the mandatory href field`() { + val geoBuilder = ValidatingGeographicLocationBuilder() + .latitude(1.0) + .longitude(2.0) + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(geoBuilder.build()).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(1.0) + prop(GeographicLocation::longitude).isEqualTo(2.0) + } + } + } + + @Test + internal fun `should populate a GeographicLocation builder with all properties from an GeographicLocation model`() { + val geo = aPodcastindexGeographicLocation() + val geoBuilder = GeographicLocation.builder().applyFrom(geo) + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(geoBuilder.build()).isNotNull().isEqualTo(geo) + } + } +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilderTest.kt new file mode 100644 index 00000000..f6532244 --- /dev/null +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingOpenStreetMapElementBuilderTest.kt @@ -0,0 +1,58 @@ +package dev.stalla.builder.validating + +import assertk.all +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import dev.stalla.builder.OpenStreetMapElementBuilder +import dev.stalla.model.aPodcastindexOpenStreetMapElement +import dev.stalla.model.podcastindex.OpenStreetMapElement +import dev.stalla.model.podcastindex.OpenStreetMapElementType +import org.junit.jupiter.api.Test + +internal class ValidatingOpenStreetMapElementBuilderTest { + + @Test + internal fun `should not build an OpenStreetMap element when the mandatory fields are missing`() { + val osmBuilder = ValidatingOpenStreetMapElementBuilder() + + assertAll { + assertThat(osmBuilder).prop(OpenStreetMapElementBuilder::hasEnoughDataToBuild).isFalse() + + assertThat(osmBuilder.build()).isNull() + } + } + + @Test + internal fun `should build a OpenStreetMap element with all the mandatory href field`() { + val osmBuilder = ValidatingOpenStreetMapElementBuilder() + .type(OpenStreetMapElementType.Relation) + .id(1L) + + assertAll { + assertThat(osmBuilder).prop(OpenStreetMapElementBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(osmBuilder.build()).isNotNull().all { + prop(OpenStreetMapElement::type).isEqualTo(OpenStreetMapElementType.Relation) + prop(OpenStreetMapElement::id).isEqualTo(1L) + } + } + } + + @Test + internal fun `should populate a OpenStreetMap element builder with all properties from an GeographicLocation model`() { + val osm = aPodcastindexOpenStreetMapElement() + val osmBuilder = OpenStreetMapElement.builder().applyFrom(osm) + + assertAll { + assertThat(osmBuilder).prop(OpenStreetMapElementBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(osmBuilder.build()).isNotNull().isEqualTo(osm) + } + } +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt index 3714bf1e..d05e4eee 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingPodcastindexLocationBuilderTest.kt @@ -10,7 +10,7 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.PodcastindexLocationBuilder -import dev.stalla.model.aPodcastindexGeoLocation +import dev.stalla.model.aPodcastindexGeographicLocation import dev.stalla.model.aPodcastindexOpenStreetMapElement import dev.stalla.model.anEpisodePodcastindexLocation import dev.stalla.model.podcastindex.PodcastindexLocation @@ -32,7 +32,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { @Test internal fun `should not build an Podcastindex Location with when the name field is missing`() { val locationBuilder = ValidatingPodcastindexLocationBuilder() - .geo(aPodcastindexGeoLocation()) + .geo(aPodcastindexGeographicLocation()) assertAll { assertThat(locationBuilder).prop(PodcastindexLocationBuilder::hasEnoughDataToBuild).isFalse() @@ -61,7 +61,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { internal fun `should build an Podcastindex Location with all the added entries to its fields`() { val locationBuilder = ValidatingPodcastindexLocationBuilder() .name("name") - .geo(aPodcastindexGeoLocation()) + .geo(aPodcastindexGeographicLocation()) .osm(aPodcastindexOpenStreetMapElement()) assertAll { @@ -69,7 +69,7 @@ internal class ValidatingPodcastindexLocationBuilderTest { assertThat(locationBuilder.build()).isNotNull().all { prop(PodcastindexLocation::name).isEqualTo("name") - prop(PodcastindexLocation::geo).isEqualTo(aPodcastindexGeoLocation()) + prop(PodcastindexLocation::geo).isEqualTo(aPodcastindexGeographicLocation()) prop(PodcastindexLocation::osm).isEqualTo(aPodcastindexOpenStreetMapElement()) } } diff --git a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt index cbeb2818..73cab6e6 100644 --- a/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/EpisodeFixtures.kt @@ -180,7 +180,7 @@ internal fun anEpisodePodcastindexPerson( @JvmOverloads internal fun anEpisodePodcastindexLocation( name: String = "episode podcastindex location name", - geo: GeographicLocation? = aPodcastindexGeoLocation(), + geo: GeographicLocation? = aPodcastindexGeographicLocation(), osm: OpenStreetMapElement? = aPodcastindexOpenStreetMapElement() ) = PodcastindexLocation(name, geo, osm) diff --git a/src/test/kotlin/dev/stalla/model/Fixtures.kt b/src/test/kotlin/dev/stalla/model/Fixtures.kt index 3f7a0c92..8d21d71f 100644 --- a/src/test/kotlin/dev/stalla/model/Fixtures.kt +++ b/src/test/kotlin/dev/stalla/model/Fixtures.kt @@ -65,7 +65,7 @@ internal fun aGoogleplayCategory( ) = category @JvmOverloads -internal fun aPodcastindexGeoLocation( +internal fun aPodcastindexGeographicLocation( coordA: Double = 48.20849, coordB: Double = 16.37208, coordC: Double? = 5.0, diff --git a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt index 706d1dd5..ab482360 100644 --- a/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt +++ b/src/test/kotlin/dev/stalla/model/PodcastFixtures.kt @@ -160,6 +160,6 @@ internal fun aPodcastPodcastindexPerson( @JvmOverloads internal fun aPodcastPodcastindexLocation( name: String = "podcast podcastindex location name", - geo: GeographicLocation? = aPodcastindexGeoLocation(), + geo: GeographicLocation? = aPodcastindexGeographicLocation(), osm: OpenStreetMapElement? = aPodcastindexOpenStreetMapElement() ) = PodcastindexLocation(name, geo, osm) From d2135d8df9d7bb4a9942280ec46b437c44043c27 Mon Sep 17 00:00:00 2001 From: Maximilian Irro Date: Thu, 29 Apr 2021 19:44:30 +0200 Subject: [PATCH 18/18] Add some and improve other tests (more assertk idiomatic) --- api/stalla.api | 235 ++++++++++++++---- src/test/kotlin/dev/stalla/Assertions.kt | 40 +++ ...ValidatingGeographicLocationBuilderTest.kt | 60 +++++ .../kotlin/dev/stalla/model/MediaTypeTest.kt | 14 +- .../podcastindex/GeographicLocationTest.kt | 11 +- .../model/podcastindex/TranscriptTypeTest.kt | 8 +- 6 files changed, 307 insertions(+), 61 deletions(-) diff --git a/api/stalla.api b/api/stalla.api index 4f485d9c..e9e6f9af 100644 --- a/api/stalla.api +++ b/api/stalla.api @@ -50,6 +50,29 @@ public abstract interface class dev/stalla/builder/Builder { public abstract fun getHasEnoughDataToBuild ()Z } +public abstract interface class dev/stalla/builder/GeographicLocationBuilder : dev/stalla/builder/Builder { + public abstract fun addAllParameters (Ljava/util/List;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun addParameter (Ldev/stalla/model/podcastindex/GeographicLocation$Parameter;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun addParameter (Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun altitude (Ljava/lang/Double;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/GeographicLocation;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun crs (Ljava/lang/String;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun hasAltitude ()Z + public abstract fun hasLatitude ()Z + public abstract fun hasLongitude ()Z + public abstract fun latitude (D)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun longitude (D)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun removeParameter (Ldev/stalla/model/podcastindex/GeographicLocation$Parameter;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun removeParameter (Ljava/lang/String;)Ldev/stalla/builder/GeographicLocationBuilder; + public abstract fun uncertainty (Ljava/lang/Double;)Ldev/stalla/builder/GeographicLocationBuilder; +} + +public final class dev/stalla/builder/GeographicLocationBuilder$DefaultImpls { + public static fun addAllParameters (Ldev/stalla/builder/GeographicLocationBuilder;Ljava/util/List;)Ldev/stalla/builder/GeographicLocationBuilder; + public static fun addParameter (Ldev/stalla/builder/GeographicLocationBuilder;Ldev/stalla/model/podcastindex/GeographicLocation$Parameter;)Ldev/stalla/builder/GeographicLocationBuilder; + public static fun applyFrom (Ldev/stalla/builder/GeographicLocationBuilder;Ldev/stalla/model/podcastindex/GeographicLocation;)Ldev/stalla/builder/GeographicLocationBuilder; +} + public abstract interface class dev/stalla/builder/HrefOnlyImageBuilder : dev/stalla/builder/Builder { public abstract fun applyFrom (Ldev/stalla/model/HrefOnlyImage;)Ldev/stalla/builder/HrefOnlyImageBuilder; public abstract fun href (Ljava/lang/String;)Ldev/stalla/builder/HrefOnlyImageBuilder; @@ -74,6 +97,43 @@ public final class dev/stalla/builder/LinkBuilder$DefaultImpls { public static fun applyFrom (Ldev/stalla/builder/LinkBuilder;Ldev/stalla/model/atom/Link;)Ldev/stalla/builder/LinkBuilder; } +public abstract interface class dev/stalla/builder/OpenStreetMapElementBuilder : dev/stalla/builder/Builder { + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/OpenStreetMapElement;)Ldev/stalla/builder/OpenStreetMapElementBuilder; + public abstract fun id (J)Ldev/stalla/builder/OpenStreetMapElementBuilder; + public abstract fun revision (Ljava/lang/Integer;)Ldev/stalla/builder/OpenStreetMapElementBuilder; + public abstract fun type (Ldev/stalla/model/podcastindex/OpenStreetMapElementType;)Ldev/stalla/builder/OpenStreetMapElementBuilder; +} + +public final class dev/stalla/builder/OpenStreetMapElementBuilder$DefaultImpls { + public static fun applyFrom (Ldev/stalla/builder/OpenStreetMapElementBuilder;Ldev/stalla/model/podcastindex/OpenStreetMapElement;)Ldev/stalla/builder/OpenStreetMapElementBuilder; + public static fun id (Ldev/stalla/builder/OpenStreetMapElementBuilder;J)Ldev/stalla/builder/OpenStreetMapElementBuilder; + public static fun revision (Ldev/stalla/builder/OpenStreetMapElementBuilder;Ljava/lang/Integer;)Ldev/stalla/builder/OpenStreetMapElementBuilder; +} + +public abstract interface class dev/stalla/builder/PodcastindexLocationBuilder : dev/stalla/builder/Builder { + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/PodcastindexLocation;)Ldev/stalla/builder/PodcastindexLocationBuilder; + public abstract fun geo (Ldev/stalla/model/podcastindex/GeographicLocation;)Ldev/stalla/builder/PodcastindexLocationBuilder; + public abstract fun name (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexLocationBuilder; + public abstract fun osm (Ldev/stalla/model/podcastindex/OpenStreetMapElement;)Ldev/stalla/builder/PodcastindexLocationBuilder; +} + +public final class dev/stalla/builder/PodcastindexLocationBuilder$DefaultImpls { + public static fun applyFrom (Ldev/stalla/builder/PodcastindexLocationBuilder;Ldev/stalla/model/podcastindex/PodcastindexLocation;)Ldev/stalla/builder/PodcastindexLocationBuilder; +} + +public abstract interface class dev/stalla/builder/PodcastindexPersonBuilder : dev/stalla/builder/Builder { + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/PodcastindexPerson;)Ldev/stalla/builder/PodcastindexPersonBuilder; + public abstract fun group (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexPersonBuilder; + public abstract fun href (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexPersonBuilder; + public abstract fun img (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexPersonBuilder; + public abstract fun name (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexPersonBuilder; + public abstract fun role (Ljava/lang/String;)Ldev/stalla/builder/PodcastindexPersonBuilder; +} + +public final class dev/stalla/builder/PodcastindexPersonBuilder$DefaultImpls { + public static fun applyFrom (Ldev/stalla/builder/PodcastindexPersonBuilder;Ldev/stalla/model/podcastindex/PodcastindexPerson;)Ldev/stalla/builder/PodcastindexPersonBuilder; +} + public abstract interface class dev/stalla/builder/RssCategoryBuilder : dev/stalla/builder/Builder { public abstract fun applyFrom (Ldev/stalla/model/rss/RssCategory;)Ldev/stalla/builder/RssCategoryBuilder; public abstract fun category (Ljava/lang/String;)Ldev/stalla/builder/RssCategoryBuilder; @@ -199,15 +259,21 @@ public final class dev/stalla/builder/episode/EpisodeItunesBuilder$DefaultImpls } public abstract interface class dev/stalla/builder/episode/EpisodePodcastindexBuilder : dev/stalla/builder/Builder { + public abstract fun addAllPersonBuilders (Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun addAllSoundbiteBuilders (Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun addAllTranscriptBuilders (Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; + public abstract fun addPersonBuilder (Ldev/stalla/builder/PodcastindexPersonBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun addSoundbiteBuilder (Ldev/stalla/builder/episode/EpisodePodcastindexSoundbiteBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun addTranscriptBuilder (Ldev/stalla/builder/episode/EpisodePodcastindexTranscriptBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun applyFrom (Ldev/stalla/model/podcastindex/EpisodePodcastindex;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public abstract fun chaptersBuilder (Ldev/stalla/builder/episode/EpisodePodcastindexChaptersBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; + public abstract fun episodeBuilder (Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; + public abstract fun locationBuilder (Ldev/stalla/builder/PodcastindexLocationBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; + public abstract fun seasonBuilder (Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; } public final class dev/stalla/builder/episode/EpisodePodcastindexBuilder$DefaultImpls { + public static fun addAllPersonBuilders (Ldev/stalla/builder/episode/EpisodePodcastindexBuilder;Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public static fun addAllSoundbiteBuilders (Ldev/stalla/builder/episode/EpisodePodcastindexBuilder;Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public static fun addAllTranscriptBuilders (Ldev/stalla/builder/episode/EpisodePodcastindexBuilder;Ljava/util/List;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; public static fun applyFrom (Ldev/stalla/builder/episode/EpisodePodcastindexBuilder;Ldev/stalla/model/podcastindex/EpisodePodcastindex;)Ldev/stalla/builder/episode/EpisodePodcastindexBuilder; @@ -223,6 +289,26 @@ public final class dev/stalla/builder/episode/EpisodePodcastindexChaptersBuilder public static fun applyFrom (Ldev/stalla/builder/episode/EpisodePodcastindexChaptersBuilder;Ldev/stalla/model/podcastindex/Chapters;)Ldev/stalla/builder/episode/EpisodePodcastindexChaptersBuilder; } +public abstract interface class dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder : dev/stalla/builder/Builder { + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/PodcastindexEpisode;)Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; + public abstract fun display (Ljava/lang/String;)Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; + public abstract fun number (D)Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; +} + +public final class dev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder$DefaultImpls { + public static fun applyFrom (Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder;Ldev/stalla/model/podcastindex/PodcastindexEpisode;)Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; +} + +public abstract interface class dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder : dev/stalla/builder/Builder { + public abstract fun applyFrom (Ldev/stalla/model/podcastindex/PodcastindexSeason;)Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; + public abstract fun name (Ljava/lang/String;)Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; + public abstract fun number (I)Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; +} + +public final class dev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder$DefaultImpls { + public static fun applyFrom (Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder;Ldev/stalla/model/podcastindex/PodcastindexSeason;)Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; +} + public abstract interface class dev/stalla/builder/episode/EpisodePodcastindexSoundbiteBuilder : dev/stalla/builder/Builder { public abstract fun applyFrom (Ldev/stalla/model/podcastindex/Soundbite;)Ldev/stalla/builder/episode/EpisodePodcastindexSoundbiteBuilder; public abstract fun duration (Ldev/stalla/model/StyledDuration$SecondsAndFraction;)Ldev/stalla/builder/episode/EpisodePodcastindexSoundbiteBuilder; @@ -376,13 +462,17 @@ public final class dev/stalla/builder/podcast/PodcastItunesOwnerBuilder$DefaultI public abstract interface class dev/stalla/builder/podcast/PodcastPodcastindexBuilder : dev/stalla/builder/Builder { public abstract fun addAllFundingBuilders (Ljava/util/List;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; + public abstract fun addAllPersonBuilders (Ljava/util/List;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; public abstract fun addFundingBuilder (Ldev/stalla/builder/podcast/PodcastPodcastindexFundingBuilder;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; + public abstract fun addPersonBuilder (Ldev/stalla/builder/PodcastindexPersonBuilder;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; public abstract fun applyFrom (Ldev/stalla/model/podcastindex/PodcastPodcastindex;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; + public abstract fun locationBuilder (Ldev/stalla/builder/PodcastindexLocationBuilder;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; public abstract fun lockedBuilder (Ldev/stalla/builder/podcast/PodcastPodcastindexLockedBuilder;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; } public final class dev/stalla/builder/podcast/PodcastPodcastindexBuilder$DefaultImpls { public static fun addAllFundingBuilders (Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder;Ljava/util/List;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; + public static fun addAllPersonBuilders (Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder;Ljava/util/List;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; public static fun applyFrom (Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder;Ldev/stalla/model/podcastindex/PodcastPodcastindex;)Ldev/stalla/builder/podcast/PodcastPodcastindexBuilder; } @@ -1311,26 +1401,43 @@ public final class dev/stalla/model/podcastindex/Funding$Factory : dev/stalla/mo public fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexFundingBuilder; } -public final class dev/stalla/model/podcastindex/GeoLocation { - public static final field Factory Ldev/stalla/model/podcastindex/GeoLocation$Factory; - public final fun getCoordA ()D - public final fun getCoordB ()D - public final fun getCoordC ()Ljava/lang/Double; - public final fun getParameters ()Ljava/util/Map; - public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation; +public final class dev/stalla/model/podcastindex/GeographicLocation { + public static final field CRS_WGS84 Ljava/lang/String; + public static final field Factory Ldev/stalla/model/podcastindex/GeographicLocation$Factory; + public fun (DDLjava/lang/Double;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;)V + public synthetic fun (DDLjava/lang/Double;Ljava/lang/String;Ljava/lang/Double;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (DDLjava/lang/Double;Ljava/lang/String;Ljava/lang/Double;Ljava/util/Map;)V + public static fun builder ()Ldev/stalla/builder/GeographicLocationBuilder; + public fun equals (Ljava/lang/Object;)Z + public final fun getAltitude ()Ljava/lang/Double; + public final fun getCrs ()Ljava/lang/String; + public final fun getLatitude ()D + public final fun getLongitude ()D + public final fun getParameters ()Ljava/util/List; + public final fun getUncertainty ()Ljava/lang/Double; + public fun hashCode ()I + public final fun match (Ldev/stalla/model/podcastindex/GeographicLocation;)Z + public final fun match (Ljava/lang/String;)Z + public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeographicLocation; + public final fun parameter (Ljava/lang/String;)Ljava/lang/String; + public fun toString ()Ljava/lang/String; + public final fun withParameter (Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeographicLocation; + public final fun withoutParameters ()Ldev/stalla/model/podcastindex/GeographicLocation; } -public final class dev/stalla/model/podcastindex/GeoLocation$Factory : dev/stalla/model/TypeFactory { - public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation; +public final class dev/stalla/model/podcastindex/GeographicLocation$Factory : dev/stalla/model/BuilderFactory, dev/stalla/model/TypeFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/GeographicLocationBuilder; + public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeographicLocation; public synthetic fun of (Ljava/lang/String;)Ljava/lang/Object; } -public final class dev/stalla/model/podcastindex/GeoLocation$Parameter { +public final class dev/stalla/model/podcastindex/GeographicLocation$Parameter { public fun (Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeoLocation$Parameter; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/GeoLocation$Parameter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/GeoLocation$Parameter; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Ldev/stalla/model/podcastindex/GeographicLocation$Parameter; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/GeographicLocation$Parameter;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/GeographicLocation$Parameter; public fun equals (Ljava/lang/Object;)Z public final fun getKey ()Ljava/lang/String; public final fun getValue ()Ljava/lang/String; @@ -1358,34 +1465,46 @@ public final class dev/stalla/model/podcastindex/Locked$Factory : dev/stalla/mod public fun builder ()Ldev/stalla/builder/podcast/PodcastPodcastindexLockedBuilder; } -public final class dev/stalla/model/podcastindex/OpenStreetMapFeature { - public fun (Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;)V - public final fun component1 ()Ldev/stalla/model/podcastindex/OsmType; - public final fun component2 ()I - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapFeature; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/OpenStreetMapFeature;Ldev/stalla/model/podcastindex/OsmType;ILjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/OpenStreetMapFeature; +public final class dev/stalla/model/podcastindex/OpenStreetMapElement { + public static final field Factory Ldev/stalla/model/podcastindex/OpenStreetMapElement$Factory; + public fun (Ldev/stalla/model/podcastindex/OpenStreetMapElementType;JLjava/lang/Integer;)V + public static fun builder ()Ldev/stalla/builder/OpenStreetMapElementBuilder; + public final fun component1 ()Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public final fun component2 ()J + public final fun component3 ()Ljava/lang/Integer; + public final fun copy (Ldev/stalla/model/podcastindex/OpenStreetMapElementType;JLjava/lang/Integer;)Ldev/stalla/model/podcastindex/OpenStreetMapElement; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/OpenStreetMapElement;Ldev/stalla/model/podcastindex/OpenStreetMapElementType;JLjava/lang/Integer;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/OpenStreetMapElement; public fun equals (Ljava/lang/Object;)Z - public final fun getOsmId ()I - public final fun getOsmRevision ()Ljava/lang/String; - public final fun getOsmType ()Ldev/stalla/model/podcastindex/OsmType; + public final fun getId ()J + public final fun getRevision ()Ljava/lang/Integer; + public final fun getType ()Ldev/stalla/model/podcastindex/OpenStreetMapElementType; public fun hashCode ()I + public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapElement; public fun toString ()Ljava/lang/String; } -public final class dev/stalla/model/podcastindex/OsmType : java/lang/Enum { - public static final field Factory Ldev/stalla/model/podcastindex/OsmType$Factory; - public static final field Node Ldev/stalla/model/podcastindex/OsmType; - public static final field Relation Ldev/stalla/model/podcastindex/OsmType; - public static final field Way Ldev/stalla/model/podcastindex/OsmType; +public final class dev/stalla/model/podcastindex/OpenStreetMapElement$Factory : dev/stalla/model/BuilderFactory, dev/stalla/model/TypeFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/OpenStreetMapElementBuilder; + public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapElement; + public synthetic fun of (Ljava/lang/String;)Ljava/lang/Object; +} + +public final class dev/stalla/model/podcastindex/OpenStreetMapElementType : java/lang/Enum { + public static final field Factory Ldev/stalla/model/podcastindex/OpenStreetMapElementType$Factory; + public static final field Node Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public static final field Relation Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public static final field Way Ldev/stalla/model/podcastindex/OpenStreetMapElementType; public final fun getType ()Ljava/lang/String; - public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; - public static fun valueOf (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; - public static fun values ()[Ldev/stalla/model/podcastindex/OsmType; + public static final fun of (Ljava/lang/Character;)Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public static fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public static fun valueOf (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public static fun values ()[Ldev/stalla/model/podcastindex/OpenStreetMapElementType; } -public final class dev/stalla/model/podcastindex/OsmType$Factory : dev/stalla/model/TypeFactory { - public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OsmType; +public final class dev/stalla/model/podcastindex/OpenStreetMapElementType$Factory : dev/stalla/model/TypeFactory { + public final fun of (Ljava/lang/Character;)Ldev/stalla/model/podcastindex/OpenStreetMapElementType; + public fun of (Ljava/lang/String;)Ldev/stalla/model/podcastindex/OpenStreetMapElementType; public synthetic fun of (Ljava/lang/String;)Ljava/lang/Object; } @@ -1416,7 +1535,9 @@ public final class dev/stalla/model/podcastindex/PodcastPodcastindex$Factory : d } public final class dev/stalla/model/podcastindex/PodcastindexEpisode { + public static final field Factory Ldev/stalla/model/podcastindex/PodcastindexEpisode$Factory; public fun (DLjava/lang/String;)V + public static fun builder ()Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; public final fun component1 ()D public final fun component2 ()Ljava/lang/String; public final fun copy (DLjava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexEpisode; @@ -1428,23 +1549,37 @@ public final class dev/stalla/model/podcastindex/PodcastindexEpisode { public fun toString ()Ljava/lang/String; } +public final class dev/stalla/model/podcastindex/PodcastindexEpisode$Factory : dev/stalla/model/BuilderFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/episode/EpisodePodcastindexEpisodeBuilder; +} + public final class dev/stalla/model/podcastindex/PodcastindexLocation { - public fun (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;)V + public static final field Factory Ldev/stalla/model/podcastindex/PodcastindexLocation$Factory; + public fun (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeographicLocation;Ldev/stalla/model/podcastindex/OpenStreetMapElement;)V + public static fun builder ()Ldev/stalla/builder/PodcastindexLocationBuilder; public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ldev/stalla/model/podcastindex/GeoLocation; - public final fun component3 ()Ldev/stalla/model/podcastindex/OpenStreetMapFeature; - public final fun copy (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;)Ldev/stalla/model/podcastindex/PodcastindexLocation; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexLocation;Ljava/lang/String;Ldev/stalla/model/podcastindex/GeoLocation;Ldev/stalla/model/podcastindex/OpenStreetMapFeature;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexLocation; + public final fun component2 ()Ldev/stalla/model/podcastindex/GeographicLocation; + public final fun component3 ()Ldev/stalla/model/podcastindex/OpenStreetMapElement; + public final fun copy (Ljava/lang/String;Ldev/stalla/model/podcastindex/GeographicLocation;Ldev/stalla/model/podcastindex/OpenStreetMapElement;)Ldev/stalla/model/podcastindex/PodcastindexLocation; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexLocation;Ljava/lang/String;Ldev/stalla/model/podcastindex/GeographicLocation;Ldev/stalla/model/podcastindex/OpenStreetMapElement;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexLocation; public fun equals (Ljava/lang/Object;)Z - public final fun getGeo ()Ldev/stalla/model/podcastindex/GeoLocation; + public final fun getGeo ()Ldev/stalla/model/podcastindex/GeographicLocation; public final fun getName ()Ljava/lang/String; - public final fun getOsm ()Ldev/stalla/model/podcastindex/OpenStreetMapFeature; + public final fun getOsm ()Ldev/stalla/model/podcastindex/OpenStreetMapElement; public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class dev/stalla/model/podcastindex/PodcastindexLocation$Factory : dev/stalla/model/BuilderFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/PodcastindexLocationBuilder; +} + public final class dev/stalla/model/podcastindex/PodcastindexPerson { + public static final field Factory Ldev/stalla/model/podcastindex/PodcastindexPerson$Factory; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public static fun builder ()Ldev/stalla/builder/PodcastindexPersonBuilder; public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; @@ -1462,19 +1597,31 @@ public final class dev/stalla/model/podcastindex/PodcastindexPerson { public fun toString ()Ljava/lang/String; } +public final class dev/stalla/model/podcastindex/PodcastindexPerson$Factory : dev/stalla/model/BuilderFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/PodcastindexPersonBuilder; +} + public final class dev/stalla/model/podcastindex/PodcastindexSeason { - public fun (DLjava/lang/String;)V - public final fun component1 ()D + public static final field Factory Ldev/stalla/model/podcastindex/PodcastindexSeason$Factory; + public fun (ILjava/lang/String;)V + public static fun builder ()Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; + public final fun component1 ()I public final fun component2 ()Ljava/lang/String; - public final fun copy (DLjava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexSeason; - public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexSeason;DLjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexSeason; + public final fun copy (ILjava/lang/String;)Ldev/stalla/model/podcastindex/PodcastindexSeason; + public static synthetic fun copy$default (Ldev/stalla/model/podcastindex/PodcastindexSeason;ILjava/lang/String;ILjava/lang/Object;)Ldev/stalla/model/podcastindex/PodcastindexSeason; public fun equals (Ljava/lang/Object;)Z public final fun getName ()Ljava/lang/String; - public final fun getNumber ()D + public final fun getNumber ()I public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class dev/stalla/model/podcastindex/PodcastindexSeason$Factory : dev/stalla/model/BuilderFactory { + public synthetic fun builder ()Ldev/stalla/builder/Builder; + public fun builder ()Ldev/stalla/builder/episode/EpisodePodcastindexSeasonBuilder; +} + public final class dev/stalla/model/podcastindex/Soundbite { public static final field Factory Ldev/stalla/model/podcastindex/Soundbite$Factory; public fun (Ldev/stalla/model/StyledDuration$SecondsAndFraction;Ldev/stalla/model/StyledDuration$SecondsAndFraction;Ljava/lang/String;)V diff --git a/src/test/kotlin/dev/stalla/Assertions.kt b/src/test/kotlin/dev/stalla/Assertions.kt index 893ff5f0..0ee9d294 100644 --- a/src/test/kotlin/dev/stalla/Assertions.kt +++ b/src/test/kotlin/dev/stalla/Assertions.kt @@ -231,3 +231,43 @@ internal fun Assert.matchPattern(expected: GeographicLocatio actual = geoLocation ) } + +/** Asserts that there exists a [MediaType.Parameter] with the expected key and value. */ +@JvmName("hasMediaTypeParameterWithValue") +internal fun Assert.hasParameterWithValue(expectedKey: String, expectedValue: String) = given { mediaType -> + val actualValue = mediaType.parameter(expectedKey) + if (actualValue != null && actualValue == expectedValue) return@given + if (actualValue == null) { + expected( + "to be have a value for key: '$expectedKey'", + expected = expectedValue, + actual = actualValue + ) + } else { + expected( + "to be: $expectedKey=$expectedValue but was: $expectedKey=$actualValue", + expected = expectedValue, + actual = actualValue + ) + } +} + +/** Asserts that there exists a [GeographicLocation.Parameter] with the expected key and value. */ +@JvmName("hasGeographicLocationParameterWithValue") +internal fun Assert.hasParameterWithValue(expectedKey: String, expectedValue: String) = given { geo -> + val actualValue = geo.parameter(expectedKey) + if (actualValue != null && actualValue == expectedValue) return@given + if (actualValue == null) { + expected( + "to be have a value for key: '$expectedKey'", + expected = expectedValue, + actual = actualValue + ) + } else { + expected( + "to be: $expectedKey=$expectedValue but was: $expectedKey=$actualValue", + expected = expectedValue, + actual = actualValue + ) + } +} diff --git a/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt b/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt index c6b9657c..fc182960 100644 --- a/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt +++ b/src/test/kotlin/dev/stalla/builder/validating/ValidatingGeographicLocationBuilderTest.kt @@ -3,6 +3,7 @@ package dev.stalla.builder.validating import assertk.all import assertk.assertAll import assertk.assertThat +import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull @@ -10,6 +11,7 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import dev.stalla.builder.GeographicLocationBuilder +import dev.stalla.hasParameterWithValue import dev.stalla.model.aPodcastindexGeographicLocation import dev.stalla.model.podcastindex.GeographicLocation import org.junit.jupiter.api.Test @@ -43,6 +45,64 @@ internal class ValidatingGeographicLocationBuilderTest { } } + @Test + internal fun `should recognize a valid GeographicLocation CRS parameter when adding it as a normal parameter`() { + val geoBuilder = ValidatingGeographicLocationBuilder() + .latitude(1.0) + .longitude(2.0) + .addParameter("crs", "TEST") + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(geoBuilder.build()).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(1.0) + prop(GeographicLocation::longitude).isEqualTo(2.0) + prop(GeographicLocation::crs).isEqualTo("TEST") + prop(GeographicLocation::parameters).hasSize(0) + } + } + } + + @Test + internal fun `should recognize a valid GeographicLocation uncertainty parameter when adding it as a normal parameter`() { + val geoBuilder = ValidatingGeographicLocationBuilder() + .latitude(1.0) + .longitude(2.0) + .addParameter("u", "4.12") + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(geoBuilder.build()).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(1.0) + prop(GeographicLocation::longitude).isEqualTo(2.0) + prop(GeographicLocation::uncertainty).isEqualTo(4.12) + prop(GeographicLocation::parameters).hasSize(0) + } + } + } + + @Test + internal fun `should treat an invalid GeographicLocation uncertainty parameter as a normal parameter when adding it as a normal parameter`() { + val geoBuilder = ValidatingGeographicLocationBuilder() + .latitude(1.0) + .longitude(2.0) + .addParameter("u", "abc") + + assertAll { + assertThat(geoBuilder).prop(GeographicLocationBuilder::hasEnoughDataToBuild).isTrue() + + assertThat(geoBuilder.build()).isNotNull().all { + prop(GeographicLocation::latitude).isEqualTo(1.0) + prop(GeographicLocation::longitude).isEqualTo(2.0) + prop(GeographicLocation::uncertainty).isNull() + prop(GeographicLocation::parameters).hasSize(1) + hasParameterWithValue("u", "abc") + } + } + } + @Test internal fun `should populate a GeographicLocation builder with all properties from an GeographicLocation model`() { val geo = aPodcastindexGeographicLocation() diff --git a/src/test/kotlin/dev/stalla/model/MediaTypeTest.kt b/src/test/kotlin/dev/stalla/model/MediaTypeTest.kt index 39072a67..55998406 100644 --- a/src/test/kotlin/dev/stalla/model/MediaTypeTest.kt +++ b/src/test/kotlin/dev/stalla/model/MediaTypeTest.kt @@ -15,9 +15,9 @@ import assertk.assertions.prop import dev.stalla.arguments import dev.stalla.doesNotMatchSymmetrically import dev.stalla.equalToString +import dev.stalla.hasParameterWithValue import dev.stalla.matchPattern import dev.stalla.matchesSymmetrically -import dev.stalla.model.podcastindex.TranscriptType import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsProvider @@ -113,7 +113,8 @@ internal class MediaTypeTest { prop(MediaType::type).isEqualTo("text") prop(MediaType::subtype).isEqualTo("plain") prop(MediaType::parameters).hasSize(1) - prop("parameter") { MediaType::parameter.call(it, "charset") }.isNotNull().isEqualTo("utf-8") + hasParameterWithValue("charset", "utf-8") +// prop("parameter") { MediaType::parameter.call(it, "charset") }.isNotNull().isEqualTo("utf-8") } } @@ -218,7 +219,7 @@ internal class MediaTypeTest { prop(MediaType::type).isEqualTo("audio") prop(MediaType::subtype).isEqualTo("*") prop(MediaType::parameters).hasSize(1) - prop("parameter") { MediaType::parameter.call(it, "attr") }.isNotNull().isEqualTo("v>alue") + hasParameterWithValue("attr", "v>alue") } } @@ -310,7 +311,7 @@ internal class MediaTypeTest { prop(MediaType::type).isEqualTo("multipart") prop(MediaType::subtype).isEqualTo("mixed") prop(MediaType::parameters).hasSize(1) - prop("parameter") { MediaType::parameter.call(it, "boundary") }.isNotNull().isEqualTo("gc0pJq0M:08jU534c0p") + hasParameterWithValue("boundary", "gc0pJq0M:08jU534c0p") } } @@ -359,7 +360,7 @@ internal class MediaTypeTest { @Test fun `should render parameters correctly`() { assertThat(MediaType.PLAIN_TEXT.withParameter("p1", "v1")).isNotNull() - .prop("toString") { MediaType::toString.call(it) }.isEqualTo("text/plain; p1=v1") + .equalToString("text/plain; p1=v1") } @Test @@ -413,8 +414,7 @@ internal class MediaTypeTest { @ParameterizedTest @ArgumentsSource(MediaTypeNameProvider::class) fun `should retrieve all predefined types from the factory method`(typeName: String) { - assertThat(MediaType.of(typeName)).isNotNull() - .prop("toString") { TranscriptType::toString.call(it) }.isEqualTo(typeName) + assertThat(MediaType.of(typeName)).isNotNull().equalToString(typeName) } @ParameterizedTest diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt index 533c4dba..7db5abeb 100644 --- a/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt +++ b/src/test/kotlin/dev/stalla/model/podcastindex/GeographicLocationTest.kt @@ -11,6 +11,7 @@ import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop +import dev.stalla.hasParameterWithValue import org.junit.jupiter.api.Test class GeographicLocationTest { @@ -60,7 +61,7 @@ class GeographicLocationTest { prop(GeographicLocation::crs).isEqualTo("wgs84") prop(GeographicLocation::uncertainty).isEqualTo(12.0) prop(GeographicLocation::parameters).hasSize(1) - prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") + hasParameterWithValue("param", "value") } } @@ -109,7 +110,7 @@ class GeographicLocationTest { prop(GeographicLocation::crs).isNull() prop(GeographicLocation::uncertainty).isNull() prop(GeographicLocation::parameters).hasSize(1) - prop("parameter") { GeographicLocation::parameter.call(it, "u") }.isNotNull().isEqualTo("invalid") + hasParameterWithValue("u", "invalid") } } @@ -142,7 +143,7 @@ class GeographicLocationTest { prop(GeographicLocation::crs).isNull() prop(GeographicLocation::uncertainty).isNull() prop(GeographicLocation::parameters).hasSize(1) - prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("with = special & chars") + hasParameterWithValue("param", "with = special & chars") } } @@ -155,8 +156,8 @@ class GeographicLocationTest { prop(GeographicLocation::crs).isEqualTo("theCrs") prop(GeographicLocation::uncertainty).isEqualTo(12.0) prop(GeographicLocation::parameters).hasSize(2) - prop("parameter") { GeographicLocation::parameter.call(it, "param") }.isNotNull().isEqualTo("value") - prop("parameter") { GeographicLocation::parameter.call(it, "param2") }.isNotNull().isEqualTo("value2") + hasParameterWithValue("param", "value") + hasParameterWithValue("param2", "value2") } } diff --git a/src/test/kotlin/dev/stalla/model/podcastindex/TranscriptTypeTest.kt b/src/test/kotlin/dev/stalla/model/podcastindex/TranscriptTypeTest.kt index 1b447e21..056cd71e 100644 --- a/src/test/kotlin/dev/stalla/model/podcastindex/TranscriptTypeTest.kt +++ b/src/test/kotlin/dev/stalla/model/podcastindex/TranscriptTypeTest.kt @@ -6,8 +6,8 @@ import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import assertk.assertions.isNull -import assertk.assertions.prop import dev.stalla.arguments +import dev.stalla.equalToString import dev.stalla.model.MediaType import dev.stalla.model.TranscriptTypeNameProvider import dev.stalla.model.allTranscriptTypeNames @@ -40,8 +40,7 @@ internal class TranscriptTypeTest { @ParameterizedTest @ArgumentsSource(TranscriptTypeNameProvider::class) fun `should retrieve all Transcript Types from the factory method`(typeName: String) { - assertThat(TranscriptType.of(typeName)).isNotNull() - .prop("toString") { TranscriptType::toString.call(it) }.isEqualTo(typeName) + assertThat(TranscriptType.of(typeName)).isNotNull().equalToString(typeName) } @ParameterizedTest @@ -77,8 +76,7 @@ internal class TranscriptTypeTest { @Test fun `should be case insensitive in the Podcastindex transcript type factory method`() { - assertThat(TranscriptType.of("TEXT/PLAIN")).isNotNull() - .prop("toString") { TranscriptType::toString.call(it) }.isEqualTo("text/plain") + assertThat(TranscriptType.of("TEXT/PLAIN")).isNotNull().equalToString("text/plain") } @Test