summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Sora/Data/Booru/BooruManager.swift19
-rw-r--r--Sora/Data/Booru/Tag/DanbooruTagParser.swift58
-rw-r--r--SoraTests/ViewDerivedDataTests.swift43
3 files changed, 118 insertions, 2 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift
index 7d11158..8f055af 100644
--- a/Sora/Data/Booru/BooruManager.swift
+++ b/Sora/Data/Booru/BooruManager.swift
@@ -308,7 +308,12 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng
guard !Task.isCancelled else { return [] }
- return BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count }
+ let parsedTags =
+ flavor == .danbooru
+ ? DanbooruTagParser(data: data).parse()
+ : BooruTagXMLParser(data: data).parse()
+
+ return parsedTags.sorted { $0.count > $1.count }
} catch {
if (error as? URLError)?.code != .cancelled {
debugPrint("BooruManager.searchTags: \(error)")
@@ -516,7 +521,17 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng
return components.url
case .danbooru:
- return nil
+ var components = URLComponents()
+
+ components.scheme = "https"
+ components.host = domain
+ components.path = "/tags.json"
+ components.queryItems = [
+ URLQueryItem(name: "search[name_matches]", value: "\(name)*"),
+ URLQueryItem(name: "limit", value: "50"),
+ ]
+
+ return components.url
}
}
diff --git a/Sora/Data/Booru/Tag/DanbooruTagParser.swift b/Sora/Data/Booru/Tag/DanbooruTagParser.swift
new file mode 100644
index 0000000..3da165a
--- /dev/null
+++ b/Sora/Data/Booru/Tag/DanbooruTagParser.swift
@@ -0,0 +1,58 @@
+import Foundation
+
+nonisolated class DanbooruTagParser {
+ private let data: Data
+
+ init(data: Data) {
+ self.data = data
+ }
+
+ func parse() -> [BooruTag] {
+ do {
+ guard let decodedTags = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
+ debugPrint("DanbooruTagParser.parse: failed to decode top-level tag array.")
+
+ return []
+ }
+
+ var parsedTags: [BooruTag] = []
+ var droppedRecordCount = 0
+
+ parsedTags.reserveCapacity(decodedTags.count)
+
+ for tag in decodedTags {
+ guard let id = tag["id"] as? Int,
+ let name = tag["name"] as? String,
+ let postCount = tag["post_count"] as? Int,
+ let category = tag["category"] as? Int
+ else {
+ droppedRecordCount += 1
+
+ continue
+ }
+
+ parsedTags.append(
+ BooruTag(
+ id: String(id),
+ name: name,
+ count: postCount,
+ type: category,
+ ambiguous: false
+ )
+ )
+ }
+
+ if droppedRecordCount > 0 {
+ debugPrint(
+ "DanbooruTagParser.parse: dropped \(droppedRecordCount) malformed records while decoding tags."
+ )
+ }
+
+ return parsedTags
+ } catch {
+ debugPrint("DanbooruTagParser.parse: failed to decode response: \(error)")
+
+ return []
+ }
+ }
+}
diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift
index ba02145..fd3f1da 100644
--- a/SoraTests/ViewDerivedDataTests.swift
+++ b/SoraTests/ViewDerivedDataTests.swift
@@ -1112,6 +1112,49 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b
)
}
+ func testBooruManagerDanbooruTagSuggestionsUseJSONSearchEndpoint() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let searchTagsSection = try extractFunction(
+ named: "func searchTags(name: String) async -> [BooruTag]",
+ from: source
+ )
+ let tagSearchSection = try extractFunction(
+ named: "private func url(forTagsSearch name: String) -> URL?",
+ from: source
+ )
+ let danbooruJSONPathCount = tokenCount(
+ matching: #"case\s+\.danbooru:\s*[\s\S]*?components\.path\s*=\s*"/tags\.json""#,
+ in: tagSearchSection
+ )
+ let danbooruWildcardQueryCount = tokenCount(
+ matching: #"URLQueryItem\(name:\s*"search\[name_matches\]""#,
+ in: tagSearchSection
+ )
+ let danbooruParserSelectionCount = tokenCount(
+ matching: #"flavor\s*==\s*\.danbooru\s*\?\s*DanbooruTagParser\(data:\s*data\)\.parse\(\)"#,
+ in: searchTagsSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ danbooruJSONPathCount,
+ 0,
+ "Danbooru tag suggestions should use the JSON `/tags.json` endpoint."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ danbooruWildcardQueryCount,
+ 0,
+ "Danbooru tag suggestions should search with `search[name_matches]=<term>*`."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ danbooruParserSelectionCount,
+ 0,
+ "Danbooru tag suggestions should decode JSON through DanbooruTagParser."
+ )
+ }
+
func testBooruRequestConfigurationSupportsOptionalAndCustomUserAgentHeaders() throws {
let source = try loadSource(at: "Sora/Data/Booru/BooruRequestConfiguration.swift")
let userAgentResolverSection = try extractFunction(