├── .circleci └── config.yml ├── .gitignore ├── Debugging.podspec ├── LICENSE ├── Package.swift ├── Package@swift-4.swift ├── README.md ├── Sources └── Debugging │ └── Debuggable.swift └── Tests ├── DebuggingTests ├── FooError.swift ├── FooErrorTests.swift ├── GeneralTests.swift └── MinimumError.swift └── LinuxMain.swift /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | macos: 5 | macos: 6 | xcode: "9.0" 7 | steps: 8 | - run: brew install vapor/tap/vapor 9 | - checkout 10 | - run: swift build 11 | - run: swift test 12 | 13 | linux-3: 14 | docker: 15 | - image: swift:3.1.1 16 | steps: 17 | - run: apt-get install -yq libssl-dev 18 | - checkout 19 | - run: swift build 20 | - run: swift test 21 | 22 | linux: 23 | docker: 24 | - image: swift:4.0.3 25 | steps: 26 | - run: apt-get install -yq libssl-dev 27 | - checkout 28 | - run: swift build 29 | - run: swift test 30 | 31 | workflows: 32 | version: 2 33 | tests: 34 | jobs: 35 | - macos 36 | - linux-3 37 | - linux 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/*/*.DS_Store 2 | # Xcode 3 | # 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | *.xcsmblueprint 21 | 22 | # CocoaPods 23 | # 24 | # We recommend against adding the Pods directory to your .gitignore. However 25 | # you should judge for yourself, the pros and cons are mentioned at: 26 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 27 | # 28 | Pods/ 29 | 30 | # Carthage 31 | # 32 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 33 | Carthage/Checkouts 34 | 35 | Carthage/Build 36 | 37 | # Swift Package Manager 38 | .build/ 39 | Packages/ 40 | *.xcodeproj 41 | Package.pins 42 | -------------------------------------------------------------------------------- /Debugging.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'Debugging' 3 | spec.version = '1.0.0' 4 | spec.license = 'MIT' 5 | spec.homepage = 'https://github.com/vapor/debugging' 6 | spec.authors = { 'Vapor' => 'contact@vapor.codes' } 7 | spec.summary = 'A library to aid Vapor users with better debugging around the framework' 8 | spec.source = { :git => "#{spec.homepage}.git", :tag => "#{spec.version}" } 9 | spec.ios.deployment_target = "8.0" 10 | spec.osx.deployment_target = "10.9" 11 | spec.watchos.deployment_target = "2.0" 12 | spec.tvos.deployment_target = "9.0" 13 | spec.requires_arc = true 14 | spec.social_media_url = 'https://twitter.com/codevapor' 15 | spec.default_subspec = "Default" 16 | 17 | spec.subspec "Default" do |ss| 18 | ss.source_files = 'Sources/**/*.{swift}' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:3.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Debugging" 6 | ) 7 | -------------------------------------------------------------------------------- /Package@swift-4.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Debugging", 6 | products: [ 7 | .library(name: "Debugging", targets: ["Debugging"]) 8 | ], 9 | targets: [ 10 | .target(name: "Debugging"), 11 | .testTarget(name: "DebuggingTests", dependencies: ["Debugging"]) 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Debugging 3 |
4 |
5 | 6 | Documentation 7 | 8 | 9 | Slack Team 10 | 11 | 12 | MIT License 13 | 14 | 15 | Continuous Integration 16 | 17 | 18 | Swift 3.1 19 | 20 |

21 | -------------------------------------------------------------------------------- /Sources/Debugging/Debuggable.swift: -------------------------------------------------------------------------------- 1 | /// `Debuggable` provides an interface that allows a type 2 | /// to be more easily debugged in the case of an error. 3 | public protocol Debuggable: Swift.Error, CustomDebugStringConvertible { 4 | /// A readable name for the error's Type. This is usually 5 | /// similar to the Type name of the error with spaces added. 6 | /// This will normally be printed proceeding the error's reason. 7 | /// - note: For example, an error named `FooError` will have the 8 | /// `readableName` `"Foo Error"`. 9 | static var readableName: String { get } 10 | 11 | /// The reason for the error. 12 | /// Typical implementations will switch over `self` 13 | /// and return a friendly `String` describing the error. 14 | /// - note: It is most convenient that `self` be a `Swift.Error`. 15 | /// 16 | /// Here is one way to do this: 17 | /// 18 | /// switch self { 19 | /// case someError: 20 | /// return "A `String` describing what went wrong including the actual error: `Error.someError`." 21 | /// // other cases 22 | /// } 23 | var reason: String { get } 24 | 25 | // MARK: Identifiers 26 | 27 | /// A unique identifier for the error's Type. 28 | /// - note: This defaults to `ModuleName.TypeName`, 29 | /// and is used to create the `identifier` property. 30 | static var typeIdentifier: String { get } 31 | 32 | /// Some unique identifier for this specific error. 33 | /// This will be used to create the `identifier` property. 34 | /// Do NOT use `String(reflecting: self)` or `String(describing: self)` 35 | /// or there will be infinite recursion 36 | var identifier: String { get } 37 | 38 | // MARK: Help 39 | 40 | /// A `String` array describing the possible causes of the error. 41 | /// - note: Defaults to an empty array. 42 | /// Provide a custom implementation to give more context. 43 | var possibleCauses: [String] { get } 44 | 45 | /// A `String` array listing some common fixes for the error. 46 | /// - note: Defaults to an empty array. 47 | /// Provide a custom implementation to be more helpful. 48 | var suggestedFixes: [String] { get } 49 | 50 | /// An array of string `URL`s linking to documentation pertaining to the error. 51 | /// - note: Defaults to an empty array. 52 | /// Provide a custom implementation with relevant links. 53 | var documentationLinks: [String] { get } 54 | 55 | /// An array of string `URL`s linking to related Stack Overflow questions. 56 | /// - note: Defaults to an empty array. 57 | /// Provide a custom implementation to link to useful questions. 58 | var stackOverflowQuestions: [String] { get } 59 | 60 | /// An array of string `URL`s linking to related issues on Vapor's GitHub repo. 61 | /// - note: Defaults to an empty array. 62 | /// Provide a custom implementation to a list of pertinent issues. 63 | var gitHubIssues: [String] { get } 64 | } 65 | 66 | // MARK: Optionals 67 | 68 | extension Debuggable { 69 | public var documentationLinks: [String] { 70 | return [] 71 | } 72 | 73 | public var stackOverflowQuestions: [String] { 74 | return [] 75 | } 76 | 77 | public var gitHubIssues: [String] { 78 | return [] 79 | } 80 | } 81 | 82 | extension Debuggable { 83 | public var fullIdentifier: String { 84 | return Self.typeIdentifier + "." + identifier 85 | } 86 | } 87 | 88 | // MARK: Defaults 89 | 90 | extension Debuggable { 91 | /// Default implementation of readable name that expands 92 | /// SomeModule.MyType.Error => My Type Error 93 | public static var readableName: String { 94 | return typeIdentifier.readableTypeName() 95 | } 96 | 97 | public static var typeIdentifier: String { 98 | return String(reflecting: self) 99 | } 100 | 101 | public var debugDescription: String { 102 | return printable 103 | } 104 | } 105 | 106 | extension String { 107 | func readableTypeName() -> String { 108 | let characterSequence = toCharacterSequence() 109 | .split(separator: ".") 110 | .dropFirst() // drop module 111 | .joined(separator: []) 112 | 113 | let characters = Array(characterSequence) 114 | guard var expanded = characters.first.flatMap({ String($0) }) else { return "" } 115 | 116 | characters.suffix(from: 1).forEach { char in 117 | if char.isUppercase { 118 | expanded.append(" ") 119 | } 120 | 121 | expanded.append(char) 122 | } 123 | 124 | return expanded 125 | } 126 | #if swift(>=4.0) 127 | private func toCharacterSequence() -> String { 128 | return self 129 | } 130 | #else 131 | private func toCharacterSequence() -> CharacterView { 132 | return self.characters 133 | } 134 | #endif 135 | 136 | } 137 | 138 | extension Character { 139 | var isUppercase: Bool { 140 | switch self { 141 | case "A"..."Z": 142 | return true 143 | default: 144 | return false 145 | } 146 | } 147 | } 148 | 149 | 150 | // MARK: Representations 151 | 152 | extension Debuggable { 153 | /// A computed property returning a `String` that encapsulates 154 | /// why the error occurred, suggestions on how to fix the problem, 155 | /// and resources to consult in debugging (if these are available). 156 | /// - note: This representation is best used with functions like print() 157 | public var printable: String { 158 | var print: [String] = [] 159 | 160 | print.append("\(Self.readableName): \(reason)") 161 | print.append("Identifier: \(fullIdentifier)") 162 | 163 | if !possibleCauses.isEmpty { 164 | print.append("Here are some possible causes: \(possibleCauses.bulletedList)") 165 | } 166 | 167 | if !suggestedFixes.isEmpty { 168 | print.append("These suggestions could address the issue: \(suggestedFixes.bulletedList)") 169 | } 170 | 171 | if !documentationLinks.isEmpty { 172 | print.append("Vapor's documentation talks about this: \(documentationLinks.bulletedList)") 173 | } 174 | 175 | if !stackOverflowQuestions.isEmpty { 176 | print.append("These Stack Overflow links might be helpful: \(stackOverflowQuestions.bulletedList)") 177 | } 178 | 179 | if !gitHubIssues.isEmpty { 180 | print.append("See these Github issues for discussion on this topic: \(gitHubIssues.bulletedList)") 181 | } 182 | 183 | return print.joined(separator: "\n\n") 184 | } 185 | } 186 | 187 | extension Sequence where Iterator.Element == String { 188 | var bulletedList: String { 189 | return map { "\n- \($0)" } .joined() 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Tests/DebuggingTests/FooError.swift: -------------------------------------------------------------------------------- 1 | import Debugging 2 | 3 | enum FooError: String, Error { 4 | case noFoo 5 | } 6 | 7 | extension FooError: Debuggable { 8 | static var readableName: String { 9 | return "Foo Error" 10 | } 11 | 12 | var identifier: String { 13 | return rawValue 14 | } 15 | 16 | var reason: String { 17 | switch self { 18 | case .noFoo: 19 | return "You do not have a `foo`." 20 | } 21 | } 22 | 23 | var possibleCauses: [String] { 24 | switch self { 25 | case .noFoo: 26 | return [ 27 | "You did not set the flongwaffle.", 28 | "The session ended before a `Foo` could be made.", 29 | "The universe conspires against us all.", 30 | "Computers are hard." 31 | ] 32 | } 33 | } 34 | 35 | var suggestedFixes: [String] { 36 | switch self { 37 | case .noFoo: 38 | return [ 39 | "You really want to use a `Bar` here.", 40 | "Take up the guitar and move to the beach." 41 | ] 42 | } 43 | } 44 | 45 | var documentationLinks: [String] { 46 | switch self { 47 | case .noFoo: 48 | return [ 49 | "http://documentation.com/Foo", 50 | "http://documentation.com/foo/noFoo" 51 | ] 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Tests/DebuggingTests/FooErrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import Debugging 4 | 5 | class FooErrorTests: XCTestCase { 6 | static let allTests = [ 7 | ("testPrintable", testPrintable), 8 | ("testOmitEmptyFields", testOmitEmptyFields), 9 | ("testReadableName", testReadableName), 10 | ("testIdentifier", testIdentifier), 11 | ("testCausesAndSuggestions", testCausesAndSuggestions), 12 | ] 13 | 14 | let error: FooError = .noFoo 15 | 16 | func testPrintable() { 17 | XCTAssertEqual( 18 | error.debugDescription, 19 | expectedPrintable, 20 | "`error`'s `debugDescription` should equal `expectedPrintable`." 21 | ) 22 | } 23 | 24 | func testOmitEmptyFields() { 25 | XCTAssertTrue( 26 | error.stackOverflowQuestions.isEmpty, 27 | "There should be no `stackOverflowQuestions`." 28 | ) 29 | 30 | XCTAssertFalse( 31 | error.debugDescription.contains("Stack Overflow"), 32 | "The `debugDescription` should contain no mention of Stack Overflow." 33 | ) 34 | } 35 | 36 | func testReadableName() { 37 | XCTAssertEqual( 38 | FooError.readableName, 39 | "Foo Error", 40 | "`readableName` should be a well-formatted `String`." 41 | ) 42 | } 43 | 44 | func testIdentifier() { 45 | XCTAssertEqual( 46 | error.identifier, 47 | "noFoo", 48 | "`instanceIdentifier` should equal `'noFoo'`." 49 | ) 50 | } 51 | 52 | func testCausesAndSuggestions() { 53 | XCTAssertEqual( 54 | error.possibleCauses, 55 | expectedPossibleCauses, 56 | "`possibleCauses` should match `expectedPossibleCauses`" 57 | ) 58 | 59 | XCTAssertEqual(error.suggestedFixes, 60 | expectedSuggestedFixes, 61 | "`suggestedFixes` should match `expectedSuggestFixes`") 62 | 63 | XCTAssertEqual(error.documentationLinks, 64 | expectedDocumentedLinks, 65 | "`documentationLinks` should match `expectedDocumentedLinks`") 66 | } 67 | } 68 | 69 | // MARK: - Fixtures 70 | 71 | private let expectedPrintable: String = { 72 | var expectation = "Foo Error: You do not have a `foo`.\n\n" 73 | expectation += "Identifier: DebuggingTests.FooError.noFoo\n\n" 74 | 75 | expectation += "Here are some possible causes: \n" 76 | expectation += "- You did not set the flongwaffle.\n" 77 | expectation += "- The session ended before a `Foo` could be made.\n" 78 | expectation += "- The universe conspires against us all.\n" 79 | expectation += "- Computers are hard.\n\n" 80 | 81 | expectation += "These suggestions could address the issue: \n" 82 | expectation += "- You really want to use a `Bar` here.\n" 83 | expectation += "- Take up the guitar and move to the beach.\n\n" 84 | 85 | expectation += "Vapor's documentation talks about this: \n" 86 | expectation += "- http://documentation.com/Foo\n" 87 | expectation += "- http://documentation.com/foo/noFoo" 88 | return expectation 89 | }() 90 | 91 | private let expectedPossibleCauses = [ 92 | "You did not set the flongwaffle.", 93 | "The session ended before a `Foo` could be made.", 94 | "The universe conspires against us all.", 95 | "Computers are hard." 96 | ] 97 | 98 | private let expectedSuggestedFixes = [ 99 | "You really want to use a `Bar` here.", 100 | "Take up the guitar and move to the beach." 101 | ] 102 | 103 | private let expectedDocumentedLinks = [ 104 | "http://documentation.com/Foo", 105 | "http://documentation.com/foo/noFoo" 106 | ] 107 | -------------------------------------------------------------------------------- /Tests/DebuggingTests/GeneralTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Debugging 3 | 4 | class GeneralTests: XCTestCase { 5 | static let allTests = [ 6 | ("testBulletedList", testBulletedList), 7 | ("testReadableName", testReadableName), 8 | ("testReadableNameEdgeCase", testReadableNameEdgeCase), 9 | ("testMinimumConformance", testMinimumConformance), 10 | ] 11 | 12 | func testBulletedList() { 13 | let todos = [ 14 | "Get groceries", 15 | "Walk the dog", 16 | "Change oil in car", 17 | "Get haircut" 18 | ] 19 | 20 | let bulleted = todos.bulletedList 21 | let expectation = "\n- Get groceries\n- Walk the dog\n- Change oil in car\n- Get haircut" 22 | XCTAssertEqual(bulleted, expectation) 23 | } 24 | 25 | func testReadableName() { 26 | let typeName = "SomeRandomModule.MyType.Error" 27 | let readableName = typeName.readableTypeName() 28 | let expectation = "My Type Error" 29 | XCTAssertEqual(readableName, expectation) 30 | } 31 | 32 | func testReadableNameEdgeCase() { 33 | let edgeCases = [ 34 | "SomeModule.": "", 35 | "SomeModule.S": "S" 36 | ] 37 | edgeCases.forEach { edgeCase, expectation in 38 | let readableName = edgeCase.readableTypeName() 39 | XCTAssertEqual(readableName, expectation) 40 | } 41 | } 42 | 43 | func testMinimumConformance() { 44 | let minimum = MinimumError.alpha 45 | let description = minimum.debugDescription 46 | let expectation = "Minimum Error: Not enabled\n\nIdentifier: DebuggingTests.MinimumError.alpha" 47 | XCTAssertEqual(description, expectation) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/DebuggingTests/MinimumError.swift: -------------------------------------------------------------------------------- 1 | import Debugging 2 | 3 | enum MinimumError: String { 4 | case alpha, beta, charlie 5 | } 6 | 7 | extension MinimumError: Debuggable { 8 | /// The reason for the error. 9 | /// Typical implementations will switch over `self` 10 | /// and return a friendly `String` describing the error. 11 | /// - note: It is most convenient that `self` be a `Swift.Error`. 12 | /// 13 | /// Here is one way to do this: 14 | /// 15 | /// switch self { 16 | /// case someError: 17 | /// return "A `String` describing what went wrong including the actual error: `Error.someError`." 18 | /// // other cases 19 | /// } 20 | var reason: String { 21 | switch self { 22 | case .alpha: 23 | return "Not enabled" 24 | case .beta: 25 | return "Enabled, but I'm not configured" 26 | case .charlie: 27 | return "Broken beyond repair" 28 | } 29 | } 30 | 31 | var identifier: String { 32 | return rawValue 33 | } 34 | 35 | /// A `String` array describing the possible causes of the error. 36 | /// - note: Defaults to an empty array. 37 | /// Provide a custom implementation to give more context. 38 | var possibleCauses: [String] { 39 | return [] 40 | } 41 | 42 | var suggestedFixes: [String] { 43 | return [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DebuggingTests 3 | 4 | XCTMain([ 5 | testCase(FooErrorTests.allTests), 6 | testCase(GeneralTests.allTests), 7 | ]) 8 | --------------------------------------------------------------------------------