Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Symbol Graph Format Extensible #39

Merged
merged 14 commits into from
Sep 14, 2022
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
24 changes: 0 additions & 24 deletions Sources/SymbolKit/Mixin.swift

This file was deleted.

72 changes: 72 additions & 0 deletions Sources/SymbolKit/Mixin/Mixin+Equals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

// `Mixin` does not conform to `Equatable` right now primarily because
// this would complicate its usage in many situtations because of "Self
// or associated type" requirements errors. Thus, in order to compare
// `Mixin`s for equality, we need to somehow get access to the `Mixin`'s
// `Equatable` conformance and the `==(lhs:rhs:)` function specifically.
//
// Note that all of this would be siginificantly easier in Swift 5.7, so
// it might be worth updating the implementation once SymbolKit adopts
// Swift 5.7 as its minimum language requirement.


// When working with `Mixin` values in a generic (non-specific) context,
// we only know their value conforms to the existential type `Mixin`. This
// extension to `Mixin` and the `equals` property defined in it is essentiall for
// the whole process to work:
// The `equals` property does not expose the `Self` type in its interface and
// therefore is accessible from the existential type `Mixin`. Inside `equals`,
// however, we have access to the concrete type `Self`, allowing us to initialize
// the `EquatableDetector` with a concrete generic type, which can know it conforms
// to `Equatable`. If we were to simply pass a value of type `Any` into the initializer
// of `EquatableDetector`, the latter would not recognize `value` as `Equatable`, even
// if the original concrete type were to conform to `Equatable`.
extension Mixin {
/// A type-erased version of this ``Mixin``s `==(lhs:rhs:)` function, available
/// only if this ``Mixin`` conforms to `Equatable`.
var equals: ((Any) -> Bool)? {
(EquatableDetector(value: self) as? AnyEquatable)?.equals
}
}

// The `AnyEquatable` protocol defines our requirement for an equality function
// in a type-erased way. It has no Self or associated type requirements and thus
// can be casted to via a simple `as?`. In Swift 5.7 we could simply cast to
// `any Equatable`, but this was not possible before.
private protocol AnyEquatable {
var equals: (Any) -> Bool { get }
}

// The `EquatableDetector` brings both pieces together by conditionally conforming
// itself to `AnyEquatable` where its generic `value` is `Equatable`.
private struct EquatableDetector<T> {
let value: T
}

extension EquatableDetector: AnyEquatable where T: Equatable {
var equals: (Any) -> Bool {
{ other in
guard let other = other as? T else {
// we are comparing `value` against `other`, but
// `other` is of a different type, so they can't be
// equal
return false
}

// we finally know that `value`, as well as `other`
// are of the same type `T`, which conforms to `Equatable`
return value == other
}
}
}
63 changes: 63 additions & 0 deletions Sources/SymbolKit/Mixin/Mixin+Hash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

// `Mixin` does not conform to `Hashable` right now primarily because
// this would complicate its usage in many situtations because of "Self
// or associated type" requirements errors. `Hashable` inherits those
// frome `Equatable`, even though its primary functionality, the `hash(into:)`
// function has no Self or associated type requirements. Thus, in order to
// access a `Mixin`'s `hash(into:)` function, we need to somehow get access to
// the `Mixin`'s `Hashable` conformance.
//
// Note that all of this would be siginificantly easier in Swift 5.7, so
// it might be worth updating the implementation once SymbolKit adopts
// Swift 5.7 as its minimum language requirement.


// When working with `Mixin` values in a generic (non-specific) context,
// we only know their value conforms to the existential type `Mixin`. This
// extension to `Mixin` and the `hash` property defined in it is essentiall for
// the whole process to work:
// The `hash` property does not expose the `Self` type in its interface and
// therefore is accessible from the existential type `Mixin`. Inside `hash`,
// however, we have access to the concrete type `Self`, allowing us to initialize
// the `HashableDetector` with a concrete generic type, which can know it conforms
// to `Hashable`. If we were to simply pass a value of type `Any` into the initializer
// of `HashableDetector`, the latter would not recognize `value` as `Hashable`, even
// if the original concrete type were to conform to `Hashable`.
extension Mixin {
/// This ``Mixin``s `hash(into:)` function, available
/// only if this ``Mixin`` conforms to `Hashable`.
var hash: ((inout Hasher) -> Void)? {
(HashableDetector(value: self) as? AnyHashable)?.hash
}
}

// The `AnyEquatable` protocol simply defines our requirement for a hash
// function. It has no Self or associated type requirements and thus can
// be casted to via a simple `as?`. In Swift 5.7 we could simply cast to
// `any Hashable`, but this was not possible before.
private protocol AnyHashable {
var hash: (inout Hasher) -> Void { get }
}

// The `HashableDetector` brings both pieces together by conditionally conforming
// itself to `AnyHashable` where its generic `value` is `Hashable`.
private struct HashableDetector<T> {
let value: T
}

extension HashableDetector: AnyHashable where T: Hashable {
var hash: (inout Hasher) -> Void {
value.hash(into:)
}
}
90 changes: 90 additions & 0 deletions Sources/SymbolKit/Mixin/Mixin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

/**
A protocol that allows extracted symbols to have extra data
aside from the base ``SymbolGraph/Symbol``.

- Note: If you intend to encode/decode a custom ``Mixin`` as part of a relationship or symbol, make sure
to register its type to your encoder/decoder instance using
``SymbolGraph/Relationship/register(mixins:to:onEncodingError:onDecodingError:)``
or ``SymbolGraph/Symbol/register(mixins:to:onEncodingError:onDecodingError:)``, respectively.
*/
public protocol Mixin: Codable {
/**
The key under which a mixin's data is filed.

> Important: With respect to deserialization, this framework assumes `mixinKey`s between instances of `SymbolMixin` are unique.
*/
static var mixinKey: String { get }
}

// This extension provides coding information for any instance of Mixin. These
// coding infos wrap the encoding and decoding logic for the respective
// instances in a type-erased way. Thus, the concrete instance type of Mixins
// does not need to be known by the encoding/decoding logic in Symbol and
// Relationship.
extension Mixin {
static var symbolCodingInfo: SymbolMixinCodingInfo {
let key = SymbolGraph.Symbol.CodingKeys(rawValue: Self.mixinKey)
return MixinCodingInformation(codingKey: key,
encode: { mixin, container in
try container.encode(mixin as! Self, forKey: key)
},
decode: { container in
try container.decode(Self.self, forKey: key)
})
}

static var relationshipCodingInfo: RelationshipMixinCodingInfo {
let key = SymbolGraph.Relationship.CodingKeys(rawValue: Self.mixinKey)
return MixinCodingInformation(codingKey: key,
encode: { mixin, container in
try container.encode(mixin as! Self, forKey: key)
},
decode: { container in
try container.decode(Self.self, forKey: key)
})
}
}

typealias SymbolMixinCodingInfo = MixinCodingInformation<SymbolGraph.Symbol.CodingKeys>

typealias RelationshipMixinCodingInfo = MixinCodingInformation<SymbolGraph.Relationship.CodingKeys>

struct MixinCodingInformation<Key: CodingKey> {
let codingKey: Key
let encode: (Mixin, inout KeyedEncodingContainer<Key>) throws -> Void
let decode: (KeyedDecodingContainer<Key>) throws -> Mixin?
}

extension MixinCodingInformation {
func with(encodingErrorHandler: @escaping (_ error: Error, _ mixin: Mixin) throws -> Void) -> Self {
MixinCodingInformation(codingKey: self.codingKey, encode: { mixin, container in
do {
try self.encode(mixin, &container)
} catch {
try encodingErrorHandler(error, mixin)
}
}, decode: self.decode)
}

func with(decodingErrorHandler: @escaping (_ error: Error) throws -> Mixin?) -> Self {
MixinCodingInformation(codingKey: self.codingKey, encode: self.encode, decode: { container in
do {
return try self.decode(container)
} catch {
return try decodingErrorHandler(error)
}
})
}
}
Loading