File: Validation.md

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (159 lines) | stat: -rw-r--r-- 6,932 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# Providing Custom Validation

Provide helpful feedback to users when things go wrong.

## Overview

While `ArgumentParser` validates that the inputs given by your user match the requirements and types that you define in each command, there are some requirements that can't easily be described in Swift's type system, such as the number of elements in an array, or an expected integer value.

### Validating Command-Line Input

To validate your commands properties after parsing, implement the ``ParsableArguments/validate()-5r0ge`` method on any ``ParsableCommand`` or ``ParsableArguments`` type. Throwing an error from the `validate()` method causes the program to print a message to standard error and exit with an error code, preventing the `run()` method from being called with invalid inputs.

Here's a command that prints out one or more random elements from the list you provide. Its `validate()` method catches three different errors that a user can make and throws a relevant error for each one.

```swift
struct Select: ParsableCommand {
    @Option var count: Int = 1
    @Argument var elements: [String] = []

    mutating func validate() throws {
        guard count >= 1 else {
            throw ValidationError("Please specify a 'count' of at least 1.")
        }

        guard !elements.isEmpty else {
            throw ValidationError("Please provide at least one element to choose from.")
        }

        guard count <= elements.count else {
            throw ValidationError("Please specify a 'count' less than the number of elements.")
        }
    }

    mutating func run() {
        print(elements.shuffled().prefix(count).joined(separator: "\n"))
    }
}
```

When you provide useful error messages, they can guide new users to success with your command-line tool!

```
% select
Error: Please provide at least one element to choose from.
Usage: select [--count <count>] [<elements> ...]
  See 'select --help' for more information.
% select --count 2 hello
Error: Please specify a 'count' less than the number of elements.
Usage: select [--count <count>] [<elements> ...]
  See 'select --help' for more information.
% select --count 0 hello hey hi howdy
Error: Please specify a 'count' of at least 1.
Usage: select [--count <count>] [<elements> ...]
  See 'select --help' for more information.
% select --count 2 hello hey hi howdy
howdy
hey
```

## Handling Post-Validation Errors

The ``ValidationError`` type is a special `ArgumentParser` error — a validation error's message is always accompanied by an appropriate usage string. You can throw other errors, from either the `validate()` or `run()` method to indicate that something has gone wrong that isn't validation-specific. Errors that conform to `CustomStringConvertible` or `LocalizedError` provide the best experience for users.

```swift
struct LineCount: ParsableCommand {
    @Argument var file: String

    mutating func run() throws {
        let contents = try String(contentsOfFile: file, encoding: .utf8)
        let lines = contents.split(separator: "\n")
        print(lines.count)
    }
}
```

The throwing `String(contentsOfFile:encoding:)` initializer fails when the user specifies an invalid file. `ArgumentParser` prints its error message to standard error and exits with an error code.

```
% line-count file1.swift
37
% line-count non-existing-file.swift
Error: The file “non-existing-file.swift” couldn’t be opened because
there is no such file.
```

If you print your error output yourself, you still need to throw an error from `validate()` or `run()`, so that your command exits with the appropriate exit code. To avoid printing an extra error message, use the `ExitCode` error, which has static properties for success, failure, and validation errors, or lets you specify a specific exit code.

```swift
struct RuntimeError: Error, CustomStringConvertible {
    var description: String
}

struct Example: ParsableCommand {
    @Argument var inputFile: String

    mutating func run() throws {
        if !ExampleCore.processFile(inputFile) {
            // ExampleCore.processFile(_:) prints its own errors
            // and returns `false` on failure.
            throw ExitCode.failure
        }
    }
}
```

## Handling Transform Errors

During argument and option parsing, you can use a closure to transform the command line strings to custom types. If this transformation fails, you can throw a `ValidationError`; its `message` property will be displayed to the user.

In addition, you can throw your own errors. Errors that conform to `CustomStringConvertible` or `LocalizedError` provide the best experience for users.

```swift
struct ExampleTransformError: Error, CustomStringConvertible {
  var description: String
}

struct ExampleDataModel: Codable {
  let identifier: UUID
  let tokens: [String]
  let tokenCount: Int

  static func dataModel(_ jsonString: String) throws -> ExampleDataModel  {
    guard let data = jsonString.data(using: .utf8) else { throw ValidationError("Badly encoded string, should be UTF-8") }
    return try JSONDecoder().decode(ExampleDataModel.self, from: data)
  }
}

struct Example: ParsableCommand {
  // Reads in the argument string and attempts to transform it to
  // an `ExampleDataModel` object using the `JSONDecoder`. If the
  // string is not valid JSON, `decode` will throw an error and
  // parsing will halt.
  @Argument(transform: ExampleDataModel.dataModel)
  var inputJSON: ExampleDataModel

  // Specifiying this option will always cause the parser to exit
  // and print the custom error.
  @Option(transform: { throw ExampleTransformError(description: "Trying to write to failOption always produces an error. Input: \($0)") })
  var failOption: String?
}
```

Throwing from a transform closure benefits users by providing context and can reduce development time by pinpointing issues quickly and more precisely.

```
% example '{"Bad JSON"}'
Error: The value '{"Bad JSON"}' is invalid for '<input-json>': dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "No value for key in object around character 11." UserInfo={NSDebugDescription=No value for key in object around character 11.})))
Usage: example <input-json> --fail-option <fail-option>
  See 'select --help' for more information.
```

While throwing standard library or Foundation errors adds context, custom errors provide the best experience for users and developers.

```
% example '{"tokenCount":0,"tokens":[],"identifier":"F77D661C-C5B7-448E-9344-267B284F66AD"}' --fail-option="Some Text Here!"
Error: The value 'Some Text Here!' is invalid for '--fail-option <fail-option>': Trying to write to failOption always produces an error. Input: Some Text Here!
Usage: example <input-json> --fail-option <fail-option>
  See 'select --help' for more information.
```