├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
--------------------------------------------------------------------------------