From fe5534b006322188080f6a8fa1d3f04bddb3b6c1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Sep 2025 09:48:34 +0300 Subject: [PATCH] Remove DUMMY parser --- CONTRIBUTING.md | 6 +- README.md | 4 +- .../kotatsu/parsers/ksp/ParserProcessor.kt | 2 - .../koitharu/kotatsu/parsers/MangaSources.kt | 4 +- .../model/search/MangaSearchQueryTest.kt | 92 ++++++------- .../parsers/util/IntentFilterGenerator.kt | 53 ++++---- .../ListFilterToSearchQueryConverterTest.kt | 106 +++++++-------- .../SearchQueryToListFilterConverterTest.kt | 126 +++++++++--------- 8 files changed, 194 insertions(+), 199 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4b873427..512979c16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,12 +63,14 @@ All members of the `MangaParser` class are documented. Pay attention to some pec - You can use _asserts_ to check some optional fields. For example, the `Manga.author` field is not required, but if your source provides this information, add `assert(it != null)`. This will not have any effect on production but help to find issues during unit testing. +- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and + responses, including image loading. - If your source website (or its API) uses pages for pagination instead of offset you should extend `PagedMangaParser` instead of `MangaParser`. - If your source website (or its API) does not provide pagination (has only one page of content) you should extend `SinglePageMangaParser` instead of `MangaParser` or `PagedMangaParser`. -- Your parser may also implement the `Interceptor` interface for additional manipulation of all network requests and - responses, including image loading. + +![parser_classes.png](docs/parser_classes.png) ## Development process diff --git a/README.md b/README.md index abfbf9784..c1beaac29 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,9 @@ JVM and Android applications. `mangaLoaderContext` is an implementation of the `MangaLoaderContext` class. See examples of [Android](https://github.com/KotatsuApp/Kotatsu/blob/devel/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt) - and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/jvmMain/kotlin/org/koitharu/kotatsu_dl/logic/MangaLoaderContextImpl.kt) + and [Non-Android](https://github.com/KotatsuApp/kotatsu-dl/blob/master/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt) implementation. - Note that the `MangaParserSource.DUMMY` parsers cannot be instantiated. - ## Projects that use the library - [Kotatsu](https://github.com/KotatsuApp/Kotatsu) diff --git a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt index 362a7ce51..c44795ca1 100644 --- a/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt +++ b/kotatsu-parsers-ksp/src/main/kotlin/org/koitharu/kotatsu/parsers/ksp/ParserProcessor.kt @@ -98,7 +98,6 @@ class ParserProcessor( factoryWriter?.write( """ - MangaParserSource.DUMMY -> throw NotImplementedError("Manga parser ${'$'}name cannot be instantiated") }.let { require(it.source == this) { "Cannot instantiate manga parser: ${'$'}name mapped to ${'$'}{it.source}" @@ -109,7 +108,6 @@ class ParserProcessor( ) sourcesWriter?.write( """ - DUMMY("Dummy", "", ContentType.OTHER, false), ; } """.trimIndent(), diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt index 3b5081f5c..752930766 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/MangaSources.kt @@ -1,8 +1,8 @@ package org.koitharu.kotatsu.parsers import org.junit.jupiter.params.provider.EnumSource -import org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE import org.koitharu.kotatsu.parsers.model.MangaParserSource -@EnumSource(MangaParserSource::class, names = ["DUMMY"], mode = EXCLUDE) +// Change 'names' to test specified parsers +@EnumSource(MangaParserSource::class, names = [], mode = EnumSource.Mode.INCLUDE) internal annotation class MangaSources diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt index 6ec3409f7..86b1757fa 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/model/search/MangaSearchQueryTest.kt @@ -12,61 +12,61 @@ import java.util.* class MangaSearchQueryCapabilitiesTest { - private val capabilities = MangaSearchQueryCapabilities( - capabilities = setOf( - SearchCapability(TITLE_NAME, setOf(Match::class), isMultiple = false, isExclusive = true), - SearchCapability(TAG, setOf(Include::class, Exclude::class), isMultiple = true, isExclusive = false), - SearchCapability(PUBLICATION_YEAR, setOf(Range::class), isMultiple = false, isExclusive = false), - SearchCapability(STATE, setOf(Include::class), isMultiple = false, isExclusive = false), - ), - ) + private val capabilities = MangaSearchQueryCapabilities( + capabilities = setOf( + SearchCapability(TITLE_NAME, setOf(Match::class), isMultiple = false, isExclusive = true), + SearchCapability(TAG, setOf(Include::class, Exclude::class), isMultiple = true, isExclusive = false), + SearchCapability(PUBLICATION_YEAR, setOf(Range::class), isMultiple = false, isExclusive = false), + SearchCapability(STATE, setOf(Include::class), isMultiple = false, isExclusive = false), + ), + ) - @Test - fun validateValidSingleCriterionQuery() { - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title")) - .build() + @Test + fun validateValidSingleCriterionQuery() { + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title")) + .build() - assertDoesNotThrow { capabilities.validate(query) } - } + assertDoesNotThrow { capabilities.validate(query) } + } - @Test - fun validateUnsupportedFieldThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH))) - .build() + @Test + fun validateUnsupportedFieldThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.ENGLISH))) + .build() - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } - @Test - fun validateUnsupportedMultiValueThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))) - .build() + @Test + fun validateUnsupportedMultiValueThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Include(STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))) + .build() - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } - @Test - fun validateMultipleCriteriaWithOtherCriteriaAllowed() { - val query = MangaSearchQuery.Builder() - .criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2")))) - .criterion(Exclude(TAG, setOf(buildTag("tag3")))) - .build() + @Test + fun validateMultipleCriteriaWithOtherCriteriaAllowed() { + val query = MangaSearchQuery.Builder() + .criterion(Include(TAG, setOf(buildTag("tag1"), buildTag("tag2")))) + .criterion(Exclude(TAG, setOf(buildTag("tag3")))) + .build() - assertDoesNotThrow { capabilities.validate(query) } - } + assertDoesNotThrow { capabilities.validate(query) } + } - @Test - fun validateMultipleCriteriaWithStrictCapabilityThrowsException() { - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title")) - .criterion(Range(PUBLICATION_YEAR, 1990, 2000)) - .build() + @Test + fun validateMultipleCriteriaWithStrictCapabilityThrowsException() { + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title")) + .criterion(Range(PUBLICATION_YEAR, 1990, 2000)) + .build() - assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } - } + assertThrows(IllegalArgumentException::class.java) { capabilities.validate(query) } + } - private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.DUMMY) + private fun buildTag(name: String) = MangaTag(title = name, key = "${name}Key", source = MangaParserSource.MANGADEX) } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt index 6b997776f..9f4501f6f 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/IntentFilterGenerator.kt @@ -8,33 +8,30 @@ import java.io.File class IntentFilterGenerator { - @Test - fun generateIntentFilter() { - val output = File("out/test/resources/intent-filter.xml") - output.printWriter(Charsets.UTF_8).use { writer -> - writer.appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - writer.appendTab().appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - writer.appendTab().appendLine("") - writer.appendTab().appendLine("") - writer.appendLine() - for (source in MangaParserSource.entries) { - if (source == MangaParserSource.DUMMY) { - continue - } - val parser = source.newParser(MangaLoaderContextMock) - parser.configKeyDomain.presetValues.forEach { domain -> - writer.appendTab().append("") - } - } - writer.appendLine() - writer.appendLine("") - } - println(output.absolutePath) - } + @Test + fun generateIntentFilter() { + val output = File("out/test/resources/intent-filter.xml") + output.printWriter(Charsets.UTF_8).use { writer -> + writer.appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + writer.appendTab().appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + writer.appendTab().appendLine("") + writer.appendTab().appendLine("") + writer.appendLine() + for (source in MangaParserSource.entries) { + val parser = source.newParser(MangaLoaderContextMock) + parser.configKeyDomain.presetValues.forEach { domain -> + writer.appendTab().append("") + } + } + writer.appendLine() + writer.appendLine("") + } + println(output.absolutePath) + } - private fun Appendable.appendTab() = append('\t') + private fun Appendable.appendTab() = append('\t') } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt index 04530beca..a6ed694ae 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/ListFilterToSearchQueryConverterTest.kt @@ -13,65 +13,65 @@ import java.util.* class ListFilterToSearchQueryConverterTest { - @Test - fun convertToMangaSearchQueryTest() { - val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val excludedTags = setOf(buildMangaTag("exclude_tag")) - val states = setOf(MangaState.ONGOING) - val contentRatings = setOf(ContentRating.SAFE) - val contentTypes = setOf(MANGA, MANHUA) - val demographics = setOf(SEINEN) + @Test + fun convertToMangaSearchQueryTest() { + val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val excludedTags = setOf(buildMangaTag("exclude_tag")) + val states = setOf(MangaState.ONGOING) + val contentRatings = setOf(ContentRating.SAFE) + val contentTypes = setOf(MANGA, MANHUA) + val demographics = setOf(SEINEN) - val filter = MangaListFilter( - query = "title_name", - tags = tags, - tagsExclude = excludedTags, - locale = Locale.ENGLISH, - originalLocale = Locale.JAPANESE, - states = states, - contentRating = contentRatings, - types = contentTypes, - demographics = demographics, - year = 2020, - yearFrom = 1997, - yearTo = 2024, - ) + val filter = MangaListFilter( + query = "title_name", + tags = tags, + tagsExclude = excludedTags, + locale = Locale.ENGLISH, + originalLocale = Locale.JAPANESE, + states = states, + contentRating = contentRatings, + types = contentTypes, + demographics = demographics, + year = 2020, + yearFrom = 1997, + yearTo = 2024, + ) - val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) + val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - val expectedQuery = MangaSearchQuery.Builder() - .offset(0) - .order(SortOrder.NEWEST) - .criterion(Match(TITLE_NAME, "title_name")) - .criterion(Include(TAG, tags)) - .criterion(Exclude(TAG, excludedTags)) - .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) - .criterion(Include(STATE, states)) - .criterion(Include(CONTENT_RATING, contentRatings)) - .criterion(Include(CONTENT_TYPE, contentTypes)) - .criterion(Include(DEMOGRAPHIC, demographics)) - .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) - .criterion(Match(PUBLICATION_YEAR, 2020)) - .build() + val expectedQuery = MangaSearchQuery.Builder() + .offset(0) + .order(SortOrder.NEWEST) + .criterion(Match(TITLE_NAME, "title_name")) + .criterion(Include(TAG, tags)) + .criterion(Exclude(TAG, excludedTags)) + .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) + .criterion(Include(STATE, states)) + .criterion(Include(CONTENT_RATING, contentRatings)) + .criterion(Include(CONTENT_TYPE, contentTypes)) + .criterion(Include(DEMOGRAPHIC, demographics)) + .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) + .criterion(Match(PUBLICATION_YEAR, 2020)) + .build() - assertEquals(expectedQuery, searchQuery) - } + assertEquals(expectedQuery, searchQuery) + } - @Test - fun convertToMangaSearchQueryWithEmptyFieldsTest() { - val filter = MangaListFilter() + @Test + fun convertToMangaSearchQueryWithEmptyFieldsTest() { + val filter = MangaListFilter() - val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) + val searchQuery = convertToMangaSearchQuery(0, SortOrder.NEWEST, filter) - assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) - } + assertEquals(MangaSearchQuery.Builder().offset(0).order(SortOrder.NEWEST).build(), searchQuery) + } - private fun buildMangaTag(name: String): MangaTag { - return MangaTag( - key = "${name}Key", - title = name, - source = MangaParserSource.DUMMY, - ) - } + private fun buildMangaTag(name: String): MangaTag { + return MangaTag( + key = "${name}Key", + title = name, + source = MangaParserSource.MANGADEX, + ) + } } diff --git a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt index ef2823783..f9dfbc11e 100644 --- a/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt +++ b/src/test/kotlin/org/koitharu/kotatsu/parsers/util/SearchQueryToListFilterConverterTest.kt @@ -17,78 +17,78 @@ import java.util.* class ConvertToMangaListFilterTest { - @Test - fun convertToMangaListFilterTest() { - val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val excludedTags = setOf(buildMangaTag("exclude_tag")) - val states = setOf(MangaState.ONGOING) - val contentRatings = setOf(ContentRating.SAFE) - val contentTypes = setOf(MANGA, MANHUA) - val demographics = setOf(SEINEN) + @Test + fun convertToMangaListFilterTest() { + val tags = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val excludedTags = setOf(buildMangaTag("exclude_tag")) + val states = setOf(MangaState.ONGOING) + val contentRatings = setOf(ContentRating.SAFE) + val contentTypes = setOf(MANGA, MANHUA) + val demographics = setOf(SEINEN) - val query = MangaSearchQuery.Builder() - .criterion(Match(TITLE_NAME, "title_name")) - .criterion(Include(TAG, tags)) - .criterion(Exclude(TAG, excludedTags)) - .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) - .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) - .criterion(Include(STATE, states)) - .criterion(Include(CONTENT_RATING, contentRatings)) - .criterion(Include(CONTENT_TYPE, contentTypes)) - .criterion(Include(DEMOGRAPHIC, demographics)) - .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) - .criterion(Match(PUBLICATION_YEAR, 2020)) - .build() + val query = MangaSearchQuery.Builder() + .criterion(Match(TITLE_NAME, "title_name")) + .criterion(Include(TAG, tags)) + .criterion(Exclude(TAG, excludedTags)) + .criterion(Include(LANGUAGE, setOf(Locale.ENGLISH))) + .criterion(Include(ORIGINAL_LANGUAGE, setOf(Locale.JAPANESE))) + .criterion(Include(STATE, states)) + .criterion(Include(CONTENT_RATING, contentRatings)) + .criterion(Include(CONTENT_TYPE, contentTypes)) + .criterion(Include(DEMOGRAPHIC, demographics)) + .criterion(Range(PUBLICATION_YEAR, 1997, 2024)) + .criterion(Match(PUBLICATION_YEAR, 2020)) + .build() - val listFilter = convertToMangaListFilter(query) + val listFilter = convertToMangaListFilter(query) - assertEquals(listFilter.query, "title_name") - assertEquals(listFilter.tags, tags) - assertEquals(listFilter.tagsExclude, excludedTags) - assertEquals(listFilter.locale, Locale.ENGLISH) - assertEquals(listFilter.originalLocale, Locale.JAPANESE) - assertEquals(listFilter.states, states) - assertEquals(listFilter.contentRating, contentRatings) - assertEquals(listFilter.types, contentTypes) - assertEquals(listFilter.demographics, demographics) - assertEquals(listFilter.year, 2020) - assertEquals(listFilter.yearFrom, 1997) - assertEquals(listFilter.yearTo, 2024) - } + assertEquals(listFilter.query, "title_name") + assertEquals(listFilter.tags, tags) + assertEquals(listFilter.tagsExclude, excludedTags) + assertEquals(listFilter.locale, Locale.ENGLISH) + assertEquals(listFilter.originalLocale, Locale.JAPANESE) + assertEquals(listFilter.states, states) + assertEquals(listFilter.contentRating, contentRatings) + assertEquals(listFilter.types, contentTypes) + assertEquals(listFilter.demographics, demographics) + assertEquals(listFilter.year, 2020) + assertEquals(listFilter.yearFrom, 1997) + assertEquals(listFilter.yearTo, 2024) + } - @Test - fun convertToMangaListFilterWithMultipleTagsIncludeTest() { - val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) - val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4")) + @Test + fun convertToMangaListFilterWithMultipleTagsIncludeTest() { + val tags1 = setOf(buildMangaTag("tag1"), buildMangaTag("tag2")) + val tags2 = setOf(buildMangaTag("tag3"), buildMangaTag("tag4")) - val query = MangaSearchQuery.Builder() - .criterion(Include(TAG, tags1)) - .criterion(Include(TAG, tags2)) - .build() + val query = MangaSearchQuery.Builder() + .criterion(Include(TAG, tags1)) + .criterion(Include(TAG, tags2)) + .build() - val listFilter = convertToMangaListFilter(query) + val listFilter = convertToMangaListFilter(query) - assertEquals(listFilter.tags, tags1 union tags2) - } + assertEquals(listFilter.tags, tags1 union tags2) + } - @Test - fun convertToMangaListFilterWithUnsupportedFieldTest() { - val query = MangaSearchQuery.Builder() - .criterion(Include(AUTHOR, setOf(buildMangaTag("author")))) - .build() + @Test + fun convertToMangaListFilterWithUnsupportedFieldTest() { + val query = MangaSearchQuery.Builder() + .criterion(Include(AUTHOR, setOf(buildMangaTag("author")))) + .build() - val exception = assertThrows { - convertToMangaListFilter(query) - } + val exception = assertThrows { + convertToMangaListFilter(query) + } - assert(exception.message!!.contains("Unsupported field for Include criterion: AUTHOR")) - } + assert(exception.message!!.contains("Unsupported field for Include criterion: AUTHOR")) + } - private fun buildMangaTag(name: String): MangaTag { - return MangaTag( - key = "${name}Key", - title = name, - source = MangaParserSource.DUMMY, - ) - } + private fun buildMangaTag(name: String): MangaTag { + return MangaTag( + key = "${name}Key", + title = name, + source = MangaParserSource.MANGADEX, + ) + } }