Skip to content

Commit

Permalink
Add default displayable messages to TheError
Browse files Browse the repository at this point in the history
  • Loading branch information
KaQuMiQ authored Aug 31, 2022
1 parent 541f263 commit c753e70
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 28 deletions.
4 changes: 3 additions & 1 deletion Sources/MQ/Errors/Cancelled.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct Cancelled: TheError {
/// - Returns: New instance of ``Cancelled`` error with given context.
public static func error(
message: StaticString = "Cancelled",
displayableMessage: DisplayableString = "Cancelled",
displayableMessage: DisplayableString = TheErrorDisplayableMessages.message(for: Self.self),
file: StaticString = #fileID,
line: UInt = #line
) -> Self {
Expand All @@ -37,3 +37,5 @@ public struct Cancelled: TheError {
/// String representation displayable to the end user.
public var displayableMessage: DisplayableString
}

extension Cancelled: Hashable {}
4 changes: 3 additions & 1 deletion Sources/MQ/Errors/InternalInconsistency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct InternalInconsistency: TheError {
/// - Returns: New instance of ``InternalInconsistency`` error with given context.
public static func error(
message: StaticString,
displayableMessage: DisplayableString = "Internal inconsistency error",
displayableMessage: DisplayableString = TheErrorDisplayableMessages.message(for: Self.self),
file: StaticString = #fileID,
line: UInt = #line
) -> Self {
Expand All @@ -35,3 +35,5 @@ public struct InternalInconsistency: TheError {
/// String representation displayable to the end user.
public var displayableMessage: DisplayableString
}

extension InternalInconsistency: Hashable {}
8 changes: 7 additions & 1 deletion Sources/MQ/Errors/StringEncodingFailure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public struct StringEncodingFailure: TheError {
for string: String,
encoding: String.Encoding,
message: StaticString = "StringEncodingFailure",
displayableMessage: DisplayableString = TheErrorDisplayableMessages.message(for: Self.self),
file: StaticString = #fileID,
line: UInt = #line
) -> Self {
Expand All @@ -32,10 +33,15 @@ public struct StringEncodingFailure: TheError {
line: line
)
.with(string, for: "string")
.with(encoding, for: "encoding")
.with(encoding, for: "encoding"),
displayableMessage: displayableMessage
)
}

/// Source code context of this error.
public var context: SourceCodeContext
/// String representation displayable to the end user.
public var displayableMessage: DisplayableString
}

extension StringEncodingFailure: Hashable {}
79 changes: 77 additions & 2 deletions Sources/MQ/Errors/TheError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,28 @@
/// It also adds convenient methods for treating errors as fatal and assertion failures.
public protocol TheError: Error, CustomStringConvertible, CustomDebugStringConvertible {

/// Group assigned to this error type.
///
/// Access ``TheErrorGroup`` associated
/// with this error. It can be used
/// to quickly identify error domains or
/// group errors by any other meaning.
/// Default group for all errors is ``TheErrorGroup.default``.
/// You can assign any group to an error
/// by overriding ``TheError.group`` implementation.
static var group: TheErrorGroup { get }

/// Source code metadata context for this error.
/// It is used to derive default implementations
/// of ``Hashable`` and ``Equatable`` protocols.
var context: SourceCodeContext { get set }
/// String representation displayable to the end user.
///
/// Used as default value for ``localizedDescription``.
/// Implemented as error type name by default.
/// Default implementation fetches message from
/// ``TheErrorDisplayableMessages``.
/// It is used to derive default implementations
/// of ``Hashable`` and ``Equatable`` protocols.
var displayableMessage: DisplayableString { get }
}

Expand Down Expand Up @@ -84,17 +100,76 @@ extension TheError /* CustomDebugStringConvertible */ {
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
extension TheError {

public static var group: TheErrorGroup {
.default
}

public var displayableMessage: DisplayableString {
"\(Self.self)"
TheErrorDisplayableMessages.message(for: Self.self)
}

public var localizedDescription: String {
self.displayableMessage.resolved
}
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
extension TheError /* Equatable */ {

public static func == (
_ lhs: Self,
_ rhs: Self
) -> Bool {
lhs.context == rhs.context
&& lhs.displayableMessage == rhs.displayableMessage
}
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
extension TheError /* Hashable */ {

public func hash(
into hasher: inout Hasher
) {
hasher.combine(Self.id)
hasher.combine(self.context)
hasher.combine(self.displayableMessage)
}
}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
extension TheError {

public static func ~= <OtherError>(
_ lhs: Self,
_ rhs: OtherError.Type
) -> Bool
where OtherError: TheError {
Self.id == OtherError.id
}

public static func ~= (
_ lhs: Self,
_ rhs: TheErrorGroup
) -> Bool {
Self.group == rhs
}
}

extension TheError {

/// Unique id of this error type.
///
/// Access ``TheErrorID`` associated
/// with this error. It can be used
/// to quickly identify errors or store it
/// in collections requiring ``Hashable`` conformance.
/// It is used to derive default implementations
/// of ``Hashable`` and ``Equatable`` protocols.
public static var id: TheErrorID {
.init(Self.self)
}

/// Terminate process with this error as the cause.
///
/// - Parameters:
Expand Down
130 changes: 130 additions & 0 deletions Sources/MQ/Errors/TheErrorDisplayableMessages.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/// ``TheErrorDisplayableMessages`` is a container
/// holding default ``DisplayableMessage`` for errors
/// based on the error type.
/// Default implementation of ``TheError.displayableMessage``
/// uses value from this container unless
/// specific error implementation overrides it.
/// This allows to customize displayable messages
/// for unowned errors which implementation comes
/// from external source. It can be also used
/// as default message source for TheError implementations.
/// Message for each error type can be set only once
/// and only unless message was already requested for
/// a given type. Default message for each type is
/// the error type name if no value was set.
public enum TheErrorDisplayableMessages {

@usableFromInline internal static let storage: CriticalSection<Dictionary<AnyHashable, DisplayableString>> = .init(
.init()
)

/// Get the message for given error.
///
/// Access ``DisplayableString`` associated
/// with given error type or its group.
/// If no message was set for requested type
/// its group will be checked for a message.
/// If requested error group does not have
/// associated message, error name will be used instead.
/// In orded to customize messages use
/// ``TheErrorDisplayableMessages.setMessage(_:for:)`` or
/// ``TheErrorDisplayableMessages.setMessage(_:forGroup:)``.
@inlinable @Sendable public static func message<ErrorType>(
for _: ErrorType.Type
) -> DisplayableString
where ErrorType: TheError {
self.storage.access { (messages: inout Dictionary<AnyHashable, DisplayableString>) -> DisplayableString in
if let message: DisplayableString = messages[ErrorType.id] {
return message
}
else if let firstMatchingGroupIdentifier: TheErrorGroup.Identifier = ErrorType.group.firstMatchingIdentifier(
messages.keys.contains(_:)
),
let message: DisplayableString = messages[firstMatchingGroupIdentifier]
{
messages[ErrorType.id] = message
return message
}
else if let message: DisplayableString = messages[TheErrorGroup.default] {
messages[ErrorType.id] = message
return message
}
else {
let message: DisplayableString = "\(ErrorType.self)"
messages[ErrorType.id] = message
return message
}
}
}

/// Set the message for given error.
///
/// Associate message with given error type.
/// Message can be set only once and only
/// if it was not requested before.
///
/// - Note: Assigned message will be cached
/// and reused. Make sure that it does not
/// provide dynamic value that is expected
/// to be changing over time.
@inlinable @Sendable public static func setMessage<ErrorType>(
_ message: DisplayableString,
for _: ErrorType.Type
) where ErrorType: TheError {
self.storage.access { (messages: inout Dictionary<AnyHashable, DisplayableString>) -> Void in
runtimeAssert(
!messages.keys.contains(ErrorType.id),
message: "Error message can be assigned only once."
)
messages[ErrorType.id] = message
}
}

/// Set the message for given error group.
///
/// Associate message with given error group.
/// Group intersections are not verified,
/// group must be equal to provided in order
/// to use customized message.
/// Message can be set only once.
///
/// - Note: Assigned message will be cached
/// and reused. Make sure that it does not
/// provide dynamic value that is expected
/// to be changing over time.
@inlinable @Sendable public static func setMessage(
_ message: DisplayableString,
forGroup groupIdentifier: TheErrorGroup.Identifier
) {
self.storage.access { (messages: inout Dictionary<AnyHashable, DisplayableString>) -> Void in
runtimeAssert(
!messages.keys.contains(groupIdentifier),
message: "Error message can be assigned only once."
)
messages[groupIdentifier] = message
}
}

/// Set the default message for errors.
///
/// Associate message with the default error group.
/// If default message is not set error type will be
/// used as the message.
/// Message can be set only once.
///
/// - Note: Assigned message will be cached
/// and reused. Make sure that it does not
/// provide dynamic value that is expected
/// to be changing over time.
@inlinable @Sendable public static func setDefaultMessage(
_ message: DisplayableString
) {
self.storage.access { (messages: inout Dictionary<AnyHashable, DisplayableString>) -> Void in
runtimeAssert(
!messages.keys.contains(TheErrorGroup.default),
message: "Error message can be assigned only once."
)
messages[TheErrorGroup.default] = message
}
}
}
54 changes: 54 additions & 0 deletions Sources/MQ/Errors/TheErrorGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
public struct TheErrorGroup {

private var identifiers: Array<Identifier>
}

extension TheErrorGroup {

public struct Identifier {

private let identifier: StaticString
}
}

extension TheErrorGroup.Identifier: Hashable {}

extension TheErrorGroup.Identifier: ExpressibleByStringLiteral {

public init(
stringLiteral value: StaticString
) {
self.identifier = value
}
}

extension TheErrorGroup: Hashable {}

extension TheErrorGroup: ExpressibleByStringLiteral {

public init(
stringLiteral value: StaticString
) {
self.identifiers = [.init(stringLiteral: value)]
}
}

extension TheErrorGroup: ExpressibleByArrayLiteral {

public init(
arrayLiteral elements: TheErrorGroup.Identifier...
) {
self.identifiers = .init(elements)
}
}

extension TheErrorGroup {

public static let `default`: Self = .init()

@usableFromInline internal func firstMatchingIdentifier(
_ matches: (Identifier) -> Bool
) -> Identifier? {
self.identifiers.first(where: matches)
}
}
24 changes: 24 additions & 0 deletions Sources/MQ/Errors/TheErrorID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// Unique identifier of TheError type.
///
/// ``TheErrorID`` is an identifier available
/// to all types of ``TheError``. It is based
/// on the actual error type ignoring its instance
/// variables.
public struct TheErrorID {

private let identifier: ObjectIdentifier

internal init<ErrorType>(
_: ErrorType.Type
) where ErrorType: TheError {
self.identifier = .init(ErrorType.self)
}
}

extension TheErrorID: Hashable {}

// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
extension TheErrorID: Identifiable {

public var id: Self { self }
}
4 changes: 3 additions & 1 deletion Sources/MQ/Errors/Undefined.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct Undefined: TheError {
/// - Returns: New instance of ``Undefined`` error with given context.
public static func error(
message: StaticString = "Undefined",
displayableMessage: DisplayableString = "Undefined error",
displayableMessage: DisplayableString = TheErrorDisplayableMessages.message(for: Self.self),
file: StaticString = #fileID,
line: UInt = #line
) -> Self {
Expand All @@ -38,3 +38,5 @@ public struct Undefined: TheError {
/// String representation displayable to the end user.
public var displayableMessage: DisplayableString
}

extension Undefined: Hashable {}
Loading

0 comments on commit c753e70

Please sign in to comment.