Skip to content

Commit 37af86b

Browse files
committed
refactor: simplify name style conversion and improve error handling
- Refactor enum bridging logic for converting PKL `Common.NameStyle` to `ExFigCore.NameStyle`. This change simplifies code by leveraging a force-unwrap approach, relying on tests to ensure raw value consistency between enums. - Implement error handling improvements while removing unnecessary name convention switch cases, replacing them with direct attribute accesses that simplify code structure. - Add error representations for missing or invalid configuration data, ensuring clear messaging and recovery guidance when issues occur. - Introduce validation extensions across multiple configuration files and add corresponding tests to cover scenarios with missing attributes. Additionally, integrate automated tests for bridging logic and configuration error handling. These changes enhance code readability, maintainability, and provide robust error handling, making it easier to extend functionality in the future while ensuring high test coverage for crucial logic.
1 parent db4cf6f commit 37af86b

19 files changed

Lines changed: 447 additions & 186 deletions

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ let package = Package(
7171
.target(
7272
name: "ExFigConfig",
7373
dependencies: [
74+
"ExFigCore",
7475
.product(name: "PklSwift", package: "pkl-swift"),
7576
]
7677
),
@@ -196,6 +197,8 @@ let package = Package(
196197
dependencies: [
197198
"ExFigCLI",
198199
"FigmaAPI",
200+
"ExFig-Flutter",
201+
"ExFig-Web",
199202
.product(name: "CustomDump", package: "swift-custom-dump"),
200203
.product(name: "LibPNG", package: "libpng"),
201204
],

Sources/ExFig-Android/Config/AndroidColorsEntry.swift

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,7 @@ public extension Android.ThemeAttributes {
4848
public extension Android.NameTransform {
4949
var resolvedStyle: NameStyle {
5050
guard let style else { return .pascalCase }
51-
switch style {
52-
case .camelCase: return .camelCase
53-
case .snake_case: return .snakeCase
54-
case .pascalCase: return .pascalCase
55-
case .flatCase: return .flatCase
56-
case .kebab_case: return .kebabCase
57-
case .sCREAMING_SNAKE_CASE: return .screamingSnakeCase
58-
}
51+
return style.coreNameStyle
5952
}
6053

6154
var resolvedPrefix: String {
@@ -70,31 +63,6 @@ public extension Android.NameTransform {
7063
// MARK: - ColorsEntry Convenience
7164

7265
public extension Android.ColorsEntry {
73-
/// Returns a validated ColorsSourceInput for use with ColorsExportContext.
74-
/// Throws if required fields (tokensFileId, tokensCollectionName, lightModeName) are nil or empty.
75-
func validatedColorsSourceInput() throws -> ColorsSourceInput {
76-
guard let tokensFileId, !tokensFileId.isEmpty else {
77-
throw ColorsConfigError.missingTokensFileId
78-
}
79-
guard let tokensCollectionName, !tokensCollectionName.isEmpty else {
80-
throw ColorsConfigError.missingTokensCollectionName
81-
}
82-
guard let lightModeName, !lightModeName.isEmpty else {
83-
throw ColorsConfigError.missingLightModeName
84-
}
85-
return ColorsSourceInput(
86-
tokensFileId: tokensFileId,
87-
tokensCollectionName: tokensCollectionName,
88-
lightModeName: lightModeName,
89-
darkModeName: darkModeName,
90-
lightHCModeName: lightHCModeName,
91-
darkHCModeName: darkHCModeName,
92-
primitivesModeName: primitivesModeName,
93-
nameValidateRegexp: nameValidateRegexp,
94-
nameReplaceRegexp: nameReplaceRegexp
95-
)
96-
}
97-
9866
/// Path to generate Compose Color Kotlin file as URL.
9967
var colorKotlinURL: URL? {
10068
colorKotlin.map { URL(fileURLWithPath: $0) }

Sources/ExFig-Android/Config/AndroidIconsEntry.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,7 @@ public extension Android.IconsEntry {
2727
/// Effective name style, defaulting to snake_case.
2828
var effectiveNameStyle: NameStyle {
2929
guard let nameStyle else { return .snakeCase }
30-
switch nameStyle {
31-
case .camelCase: return .camelCase
32-
case .snake_case: return .snakeCase
33-
case .pascalCase: return .pascalCase
34-
case .flatCase: return .flatCase
35-
case .kebab_case: return .kebabCase
36-
case .sCREAMING_SNAKE_CASE: return .screamingSnakeCase
37-
}
30+
return nameStyle.coreNameStyle
3831
}
3932

4033
/// Effective compose format, defaulting to resourceReference.

Sources/ExFig-Android/Config/AndroidImagesEntry.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,7 @@ public extension Android.ImagesEntry {
5555
/// Effective name style, defaulting to snake_case.
5656
var effectiveNameStyle: NameStyle {
5757
guard let nameStyle else { return .snakeCase }
58-
switch nameStyle {
59-
case .camelCase: return .camelCase
60-
case .snake_case: return .snakeCase
61-
case .pascalCase: return .pascalCase
62-
case .flatCase: return .flatCase
63-
case .kebab_case: return .kebabCase
64-
case .sCREAMING_SNAKE_CASE: return .screamingSnakeCase
65-
}
58+
return nameStyle.coreNameStyle
6659
}
6760

6861
/// Converts generated ImageFormat to ExFigCore ImageOutputFormat.

Sources/ExFig-Android/Export/AndroidIconsExporter.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,11 @@ private extension AndroidIconsExporter {
169169
context: some IconsExportContext
170170
) async throws -> Int {
171171
guard let packageName = entry.composePackageName else {
172-
context.warning("composePackageName is required for ImageVector output")
173-
return 0
172+
throw AndroidIconsExportError.missingComposePackageName
174173
}
175174

176175
guard let srcDirectory = platformConfig.mainSrc else {
177-
context.warning("mainSrc is required for ImageVector output")
178-
return 0
176+
throw AndroidIconsExportError.missingMainSrc
179177
}
180178

181179
let (iconPairs, tempDirs) = try await loadAndProcess(entry: entry, context: context)
@@ -212,8 +210,11 @@ private extension AndroidIconsExporter {
212210
var svgFiles: [String: Data] = [:]
213211
for file in localFiles {
214212
let iconName = file.destination.file.deletingPathExtension().lastPathComponent
215-
if let data = try? Data(contentsOf: file.destination.url) {
213+
do {
214+
let data = try Data(contentsOf: file.destination.url)
216215
svgFiles[iconName] = data
216+
} catch {
217+
context.warning("Failed to read SVG file '\(iconName)': \(error.localizedDescription)")
217218
}
218219
}
219220

@@ -338,4 +339,33 @@ private enum AndroidIconsHelpers {
338339
}
339340
}
340341

342+
// MARK: - Errors
343+
344+
/// Errors that can occur during Android icons export.
345+
public enum AndroidIconsExportError: LocalizedError {
346+
/// composePackageName is required for ImageVector output.
347+
case missingComposePackageName
348+
349+
/// mainSrc directory is required for ImageVector output.
350+
case missingMainSrc
351+
352+
public var errorDescription: String? {
353+
switch self {
354+
case .missingComposePackageName:
355+
"composePackageName is required for ImageVector output"
356+
case .missingMainSrc:
357+
"mainSrc is required for ImageVector output"
358+
}
359+
}
360+
361+
public var recoverySuggestion: String? {
362+
switch self {
363+
case .missingComposePackageName:
364+
"Add 'composePackageName' to your Android icons entry"
365+
case .missingMainSrc:
366+
"Add 'mainSrc' to your Android platform configuration"
367+
}
368+
}
369+
}
370+
341371
// swiftlint:enable file_length

Sources/ExFig-Android/Export/AndroidImagesExporter.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ public struct AndroidImagesExporter: ImagesExporter {
7171
entry: entry, platformConfig: platformConfig, context: context
7272
)
7373
case (.png, .svg):
74-
context.warning("Cannot convert PNG source to VectorDrawable. Use SVG source for vector output.")
75-
return 0
74+
throw AndroidImagesExporterError.incompatibleFormat(source: "PNG", output: "VectorDrawable")
7675
}
7776
}
7877
}
@@ -482,11 +481,23 @@ private enum AndroidImagesHelpers {
482481

483482
enum AndroidImagesExporterError: LocalizedError {
484483
case invalidFileName(String)
484+
case incompatibleFormat(source: String, output: String)
485485

486486
var errorDescription: String? {
487487
switch self {
488488
case let .invalidFileName(name):
489489
"Invalid file name: \(name)"
490+
case let .incompatibleFormat(source, output):
491+
"Cannot convert \(source) source to \(output) output"
492+
}
493+
}
494+
495+
var recoverySuggestion: String? {
496+
switch self {
497+
case .invalidFileName:
498+
nil
499+
case .incompatibleFormat:
500+
"Use SVG source format for vector output, or change the output format to PNG or WebP"
490501
}
491502
}
492503
}
Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,4 @@
11
import ExFigConfig
2-
import ExFigCore
3-
import Foundation
42

53
/// Typealias for backward compatibility with consumers.
64
public typealias FlutterColorsEntry = Flutter.ColorsEntry
7-
8-
// MARK: - Convenience Extensions
9-
10-
public extension Flutter.ColorsEntry {
11-
/// Returns a validated ColorsSourceInput for use with ColorsExportContext.
12-
/// Throws if required fields (tokensFileId, tokensCollectionName, lightModeName) are nil or empty.
13-
func validatedColorsSourceInput() throws -> ColorsSourceInput {
14-
guard let tokensFileId, !tokensFileId.isEmpty else {
15-
throw ColorsConfigError.missingTokensFileId
16-
}
17-
guard let tokensCollectionName, !tokensCollectionName.isEmpty else {
18-
throw ColorsConfigError.missingTokensCollectionName
19-
}
20-
guard let lightModeName, !lightModeName.isEmpty else {
21-
throw ColorsConfigError.missingLightModeName
22-
}
23-
return ColorsSourceInput(
24-
tokensFileId: tokensFileId,
25-
tokensCollectionName: tokensCollectionName,
26-
lightModeName: lightModeName,
27-
darkModeName: darkModeName,
28-
lightHCModeName: lightHCModeName,
29-
darkHCModeName: darkHCModeName,
30-
primitivesModeName: primitivesModeName,
31-
nameValidateRegexp: nameValidateRegexp,
32-
nameReplaceRegexp: nameReplaceRegexp
33-
)
34-
}
35-
}
Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,4 @@
11
import ExFigConfig
2-
import ExFigCore
3-
import Foundation
42

53
/// Typealias for backward compatibility with consumers.
64
public typealias WebColorsEntry = Web.ColorsEntry
7-
8-
// MARK: - Convenience Extensions
9-
10-
public extension Web.ColorsEntry {
11-
/// Returns a validated ColorsSourceInput for use with ColorsExportContext.
12-
/// Throws if required fields (tokensFileId, tokensCollectionName, lightModeName) are nil or empty.
13-
func validatedColorsSourceInput() throws -> ColorsSourceInput {
14-
guard let tokensFileId, !tokensFileId.isEmpty else {
15-
throw ColorsConfigError.missingTokensFileId
16-
}
17-
guard let tokensCollectionName, !tokensCollectionName.isEmpty else {
18-
throw ColorsConfigError.missingTokensCollectionName
19-
}
20-
guard let lightModeName, !lightModeName.isEmpty else {
21-
throw ColorsConfigError.missingLightModeName
22-
}
23-
return ColorsSourceInput(
24-
tokensFileId: tokensFileId,
25-
tokensCollectionName: tokensCollectionName,
26-
lightModeName: lightModeName,
27-
darkModeName: darkModeName,
28-
lightHCModeName: lightHCModeName,
29-
darkHCModeName: darkHCModeName,
30-
primitivesModeName: primitivesModeName,
31-
nameValidateRegexp: nameValidateRegexp,
32-
nameReplaceRegexp: nameReplaceRegexp
33-
)
34-
}
35-
}

Sources/ExFig-iOS/Config/iOSColorsEntry.swift

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,9 @@ public typealias iOSColorsEntry = iOS.ColorsEntry
1010
// MARK: - Convenience Extensions
1111

1212
public extension iOS.ColorsEntry {
13-
/// Returns a validated ColorsSourceInput for use with ColorsExportContext.
14-
/// Throws if required fields (tokensFileId, tokensCollectionName, lightModeName) are nil or empty.
15-
func validatedColorsSourceInput() throws -> ColorsSourceInput {
16-
guard let tokensFileId, !tokensFileId.isEmpty else {
17-
throw ColorsConfigError.missingTokensFileId
18-
}
19-
guard let tokensCollectionName, !tokensCollectionName.isEmpty else {
20-
throw ColorsConfigError.missingTokensCollectionName
21-
}
22-
guard let lightModeName, !lightModeName.isEmpty else {
23-
throw ColorsConfigError.missingLightModeName
24-
}
25-
return ColorsSourceInput(
26-
tokensFileId: tokensFileId,
27-
tokensCollectionName: tokensCollectionName,
28-
lightModeName: lightModeName,
29-
darkModeName: darkModeName,
30-
lightHCModeName: lightHCModeName,
31-
darkHCModeName: darkHCModeName,
32-
primitivesModeName: primitivesModeName,
33-
nameValidateRegexp: nameValidateRegexp,
34-
nameReplaceRegexp: nameReplaceRegexp
35-
)
36-
}
37-
3813
/// Converts PKL NameStyle to ExFigCore NameStyle.
3914
var coreNameStyle: NameStyle {
40-
switch nameStyle {
41-
case .camelCase: .camelCase
42-
case .snake_case: .snakeCase
43-
case .pascalCase: .pascalCase
44-
case .flatCase: .flatCase
45-
case .kebab_case: .kebabCase
46-
case .sCREAMING_SNAKE_CASE: .screamingSnakeCase
47-
}
15+
nameStyle.coreNameStyle
4816
}
4917

5018
/// Path to generate UIColor extension as URL.

Sources/ExFig-iOS/Config/iOSIconsEntry.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,7 @@ public extension iOS.IconsEntry {
2929

3030
/// Converts PKL NameStyle to ExFigCore NameStyle.
3131
var coreNameStyle: NameStyle {
32-
switch nameStyle {
33-
case .camelCase: .camelCase
34-
case .snake_case: .snakeCase
35-
case .pascalCase: .pascalCase
36-
case .flatCase: .flatCase
37-
case .kebab_case: .kebabCase
38-
case .sCREAMING_SNAKE_CASE: .screamingSnakeCase
39-
}
32+
nameStyle.coreNameStyle
4033
}
4134

4235
/// Path to generate UIImage extension as URL.

0 commit comments

Comments
 (0)