• Stars
    star
    766
  • Rank 57,148 (Top 2 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 3 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A collection of tools for debugging, diffing, and testing your application's data structures.

Custom Dump

CI

A collection of tools for debugging, diffing, and testing your application's data structures.

Motivation

Swift comes with a wonderful tool for dumping the contents of any value to a string, and it's called dump. It prints all the fields and sub-fields of a value into a tree-like description:

struct User {
  var favoriteNumbers: [Int]
  var id: Int
  var name: String
}

let user = User(
  favoriteNumbers: [42, 1729],
  id: 2,
  name: "Blob"
)

dump(user)
โ–ฟ User
  โ–ฟ favoriteNumbers: 2 elements
    - 42
    - 1729
  - id: 2
  - name: "Blob"

This is really useful, and can be great for building debug tools that visualize the data held in runtime values of our applications, but sometimes its output is not ideal.

For example, dumping dictionaries leads to a verbose output that can be hard to read (also note that the keys are unordered):

dump([1: "one", 2: "two", 3: "three"])
โ–ฟ 3 key/value pairs
  โ–ฟ (2 elements)
    - key: 2
    - value: "two"
  โ–ฟ (2 elements)
    - key: 3
    - value: "three"
  โ–ฟ (2 elements)
    - key: 1
    - value: "one"

Similarly enums have a very verbose output:

dump(Result<Int, Error>.success(42))
โ–ฟ Swift.Result<Swift.Int, Swift.Error>.success
  - success: 42

It gets even harder to read when dealing with deeply nested structures:

dump([1: Result<User, Error>.success(user)])
โ–ฟ 1 key/value pair
  โ–ฟ (2 elements)
    - key: 1
    โ–ฟ value: Swift.Result<User, Swift.Error>.success
      โ–ฟ success: User
        โ–ฟ favoriteNumbers: 2 elements
          - 42
          - 1729
        - id: 2
        - name: "Blob"

There are also times that dump simply does not print useful information, such as enums imported from Objective-C:

import UserNotifications

dump(UNNotificationSetting.disabled)
- __C.UNNotificationSetting

So, while the dump function can be handy, it is often too crude of a tool to use. This is the motivation for the customDump function.

customDump

The customDump function emulates the behavior of dump, but provides a more refined output of nested structures, optimizing for readability. For example, structs are dumped in a format that more closely mimics the struct syntax in Swift, and arrays are dumped with the indices of each element:

import CustomDump

customDump(user)
User(
  favoriteNumbers: [
    [0]: 42,
    [1]: 1729
  ],
  id: 2,
  name: "Blob"
)

Dictionaries are dumped in a more compact format that mimics Swift's syntax, and automatically orders the keys:

customDump([1: "one", 2: "two", 3: "three"])
[
  1: "one",
  2: "two",
  3: "three"
]

Similarly, enums also dump in a more compact, readable format:

customDump(Result<Int, Error>.success(42))
Result.success(42)

And deeply nested structures have a simplified tree-structure:

customDump([1: Result<User, Error>.success(user)])
[
  1: Result.success(
    User(
      favoriteNumbers: [
        [0]: 42,
        [1]: 1729
      ],
      id: 2,
      name: "Blob"
    )
  )
]

diff

Using the output of the customDump function we can build a very lightweight way to textually diff any two values in Swift:

var other = user
other.favoriteNumbers[1] = 91

print(diff(user, other)!)
โ€‡ User(
โ€‡   favoriteNumbers: [
โ€‡     [0]: 42,
-     [1]: 1729
+     [1]: 91
โ€‡   ],
โ€‡   id: 2,
โ€‡   name: "Blob"
โ€‡ )

Further, extra work is done to minimize the size of the diff when parts of the structure haven't changed, such as a single element changing in a large collection:

let users = (1...5).map {
  User(
    favoriteNumbers: [$0],
    id: $0,
    name: "Blob \($0)"
  )
}

var other = users
other.append(
  .init(
    favoriteNumbers: [42, 1729],
    id: 100,
    name: "Blob Sr."
  )
)

print(diff(users, other)!)
โ€‡ [
โ€‡   โ€ฆ (4 unchanged),
+   [4]: User(
+     favoriteNumbers: [
+       [0]: 42,
+       [1]: 1729
+     ],
+     id: 100,
+     name: "Blob Sr."
+   )
โ€‡ ]

For a real world use case we modified Apple's Landmarks tutorial application to print the before and after state when favoriting a landmark:

โ€‡ [
โ€‡   [0]: Landmark(
โ€‡     id: 1001,
โ€‡     name: "Turtle Rock",
โ€‡     park: "Joshua Tree National Park",
โ€‡     state: "California",
โ€‡     description: "This very large formation lies south of the large Real Hidden Valley parking lot and immediately adjacent to (south of) the picnic areas.",
-     isFavorite: true,
+     isFavorite: false,
โ€‡     isFeatured: true,
โ€‡     category: Category.rivers,
โ€‡     imageName: "turtlerock",
โ€‡     coordinates: Coordinates(โ€ฆ)
โ€‡   ),
โ€‡   โ€ฆย (11 unchanged)
โ€‡ ]

XCTAssertNoDifference

The XCTAssertEqual function from XCTest allows you to assert that two values are equal, and if they are not the test suite will fail with a message:

var other = user
other.name += "!"

XCTAssertEqual(user, other)
XCTAssertEqual failed: ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")") is not equal to ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!")")

Unfortunately this failure message is quite difficult to visually parse and understand. It takes a few moments of hunting through the message to see that the only difference is the exclamation mark at the end of the name. The problem gets worse if the type is more complex, consisting of nested structures and large collections.

This library also ships with an XCTAssertNoDifference function to mitigate these problems. It works like XCTAssertEqual except the failure message uses a nicely formatted diff to show exactly what is different between the two values:

XCTAssertNoDifference(user, other)
XCTAssertNoDifference failed: โ€ฆ

  โ€‡ User(
  โ€‡   favoriteNumbers: [โ€ฆ],
  โ€‡   id: 2,
  โˆ’   name: "Blob"
  +   name: "Blob!"
  โ€‡ )

(First: โˆ’, Second: +)

Customization

Custom Dump provides a few important ways to customize how a data type is dumped: CustomDumpStringConvertible, CustomDumpReflectable, and CustomDumpRepresentable.

CustomDumpStringConvertible

The CustomDumpStringConvertible protocol provides a simple way of converting a type to a raw string for the purpose of dumping. It is most appropriate for types that have a simple, un-nested internal representation, and typically its output fits on a single line, for example dates, UUIDs, URLs, etc:

extension URL: CustomDumpStringConvertible {
  public var customDumpDescription: String {
    "URL(\(self.absoluteString))"
  }
}

customDump(URL(string: "https://www.pointfree.co/")!)
URL(https://www.pointfree.co/)

Custom Dump also uses this protocol internally to provide more useful output for enums imported from Objective-C:

import UserNotifications

print("dump:")
dump(UNNotificationSetting.disabled)
print("customDump:")
customDump(UNNotificationSetting.disabled)
dump:
- __C.UNNotificationSetting
customDump:
UNNotificationSettings.disabled

Encounter an Objective-C enum that doesn't print nicely? See the contributing section of this README to help submit a fix.

CustomDumpReflectable

The CustomDumpReflectable protocol provides a more comprehensive way of dumping a type into a more structured output. It allows you to construct a custom mirror that describes the structure that should be dumped. You can omit, add, and replace fields, or even change the "display style" of how the structure is dumped.

For example, let's say you have a struct representing state that holds a secure token in memory that should never be written to your logs. You can omit the token from customDump by providing a mirror that omits this field:

struct LoginState: CustomDumpReflectable {
  var username: String
  var token: String

  var customDumpMirror: Mirror {
    .init(
      self,
      children: [
        "username": self.username,
        // omit token from logs
      ],
      displayStyle: .struct
    )
  }
}

customDump(
  LoginState(
    username: "blob",
    token: "secret"
  )
)
LoginState(username: "blob")

And just like that, no token data will be written to the dump.

CustomDumpRepresentable

The CustomDumpRepresentable protocol allows you to return any value for the purpose of dumping. This can be useful to flatten the dump representation of wrapper types. For example, a type-safe identifier may want to dump its raw value directly:

struct ID: RawRepresentable {
  var rawValue: String
}

extension ID: CustomDumpRepresentable {
  var customDumpValue: Any {
    self.rawValue
  }
}

customDump(ID(rawValue: "deadbeef")
"deadbeef"

Contributing

There are many types in Apple's ecosystem that do not dump to a nicely formatted string. In particular, all enums that are imported from Objective-C dump to strings that are not very helpful:

import UserNotifications

dump(UNNotificationSetting.disabled)
- __C.UNNotificationSetting

For this reason we have conformed a bunch of Apple's types to the CustomDumpStringConvertible protocol so that they print out more reasonable descriptions. If you come across types that do not print useful information then we would happily accept a PR to conform those types to CustomDumpStringConvertible.

Installation

You can add Custom Dump to an Xcode project by adding it as a package dependency.

https://github.com/pointfreeco/swift-custom-dump

If you want to use Custom Dump in a SwiftPM project, it's as simple as adding it to a dependencies clause in your Package.swift:

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0")
]

Documentation

The latest documentation for the Custom Dump APIs is available here.

Other libraries

License

This library is released under the MIT license. See LICENSE for details.

More Repositories

1

swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
Swift
10,990
star
2

swift-snapshot-testing

๐Ÿ“ธ Delightful Swift snapshot testing.
Swift
3,559
star
3

isowords

Open source game built in SwiftUI and the Composable Architecture.
Swift
2,515
star
4

swiftui-navigation

Tools for making SwiftUI navigation simpler, more ergonomic and more precise.
Swift
1,654
star
5

swift-dependencies

A dependency management library inspired by SwiftUI's "environment."
Swift
1,322
star
6

swift-tagged

๐Ÿท A wrapper type for safer, expressive code.
Swift
1,289
star
7

swift-overture

๐ŸŽผ A library for function composition.
Swift
1,115
star
8

pointfreeco

๐ŸŽฌ The source for www.pointfree.co, a video series on functional programming and the Swift programming language.
Swift
1,054
star
9

episode-code-samples

๐Ÿ’พ Point-Free episode code.
Swift
922
star
10

swift-case-paths

๐Ÿงฐ Case paths extends the key path hierarchy to enum cases.
Swift
852
star
11

swift-nonempty

๐ŸŽ A compile-time guarantee that a collection contains a value.
Swift
817
star
12

swift-parsing

A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.
Swift
802
star
13

swift-html

๐Ÿ—บ A Swift DSL for type-safe, extensible, and transformable HTML documents.
Swift
726
star
14

combine-schedulers

โฐ A few schedulers that make working with Combine more testable and more versatile.
Swift
671
star
15

swift-web

๐Ÿ•ธ A collection of Swift server-side frameworks for handling HTML, CSS, routing and middleware.
Swift
477
star
16

swift-identified-collections

A library of data structures for working with collections of identifiable elements in an ergonomic, performant way.
Swift
476
star
17

swift-prelude

๐ŸŽถ A collection of types and functions that enhance the Swift language.
Swift
456
star
18

swift-perception

Observable tools, backported.
Swift
399
star
19

swift-validated

๐Ÿ›‚ A result type that accumulates multiple errors.
Swift
389
star
20

swift-url-routing

A bidirectional router with more type safety and less fuss.
Swift
321
star
21

swift-concurrency-extras

Useful, testable Swift concurrency.
Swift
267
star
22

swift-gen

๐ŸŽฑ Composable, transformable, controllable randomness.
Swift
262
star
23

swift-clocks

โฐ A few clocks that make working with Swift concurrency more testable and more versatile.
Swift
229
star
24

swift-enum-properties

๐Ÿค Struct and enum data access in harmony.
Swift
198
star
25

xctest-dynamic-overlay

Define XCTest assertion helpers directly in your application and library code.
Swift
197
star
26

swift-macro-testing

Magical testing tools for Swift macros.
Swift
192
star
27

syncups

A rebuild of Appleโ€™s โ€œScrumdingerโ€ application using modern, best practices for SwiftUI development.
Swift
153
star
28

composable-core-location

A library that bridges the Composable Architecture and Core Location.
Swift
100
star
29

vapor-routing

A bidirectional Vapor router with more type safety and less fuss.
Swift
83
star
30

swift-html-vapor

๐Ÿ’ง Vapor plugin for type-safe, transformable HTML views.
Swift
82
star
31

swift-playground-templates

๐Ÿซ A collection of helpful Xcode playground templates.
Makefile
80
star
32

pointfreeco-server

Point-Free server code.
39
star
33

swift-boundaries

๐Ÿฃ Functional core, imperative shell.
Swift
27
star
34

composable-core-motion

A library that bridges the Composable Architecture and Core Motion.
Swift
26
star
35

swift-quickcheck

๐Ÿ An implementation of QuickCheck in Swift.
Swift
24
star
36

swift-algebras

Algebraic laws bundled into concrete data types.
19
star
37

swift-either

For those times you want A or B!
Swift
19
star
38

swift-parser-printer

โ†”๏ธ Parsing and printing
Swift
14
star
39

swift-html-kitura

โ˜๏ธ Kitura plugin for type-safe, transformable HTML views.
Swift
13
star
40

homebrew-swift

Ruby
2
star
41

swift-bugs

2
star
42

Ccmark

Swift
1
star