From c695a7200d59995ae48f2eb982617694494a22b3 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 20 May 2026 23:33:49 -0700 Subject: [PATCH 1/3] add ref definition in support of a union protocol --- .changeset/open-dodos-battle.md | 5 ++ Sources/AtprotoTypes/Atproto/Ref.swift | 18 +++++ .../Strings}/AtIdentifier.swift | 0 .../Strings}/Datetime.swift | 0 .../Strings}/StringRepresentable.swift | 0 Sources/AtprotoTypes/LexiconTypes/Union.swift | 49 ++++++++++++ Tests/AtprotoTypesTests/UnionTests.swift | 78 +++++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 .changeset/open-dodos-battle.md create mode 100644 Sources/AtprotoTypes/Atproto/Ref.swift rename Sources/AtprotoTypes/{LexiconStrings => LexiconTypes/Strings}/AtIdentifier.swift (100%) rename Sources/AtprotoTypes/{LexiconStrings => LexiconTypes/Strings}/Datetime.swift (100%) rename Sources/AtprotoTypes/{LexiconStrings => LexiconTypes/Strings}/StringRepresentable.swift (100%) create mode 100644 Sources/AtprotoTypes/LexiconTypes/Union.swift create mode 100644 Tests/AtprotoTypesTests/UnionTests.swift 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/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..1657d2f --- /dev/null +++ b/Sources/AtprotoTypes/LexiconTypes/Union.swift @@ -0,0 +1,49 @@ +// +// 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: Decodable { + static func type(ref: Atproto.Ref) throws -> any Decodable.Type + + init(object: any Decodable) 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 typeNsid = try container.decode(Atproto.Ref.self, forKey: .type) + + let type = try Self.type(ref: typeNsid) + + 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..7a87311 --- /dev/null +++ b/Tests/AtprotoTypesTests/UnionTests.swift @@ -0,0 +1,78 @@ +// +// 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, Sendable { + 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: Codable, Sendable { + public let actor: LexiconString.AtIdentifier + var notFound: Bool = true + + public init(actor: LexiconString.AtIdentifier) { + self.actor = actor + } + } +} + +extension SimpleRelationshipResult: LexiconUnion { + static func type( + ref: Atproto.Ref + ) throws -> any Decodable.Type { + switch ref.rawValue { + case "app.bsky.graph.defs#relationship": + Relationships.self + case "app.bsky.graph.defs#notFoundActor": + NotFoundActor.self + default: + throw LexionUnionError.unknownType(ref) + } + } + + init(object: any Decodable) throws { + if let relationships = object as? Relationships { + self = .relationship(relationships) + } else if let notFound = object as? NotFoundActor { + self = .notFound(notFound) + } else { + throw LexionUnionError.unknownObject + } + } +} From 852d4c5f69936a1f3a41cd356d3370351d5d459e Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 20 May 2026 23:38:06 -0700 Subject: [PATCH 2/3] LexiconUnion should be an encodable too --- Sources/AtprotoTypes/LexiconTypes/Union.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/AtprotoTypes/LexiconTypes/Union.swift b/Sources/AtprotoTypes/LexiconTypes/Union.swift index 1657d2f..372921a 100644 --- a/Sources/AtprotoTypes/LexiconTypes/Union.swift +++ b/Sources/AtprotoTypes/LexiconTypes/Union.swift @@ -14,10 +14,10 @@ import Foundation ///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: Decodable { - static func type(ref: Atproto.Ref) throws -> any Decodable.Type +public protocol LexiconUnion: Decodable, Sendable { + static func type(ref: Atproto.Ref) throws -> any Codable.Type - init(object: any Decodable) throws + init(object: any Codable) throws } enum TypeHeader: String, CodingKey { @@ -32,7 +32,6 @@ extension LexiconUnion { let type = try Self.type(ref: typeNsid) try self.init(object: try type.init(from: decoder)) - } } From 1f708112d33b9793d08d6e6c5959cb5800ea701b Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 20 May 2026 23:56:01 -0700 Subject: [PATCH 3/3] make better use of coding and new schema protocol --- Sources/AtprotoTypes/Atproto/Schema.swift | 14 +++++++++ Sources/AtprotoTypes/LexiconTypes/Union.swift | 14 +++++---- Tests/AtprotoTypesTests/UnionTests.swift | 30 ++++++++++--------- 3 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 Sources/AtprotoTypes/Atproto/Schema.swift 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/LexiconTypes/Union.swift b/Sources/AtprotoTypes/LexiconTypes/Union.swift index 372921a..4b0573a 100644 --- a/Sources/AtprotoTypes/LexiconTypes/Union.swift +++ b/Sources/AtprotoTypes/LexiconTypes/Union.swift @@ -14,9 +14,9 @@ import Foundation ///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: Decodable, Sendable { - static func type(ref: Atproto.Ref) throws -> any Codable.Type - +public protocol LexiconUnion: Codable, Sendable { + static var members: [Atproto.Ref: any Atproto.Schema.Type] { get } + init(object: any Codable) throws } @@ -27,9 +27,11 @@ enum TypeHeader: String, CodingKey { extension LexiconUnion { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: TypeHeader.self) - let typeNsid = try container.decode(Atproto.Ref.self, forKey: .type) - - let type = try Self.type(ref: typeNsid) + 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)) } diff --git a/Tests/AtprotoTypesTests/UnionTests.swift b/Tests/AtprotoTypesTests/UnionTests.swift index 7a87311..30696a2 100644 --- a/Tests/AtprotoTypesTests/UnionTests.swift +++ b/Tests/AtprotoTypesTests/UnionTests.swift @@ -32,7 +32,10 @@ enum SimpleRelationshipResult { case relationship(Relationships) case notFound(NotFoundActor) - struct Relationships: Codable, Sendable { + 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? @@ -42,7 +45,11 @@ enum SimpleRelationshipResult { let blockingbyList: Atproto.ATURI? } - struct NotFoundActor: Codable, Sendable { + 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 @@ -53,20 +60,15 @@ enum SimpleRelationshipResult { } extension SimpleRelationshipResult: LexiconUnion { - static func type( - ref: Atproto.Ref - ) throws -> any Decodable.Type { - switch ref.rawValue { - case "app.bsky.graph.defs#relationship": - Relationships.self - case "app.bsky.graph.defs#notFoundActor": - NotFoundActor.self - default: - throw LexionUnionError.unknownType(ref) - } + static var members: [Atproto.Ref : any Atproto.Schema.Type] { + [ + Relationships.ref: Relationships.self, + NotFoundActor.ref: NotFoundActor.self + ] } - init(object: any Decodable) throws { + + init(object: any Codable) throws { if let relationships = object as? Relationships { self = .relationship(relationships) } else if let notFound = object as? NotFoundActor {