Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/open-dodos-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@germ-network/atprototypes": patch
---

add ref definition in support of a union protocol
18 changes: 18 additions & 0 deletions Sources/AtprotoTypes/Atproto/Ref.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
14 changes: 14 additions & 0 deletions Sources/AtprotoTypes/Atproto/Schema.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
}
50 changes: 50 additions & 0 deletions Sources/AtprotoTypes/LexiconTypes/Union.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
80 changes: 80 additions & 0 deletions Tests/AtprotoTypesTests/UnionTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading