Sources/SwiftBuffet/Parser.swift (206 lines of code) (raw):
import Foundation
/// Parses a protocol buffer file from the given URL path.
///
/// - Parameter path: The URL of the .proto file to be parsed.
/// - Returns: A tuple containing arrays of `ProtoMessage` and `ProtoEnum`.
/// - Throws: An error if the file cannot be read.
internal func parseProtoFile(
at path: URL,
with swiftPrefix: String,
verbose: Bool,
quite: Bool
) throws -> ([ProtoMessage], [ProtoEnum]) {
var content = try String(contentsOf: path)
return parseContent(
&content,
parent: nil,
with: swiftPrefix,
verbose: verbose,
quite: quite
)
}
/// Recursively parses the content of a protocol buffer file.
///
/// - Parameters:
/// - content: The content of the .proto file.
/// - parent: The name of the parent message or enum, if any.
/// - Returns: A tuple containing arrays of `ProtoMessage` and `ProtoEnum`.
internal func parseContent(
_ content: inout String,
parent: String?,
with swiftPrefix: String,
verbose: Bool,
quite: Bool
) -> ([ProtoMessage], [ProtoEnum]) {
var localMessages: [ProtoMessage] = []
var localEnums: [ProtoEnum] = []
// Parsing messages
let messageMatches = content.matches(of: messagePattern)
for match in messageMatches {
let messageName = String(match.1)
var messageBody = String(match.2)
// This will recessively call parse content on the
// body to find nested messages and enums
processMessage(
messageName,
&messageBody,
&localMessages,
&localEnums,
parent,
swiftPrefix,
verbose: verbose,
quite: quite
)
// Remove the parsed content to avoid duplication
content = content.replacingOccurrences(
of: String(match.0),
with: ""
)
}
// Parsing enums
let enumMatches = content.matches(of: enumPattern)
for match in enumMatches {
let enumName = String(match.1)
var enumBody = String(match.2)
// This will recessively call parse content on the
// body to find nested messages and enums
processEnum(
enumName,
&enumBody,
&localMessages,
&localEnums,
parent,
swiftPrefix,
verbose: verbose,
quite: quite
)
// Remove the parsed content to avoid duplication
content = content.replacingOccurrences(
of: String(match.0),
with: ""
)
}
return (localMessages, localEnums)
}
/// Processes a protocol buffer enum by parsing its content and extracting nested messages and enums.
///
/// - Parameters:
/// - name: The name of the enum.
/// - body: The content of the enum.
/// - localMessages: A mutable array to store parsed `ProtoMessage` objects.
/// - localEnums: A mutable array to store parsed `ProtoEnum` objects.
/// - parent: The name of the parent message or enum, if any.
internal func processEnum(
_ name: String,
_ body: inout String,
_ localMessages: inout [ProtoMessage],
_ localEnums: inout [ProtoEnum],
_ parent: String?,
_ prefix: String,
verbose: Bool,
quite: Bool
) {
if quite == false {
print("Matched enum: \(name)")
}
if verbose == true {
print("Matched enum body: \(body)")
}
let (nestedMessages, nestedEnums) = parseContent(
&body,
parent: name,
with: prefix,
verbose: verbose,
quite: quite
)
localMessages.append(contentsOf: nestedMessages)
localEnums.append(contentsOf: nestedEnums)
let cases = parseEnumCases(
from: body,
verbose: verbose,
quite: quite
)
localEnums.append(
ProtoEnum(
name: name,
cases: cases,
parentName: parent
)
)
}
/// Processes a protocol buffer message by parsing its content and extracting nested messages and enums.
///
/// - Parameters:
/// - name: The name of the message.
/// - body: The content of the message.
/// - localMessages: A mutable array to store parsed `ProtoMessage` objects.
/// - localEnums: A mutable array to store parsed `ProtoEnum` objects.
/// - parent: The name of the parent message, if any.
internal func processMessage(
_ name: String,
_ body: inout String,
_ localMessages: inout [ProtoMessage],
_ localEnums: inout [ProtoEnum],
_ parent: String?,
_ prefix: String,
verbose: Bool,
quite: Bool
) {
if quite == false {
print("Matched message: \(name)")
}
if verbose == true {
print("Message body: \(body)")
}
let (nestedMessages, nestedEnums) = parseContent(
&body,
parent: name,
with: prefix,
verbose: verbose,
quite: quite
)
localMessages.append(contentsOf: nestedMessages)
localEnums.append(contentsOf: nestedEnums)
let fields = parseMessageFields(
from: body,
with: prefix,
verbose: verbose,
quite: quite
)
localMessages.append(
ProtoMessage(
name: name,
fields: fields,
parentName: parent
)
)
}
/// Parses the fields of a protocol buffer message.
///
/// - Parameter content: The content of the message.
/// - Returns: An array of `ProtoField` representing the fields of the message.
internal func parseMessageFields(
from content: String,
with swiftPrefix: String,
verbose: Bool,
quite: Bool
) -> [ProtoField] {
var fields: [ProtoField] = []
// Use Swift's Regex for parsing fields
let fieldMatches = content.matches(of: fieldPattern)
if quite == false {
print("Field matches count: \(fieldMatches.count)")
}
for match in fieldMatches {
let comment = match.output.1.map { String($0) }
let fieldModifier = match.output.3.map { String($0) }
let fieldType = String(match.output.4)
let fieldName = String(match.output.5)
let fieldOptions = match.output.6.map { String($0) }
let isOptional = fieldModifier?.contains("optional") == true
let isRepeated = fieldModifier?.contains("repeated") == true
let isMap = fieldModifier?.starts(with: "map") == true
let isDeprecated = fieldOptions?.contains("deprecated = true") == true
if verbose {
print("Matched field type: \(fieldType), field name: \(fieldName), isOptional: \(isOptional), isRepeated: \(isRepeated), isMap: \(isMap))")
}
fields.append(
ProtoField(
swiftPrefix: swiftPrefix,
name: fieldName,
type: fieldType,
comment: comment,
isOptional: isOptional,
isRepeated: isRepeated,
isMap: isMap,
isDeprecated: isDeprecated
)
)
}
return fields
}
/// Parses the cases of a protocol buffer enum.
///
/// - Parameter content: The content of the enum.
/// - Returns: An array of `ProtoEnumCase` representing the cases of the enum.
internal func parseEnumCases(
from content: String,
verbose: Bool,
quite: Bool
) -> [ProtoEnumCase] {
var cases: [ProtoEnumCase] = []
// Use Swift's Regex for parsing enum cases
let caseMatches = content.matches(of: enumCasePattern)
if quite == false {
print("Enum case matches count: \(caseMatches.count)")
}
for match in caseMatches {
let caseName = String(match.1)
let caseValue = Int(match.2)!
if verbose {
print("Matched enum case name: \(caseName), value: \(caseValue)")
}
cases.append(
ProtoEnumCase(
name: caseName,
value: caseValue
)
)
}
return cases
}