Repository: TootSDK/TootSDK
Pull request #325 (2024-11-15 17:12:54)

Pleroma fixes
[Fix error when trying to follow account without extra params](https://github.com/TootSDK/TootSDK/commit/f8d5b75f47ab55e70e0b39150769c7bfed0f5795)
[Fix decoding of card with width/height as string instead of int](https://github.com/TootSDK/TootSDK/commit/67880f5eeb8db42f9ff34dc563316a87dfbde618)


Patch:
diff --git a/Package.swift b/Package.swift
index fec5367..724ca37 100644
--- a/Package.swift
+++ b/Package.swift
@@ -59,6 +59,7 @@ let package = Package(
                 .copy("Resources/account_pixelfed.json"),
                 .copy("Resources/account_pixelfed_mutes_blocks.json"),
                 .copy("Resources/activity.json"),
+                .copy("Resources/card_string_size.json"),
                 .copy("Resources/familiar_followers_nofollowers.json"),
                 .copy("Resources/featured_tag.json"),
                 .copy("Resources/featured_tag_unused.json"),
diff --git a/Sources/TootSDK/Extensions/Decoding+Helpers.swift b/Sources/TootSDK/Extensions/Decoding+Helpers.swift
index 5bfc92a..2b3f368 100644
--- a/Sources/TootSDK/Extensions/Decoding+Helpers.swift
+++ b/Sources/TootSDK/Extensions/Decoding+Helpers.swift
@@ -29,4 +29,18 @@ extension KeyedDecodingContainerProtocol {
             throw error
         }
     }
+
+    func decodeIntFromStringIfPresent(forKey key: Key) throws -> Int? {
+        do {
+            return try decodeIfPresent(Int.self, forKey: key)
+        } catch {
+            guard let string = try decodeIfPresent(String.self, forKey: key) else {
+                return nil
+            }
+            if let int = Int(string) {
+                return int
+            }
+            throw error
+        }
+    }
 }
diff --git a/Sources/TootSDK/Models/Card.swift b/Sources/TootSDK/Models/Card.swift
index 91eddfc..38f768d 100644
--- a/Sources/TootSDK/Models/Card.swift
+++ b/Sources/TootSDK/Models/Card.swift
@@ -101,4 +101,26 @@ public struct Card: Codable, Hashable, Sendable {
     public var embedUrl: String?
     /// A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.
     public var blurhash: String?
+
+    public init(from decoder: any Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.url = try container.decode(String.self, forKey: .url)
+        self.title = try container.decode(String.self, forKey: .title)
+        self.description = try container.decode(String.self, forKey: .description)
+        self.language = try container.decodeIfPresent(String.self, forKey: .language)
+        self.type = try container.decode(Card.CardType.self, forKey: .type)
+        self.authorName = try container.decodeIfPresent(String.self, forKey: .authorName)
+        self.authorUrl = try container.decodeIfPresent(String.self, forKey: .authorUrl)
+        self.authors = try container.decodeIfPresent([Card.Author].self, forKey: .authors)
+        self.publishedAt = try container.decodeIfPresent(Date.self, forKey: .publishedAt)
+        self.providerName = try container.decodeIfPresent(String.self, forKey: .providerName)
+        self.providerUrl = try container.decodeIfPresent(String.self, forKey: .providerUrl)
+        self.html = try container.decodeIfPresent(String.self, forKey: .html)
+        self.width = try container.decodeIntFromStringIfPresent(forKey: .width)
+        self.height = try container.decodeIntFromStringIfPresent(forKey: .height)
+        self.image = try container.decodeIfPresent(String.self, forKey: .image)
+        self.imageDescription = try container.decodeIfPresent(String.self, forKey: .imageDescription)
+        self.embedUrl = try container.decodeIfPresent(String.self, forKey: .embedUrl)
+        self.blurhash = try container.decodeIfPresent(String.self, forKey: .blurhash)
+    }
 }
diff --git a/Sources/TootSDK/TootClient/TootClient+Relationships.swift b/Sources/TootSDK/TootClient/TootClient+Relationships.swift
index dd10008..22104c7 100644
--- a/Sources/TootSDK/TootClient/TootClient+Relationships.swift
+++ b/Sources/TootSDK/TootClient/TootClient+Relationships.swift
@@ -11,7 +11,9 @@ extension TootClient {
         let req = try HTTPRequestBuilder {
             $0.url = getURL(["api", "v1", "accounts", id, "follow"])
             $0.method = .post
-            $0.body = try .json(params)
+            if let params {
+                $0.body = try .json(params)
+            }
         }
         return try await fetch(Relationship.self, req)
     }


Test patch:
diff --git a/Tests/TootSDKTests/CardTests.swift b/Tests/TootSDKTests/CardTests.swift
new file mode 100644
index 0000000..361ae63
--- /dev/null
+++ b/Tests/TootSDKTests/CardTests.swift
@@ -0,0 +1,24 @@
+//
+//  CardTests.swift
+//  TootSDK
+//
+//  Created by Łukasz Rutkowski on 15/11/2024.
+//
+
+import Foundation
+import XCTest
+@testable import TootSDK
+
+final class CardTests: XCTestCase {
+    func testDecodeCard() throws {
+        let json = localContent("card_string_size")
+        let decoder = TootDecoder()
+
+        let result = try decoder.decode(Card.self, from: json)
+
+        XCTAssertNotNil(result)
+        XCTAssertEqual(result.title, "Example")
+        XCTAssertEqual(result.width, 520)
+        XCTAssertEqual(result.height, 347)
+    }
+}
diff --git a/Tests/TootSDKTests/DecodingTests.swift b/Tests/TootSDKTests/DecodingTests.swift
index 9c4f4d1..2c05d94 100644
--- a/Tests/TootSDKTests/DecodingTests.swift
+++ b/Tests/TootSDKTests/DecodingTests.swift
@@ -18,10 +18,19 @@ final class DecodingTests: XCTestCase {
         try assertDecodes(#"{"int": "-7"}"#, as: Element(-7))
     }
 
-    private func assertDecodes(_ json: String, as element: Element) throws {
+    func testDecodeIfPresentIntFromString() throws {
+        try assertDecodes(#"{"int": 2}"#, as: OptionalElement(2))
+        try assertDecodes(#"{"int": "5"}"#, as: OptionalElement(5))
+        try assertDecodes(#"{"int": -12}"#, as: OptionalElement(-12))
+        try assertDecodes(#"{"int": "-7"}"#, as: OptionalElement(-7))
+        try assertDecodes(#"{"int": null}"#, as: OptionalElement(nil))
+        try assertDecodes(#"{}"#, as: OptionalElement(nil))
+    }
+
+    private func assertDecodes<T: Equatable & Decodable>(_ json: String, as element: T) throws {
         let decoder = JSONDecoder()
         let data = Data(json.utf8)
-        let decodedElement = try decoder.decode(Element.self, from: data)
+        let decodedElement = try decoder.decode(T.self, from: data)
         XCTAssertEqual(decodedElement, element)
     }
 
@@ -41,4 +50,21 @@ final class DecodingTests: XCTestCase {
             self.int = try container.decodeIntFromString(forKey: .int)
         }
     }
+
+    struct OptionalElement: Decodable, Equatable {
+        let int: Int?
+
+        init(_ int: Int?) {
+            self.int = int
+        }
+
+        enum CodingKeys: CodingKey {
+            case int
+        }
+
+        init(from decoder: Decoder) throws {
+            let container = try decoder.container(keyedBy: CodingKeys.self)
+            self.int = try container.decodeIntFromStringIfPresent(forKey: .int)
+        }
+    }
 }
diff --git a/Tests/TootSDKTests/Resources/card_string_size.json b/Tests/TootSDKTests/Resources/card_string_size.json
new file mode 100644
index 0000000..f011e7c
--- /dev/null
+++ b/Tests/TootSDKTests/Resources/card_string_size.json
@@ -0,0 +1,16 @@
+{
+    "description" : "Description",
+    "blurhash" : null,
+    "html" : "",
+    "provider_url" : "https://example.com",
+    "provider_name" : "Example",
+    "width" : "520",
+    "author_url" : null,
+    "url" : "https://example.com/example",
+    "type" : "link",
+    "title" : "Example",
+    "image" : null,
+    "height" : "347",
+    "author_name" : "Author",
+    "embed_url" : "https://eample.com/embed"
+}