Skip to content

Implementation clean up #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: stable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,66 @@ import Foundation
import PathKit

extension Strings {
final class StringsDictFileParser: StringsFileTypeParser {
private let options: ParserOptionValues

init(options: ParserOptionValues) {
self.options = options
}

static let extensions = ["stringsdict"]
final class StringsDictFileParser: StringsFileTypeParser {
private let options: ParserOptionValues

init(options: ParserOptionValues) {
self.options = options
}

static let extensions = ["stringsdict"]

func parseFile(at path: Path) throws -> [Strings.Entry] {
guard let data = try? path.read() else {
throw ParserError.failureOnLoading(path: path)
}

do {
let plurals = try PropertyListDecoder()
.decode([String: StringsDict].self, from: data)
.compactMapValues { stringsDict -> StringsDict.PluralEntry? in
// We only support .pluralEntry (and not .variableWidthEntry) for now, so filter out the rest
guard case let .pluralEntry(pluralEntry) = stringsDict else { return nil }
return pluralEntry
}

func parseFile(at path: Path) throws -> [Strings.Entry] {
guard let data = try? path.read() else {
throw ParserError.failureOnLoading(path: path)
}

do {
let plurals = try PropertyListDecoder()
.decode([String: StringsDict].self, from: data)
.compactMapValues { stringsDict -> StringsDict.PluralEntry? in
// We only support .pluralEntry (and not .variableWidthEntry) for now, so filter out the rest
guard case let .pluralEntry(pluralEntry) = stringsDict else { return nil }
return pluralEntry
}

return try plurals.map { keyValuePair -> Entry in
let (key, pluralEntry) = keyValuePair
return Entry(
key: key,
translation: "Plural format key: \"\(pluralEntry.formatKey)\"",
types: try PlaceholderType.placeholderTypes(
fromFormat: pluralEntry.formatKeyWithVariableValueTypes
),
parameters: Parameter.extractParameterNames(
from: pluralEntry.firstOtherRule,
type: pluralEntry.placeholderType
),
keyStructureSeparator: options[Option.separator]
)
}
} catch DecodingError.keyNotFound(let codingKey, let context) {
throw ParserError.invalidPluralFormat(
missingVariableKey: codingKey.stringValue,
pluralKey: context.codingPath.first?.stringValue ?? ""
)
}
return try plurals.map { keyValuePair -> Entry in
let (key, pluralEntry) = keyValuePair
return Entry(
key: key,
translation: "Plural format key: \"\(pluralEntry.formatKey)\"",
types: try PlaceholderType.placeholderTypes(
fromFormat: pluralEntry.formatKeyWithVariableValueTypes
),
parameters: Parameter.extractParameterNames(
from: pluralEntry.firstOtherRule,
type: pluralEntry.placeholderType
),
keyStructureSeparator: options[Option.separator]
)
}
} catch DecodingError.keyNotFound(let codingKey, let context) {
throw ParserError.invalidPluralFormat(
missingVariableKey: codingKey.stringValue,
pluralKey: context.codingPath.first?.stringValue ?? ""
)
}
}
}
}

// MARK: - Private Helpers

private extension StringsDict.PluralEntry {
var firstOtherRule: String {
self.variables.map { $0.rule.other }.first ?? ""
}
/// - Note: For the first iteration of the named parameters work, we expect all
/// `.stringsdict` entries to be a count so we can return an integer for the time being.
///
/// - Returns: .int
var placeholderType: Strings.PlaceholderType {
.int
}
var firstOtherRule: String {
self.variables.map { $0.rule.other }.first ?? ""
}

/// - Note: For the first iteration of the named parameters work, we expect all
/// `.stringsdict` entries to be a count so we can return an integer for the time being.
///
/// - Returns: .int
var placeholderType: Strings.PlaceholderType {
.int
}
}
132 changes: 66 additions & 66 deletions Sources/SwiftGenKit/Parsers/Strings/Parameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,89 @@ import Foundation
// MARK: - Parameter Model

extension Strings {
public struct Parameter: Equatable {
public let name: String
public let type: PlaceholderType
}
public struct Parameter: Equatable {
public let name: String
public let type: PlaceholderType
}
}

// MARK: - Extract Parameter Names

extension Strings.Parameter {
/// Extracts parameter names from a given string
///
/// - Parameters:
/// - string: The input string containing potential parameters
/// - type: The placeholder type used to assign a type to each extracted parameter
/// - Returns: An array of `Parameter` objects representing the extracted parameters
///
/// Example: "Welcome, {{displayName}}" --> [Parameter(name: "displayName", type: .string)]
static func extractParameterNames(
from string: String,
type: Strings.PlaceholderType
) -> [Strings.Parameter] {
let results = namedParameterRegEx.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.utf16.count)
)

let parameterNames = results.compactMap { result -> String? in
/// `result.range(at: 1)` extracts the first capture group (i.e. the parameter name)
guard let range = Range(result.range(at: 1), in: string) else {
return nil
}
return String(string[range])
}

return parameterNames.map { .init(name: $0.snakeToCamelCase, type: type) }
/// Extracts parameter names from a given string
///
/// - Parameters:
/// - string: The input string containing potential parameters
/// - type: The placeholder type used to assign a type to each extracted parameter
/// - Returns: An array of `Parameter` objects representing the extracted parameters
///
/// Example: "Welcome, {{displayName}}" --> [Parameter(name: "displayName", type: .string)]
static func extractParameterNames(
from string: String,
type: Strings.PlaceholderType
) -> [Strings.Parameter] {
let results = namedParameterRegEx.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.utf16.count)
)

let parameterNames = results.compactMap { result -> String? in
/// `result.range(at: 1)` extracts the first capture group (i.e. the parameter name)
guard let range = Range(result.range(at: 1), in: string) else {
return nil
}
return String(string[range])
}

return parameterNames.map { .init(name: $0.snakeToCamelCase, type: type) }
}
}

// MARK: - Private Helpers

// MARK: Parameter Regex

private extension Strings.Parameter {
/// This regular expression is used to match named parameters embedded within double curly braces in a string.
///
/// - Note: The pattern will match sequences like `{{parameterName}}`, where `parameterName` consists of
/// alphanumeric characters and underscores, and must start with a lowercase letter. The expression allows
/// for optional whitespace both inside and outside the braces.
static let namedParameterRegEx: NSRegularExpression = {
do {
return try NSRegularExpression(
pattern: #"\{\{\s*([a-z][a-z0-9_]*)\s*\}\}"#,
options: []
)
} catch {
fatalError("Error building the regular expression used to match the named parameter format")
}
}()
/// This regular expression is used to match named parameters embedded within double curly braces in a string.
///
/// - Note: The pattern will match sequences like `{{parameterName}}`, where `parameterName` consists of
/// alphanumeric characters and underscores, and must start with a lowercase letter. The expression allows
/// for optional whitespace both inside and outside the braces.
static let namedParameterRegEx: NSRegularExpression = {
do {
return try NSRegularExpression(
pattern: #"\{\{\s*([a-z][a-z0-9_]*)\s*\}\}"#,
options: []
)
} catch {
fatalError("Error building the regular expression used to match the named parameter format")
}
}()
}

// MARK: String Formatting

private extension String {
/// Converts a `snake_case` string to `camelCase`.
///
/// - Note: Handles edge cases such as leading, trailing, and multiple underscores by ignoring them
/// and not including empty segments in the output. If the input is an empty string or consists only
/// of underscores, the computed property will return an empty string.
var snakeToCamelCase: String {
guard !self.isEmpty else { return "" }

let components = self.split(separator: "_")

guard !components.isEmpty else { return "" }

return components.enumerated().map { index, element in
let lowercased = element.lowercased()
return index == 0 ? lowercased : lowercased.capitalizingFirstLetter
}.joined()
}
/// Converts a `snake_case` string to `camelCase`.
///
/// - Note: Handles edge cases such as leading, trailing, and multiple underscores by ignoring them
/// and not including empty segments in the output. If the input is an empty string or consists only
/// of underscores, the computed property will return an empty string.
var snakeToCamelCase: String {
guard !self.isEmpty else { return "" }

var capitalizingFirstLetter: String {
prefix(1).capitalized + dropFirst()
}
let components = self.split(separator: "_")

guard !components.isEmpty else { return "" }

return components.enumerated().map { index, element in
let lowercased = element.lowercased()
return index == 0 ? lowercased : lowercased.capitalizingFirstLetter
}.joined()
}

var capitalizingFirstLetter: String {
prefix(1).capitalized + dropFirst()
}
}
Loading