Composing Parsers with the swift-parsing Library
One of the selling points of the swift-parsing library is that you can create small parsers that do one simple thing and combine them to parse more complex data. This article shows how to combine smaller parsers to create a larger parser.
Creating a parser
A swift-parsing parser is a struct that conforms to the Parser protocol. Conform to the protocol by creating a body property that returns a parser that conforms to Parser.
public struct ParserName: Parser {
public var body: some Parser<T, U> {
}
}
Notice that the struct and the body property have public access. The swift-parsing library requires public access to parse the data. If you forget to add public, you will get build errors.
The T type is the input type the parser takes. Most parsers use the Substring type as the input type. If you are parsing lots of data and require maximum performance, use UTF8View as the input type. The examples in this article use substrings.
The U type is the output type the parser returns. The return type can be a simple type, such as an integer or Boolean value, or it can be more complex, such as one of your app’s structs or classes.
Parsing example
The example in this article parses the output of the Jujutsu version control system’s jj show command that shows details of a change. A Jujutsu change is similar to a git commit. The output of the jj show command looks similar to the following text:
Commit ID: 73d31d1766b0bc211d22b87f2f911429f3846a61
Change ID: lyrzonnxolkpxytwwykkqpvvlqmvlxqm
Author : Bob Ducca <ducca@example.com> (2025-06-23 18:04:09)
Committer: Bob Ducca <ducca@example.com> (2025-06-23 18:04:49)
Add sentence that you have to set a bookmark and push.
Add a paragraph so the change description has multiple paragraphs, which is good for unit testing change descriptions.
Modified regular file GitHub.md:
...
4 4:
5 5: ## Working with Existing GitHub Repo
6 6:
7 7: In most cases you have an existing GitHub repo. This is what you do.
8:
9: You have to set a bookmark and call `jj git push`.
The Change struct stores the change information.
public struct Change {
var changeId: String
var author: String
var timestamp: String
var description: String
}
Parsing the change ID
Parsing the change ID involves the following steps:
- Skip everything through the text
Change ID:. - Skip any additional whitespace.
- Capture everything up to the end of the line.
public struct ChangeId: Parser {
public var body: some Parser<Substring,
Substring> {
Skip {
PrefixThrough("Change ID:")
Whitespace()
}
// Capture everything until the next whitespace.
PrefixUpTo("\n")
}
}
Parsing the author
Parsing the author requires the following steps:
- Skip everything through the text
Author. - Skip whitespace.
- Skip the colon.
- Skip more whitespace.
- Capture everything up to the
<character, which marks the start of the author email address.
public struct ChangeAuthor: Parser {
public var body: some Parser<Substring,
Substring> {
Skip {
PrefixThrough("Author")
Whitespace()
": "
}
// Capture everything until the start of the email address.
PrefixUpTo(" <")
}
}
Parsing the timestamp
The timestamp looks like the following:
(2025-06-23 18:04:09)
Parsing the timestamp involves the following steps:
- Skip everything through the first left parenthesis.
- Capture everything up to the next right parenthesis.
public struct ChangeTimestamp: Parser {
public var body: some Parser<Substring,
Substring> {
Skip {
PrefixThrough("(")
}
PrefixUpTo(")")
}
}
Parsing the change description
The change description starts after the first blank line. The description ends with a blank line and one of the following phrases:
Modified regular fileAdded regular fileRemoved regular file
Start by writing a parser for each possible end of a description.
let modifiedFile = Parse {
PrefixUpTo("\n\nModified regular file")
}
let addedFile = Parse {
PrefixUpTo("\n\nAdded regular file")
}
let removedFile = Parse {
PrefixUpTo("\n\nRemoved regular file")
}
Use the OneOf parser to capture everything until you reach one of the possible ends of the description. Use the Rest() parser as a fallback if you hit the end of the text before reaching one of the possible description ends.
public struct ChangeDescription: Parser {
public var body: some Parser<Substring,
Substring> {
let modifiedFile = Parse {
PrefixUpTo("\n\nModified regular")
}
let addedFile = Parse {
PrefixUpTo("\n\nAdded regular")
}
let removedFile = Parse {
PrefixUpTo("\n\nRemoved regular")
}
// Skip everything to the first blank line and any whitespace after the blank line.
Skip {
PrefixThrough("\n\n")
Whitespace()
}
// Capture everything up to one of the phrases that marks the end of a description.
OneOf {
modifiedFile
addedFile
removedFile
Rest()
}
}
}
Composing a change parser
The last part is to build a change parser that runs the parsers for each piece of the change and returns a Change instance.
The tough part is building the Change instance. Each of the smaller parsers returns a string. How do you build a Change instance from a group of strings?
Add a local variable for the change to the body property. After running each parser, add a map modifier and supply a closure to fill the change’s property with the output from the parser. Return the local variable after running the parsers.
public struct ChangeParser: Parser {
public var body: some Parser<Substring,
Change> {
let change = Change()
Parse {
ChangeId()
.map { id in
change.changeId = String(id)
}
ChangeAuthor()
.map { author in
change.author = String(author)
}
ChangeTimestamp()
.map { time in
change.timestamp = String(time)
}
ChangeDescription()
.map { message in
change.description = String(message)
return change
}
} // End Parse block
} // End body
}