diff --git a/Sources/CodableKit/File.swift b/Sources/CodableKit/File.swift index e3da17ff..3ed82689 100644 --- a/Sources/CodableKit/File.swift +++ b/Sources/CodableKit/File.swift @@ -1,7 +1,7 @@ import Debugging /// Errors that can be thrown while working with TCP sockets. -public struct CodableError: Traceable, Debuggable, Helpable, Swift.Error, Encodable { +public struct CodableError: Debuggable { public static let readableName = "Codable Error" public let identifier: String public var reason: String diff --git a/Sources/Debugging/Debuggable.swift b/Sources/Debugging/Debuggable.swift index 362e74cf..64e7c511 100644 --- a/Sources/Debugging/Debuggable.swift +++ b/Sources/Debugging/Debuggable.swift @@ -1,9 +1,116 @@ +import Foundation + /// `Debuggable` provides an interface that allows a type /// to be more easily debugged in the case of an error. -public protocol Debuggable: CustomDebugStringConvertible, CustomStringConvertible, Identifiable {} +public protocol Debuggable: CustomDebugStringConvertible, CustomStringConvertible, LocalizedError { + /// A readable name for the error's Type. This is usually + /// similar to the Type name of the error with spaces added. + /// This will normally be printed proceeding the error's reason. + /// - note: For example, an error named `FooError` will have the + /// `readableName` `"Foo Error"`. + static var readableName: String { get } + + /// A unique identifier for the error's Type. + /// - note: This defaults to `ModuleName.TypeName`, + /// and is used to create the `identifier` property. + static var typeIdentifier: String { get } + + /// Some unique identifier for this specific error. + /// This will be used to create the `identifier` property. + /// Do NOT use `String(reflecting: self)` or `String(describing: self)` + /// or there will be infinite recursion + var identifier: String { get } + + /// The reason for the error. Usually one sentence (that should end with a period). + var reason: String { get } + + /// Optional source location for this error + var sourceLocation: SourceLocation? { get } + + /// Stack trace from which this error originated (must set this from the error's init) + var stackTrace: [String]? { get } + + /// A `String` array describing the possible causes of the error. + /// - note: Defaults to an empty array. + /// Provide a custom implementation to give more context. + var possibleCauses: [String] { get } + + /// A `String` array listing some common fixes for the error. + /// - note: Defaults to an empty array. + /// Provide a custom implementation to be more helpful. + var suggestedFixes: [String] { get } + + /// An array of string `URL`s linking to documentation pertaining to the error. + /// - note: Defaults to an empty array. + /// Provide a custom implementation with relevant links. + var documentationLinks: [String] { get } + + /// An array of string `URL`s linking to related Stack Overflow questions. + /// - note: Defaults to an empty array. + /// Provide a custom implementation to link to useful questions. + var stackOverflowQuestions: [String] { get } + + /// An array of string `URL`s linking to related issues on Vapor's GitHub repo. + /// - note: Defaults to an empty array. + /// Provide a custom implementation to a list of pertinent issues. + var gitHubIssues: [String] { get } +} + + +/// MARK: Computed + +extension Debuggable { + /// Generates a stack trace from the call point. + /// Must call this from the error's init. + public static func makeStackTrace() -> [String] { + return StackTrace.get() + } +} + +extension Debuggable { + public var fullIdentifier: String { + return Self.typeIdentifier + "." + identifier + } +} // MARK: Defaults +extension Debuggable { + /// Default implementation of readable name that expands + /// SomeModule.MyType.Error => My Type Error + public static var readableName: String { + return typeIdentifier + } + + /// See `Identifiable.typeIdentifier` + public static var typeIdentifier: String { + let type = "\(self)" + return type.split(separator: ".").last.flatMap(String.init) ?? type + } + + public var documentationLinks: [String] { + return [] + } + + public var stackOverflowQuestions: [String] { + return [] + } + + public var gitHubIssues: [String] { + return [] + } + + public var sourceLocation: SourceLocation? { + return nil + } + + public var stackTrace: [String]? { + return nil + } +} + +/// MARK: Custom...StringConvertible + extension Debuggable { public var debugDescription: String { return debuggableHelp(format: .long) @@ -14,15 +121,30 @@ extension Debuggable { } } +// MARK: Localized + extension Debuggable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.reason) - } + /// A localized message describing what error occurred. + public var errorDescription: String? { return description } + + /// A localized message describing the reason for the failure. + public var failureReason: String? { return reason } + + /// A localized message describing how one might recover from the failure. + public var recoverySuggestion: String? { return suggestedFixes.first } + + /// A localized message providing "help" text if the user requests help. + public var helpAnchor: String? { return documentationLinks.first } } + // MARK: Representations +public enum HelpFormat { + case short + case long +} + extension Debuggable { /// A computed property returning a `String` that encapsulates /// why the error occurred, suggestions on how to fix the problem, @@ -31,27 +153,78 @@ extension Debuggable { public func debuggableHelp(format: HelpFormat) -> String { var print: [String] = [] - print.append(identifiableHelp(format: format)) - - if let traceable = self as? Traceable { - print.append(traceable.traceableHelp(format: format)) + switch format { + case .long: + print.append("⚠️ \(Self.readableName): \(reason)\n- id: \(fullIdentifier)") + case .short: + print.append("⚠️ [\(fullIdentifier): \(reason)]") } - if let helpable = self as? Helpable { - print.append(helpable.helpableHelp(format: format)) + if let source = sourceLocation { + switch format { + case .long: + var help: [String] = [] + help.append("File: \(source.file)") + help.append(" - func: \(source.function)") + help.append(" - line: \(source.line)") + help.append(" - column: \(source.column)") + if let range = source.range { + help.append("- range: \(range)") + } + print.append(help.joined(separator: "\n")) + case .short: + var string = "[\(source.file):\(source.line):\(source.column)" + if let range = source.range { + string += " (\(range))" + } + string += "]" + print.append(string) + } } - if let traceable = self as? Traceable, format == .long { - let lines = ["Stack Trace:"] + traceable.stackTrace - print.append(lines.joined(separator: "\n")) - } + switch format { + case .long: + if !possibleCauses.isEmpty { + print.append("Here are some possible causes: \(possibleCauses.bulletedList)") + } + + if !suggestedFixes.isEmpty { + print.append("These suggestions could address the issue: \(suggestedFixes.bulletedList)") + } + + if !documentationLinks.isEmpty { + print.append("Vapor's documentation talks about this: \(documentationLinks.bulletedList)") + } + if !stackOverflowQuestions.isEmpty { + print.append("These Stack Overflow links might be helpful: \(stackOverflowQuestions.bulletedList)") + } + + if !gitHubIssues.isEmpty { + print.append("See these Github issues for discussion on this topic: \(gitHubIssues.bulletedList)") + } + case .short: + if possibleCauses.count > 0 { + print.append("[Possible causes: \(possibleCauses.joined(separator: " "))]") + } + if suggestedFixes.count > 0 { + print.append("[Suggested fixes: \(suggestedFixes.joined(separator: " "))]") + } + } switch format { case .long: - return print.joined(separator: "\n\n") + return print.joined(separator: "\n\n") + "\n" case .short: return print.joined(separator: " ") } } } + + +extension Sequence where Iterator.Element == String { + var bulletedList: String { + return map { "\n- \($0)" } .joined() + } +} + diff --git a/Sources/Debugging/Helpable.swift b/Sources/Debugging/Helpable.swift deleted file mode 100644 index 782b8981..00000000 --- a/Sources/Debugging/Helpable.swift +++ /dev/null @@ -1,90 +0,0 @@ -public protocol Helpable { - /// A `String` array describing the possible causes of the error. - /// - note: Defaults to an empty array. - /// Provide a custom implementation to give more context. - var possibleCauses: [String] { get } - - /// A `String` array listing some common fixes for the error. - /// - note: Defaults to an empty array. - /// Provide a custom implementation to be more helpful. - var suggestedFixes: [String] { get } - - /// An array of string `URL`s linking to documentation pertaining to the error. - /// - note: Defaults to an empty array. - /// Provide a custom implementation with relevant links. - var documentationLinks: [String] { get } - - /// An array of string `URL`s linking to related Stack Overflow questions. - /// - note: Defaults to an empty array. - /// Provide a custom implementation to link to useful questions. - var stackOverflowQuestions: [String] { get } - - /// An array of string `URL`s linking to related issues on Vapor's GitHub repo. - /// - note: Defaults to an empty array. - /// Provide a custom implementation to a list of pertinent issues. - var gitHubIssues: [String] { get } -} - -extension Helpable { - public func helpableHelp(format: HelpFormat) -> String { - switch format { - case .long: - var print: [String] = [] - - if !possibleCauses.isEmpty { - print.append("Here are some possible causes: \(possibleCauses.bulletedList)") - } - - if !suggestedFixes.isEmpty { - print.append("These suggestions could address the issue: \(suggestedFixes.bulletedList)") - } - - if !documentationLinks.isEmpty { - print.append("Vapor's documentation talks about this: \(documentationLinks.bulletedList)") - } - - if !stackOverflowQuestions.isEmpty { - print.append("These Stack Overflow links might be helpful: \(stackOverflowQuestions.bulletedList)") - } - - if !gitHubIssues.isEmpty { - print.append("See these Github issues for discussion on this topic: \(gitHubIssues.bulletedList)") - } - - return print.joined(separator: "\n\n") - case .short: - var string: [String] = [] - if possibleCauses.count > 0 { - string.append("[Possible causes: \(possibleCauses.joined(separator: ","))]") - } - if suggestedFixes.count > 0 { - string.append("[Suggested fixes: \(suggestedFixes.joined(separator: ","))]") - } - return string.joined(separator: " ") - } - } -} - - -// MARK: Optionals - -extension Helpable { - public var documentationLinks: [String] { - return [] - } - - public var stackOverflowQuestions: [String] { - return [] - } - - public var gitHubIssues: [String] { - return [] - } -} - - -extension Sequence where Iterator.Element == String { - var bulletedList: String { - return map { "\n- \($0)" } .joined() - } -} diff --git a/Sources/Debugging/Identifiable.swift b/Sources/Debugging/Identifiable.swift deleted file mode 100644 index ff65c1bd..00000000 --- a/Sources/Debugging/Identifiable.swift +++ /dev/null @@ -1,99 +0,0 @@ -public protocol Identifiable { - /// A readable name for the error's Type. This is usually - /// similar to the Type name of the error with spaces added. - /// This will normally be printed proceeding the error's reason. - /// - note: For example, an error named `FooError` will have the - /// `readableName` `"Foo Error"`. - static var readableName: String { get } - - /// The reason for the error. - /// Typical implementations will switch over `self` - /// and return a friendly `String` describing the error. - /// - note: It is most convenient that `self` be a `Swift.Error`. - /// - /// Here is one way to do this: - /// - /// switch self { - /// case someError: - /// return "A `String` describing what went wrong including the actual error: `Error.someError`." - /// // other cases - /// } - var reason: String { get } - - // MARK: Identifiers - - /// A unique identifier for the error's Type. - /// - note: This defaults to `ModuleName.TypeName`, - /// and is used to create the `identifier` property. - static var typeIdentifier: String { get } - - /// Some unique identifier for this specific error. - /// This will be used to create the `identifier` property. - /// Do NOT use `String(reflecting: self)` or `String(describing: self)` - /// or there will be infinite recursion - var identifier: String { get } -} - -extension Identifiable { - public func identifiableHelp(format: HelpFormat) -> String { - var print: [String] = [] - - switch format { - case .long: - print.append("⚠️ \(Self.readableName): \(reason)") - print.append("- id: \(fullIdentifier)") - case .short: - print.append("⚠️ [\(fullIdentifier): \(reason)]") - } - - return print.joined(separator: "\n") - } - - public var fullIdentifier: String { - return Self.typeIdentifier + "." + identifier - } -} - -extension Identifiable { - /// Default implementation of readable name that expands - /// SomeModule.MyType.Error => My Type Error - public static var readableName: String { - return typeIdentifier.readableTypeName() - } - - public static var typeIdentifier: String { - return String(reflecting: self) - } -} - -extension String { - func readableTypeName() -> String { - let characterSequence = self.split(separator: ".") - .dropFirst() // drop module - .joined(separator: []) - - let characters = Array(characterSequence) - guard var expanded = characters.first.flatMap({ String($0) }) else { return "" } - - characters.suffix(from: 1).forEach { char in - if char.isUppercase { - expanded.append(" ") - } - - expanded.append(char) - } - - return expanded - } -} - -extension Character { - var isUppercase: Bool { - switch self { - case "A"..."Z": - return true - default: - return false - } - } -} diff --git a/Sources/Debugging/SourceLocation.swift b/Sources/Debugging/SourceLocation.swift new file mode 100644 index 00000000..db6cfed4 --- /dev/null +++ b/Sources/Debugging/SourceLocation.swift @@ -0,0 +1,7 @@ +public struct SourceLocation { + var file: String + var function: String + var line: UInt + var column: UInt + var range: Range? +} diff --git a/Sources/Debugging/Traceable.swift b/Sources/Debugging/Traceable.swift deleted file mode 100644 index babde631..00000000 --- a/Sources/Debugging/Traceable.swift +++ /dev/null @@ -1,52 +0,0 @@ -public protocol Traceable { - var file: String { get } - var function: String { get } - var line: UInt { get } - var column: UInt { get } - var stackTrace: [String] { get } - var range: Range? { get } -} - -extension Traceable { - public func traceableHelp(format: HelpFormat) -> String { - - switch format { - case .long: - var help: [String] = [] - help.append("File: \(file)") - help.append(" - func: \(function)") - help.append(" - line: \(line)") - help.append(" - column: \(column)") - if let range = range { - help.append("- range: \(range)") - } - return help.joined(separator: "\n") - case .short: - var string = "[\(file):\(line):\(column)" - if let range = range { - string += " (\(range))" - } - string += "]" - return string - } - - } -} - -extension Traceable { - public static func makeStackTrace() -> [String] { - return StackTrace.get() - } -} - -extension Traceable { - public var range: Range? { - return nil - } -} - - -public enum HelpFormat { - case short - case long -} diff --git a/Tests/DebuggingTests/FooError.swift b/Tests/DebuggingTests/FooError.swift index d466d6e1..54e236b5 100644 --- a/Tests/DebuggingTests/FooError.swift +++ b/Tests/DebuggingTests/FooError.swift @@ -4,7 +4,7 @@ enum FooError: String, Error { case noFoo } -extension FooError: Debuggable, Helpable { +extension FooError: Debuggable { static var readableName: String { return "Foo Error" } diff --git a/Tests/DebuggingTests/FooErrorTests.swift b/Tests/DebuggingTests/FooErrorTests.swift index c61ddd5e..8a33311e 100644 --- a/Tests/DebuggingTests/FooErrorTests.swift +++ b/Tests/DebuggingTests/FooErrorTests.swift @@ -13,7 +13,8 @@ class FooErrorTests: XCTestCase { let error: FooError = .noFoo - func testPrintable() { + func testPrintable() throws { + print(error.debugDescription) XCTAssertEqual( error.debugDescription, expectedPrintable, @@ -70,7 +71,7 @@ class FooErrorTests: XCTestCase { private let expectedPrintable: String = { var expectation = "⚠️ Foo Error: You do not have a `foo`.\n" - expectation += "- id: DebuggingTests.FooError.noFoo\n\n" + expectation += "- id: FooError.noFoo\n\n" expectation += "Here are some possible causes: \n" expectation += "- You did not set the flongwaffle.\n" @@ -84,7 +85,7 @@ private let expectedPrintable: String = { expectation += "Vapor's documentation talks about this: \n" expectation += "- http://documentation.com/Foo\n" - expectation += "- http://documentation.com/foo/noFoo" + expectation += "- http://documentation.com/foo/noFoo\n" return expectation }() diff --git a/Tests/DebuggingTests/GeneralTests.swift b/Tests/DebuggingTests/GeneralTests.swift index 1e23ec50..09a88620 100644 --- a/Tests/DebuggingTests/GeneralTests.swift +++ b/Tests/DebuggingTests/GeneralTests.swift @@ -15,35 +15,15 @@ class GeneralTests: XCTestCase { XCTAssertEqual(bulleted, expectation) } - func testReadableName() { - let typeName = "SomeRandomModule.MyType.Error" - let readableName = typeName.readableTypeName() - let expectation = "My Type Error" - XCTAssertEqual(readableName, expectation) - } - - func testReadableNameEdgeCase() { - let edgeCases = [ - "SomeModule.": "", - "SomeModule.S": "S" - ] - edgeCases.forEach { edgeCase, expectation in - let readableName = edgeCase.readableTypeName() - XCTAssertEqual(readableName, expectation) - } - } - func testMinimumConformance() { let minimum = MinimumError.alpha let description = minimum.debugDescription - let expectation = "⚠️ Minimum Error: Not enabled\n- id: DebuggingTests.MinimumError.alpha" + let expectation = "⚠️ MinimumError: Not enabled\n- id: MinimumError.alpha\n" XCTAssertEqual(description, expectation) } static let allTests = [ ("testBulletedList", testBulletedList), - ("testReadableName", testReadableName), - ("testReadableNameEdgeCase", testReadableNameEdgeCase), ("testMinimumConformance", testMinimumConformance), ] } diff --git a/Tests/DebuggingTests/TestError.swift b/Tests/DebuggingTests/TestError.swift index b4a2a6d1..c7d7d006 100644 --- a/Tests/DebuggingTests/TestError.swift +++ b/Tests/DebuggingTests/TestError.swift @@ -30,16 +30,11 @@ struct TestError: Error { } } -extension TestError: Identifiable { +extension TestError: Debuggable { var identifier: String { return kind.rawValue } -} - -extension TestError: Traceable { } -extension TestError: Debuggable { } - -extension TestError: Helpable { + var possibleCauses: [String] { switch kind { case .foo: