Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
44d267c
feat: Add Swift config file reader & tests
jbelkins Dec 30, 2025
a371933
Created config file reader & test targets
jbelkins Dec 30, 2025
d94d990
Removed error extension
AntAmazonian Dec 31, 2025
df038c0
Changed file URL to use appendingPathComponent
AntAmazonian Dec 31, 2025
ca5d326
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 2, 2026
c83dccb
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 5, 2026
666f59b
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 5, 2026
edd5fd2
Refactored helper functions, simplified code logic and corrected synt…
AntAmazonian Jan 9, 2026
f1ebc24
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 9, 2026
6853fd0
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 9, 2026
2cef0d0
Refactored most of func config() and add new helpers, along with upda…
AntAmazonian Jan 16, 2026
cd381a0
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 20, 2026
983ccad
Refactored code to use a new unique keys for property megring across …
AntAmazonian Jan 22, 2026
6e4652f
Commented the ConfigFileReaderTest, and added different default file …
AntAmazonian Jan 22, 2026
6450c23
Re-organized code hierarchy, reduced configuration task to single res…
AntAmazonian Feb 3, 2026
025d0d0
Updated the struct ConfigFile to look up other section types besides …
AntAmazonian Feb 4, 2026
252a490
Changed logic to use modern logger if available but fall back to olde…
AntAmazonian Feb 4, 2026
f77c6a7
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 4, 2026
06a7119
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 5, 2026
3d57bac
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 13, 2026
9ef2707
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 17, 2026
fe932b0
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 18, 2026
8f42944
Refactored ConfigFileReader into 3 components enable a 3 stage pipeli…
AntAmazonian Mar 23, 2026
749e155
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 24, 2026
a8c4e4a
Created a unit tests that first creates temp file to tests env loc ke…
AntAmazonian Mar 31, 2026
4ba5186
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 31, 2026
bafcbb3
Merge branch 'af/config_file_reader' of github.com:awslabs/aws-sdk-sw…
AntAmazonian Mar 31, 2026
4eacd14
Added the last function to utilize JSON testing parameters.
AntAmazonian Apr 1, 2026
4d4e071
Corrected headers to align AWS standards
AntAmazonian Apr 1, 2026
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 @@ -213,6 +213,13 @@ private var runtimeTargets: [Target] {
name: "AWSSDKDynamic",
path: "Sources/Core/AWSSDKDynamic/Sources/AWSSDKDynamic"
),
.target(
name: "AWSSDKConfigFileReader",
dependencies: [
.AWSSDKCommon,
],
path: "Sources/Core/AWSSDKConfigFileReader/Sources/AWSSDKConfigFileReader"
),
] + internalServiceTargets
}

Expand Down Expand Up @@ -271,6 +278,15 @@ private var runtimeTestTargets: [Target] {
path: "Sources/Core/AWSSDKIdentity/Tests/AWSSDKIdentityTests",
resources: [.process("Resources")]
),
.testTarget(
name: "AWSSDKConfigFileReaderTests",
dependencies: [
.AWSSDKCommon,
"AWSSDKConfigFileReader",
],
path: "Sources/Core/AWSSDKConfigFileReader/Tests/AWSSDKConfigFileReaderTests",
resources: [.process("Resources")]
),
]
}

Expand Down
16 changes: 16 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,13 @@ private var runtimeTargets: [Target] {
name: "AWSSDKDynamic",
path: "Sources/Core/AWSSDKDynamic/Sources/AWSSDKDynamic"
),
.target(
name: "AWSSDKConfigFileReader",
dependencies: [
.AWSSDKCommon,
],
path: "Sources/Core/AWSSDKConfigFileReader/Sources/AWSSDKConfigFileReader"
),
] + internalServiceTargets
}

Expand Down Expand Up @@ -2423,6 +2430,15 @@ private var runtimeTestTargets: [Target] {
path: "Sources/Core/AWSSDKIdentity/Tests/AWSSDKIdentityTests",
resources: [.process("Resources")]
),
.testTarget(
name: "AWSSDKConfigFileReaderTests",
dependencies: [
.AWSSDKCommon,
"AWSSDKConfigFileReader",
],
path: "Sources/Core/AWSSDKConfigFileReader/Tests/AWSSDKConfigFileReaderTests",
resources: [.process("Resources")]
),
]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

@_spi(FileBasedConfig) import AWSSDKCommon
import Foundation

struct ConfigFile: FileBasedConfiguration {
let sections: [String: ConfigFileSection]

func section(for name: String, type: FileBasedConfigurationSectionType) -> (any FileBasedConfigurationSection)? {
let typePrefix: String
switch type {
case .profile:
typePrefix = "profile"
case .ssoSession:
typePrefix = "sso-session"
case .services:
typePrefix = "services"
}

let fullKey: String
if name == "default" {
if let explicit = sections["profile default"] { return explicit }
fullKey = "default"
} else {
fullKey = "\(typePrefix) \(name)"
}

// First try the exact type match
if let section = sections[fullKey] {
return section
}

// If no exact match and we're looking for a profile, try other section types
// This allows the default section(for: name) method to find any section type
if type == .profile {
// Try sso-session
if let ssoSection = sections["sso-session \(name)"] {
return ssoSection
}
// Try services
if let servicesSection = sections["services \(name)"] {
return servicesSection
}
}

return nil
}
}

struct ConfigFileSection: FileBasedConfigurationSection {
let name: String
var subproperties: [String: [String: String]] = [:]
var properties: [String: String] = [:]

func property(for name: FileBasedConfigurationKey) -> FileBasedConfigurationProperty? {
if let value = properties[name.rawValue]{
return .string(value)
} else if let subproperties = subproperties[name.rawValue] {
return .subsection(Subsection(subproperties: subproperties))
}
return nil
}
}

struct Config: FileBasedConfigurationSectionProviding {
func section(for name: String, type: AWSSDKCommon.FileBasedConfigurationSectionType) -> (any AWSSDKCommon.FileBasedConfigurationSection)? {
return nil
}
}

struct Subsection: FileBasedConfigurationSubsection {
var subproperties: [String: String]

func value(for name: AWSSDKCommon.FileBasedConfigurationKey) -> String? {
subproperties[name.rawValue]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

class ConfigFileParser {
private var sections: [String: [String: String]] = [:]
private var currentSection: String?
private var currentProperty: String?

func parse(_ content: String) throws -> [String: [String: String]] {
for line in content.split(whereSeparator: \.isNewline) {
let lineString = String(line)
switch lineString.classification {
case .blank, .comment: continue
case .section(let raw, _): try readSectionLine(raw)
case .property(let raw, _, _): try readPropertyLine(raw)
case .multiLine(let raw, _): try readContinuationLine(raw)
case .unknown:
guard currentSection != nil else { throw ParserError.noSectionDefined }
throw ParserError.missingEqualsSign
}
}
return sections
}

enum ParserError: Error, LocalizedError {
case noSectionDefined
case noPropertyDefined
case emptyPropertyKey
case missingEqualsSign
case missingEqualsSignInSubProperty
case sectionMissingClosingBracket
case emptySubPropertyKey

var errorDescription: String? {
switch self {
case .noSectionDefined: return "Expected a section definition"
case .noPropertyDefined: return "Expected a property definition, found continuation"
case .emptyPropertyKey: return "Property did not have a name"
case .missingEqualsSign: return "Expected an '=' sign defining a property"
case .missingEqualsSignInSubProperty: return "Expected an '=' sign defining a property in sub-property"
case .sectionMissingClosingBracket: return "Section definition must end with ']'"
case .emptySubPropertyKey: return "Property did not have a name in sub-property"
}
}
}


private func readSectionLine(_ line: String) throws {
// Strip trailing comments (section headers allow # or ; without space)
var clean = line
if let range = clean.range(of: #"\s*[#;].*$"#, options: .regularExpression) {
clean = String(clean[..<range.lowerBound])
}
clean = clean.trimmingCharacters(in: .whitespaces)
guard clean.hasSuffix("]") else {throw ParserError.sectionMissingClosingBracket}
// Remove brackets
let inner = String(clean.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
currentSection = inner
currentProperty = nil
if sections[inner] == nil { sections[inner] = [:] }
}

private func readPropertyLine(_ line: String) throws {
guard let section = currentSection else { throw ParserError.noSectionDefined }
// Property comments require whitespace before # or ;
var clean = line
for pattern in [" #", " ;", "\t#", "\t;"] {
if let idx = clean.range(of: pattern) {
clean = String(clean[..<idx.lowerBound])
}
}
clean = clean.trimmingCharacters(in: .whitespaces)
guard let eqIdx = clean.firstIndex(of: "=") else { throw ParserError.missingEqualsSign }
let key = String(clean[..<eqIdx]).trimmingCharacters(in: .whitespaces).lowercased()
guard !key.isEmpty else { throw ParserError.emptyPropertyKey }

let value = String(clean[clean.index(after: eqIdx)...]).trimmingCharacters(in: .whitespaces)
currentProperty = key
sections[section]?[key] = value
}

private func readContinuationLine(_ line: String) throws {
guard currentSection != nil else {throw ParserError.noSectionDefined}
guard let prop = currentProperty else { throw ParserError.noPropertyDefined }
// No comment stripping on continuation lines — they're part of the value
let trimmed = line.trimmingCharacters(in: .whitespaces)
let existing = sections[currentSection!]?[prop] ?? ""

if existing.isEmpty || existing.hasPrefix("\n") {
// This is a sub-property line — validate it has a non-empty key
guard trimmed.contains("=") else { throw ParserError.missingEqualsSignInSubProperty }
let eqIdx = trimmed.firstIndex(of: "=")!
let subKey = String(trimmed[..<eqIdx]).trimmingCharacters(in: .whitespaces)
guard !subKey.isEmpty else { throw ParserError.emptySubPropertyKey } // new case needed
sections[currentSection!]?[prop] = existing + "\n" + trimmed
return
}

// If the parent property has no value (subsection placeholder),
// continuation lines must be "key = value" sub-properties
if existing.isEmpty && !trimmed.contains("=") {
throw ParserError.missingEqualsSignInSubProperty
}
sections[currentSection!]?[prop] = existing.isEmpty ? "\n" + trimmed : existing + "\n" + trimmed
}
}
Loading
Loading