Last active
August 1, 2020 19:17
-
-
Save aceontech/07929e0f6b5a880cad8f08fefa48917a to your computer and use it in GitHub Desktop.
Prototype implementation of a .strings file parser written in Swift, using Parser Combinators, as explained by PointFree.co. See https://www.pointfree.co/collections/parsing/parser-combinators for all videos on this topic.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public protocol FileParser { | |
func parse(string: String) -> [Entry] | |
} | |
public struct FileParserFactory { | |
public static func unordered() -> FileParser { | |
UnorderedFileParser() | |
} | |
public static func ordered() -> FileParser { | |
LineOrderedFileParser(unorderedParser: unordered()) | |
} | |
} | |
struct UnorderedFileParser: FileParser { | |
func parse(string: String) -> [Entry] { | |
stringsFile.run(string).match ?? [] | |
} | |
} | |
struct LineOrderedFileParser: FileParser { | |
let unorderedParser: FileParser | |
func parse(string: String) -> [Entry] { | |
unorderedParser | |
.parse(string: string) | |
.enumerated() | |
.map { index, entry in | |
entry.map(withOrder: index) | |
} | |
} | |
} | |
// MARK: Parser helpers | |
private let quote = literal("\"") | |
private let newLine = Character("\n") | |
private let stringsFile: Parser<[Entry]> = zeroOrMore( | |
oneOf([ | |
entry, | |
singleLineSlashComment.map { Entry(comment: $0) }, | |
singleLineSlashAsteriskComment.map { Entry(comment: $0) } | |
]), | |
separatedBy: zeroOrMore(char: newLine) | |
) | |
private let entry = zip( | |
operand, | |
assignOperator, | |
operand, | |
semicolon | |
).map { key, _, value, _ in | |
Entry( | |
key: String(key), | |
value: String(value), | |
order: 0 | |
) | |
} | |
private let operand = zip(quote, anyString(until: quote)).map { $1 } | |
private let assignOperator = zip( | |
zeroOrMoreSpaces, | |
literal("="), | |
zeroOrMoreSpaces | |
).map { _ in () } | |
private let semicolon = literal(";") | |
private let singleLineSlashComment = zip( | |
zeroOrMoreSpaces, | |
literal("//"), | |
zeroOrMoreSpaces, | |
prefix(while: { $0 != newLine }) | |
).map { _, _, _, commentText in | |
String(commentText) | |
} | |
private let singleLineSlashAsteriskComment = zip( | |
zeroOrMoreSpaces, | |
literal("/*"), | |
anyString(until: zip( | |
literal("*/"), | |
zeroOrMore(char: newLine) | |
)) | |
).map { _, _, commentText in | |
commentText.trimmingCharacters(in: .whitespaces) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
import Data | |
class StringsFileParserTests: XCTestCase { | |
func testSeparatedByNewlines() throws { | |
let entries = FileParserFactory.ordered().parse(string: | |
""" | |
"editor.button.save" = "Save"; | |
"editor.button.open" = "Open"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 2) | |
XCTAssertEqual(entries[0].content, .key("editor.button.save", value: "Save")) | |
XCTAssertEqual(entries[0].order, 0) | |
XCTAssertEqual(entries[1].content, .key("editor.button.open", value: "Open")) | |
XCTAssertEqual(entries[1].order, 1) | |
} | |
func testSeparatedBySemicolonsOnly() throws { | |
let entries = FileParserFactory.ordered().parse(string: | |
""" | |
"editor.button.save" = "Save";"editor.button.open" = "Open"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 2) | |
XCTAssertEqual(entries[0].content, .key("editor.button.save", value: "Save")) | |
XCTAssertEqual(entries[0].order, 0) | |
XCTAssertEqual(entries[1].content, .key("editor.button.open", value: "Open")) | |
XCTAssertEqual(entries[1].order, 1) | |
} | |
func testLineOrder() throws { | |
let entries = FileParserFactory.ordered().parse(string: | |
""" | |
"line.0" = "Line 0"; | |
"line.1" = "Line 1"; | |
"line.2" = "Line 2"; | |
"line.3" = "Line 3"; | |
"line.4" = "Line 4"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 5) | |
XCTAssertEqual(entries, [ | |
Entry(key: "line.0", value: "Line 0", order: 0), | |
Entry(key: "line.1", value: "Line 1", order: 1), | |
Entry(key: "line.2", value: "Line 2", order: 2), | |
Entry(key: "line.3", value: "Line 3", order: 3), | |
Entry(key: "line.4", value: "Line 4", order: 4) | |
]) | |
} | |
func testSingleLineSlashComment() throws { | |
var entries: [Entry] | |
// In the middle | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
"line.0" = "Line 0"; | |
// A single line comment | |
"line.1" = "Line 1"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 3) | |
XCTAssertEqual(entries, [ | |
Entry(key: "line.0", value: "Line 0"), | |
Entry(comment: "A single line comment"), | |
Entry(key: "line.1", value: "Line 1") | |
]) | |
// As first line | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
// A single line comment | |
"line.0" = "Line 0"; | |
"line.1" = "Line 1"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 3) | |
XCTAssertEqual(entries, [ | |
Entry(comment: "A single line comment"), | |
Entry(key: "line.0", value: "Line 0"), | |
Entry(key: "line.1", value: "Line 1") | |
]) | |
// As last line | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
"line.0" = "Line 0"; | |
"line.1" = "Line 1"; | |
// A single line comment | |
""" | |
) | |
XCTAssertEqual(entries.count, 3) | |
XCTAssertEqual(entries, [ | |
Entry(key: "line.0", value: "Line 0"), | |
Entry(key: "line.1", value: "Line 1"), | |
Entry(comment: "A single line comment") | |
]) | |
// Leading and trailing whitespace handling | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
"line.0" = "Line 0"; | |
// A single line comment | |
"line.1" = "Line 1"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 3) | |
XCTAssertEqual(entries, [ | |
Entry(key: "line.0", value: "Line 0"), | |
Entry(comment: "A single line comment"), | |
Entry(key: "line.1", value: "Line 1") | |
]) | |
// Only comment | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
// A single line comment | |
""" | |
) | |
XCTAssertEqual(entries.count, 1) | |
XCTAssertEqual(entries, [ | |
Entry(comment: "A single line comment") | |
]) | |
} | |
func testSingleLineSlashAsteriskComment() throws { | |
var entries: [Entry] | |
// In the middle | |
entries = FileParserFactory.unordered().parse(string: | |
""" | |
"line.0" = "Line 0"; | |
/* A single line comment */ | |
"line.1" = "Line 1"; | |
""" | |
) | |
XCTAssertEqual(entries.count, 3) | |
XCTAssertEqual(entries, [ | |
Entry(key: "line.0", value: "Line 0"), | |
Entry(comment: "A single line comment"), | |
Entry(key: "line.1", value: "Line 1") | |
]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment