Skip to content

Commit b45d1f2

Browse files
authored
Make Symbol Graph Format Extensible (#39)
* introduce "extension" symbol kind + "extensionTo" relationship kind and make graph format extensible - add symbol and relationship kinds introduced in apple/swift #59047 (swiftlang/swift#59047) - change Symbol.KindIdentifier to use a struct-type with static properties for known values - change Codable conformance of Symbol and Relationship to allow for encoding and decoding unknown Mixins * use Coder's userInfo instead of static property for storing and registering unknown Mixin types * add identifier storage to ensure language prefixes are treated as usual * add tests for custom Mixin coding + Relationship's Hashable conformance * allow registering custom Symbol kinds to Decoder + add documentation * add test checking Symbol.KindIdentifier.allCases * remove CustomizableCoder protocol * refactor handling of mixin coding errors * move decoding/encoding closure from CodingKeys to MixinCodingInformation + unify error handling closures and coding closures * mention that KindIdenifier/register(_:) should not be used concurrently in docs * refactor Mixin Hashable conformance + improve code-docs * add relationship hashing/equality test cases * fix CodingUserInfoKey values to use org.swift prefix
1 parent ccad2c3 commit b45d1f2

File tree

15 files changed

+1168
-305
lines changed

15 files changed

+1168
-305
lines changed

Sources/SymbolKit/Mixin.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
// `Mixin` does not conform to `Equatable` right now primarily because
14+
// this would complicate its usage in many situtations because of "Self
15+
// or associated type" requirements errors. Thus, in order to compare
16+
// `Mixin`s for equality, we need to somehow get access to the `Mixin`'s
17+
// `Equatable` conformance and the `==(lhs:rhs:)` function specifically.
18+
//
19+
// Note that all of this would be siginificantly easier in Swift 5.7, so
20+
// it might be worth updating the implementation once SymbolKit adopts
21+
// Swift 5.7 as its minimum language requirement.
22+
23+
24+
// When working with `Mixin` values in a generic (non-specific) context,
25+
// we only know their value conforms to the existential type `Mixin`. This
26+
// extension to `Mixin` and the `equals` property defined in it is essentiall for
27+
// the whole process to work:
28+
// The `equals` property does not expose the `Self` type in its interface and
29+
// therefore is accessible from the existential type `Mixin`. Inside `equals`,
30+
// however, we have access to the concrete type `Self`, allowing us to initialize
31+
// the `EquatableDetector` with a concrete generic type, which can know it conforms
32+
// to `Equatable`. If we were to simply pass a value of type `Any` into the initializer
33+
// of `EquatableDetector`, the latter would not recognize `value` as `Equatable`, even
34+
// if the original concrete type were to conform to `Equatable`.
35+
extension Mixin {
36+
/// A type-erased version of this ``Mixin``s `==(lhs:rhs:)` function, available
37+
/// only if this ``Mixin`` conforms to `Equatable`.
38+
var equals: ((Any) -> Bool)? {
39+
(EquatableDetector(value: self) as? AnyEquatable)?.equals
40+
}
41+
}
42+
43+
// The `AnyEquatable` protocol defines our requirement for an equality function
44+
// in a type-erased way. It has no Self or associated type requirements and thus
45+
// can be casted to via a simple `as?`. In Swift 5.7 we could simply cast to
46+
// `any Equatable`, but this was not possible before.
47+
private protocol AnyEquatable {
48+
var equals: (Any) -> Bool { get }
49+
}
50+
51+
// The `EquatableDetector` brings both pieces together by conditionally conforming
52+
// itself to `AnyEquatable` where its generic `value` is `Equatable`.
53+
private struct EquatableDetector<T> {
54+
let value: T
55+
}
56+
57+
extension EquatableDetector: AnyEquatable where T: Equatable {
58+
var equals: (Any) -> Bool {
59+
{ other in
60+
guard let other = other as? T else {
61+
// we are comparing `value` against `other`, but
62+
// `other` is of a different type, so they can't be
63+
// equal
64+
return false
65+
}
66+
67+
// we finally know that `value`, as well as `other`
68+
// are of the same type `T`, which conforms to `Equatable`
69+
return value == other
70+
}
71+
}
72+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
// `Mixin` does not conform to `Hashable` right now primarily because
14+
// this would complicate its usage in many situtations because of "Self
15+
// or associated type" requirements errors. `Hashable` inherits those
16+
// frome `Equatable`, even though its primary functionality, the `hash(into:)`
17+
// function has no Self or associated type requirements. Thus, in order to
18+
// access a `Mixin`'s `hash(into:)` function, we need to somehow get access to
19+
// the `Mixin`'s `Hashable` conformance.
20+
//
21+
// Note that all of this would be siginificantly easier in Swift 5.7, so
22+
// it might be worth updating the implementation once SymbolKit adopts
23+
// Swift 5.7 as its minimum language requirement.
24+
25+
26+
// When working with `Mixin` values in a generic (non-specific) context,
27+
// we only know their value conforms to the existential type `Mixin`. This
28+
// extension to `Mixin` and the `hash` property defined in it is essentiall for
29+
// the whole process to work:
30+
// The `hash` property does not expose the `Self` type in its interface and
31+
// therefore is accessible from the existential type `Mixin`. Inside `hash`,
32+
// however, we have access to the concrete type `Self`, allowing us to initialize
33+
// the `HashableDetector` with a concrete generic type, which can know it conforms
34+
// to `Hashable`. If we were to simply pass a value of type `Any` into the initializer
35+
// of `HashableDetector`, the latter would not recognize `value` as `Hashable`, even
36+
// if the original concrete type were to conform to `Hashable`.
37+
extension Mixin {
38+
/// This ``Mixin``s `hash(into:)` function, available
39+
/// only if this ``Mixin`` conforms to `Hashable`.
40+
var hash: ((inout Hasher) -> Void)? {
41+
(HashableDetector(value: self) as? AnyHashable)?.hash
42+
}
43+
}
44+
45+
// The `AnyEquatable` protocol simply defines our requirement for a hash
46+
// function. It has no Self or associated type requirements and thus can
47+
// be casted to via a simple `as?`. In Swift 5.7 we could simply cast to
48+
// `any Hashable`, but this was not possible before.
49+
private protocol AnyHashable {
50+
var hash: (inout Hasher) -> Void { get }
51+
}
52+
53+
// The `HashableDetector` brings both pieces together by conditionally conforming
54+
// itself to `AnyHashable` where its generic `value` is `Hashable`.
55+
private struct HashableDetector<T> {
56+
let value: T
57+
}
58+
59+
extension HashableDetector: AnyHashable where T: Hashable {
60+
var hash: (inout Hasher) -> Void {
61+
value.hash(into:)
62+
}
63+
}

Sources/SymbolKit/Mixin/Mixin.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/**
14+
A protocol that allows extracted symbols to have extra data
15+
aside from the base ``SymbolGraph/Symbol``.
16+
17+
- Note: If you intend to encode/decode a custom ``Mixin`` as part of a relationship or symbol, make sure
18+
to register its type to your encoder/decoder instance using
19+
``SymbolGraph/Relationship/register(mixins:to:onEncodingError:onDecodingError:)``
20+
or ``SymbolGraph/Symbol/register(mixins:to:onEncodingError:onDecodingError:)``, respectively.
21+
*/
22+
public protocol Mixin: Codable {
23+
/**
24+
The key under which a mixin's data is filed.
25+
26+
> Important: With respect to deserialization, this framework assumes `mixinKey`s between instances of `SymbolMixin` are unique.
27+
*/
28+
static var mixinKey: String { get }
29+
}
30+
31+
// This extension provides coding information for any instance of Mixin. These
32+
// coding infos wrap the encoding and decoding logic for the respective
33+
// instances in a type-erased way. Thus, the concrete instance type of Mixins
34+
// does not need to be known by the encoding/decoding logic in Symbol and
35+
// Relationship.
36+
extension Mixin {
37+
static var symbolCodingInfo: SymbolMixinCodingInfo {
38+
let key = SymbolGraph.Symbol.CodingKeys(rawValue: Self.mixinKey)
39+
return MixinCodingInformation(codingKey: key,
40+
encode: { mixin, container in
41+
try container.encode(mixin as! Self, forKey: key)
42+
},
43+
decode: { container in
44+
try container.decode(Self.self, forKey: key)
45+
})
46+
}
47+
48+
static var relationshipCodingInfo: RelationshipMixinCodingInfo {
49+
let key = SymbolGraph.Relationship.CodingKeys(rawValue: Self.mixinKey)
50+
return MixinCodingInformation(codingKey: key,
51+
encode: { mixin, container in
52+
try container.encode(mixin as! Self, forKey: key)
53+
},
54+
decode: { container in
55+
try container.decode(Self.self, forKey: key)
56+
})
57+
}
58+
}
59+
60+
typealias SymbolMixinCodingInfo = MixinCodingInformation<SymbolGraph.Symbol.CodingKeys>
61+
62+
typealias RelationshipMixinCodingInfo = MixinCodingInformation<SymbolGraph.Relationship.CodingKeys>
63+
64+
struct MixinCodingInformation<Key: CodingKey> {
65+
let codingKey: Key
66+
let encode: (Mixin, inout KeyedEncodingContainer<Key>) throws -> Void
67+
let decode: (KeyedDecodingContainer<Key>) throws -> Mixin?
68+
}
69+
70+
extension MixinCodingInformation {
71+
func with(encodingErrorHandler: @escaping (_ error: Error, _ mixin: Mixin) throws -> Void) -> Self {
72+
MixinCodingInformation(codingKey: self.codingKey, encode: { mixin, container in
73+
do {
74+
try self.encode(mixin, &container)
75+
} catch {
76+
try encodingErrorHandler(error, mixin)
77+
}
78+
}, decode: self.decode)
79+
}
80+
81+
func with(decodingErrorHandler: @escaping (_ error: Error) throws -> Mixin?) -> Self {
82+
MixinCodingInformation(codingKey: self.codingKey, encode: self.encode, decode: { container in
83+
do {
84+
return try self.decode(container)
85+
} catch {
86+
return try decodingErrorHandler(error)
87+
}
88+
})
89+
}
90+
}

0 commit comments

Comments
 (0)