diff --git a/.changeset/open-dodos-battle.md b/.changeset/open-dodos-battle.md new file mode 100644 index 0000000..8557208 --- /dev/null +++ b/.changeset/open-dodos-battle.md @@ -0,0 +1,5 @@ +--- +"@germ-network/atprototypes": patch +--- + +add ref definition in support of a union protocol diff --git a/Sources/AtprotoTypes/Atproto/Ref.swift b/Sources/AtprotoTypes/Atproto/Ref.swift new file mode 100644 index 0000000..12955b1 --- /dev/null +++ b/Sources/AtprotoTypes/Atproto/Ref.swift @@ -0,0 +1,18 @@ +// +// Ref.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 5/20/26. +// + +import Foundation + +//https://atproto.com/specs/lexicon#ref +extension Atproto { + public struct Ref: StringRepresentable, Codable { + public init(string: String) { + self.rawValue = string + } + public private(set) var rawValue: String + } +} diff --git a/Sources/AtprotoTypes/Atproto/Schema.swift b/Sources/AtprotoTypes/Atproto/Schema.swift new file mode 100644 index 0000000..925aa6e --- /dev/null +++ b/Sources/AtprotoTypes/Atproto/Schema.swift @@ -0,0 +1,14 @@ +// +// Schema.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 5/20/26. +// + +import Foundation + +extension Atproto { + public protocol Schema: Sendable, Codable { + static var ref: Atproto.Ref { get } + } +} diff --git a/Sources/AtprotoTypes/LexiconStrings/AtIdentifier.swift b/Sources/AtprotoTypes/LexiconTypes/Strings/AtIdentifier.swift similarity index 100% rename from Sources/AtprotoTypes/LexiconStrings/AtIdentifier.swift rename to Sources/AtprotoTypes/LexiconTypes/Strings/AtIdentifier.swift diff --git a/Sources/AtprotoTypes/LexiconStrings/Datetime.swift b/Sources/AtprotoTypes/LexiconTypes/Strings/Datetime.swift similarity index 100% rename from Sources/AtprotoTypes/LexiconStrings/Datetime.swift rename to Sources/AtprotoTypes/LexiconTypes/Strings/Datetime.swift diff --git a/Sources/AtprotoTypes/LexiconStrings/StringRepresentable.swift b/Sources/AtprotoTypes/LexiconTypes/Strings/StringRepresentable.swift similarity index 100% rename from Sources/AtprotoTypes/LexiconStrings/StringRepresentable.swift rename to Sources/AtprotoTypes/LexiconTypes/Strings/StringRepresentable.swift diff --git a/Sources/AtprotoTypes/LexiconTypes/Union.swift b/Sources/AtprotoTypes/LexiconTypes/Union.swift new file mode 100644 index 0000000..4b0573a --- /dev/null +++ b/Sources/AtprotoTypes/LexiconTypes/Union.swift @@ -0,0 +1,50 @@ +// +// Union.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 5/20/26. +// + +import Foundation + +///This protocol provides default coding/encoding implementation for a union of a set of types known at +///compile time. See UnionTests for an example +///https://atproto.com/specs/lexicon#union +/// +///This matches logically an enum and is likely most easily implemetned as an enum, however +/// +///Currently implemented as a union of records but could be any lexicon schema +public protocol LexiconUnion: Codable, Sendable { + static var members: [Atproto.Ref: any Atproto.Schema.Type] { get } + + init(object: any Codable) throws +} + +enum TypeHeader: String, CodingKey { + case type = "$type" +} + +extension LexiconUnion { + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: TypeHeader.self) + let ref = try container.decode(Atproto.Ref.self, forKey: .type) + + let type = try Self.members[ref].tryUnwrap( + LexionUnionError.unknownType(ref) + ) + + try self.init(object: try type.init(from: decoder)) + } +} + +public enum LexionUnionError: LocalizedError { + case unknownType(Atproto.Ref) + case unknownObject + + public var errorDescription: String? { + switch self { + case .unknownType(let ref): "encountered unknown type \(ref)" + case .unknownObject: "Unknown object" + } + } +} diff --git a/Tests/AtprotoTypesTests/UnionTests.swift b/Tests/AtprotoTypesTests/UnionTests.swift new file mode 100644 index 0000000..30696a2 --- /dev/null +++ b/Tests/AtprotoTypesTests/UnionTests.swift @@ -0,0 +1,80 @@ +// +// UnionTests.swift +// AtprotoTypes +// +// Created by Mark @ Germ on 5/20/26. +// + +import AtprotoTypes +import Foundation +import GermConvenience +import Testing + +struct UnionTests { + static let relationshipJsonString = + """ + [{\"did\":\"did:plc:kta7dqcqoamo5ixlajxbtjps\",\"following\":\"at://did:plc:4yvwfwxfz5sney4twepuzdu7/app.bsky.graph.follow/3meoz6k62qk2i\",\"$type\":\"app.bsky.graph.defs#relationship\"},{\"actor\":\"example.com\",\"notFound\":true,\"$type\":\"app.bsky.graph.defs#notFoundActor\"} + ] + """ + + @Test func testUnion() async throws { + let result = try JSONDecoder().decode( + [SimpleRelationshipResult].self, + from: Self.relationshipJsonString.utf8Data + ) + print(result) + } + +} + +//taken from //c +enum SimpleRelationshipResult { + case relationship(Relationships) + case notFound(NotFoundActor) + + struct Relationships: Codable, Atproto.Schema { + static var ref: Atproto.Ref { + .init(string: "app.bsky.graph.defs#relationship") + } + let did: Atproto.DID + let blocking: Atproto.ATURI? + let blockedBy: Atproto.ATURI? + let following: Atproto.ATURI? + let followedBy: Atproto.ATURI? + let blockedByList: Atproto.ATURI? + let blockingbyList: Atproto.ATURI? + } + + struct NotFoundActor: Atproto.Schema { + static var ref: Atproto.Ref { + .init(string: "app.bsky.graph.defs#notFoundActor") + } + + public let actor: LexiconString.AtIdentifier + var notFound: Bool = true + + public init(actor: LexiconString.AtIdentifier) { + self.actor = actor + } + } +} + +extension SimpleRelationshipResult: LexiconUnion { + static var members: [Atproto.Ref : any Atproto.Schema.Type] { + [ + Relationships.ref: Relationships.self, + NotFoundActor.ref: NotFoundActor.self + ] + } + + + init(object: any Codable) throws { + if let relationships = object as? Relationships { + self = .relationship(relationships) + } else if let notFound = object as? NotFoundActor { + self = .notFound(notFound) + } else { + throw LexionUnionError.unknownObject + } + } +}