diff options
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 19 | ||||
| -rw-r--r-- | Sora/Data/Booru/Tag/DanbooruTagParser.swift | 58 | ||||
| -rw-r--r-- | SoraTests/ViewDerivedDataTests.swift | 43 |
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( |