From cdb3cd9f31af948969766fadfaa5ad4a34e16d2e Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Fri, 20 Mar 2026 10:27:58 -0400 Subject: [PATCH 1/5] Update scripts, docs, and workflows to latest and greatest - Re-enable non-macOS builds - Update lint rules to be slightly more permissive - Update GitHub Actions workflow to be a little more sophisticated - Clean up scripts - Update docs and guides - Update source files with new lint rules --- .github/workflows/VerifyChanges.yaml | 56 ++-- .swift-format | 5 +- App/Sources/App/ContentView.swift | 2 +- App/Sources/App/ContentViewModel.swift | 6 +- CLAUDE.md | 6 +- Documentation/TestMocks.md | 100 +++--- Documentation/TestingGuidelines.md | 307 ++++++++++++------ README.md | 2 +- Scripts/install-git-hooks | 44 +-- .../EventBusAccessReporter.swift | 6 +- .../Core/CodableValueRepresentation.swift | 6 +- .../Core/ConfigVariable.swift | 11 +- .../Core/ConfigVariableContent.swift | 170 +++++----- .../Core/ConfigVariableReader.swift | 22 +- .../Core/RegisteredConfigVariable.swift | 4 +- .../ConfigVariableDetailView.swift | 8 +- .../ConfigVariableListView.swift | 4 +- .../ConfigVariableListViewModel.swift | 2 +- .../Editor/ConfigVariableEditor.swift | 14 +- .../Editor/Data Models/EditorDocument.swift | 18 +- .../Data Models/EditorOverrideProvider.swift | 4 +- .../Extensions/ConfigContent+Additions.swift | 2 +- ...ndomValueGenerating+DevConfiguration.swift | 14 +- .../EventBusAccessReporterTests.swift | 6 +- .../Core/ConfigVariableReaderArrayTests.swift | 12 +- .../ConfigVariableReaderCodableTests.swift | 42 +-- ...gVariableReaderConfigExpressionTests.swift | 10 +- ...ariableReaderDataRepresentationTests.swift | 24 +- .../ConfigVariableReaderEditorTests.swift | 10 +- ...gVariableReaderRawRepresentableTests.swift | 10 +- ...onfigVariableReaderRegistrationTests.swift | 10 +- .../ConfigVariableReaderScalarTests.swift | 12 +- .../Core/ConfigVariableReaderTests.swift | 2 +- .../Core/RegisteredConfigVariableTests.swift | 8 +- .../ConfigVariableDetailViewModelTests.swift | 30 +- .../ConfigVariableListViewModelTests.swift | 22 +- .../Data Models/EditorDocumentTests.swift | 28 +- 37 files changed, 573 insertions(+), 466 deletions(-) diff --git a/.github/workflows/VerifyChanges.yaml b/.github/workflows/VerifyChanges.yaml index 5ec7ed8..1f9f8db 100644 --- a/.github/workflows/VerifyChanges.yaml +++ b/.github/workflows/VerifyChanges.yaml @@ -7,7 +7,7 @@ on: branches: ["main"] env: - XCODE_VERSION: 26.0.1 + XCODE_VERSION: 26.3 jobs: lint: @@ -15,12 +15,11 @@ jobs: runs-on: macos-26 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Select Xcode ${{ env.XCODE_VERSION }} - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app + run: sudo xcode-select -s /Applications/Xcode_"$XCODE_VERSION".app - name: Lint - run: | - Scripts/lint + run: Scripts/lint build-and-test: name: Build and Test (${{ matrix.platform }}) @@ -30,22 +29,14 @@ jobs: fail-fast: false matrix: include: -# - platform: iOS -# xcode_destination: "platform=iOS Simulator,name=GitHub_Actions_Simulator" -# simulator_device_type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro" -# simulator_runtime: "com.apple.CoreSimulator.SimRuntime.iOS-26-0" + - platform: iOS + xcode_destination: "platform=iOS Simulator,name=iPhone 17 Pro" - platform: macOS xcode_destination: "platform=macOS,arch=arm64" -# simulator_device_type: "" -# simulator_runtime: "" -# - platform: tvOS -# xcode_destination: "platform=tvOS Simulator,name=GitHub_Actions_Simulator" -# simulator_device_type: "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K" -# simulator_runtime: "com.apple.CoreSimulator.SimRuntime.tvOS-26-0" -# - platform: watchOS -# xcode_destination: "platform=watchOS Simulator,name=GitHub_Actions_Simulator" -# simulator_device_type: "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-10-46mm" -# simulator_runtime: "com.apple.CoreSimulator.SimRuntime.watchOS-26-0" + - platform: tvOS + xcode_destination: "platform=tvOS Simulator,name=Apple TV 4K (3rd generation)" + - platform: watchOS + xcode_destination: "platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)" env: DEV_BUILDS: DevBuilds/Sources @@ -59,13 +50,13 @@ jobs: steps: - name: Select Xcode ${{ env.XCODE_VERSION }} - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app + run: sudo xcode-select -s /Applications/Xcode_"$XCODE_VERSION".app - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Checkout DevBuilds - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: DevKitOrganization/DevBuilds path: DevBuilds @@ -73,22 +64,21 @@ jobs: - name: Restore XCTestProducts if: github.event_name != 'push' id: cache-xctestproducts-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ${{ env.XCODE_TEST_PRODUCTS_PATH }} - key: cache-xctestproducts-${{ github.workflow }}-${{ matrix.platform }}-${{ env.XCODE_VERSION }}-${{ github.sha }} + key: cache-xctestproducts-${{ github.workflow }}-${{ matrix.platform }}-${{ github.sha }} - uses: irgaly/xcode-cache@v1 if: steps.cache-xctestproducts-restore.outputs.cache-hit != 'true' with: - key: xcode-cache-deriveddata-${{ github.workflow }}-${{ matrix.platform }}-${{ env.XCODE_VERSION }}-${{ github.sha }} + key: xcode-cache-deriveddata-${{ env.XCODE_VERSION }}-${{ github.workflow }}-${{ matrix.platform }}-${{ github.sha }} restore-keys: | - xcode-cache-deriveddata-${{ github.workflow }}-${{ matrix.platform }}- - xcode-cache-deriveddata- + xcode-cache-deriveddata-${{ env.XCODE_VERSION }}-${{ github.workflow }}-${{ matrix.platform }}- + xcode-cache-deriveddata-${{ env.XCODE_VERSION }} deriveddata-directory: .build/DerivedData sourcepackages-directory: .build/DerivedData/SourcePackages swiftpm-package-resolved-file: Package.resolved - verbose: true - name: Build for Testing id: build-for-testing @@ -101,8 +91,8 @@ jobs: run: ${{ env.DEV_BUILDS }}/build_and_test.sh --action test-without-building - name: Save XCTestProducts - if: failure() && steps.cache-xctestproducts-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + if: (cancelled() || failure()) && steps.cache-xctestproducts-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 with: path: ${{ env.XCODE_TEST_PRODUCTS_PATH }} key: ${{ steps.cache-xctestproducts-restore.outputs.cache-primary-key }} @@ -125,7 +115,7 @@ jobs: - name: Upload Logs and XCResults if: success() || failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Logs_and_XCResults-${{ matrix.platform }} path: | @@ -135,7 +125,7 @@ jobs: - name: Upload xccovPretty output if: github.event_name != 'push' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: xccovPrettyOutput-${{ matrix.platform }} path: .build/xccovPretty-${{ matrix.platform }}.output @@ -151,7 +141,7 @@ jobs: steps: - name: Download xccovPretty output - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: xccovPrettyOutput-macOS diff --git a/.swift-format b/.swift-format index 6cd31d6..6e99bc1 100644 --- a/.swift-format +++ b/.swift-format @@ -14,6 +14,7 @@ "lineBreakBetweenDeclarationAttributes": false, "lineLength": 120, "maximumBlankLines": 2, + "multilineTrailingCommaBehavior": "alwaysUsed", "multiElementCollectionTrailingCommas": true, "noAssignmentInExpressions": { "allowedFunctions": [] @@ -58,14 +59,14 @@ "ReplaceForEachWithForLoop": true, "ReturnVoidInsteadOfEmptyTuple": true, "TypeNamesShouldBeCapitalized": true, - "UseEarlyExits": true, + "UseEarlyExits": false, "UseExplicitNilCheckInConditions": true, "UseLetInEveryBoundCaseVariable": true, "UseShorthandTypeNames": true, "UseSingleLinePropertyGetter": true, "UseSynthesizedInitializer": true, "UseTripleSlashForDocumentationComments": true, - "UseWhereClausesInForLoops": true, + "UseWhereClausesInForLoops": false, "ValidateDocumentationComments": false }, "spacesAroundRangeFormationOperators": true, diff --git a/App/Sources/App/ContentView.swift b/App/Sources/App/ContentView.swift index b7c6d80..588cba9 100644 --- a/App/Sources/App/ContentView.swift +++ b/App/Sources/App/ContentView.swift @@ -28,7 +28,7 @@ struct ContentView: View { .sheet(isPresented: $isPresentingConfigEditor) { ConfigVariableEditor( reader: viewModel.configVariableReader, - customSectionTitle: "Actions" + customSectionTitle: "Actions", ) { Button("Do something", role: .destructive) { print("Did something!") diff --git a/App/Sources/App/ContentViewModel.swift b/App/Sources/App/ContentViewModel.swift index 2b83a6c..1708afd 100644 --- a/App/Sources/App/ContentViewModel.swift +++ b/App/Sources/App/ContentViewModel.swift @@ -33,13 +33,13 @@ final class ContentViewModel { .metadata(\.isEditable, false) let stringArrayVariable = ConfigVariable( key: "string_array", - defaultValue: ["Thom", "Jonny", "Ed", "Colin", "Phil"] + defaultValue: ["Thom", "Jonny", "Ed", "Colin", "Phil"], ).metadata(\.displayName, "String Array Example") let jsonVariable = ConfigVariable( key: "complexConfig", defaultValue: ComplexConfiguration(field1: "a", field2: 1), - content: .json(representation: .string()) + content: .json(representation: .string()), ).metadata(\.displayName, "Complex Config") let intBackedVariable = ConfigVariable(key: "favoriteCardSuit", defaultValue: CardSuit.spades, isSecret: true) @@ -56,7 +56,7 @@ final class ContentViewModel { NamedConfigProvider(inMemoryProvider, displayName: "In-Memory"), ], eventBus: eventBus, - isEditorEnabled: true + isEditorEnabled: true, ) configVariableReader.register(boolVariable) diff --git a/CLAUDE.md b/CLAUDE.md index 6541f60..85232ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ repository. - **Lint**: `Scripts/lint` (uses `swift format lint --recursive --strict`) - **Format**: `Scripts/format` - - **Setup git hooks**: `Scripts/install-git-hooks` (auto-formats on commit) + - **Setup git hooks**: `Scripts/install-git-hooks` (lints on push) ### GitHub Actions @@ -25,9 +25,9 @@ The repository uses GitHub Actions for CI/CD with the workflow in `.github/workflows/VerifyChanges.yaml`. The workflow: - Lints code on PRs using `swift format` - - Builds and tests on macOS only (other platforms disabled due to GitHub Actions stability) + - Builds and tests on iOS, macOS, tvOS, and watchOS - Generates code coverage reports using xccovPretty - - Requires Xcode 26.0.1 and macOS 26 runners + - Requires Xcode 26.3 and macOS 26 runners ## Architecture Overview diff --git a/Documentation/TestMocks.md b/Documentation/TestMocks.md index 5a4cbc6..afca4c9 100644 --- a/Documentation/TestMocks.md +++ b/Documentation/TestMocks.md @@ -1,17 +1,16 @@ # Test Mock Documentation -This document outlines the patterns and conventions for writing test mocks in the DevConfiguration -codebase. +This document outlines the patterns and conventions for writing test mocks in Swift. -Its helpful to read the [Dependency Injection guide](Documentation/DependencyInjection.md) before -reading this guide, as it introduces core principles for how we think about dependency injection. +Its helpful to read the [Dependency Injection guide](DependencyInjection.md) before reading this +guide, as it introduces core principles for how we think about dependency injection. ## Overview -The codebase uses a consistent approach to mocking based on the DevTesting package’s `Stub` and -`ThrowingStub` types. All mocks follow standardized patterns that make them predictable, testable, -and maintainable. +We use a consistent approach to mocking based on the DevTesting package's `Stub` and `ThrowingStub` +types. All mocks follow standardized patterns that make them predictable, testable, and +maintainable. ## When to Mock vs. Use Types Directly @@ -20,7 +19,7 @@ Create mock protocols when - The type has **non-deterministic behavior** (network calls, file I/O, time-dependent operations) - You need to **control or observe the behavior** in tests - - The type’s behavior **varies across environments** + - The type's behavior **varies across environments** Use types directly when @@ -28,7 +27,7 @@ Use types directly when - Testing with the real implementation provides **sufficient coverage** - Creating abstractions adds **complexity without testing benefits** -It’s worth pointing out that the following foundational types should be used directly. +It's worth pointing out that the following foundational types should be used directly. - **`NotificationCenter`**: Posting and observing notifications is predictable - **`UserDefaults`**: Simple key-value storage with consistent behavior @@ -40,7 +39,7 @@ It’s worth pointing out that the following foundational types should be used d ### 1. Stub-Based Architecture -All mocks use `DevTesting`’s `Stub` or `ThrowingStub` types +All mocks use `DevTesting`'s `Stub` or `ThrowingStub` types for function and property implementations: import DevTesting @@ -225,34 +224,34 @@ This applies to ANY mock (Observable or not) whose stubs are accessed by backgro #### Why This is Required: When a type spawns internal `Task`s during initialization, those tasks may access properties or -methods on injected dependencies. If those dependencies are mocks with force-unwrapped stubs, and +functions on injected dependencies. If those dependencies are mocks with force-unwrapped stubs, and the stubs haven't been initialized, the app will crash when the internal task tries to access them. **Example crash scenario:** // Any mock type (Observable or not) @MainActor - final class MockUser: User { - nonisolated(unsafe) var allSavesMembershipStub: Stub! - var allSavesMembership: Membership { - allSavesMembershipStub(id) // ❌ Crashes if stub is nil + final class MockDataSource: DataSource { + nonisolated(unsafe) var currentItemsStub: Stub! + var currentItems: [Item] { + currentItemsStub(id) // Crashes if stub is nil } } // Type spawns internal task during init - init(user: any User) { - self.user = user + init(dataSource: any DataSource) { + self.dataSource = dataSource Task { - // This accesses user.allSavesMembership, triggering the stub - let membership = user.allSavesMembership + // This accesses dataSource.currentItems, triggering the stub + let items = dataSource.currentItems } } // Test doesn't initialize the stub @Test func myTest() { - let mockUser = MockUser() // ❌ allSavesMembershipStub is nil - let viewModel = MyViewModel(user: mockUser) // ❌ Crashes in internal task + let mockDataSource = MockDataSource() // currentItemsStub is nil + let processor = ItemProcessor(dataSource: mockDataSource) // Crashes in internal task } #### Solution Pattern: Initialize in Test Setup @@ -260,30 +259,31 @@ the stubs haven't been initialized, the app will crash when the internal task tr Initialize all stubs that internal tasks will access: @MainActor - struct MyViewModelTests: RandomValueGenerating { - var mockUser = MockUser() + struct ItemProcessorTests: RandomValueGenerating { + var mockDataSource = MockDataSource() init() { // CRITICAL: Initialize ALL stubs that the type's internal tasks will access // Even if this particular test doesn't verify these stubs, they must be non-nil - mockUser.fetchAllSavesIfNeededStub = ThrowingStub(defaultError: nil) - mockUser.allSavesMembershipStub = Stub(defaultReturnValue: .unknown) + mockDataSource.fetchItemsStub = ThrowingStub(defaultError: nil) + mockDataSource.currentItemsStub = Stub(defaultReturnValue: []) } @Test mutating func myTest() async throws { - // Create type that spawns internal tasks using mockUser - let viewModel = MyViewModel(user: mockUser, ...) + // Create type that spawns internal tasks using mockDataSource + let processor = ItemProcessor(dataSource: mockDataSource) // ... test logic ... } } #### How to Identify Required Stubs: -1. Look for `Task { }` blocks in the type's initializer -2. Trace what properties/methods those tasks access on dependencies -3. Initialize stubs for all accessed properties/methods in test `init()` -4. Run tests - crashes will identify any missed stubs + 1. Look for `Task { }` blocks in the type's initializer + 2. Trace what properties/functions those tasks access on dependencies + 3. Initialize stubs for all accessed properties/functions in test `init()` + 4. Run tests — crashes will identify any missed stubs + ### 3. Prologue and Epilogue Closures for Execution Control @@ -294,18 +294,18 @@ epilogue closures that execute before and after the stub. Prologues execute before the stub is called: - final class MockAnalyticsClient: AnalyticsClient { - nonisolated(unsafe) var sendEventsPrologue: (() async throws -> Void)? - nonisolated(unsafe) var sendEventsStub: ThrowingStub< - [Event], + final class MockNetworkClient: NetworkClient { + nonisolated(unsafe) var sendRequestPrologue: (() async throws -> Void)? + nonisolated(unsafe) var sendRequestStub: ThrowingStub< + URLRequest, Response, any Error >! - func sendEvents(_ events: [Event]) async throws -> Response { - try await sendEventsPrologue?() - return try sendEventsStub(events) + func sendRequest(_ request: URLRequest) async throws -> Response { + try await sendRequestPrologue?() + return try sendRequestStub(request) } } @@ -313,12 +313,12 @@ Prologues execute before the stub is called: Epilogues execute after the stub is called. Run the epilogue in a `Task` within a `defer` block: - final class MockTelemetryEventLogger: TelemetryEventLogging { + final class MockEventLogger: EventLogging { nonisolated(unsafe) var logEventStub: Stub! nonisolated(unsafe) var logEventEpilogue: (() async throws -> Void)? - func logEvent(_ event: some TelemetryEvent) { + func logEvent(_ event: some Event) { defer { if let epilogue = logEventEpilogue { Task { try? await epilogue() } @@ -346,10 +346,10 @@ Epilogues execute after the stub is called. Run the epilogue in a `Task` within #### Example: Blocking with AsyncStream let (signalStream, signaler) = AsyncStream.makeStream() - mockClient.sendEventsPrologue = { + mockClient.sendRequestPrologue = { await signalStream.first(where: { _ in true }) } - mockClient.sendEventsStub = ThrowingStub(defaultResult: .success(.init())) + mockClient.sendRequestStub = ThrowingStub(defaultReturnValue: .init()) // Start operation that calls the mock instance.performAction() @@ -362,11 +362,11 @@ Epilogues execute after the stub is called. Run the epilogue in a `Task` within #### Example: Signaling Completion with Epilogue - let telemetryLogger = MockTelemetryEventLogger() - telemetryLogger.logEventStub = Stub() + let eventLogger = MockEventLogger() + eventLogger.logEventStub = Stub() let (signalStream, signaler) = AsyncStream.makeStream() - telemetryLogger.logEventEpilogue = { + eventLogger.logEventEpilogue = { signaler.yield() } @@ -377,11 +377,11 @@ Epilogues execute after the stub is called. Run the epilogue in a `Task` within await signalStream.first { _ in true } // Verify the mock was called - #expect(telemetryLogger.logEventStub.calls.count == 1) + #expect(eventLogger.logEventStub.calls.count == 1) #### Example: Adding Delays - mockClient.sendEventsPrologue = { + mockClient.sendRequestPrologue = { try await Task.sleep(for: .milliseconds(100)) } @@ -391,7 +391,7 @@ Epilogues execute after the stub is called. Run the epilogue in a `Task` within - Enables testing at different execution phases (before/after stub) - More precise than arbitrary `Task.sleep()` delays in tests - Eliminates race conditions from timing-based coordination - - Optional - tests can ignore if timing control isn't needed + - Optional — tests can ignore if timing control isn't needed ### 4. Protocol Imports with @testable @@ -465,7 +465,7 @@ Import protocols under test with `@testable` when accessing internal details: 1. **Always configure stubs**: Force-unwrapped stubs will crash if not configured 2. **Use argument structures**: Simplifies complex parameter verification - 3. **Leverage DevTesting**: Use the package’s call tracking and verification capabilities + 3. **Leverage DevTesting**: Use the package's call tracking and verification capabilities 4. **Keep mocks simple**: Avoid complex logic in mock implementations 5. **Group related mocks**: Place mocks in appropriate Testing Support directories 6. **Follow naming conventions**: Consistent naming improves maintainability @@ -478,6 +478,6 @@ All mocks use `nonisolated(unsafe)` markings for Swift 6 compatibility. This ass - Tests run on a single thread or properly synchronize access - Stub configuration happens during test setup before concurrent access - - Mock usage patterns don’t require additional synchronization + - Mock usage patterns don't require additional synchronization When mocking concurrent code, consider additional synchronization mechanisms if needed. diff --git a/Documentation/TestingGuidelines.md b/Documentation/TestingGuidelines.md index b76dd9d..53e7773 100644 --- a/Documentation/TestingGuidelines.md +++ b/Documentation/TestingGuidelines.md @@ -3,6 +3,7 @@ This file provides specific guidance for Claude Code when creating, updating, and maintaining tests in this repository. + ## Swift Testing Framework **IMPORTANT**: This project uses **Swift Testing framework**, NOT XCTest. Do not apply XCTest @@ -68,7 +69,7 @@ differ from regular `Stub`. Using incorrect initializers will cause compilation // For error cases: ThrowingStub(defaultError: error) - // For void return types that not throw: + // For void return types that don't throw: ThrowingStub(defaultError: nil) #### Common Mistakes to Avoid: @@ -80,7 +81,8 @@ differ from regular `Stub`. Using incorrect initializers will cause compilation Follow established patterns from `@Documentation/TestMocks.md`: - - **Stub-based architecture**: Use `Stub` and `ThrowingStub` + - **Stub-based architecture**: Use `Stub` and + `ThrowingStub` - **Thread safety**: Mark stub properties with `nonisolated(unsafe)` - **Protocol conformance**: Mock the protocol, not the concrete implementation - **Argument structures**: For complex parameters, create dedicated argument structures @@ -94,10 +96,10 @@ Example mock structure: functionStub(input) } - nonisolated(unsafe) var throwingMethodStub: ThrowingStub! + nonisolated(unsafe) var throwingFunctionStub: ThrowingStub! - func throwingMethod(input: InputType) throws -> OutputType { - try throwingMethodStub(input) + func throwingFunction(input: InputType) throws -> OutputType { + try throwingFunctionStub(input) } } @@ -133,8 +135,7 @@ Example mock structure: generation - **Centralized functions**: Move random value creation functions to these dedicated extension files - - **Consistent patterns**: Follow existing patterns from other modules (e.g., - `RandomValueGenerating+AppPlatform.swift`) + - **Consistent patterns**: Follow existing patterns from other modules - **Proper imports**: Include necessary `@testable import` statements for modules being extended @@ -153,6 +154,7 @@ Example structure: } } + ## File Organization ### Test Files @@ -181,6 +183,7 @@ Example structure: - **Regular imports**: Use regular imports for testing frameworks and utilities - **Specific imports**: Import only what's needed to keep dependencies clear + ## Test Coverage Guidelines ### Protocols @@ -216,6 +219,94 @@ otherwise you'll get "Errors thrown from here are not handled" compilation error } } +### Boolean Expressions in Expectations + +Do not compare boolean expressions to `== true` or `== false`. Use the boolean directly or +negate it with `!`. The exception is when the expression is an optional `Bool?`, where the +comparison is needed to unwrap and disambiguate the value. + + // ✅ Good + #expect(instance.isLoading) + #expect(!instance.isLoading) + + // ✅ Good - optional Bool? requires comparison + #expect(instance.optionalFlag == true) + #expect(instance.optionalFlag == false) + + // ❌ Bad - unnecessary comparison + #expect(instance.isLoading == true) + #expect(instance.isLoading == false) + +### Use `#require` Instead of Optional Chaining + +When accessing optional values in tests, use `#require` to unwrap them rather than optional +chaining. This ensures the test fails with a clear message at the point of unwrapping rather +than silently skipping assertions. + + // ✅ Good - fails clearly if nil + let value = try #require(instance.optionalProperty) + #expect(value.name == "expected") + + // ❌ Bad - silently passes if nil + #expect(instance.optionalProperty?.name == "expected") + +### Safe Array Access in Tests + +Never subscript arrays directly based on a prior `#expect` count check. If the expectation +fails, the test continues and the subscript will crash. Use `#require` or a guard to safely +verify bounds before indexing. + + // ✅ Good - test fails safely if count is wrong + #expect(items.count == 2) + let first = try #require(items.first) + #expect(first.name == "expected") + + // ✅ Good - compare mapped arrays to avoid indexing entirely + #expect(items.map(\.name) == ["expected", "other"]) + + // ✅ Good - also safe with explicit bounds check + guard items.count == 2 else { + Issue.record("Expected 2 items, got \(items.count)") + return + } + #expect(items[0].name == "expected") + #expect(items[1].name == "other") + + // ❌ Bad - crashes if count expectation fails + #expect(items.count == 2) + #expect(items[0].name == "expected") + #expect(items[1].name == "other") + +### Unconditional Test Failures + +When a code path should never be reached in a passing test (e.g., a pattern match fails), use +`Issue.record(...)` to record an unconditional failure rather than `#expect(Bool(false), ...)`. +`Issue.record` is the Swift Testing idiomatic API for this, and it produces cleaner failure +output. + +#### Correct Pattern: + + guard case .someCase(let value) = result else { + Issue.record("Expected .someCase, got \(result)") + return + } + #expect(value == expectedValue) + +#### Common Mistake to Avoid: + + // ❌ Avoid - not idiomatic Swift Testing + guard case .someCase(let value) = result else { + #expect(Bool(false), "Expected .someCase, got \(result)") + return + } + +#### Key Points: + + - **Import Testing**: `Issue.record` is part of the `Testing` module — ensure it is imported + - **Provide a descriptive message**: Include what was expected and what was received + - **Use `return` after recording**: Since `Issue.record` does not stop execution, always + return explicitly to prevent further test code from running with invalid state + ### Main Actor Considerations - **Test isolation**: Mark test structs and functions with `@MainActor` when testing MainActor-isolated code @@ -229,11 +320,85 @@ otherwise you'll get "Errors thrown from here are not handled" compilation error - **State verification**: Check both mock call counts and state changes in the system under test + +## Reducing Repetition in Tests + +### Shared Setup in `init` + +When most tests in a struct need the same mocks or dependencies, create them in the struct's +`init` rather than repeating the setup in every test. This keeps individual tests focused on +what makes them unique. + + @MainActor + struct ItemProcessorTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + let mockService: MockService + let mockDelegate: MockDelegate + let processor: ItemProcessor + + init() { + mockService = MockService() + mockService.fetchItemsStub = ThrowingStub(defaultReturnValue: []) + + mockDelegate = MockDelegate() + mockDelegate.didUpdateStub = Stub() + + processor = ItemProcessor( + dependencies: .init(service: mockService), + delegate: mockDelegate + ) + } + + @Test + func loadItemsCallsService() async throws { + // exercise the processor — no setup needed, init handled it + await processor.loadItems() + + // expect the service was called + #expect(mockService.fetchItemsStub.calls.count == 1) + } + + @Test + func loadItemsWithCustomData() async throws { + // set up only what differs from the default + let items = [Item(name: "Custom")] + mockService.fetchItemsStub = ThrowingStub(defaultReturnValue: items) + + // exercise + await processor.loadItems() + + // expect + #expect(processor.items.map(\.name) == ["Custom"]) + } + } + +### Helper Functions for Common Test Objects + +When multiple tests construct similar objects with minor variations, extract a helper function. +Keep helpers private to the test file and give them clear names. + + extension ItemProcessorTests { + private func makeItem( + name: String = "Default", + price: Float64 = 9.99 + ) -> Item { + Item(name: name, price: price) + } + } + +### Keep Test-Specific Setup in the Test + +Shared setup should cover the common baseline. Anything unique to a specific test scenario +belongs in that test's "set up" section so readers can see the full context without jumping +to `init` or helpers. + + ## Common Testing Patterns **IMPORTANT**: Avoid using `Task.sleep()` for test coordination whenever possible. Instead, use -precise synchronization mechanisms like `AsyncStream`, `confirmation`, mock prologues/epilogues, or -returning tasks. Arbitrary delays make tests slower and less reliable. +precise synchronization mechanisms like `AsyncStream`, `confirmation`, mock prologues/epilogues, +or returning tasks. Arbitrary delays make tests slower and less reliable. ### Testing Initialization @@ -275,24 +440,24 @@ returning tasks. Arbitrary delays make tests slower and less reliable. ### Testing Async Operations @Test - mutating func asyncMethodCompletesSuccessfully() async throws { + mutating func asyncFunctionCompletesSuccessfully() async throws { let mock = MockDependency() - mock.asyncMethodStub = Stub(defaultReturnValue: expectedResult) + mock.asyncFunctionStub = Stub(defaultReturnValue: expectedResult) let instance = SystemUnderTest(dependency: mock) let result = await instance.performAsyncAction() #expect(result == expectedResult) - #expect(mock.asyncMethodStub.calls.count == 1) + #expect(mock.asyncFunctionStub.calls.count == 1) } ### Using Confirmation to Verify Callbacks -Swift Testing's `confirmation` API ensures that specific code paths execute. Use it to verify that -callbacks, handlers, or closures are invoked: +Swift Testing's `confirmation` API ensures that specific code paths execute. Use it to verify +that callbacks, handlers, or closures are invoked: @Test - mutating func linkedTextInitializationWithCustomUUID() async throws { + mutating func handlerIsInvoked() async throws { let text = randomBasicLatinString() let customID = randomUUID() @@ -310,7 +475,8 @@ callbacks, handlers, or closures are invoked: **Key points:** - - Call the confirmation callback (e.g., `handlerCalled()`) when the expected code path executes + - Call the confirmation callback (e.g., `handlerCalled()`) when the expected code path + executes - The test will fail if the callback is never invoked - Use `defer` in handlers to ensure confirmation happens even if early returns occur @@ -339,7 +505,7 @@ test expectations rather than arbitrary sleep durations: } // exercise the test by calling the synchronous function - instance.performSynchronousMethod() + instance.performSynchronousFunction() // wait for the asynchronous work to complete await signalStream.first { _ in true } @@ -401,59 +567,34 @@ coordination: ### Testing Observable Type State Changes -**PREFERRED PATTERN**: When testing `@Observable` types that update asynchronously through internal -tasks, use the `Observations` helper from Swift Foundation. This pattern is simpler and more reliable -than manual observation tracking. +**PREFERRED PATTERN**: When testing `@Observable` types that update asynchronously through +internal tasks, use the `Observations` helper from DevFoundation. This pattern is simpler and +more reliable than manual observation tracking. #### Preferred: Using Observations Helper The `Observations` helper directly observes property changes and waits for specific conditions: @Test @MainActor - mutating func navigationTitleReflectsCurrentChatTitle() async throws { - // set up the test with a task that the view model will await and use to update its - // navigationTitle property - let pendingTask = Task { - return messageResponse + mutating func propertyUpdatesAsynchronously() async throws { + // set up the test with a task that the type will await + let pendingTask = Task { + return response } - // exercise: create view model that spawns internal Task observing chat.title - let viewModel = SearchResultsViewModel( + // exercise: create instance that spawns internal Task + let instance = SystemUnderTest( dependencies: dependencies, - pendingMessageResponseTask: pendingTask, - delegate: mockDelegate + pendingTask: pendingTask ) // wait for internal task to process initial state - _ = await Observations({ viewModel.navigationTitle }).first { @Sendable _ in true } + _ = await Observations({ instance.title }).first { @Sendable _ in true } - // expect that navigationTitle was updated by internal task - #expect(viewModel.navigationTitle == "Initial Title") + // expect that title was updated by internal task + #expect(instance.title == "Initial Title") } -#### Why Observations is Preferred: - -**❌ Wrong Pattern - AsyncStream Signaler:** - -This pattern doesn't actually observe state changes. It only continues the pending task but doesn't -ensure the view model's internal observation task has completed its work: - - let (signalStream, signaler) = AsyncStream.makeStream() - let pendingTask = Task { - await signalStream.first { _ in true } - return response - } - // ... create view model ... - signaler.yield() // ❌ Only unblocks pendingTask, doesn't observe state change - -**✅ Correct Pattern - Observations:** - -This pattern actually observes the property and waits for the specific condition to be true: - - _ = await Observations({ viewModel.navigationTitle }).first { @Sendable title in - title == expectedValue - } // ✅ Waits for actual state change - #### Key Points for Observations Pattern: - **Use for internal async tasks**: When the type spawns internal `Task`s that update state @@ -461,14 +602,6 @@ This pattern actually observes the property and waits for the specific condition - **Wait for specific conditions**: Use `.first { condition }` to wait for expected state - **Mark closure as `@Sendable`**: Required for concurrency safety - **Simpler than `withObservationTracking`**: No need for manual stream coordination - - **More reliable than signalers**: Actually observes state changes rather than just unblocking tasks - -#### When to Use Each Pattern: - - - **`Observations` helper**: Testing `@Observable` types with internal async state updates (preferred) - - **`withObservationTracking`**: When you need manual control over observation lifecycle - - **`AsyncStream` with signaler**: Testing synchronous interfaces with async work (event bus, etc.) - - **Returned tasks**: When possible, return tasks from functions for direct awaiting #### Alternative: Manual Observation Tracking @@ -512,20 +645,20 @@ When you need manual control over the observation lifecycle, use `withObservatio ### Testing with Mock Prologue and Epilogue Closures -When testing code that calls mock functions, you can use prologue and epilogue closures to control -execution timing. Prologues execute before the stub, epilogues execute after. See +When testing code that calls mock functions, you can use prologue and epilogue closures to +control execution timing. Prologues execute before the stub, epilogues execute after. See `@Documentation/TestMocks.md` for the mock implementation patterns. #### Pattern: Blocking Mock Execution @Test - mutating func testIntermediateStateWhileSending() async throws { + mutating func testIntermediateStateWhileProcessing() async throws { // set up the test with a blocking prologue let (signalStream, signaler) = AsyncStream.makeStream() - mockClient.sendEventsPrologue = { + mockClient.sendRequestPrologue = { await signalStream.first(where: { _ in true }) } - mockClient.sendEventsStub = ThrowingStub(defaultResult: .success(.init())) + mockClient.sendRequestStub = ThrowingStub(defaultReturnValue: .init()) let instance = SystemUnderTest(client: mockClient) @@ -534,49 +667,21 @@ execution timing. Prologues execute before the stub, epilogues execute after. Se // expect intermediate state while mock is blocked await #expect(instance.isProcessing) - await #expect(instance.queuedItems.count == 5) // signal completion to unblock the mock signaler.yield() - - // allow async processing to complete - try await Task.sleep(for: .milliseconds(100)) - - // expect final state after mock completes - await #expect(!instance.isProcessing) - await #expect(instance.queuedItems.isEmpty) - } - -#### Pattern: Adding Controlled Delays - - @Test - mutating func testTimeoutBehavior() async throws { - // set up the test with a delay in the mock - mockClient.sendEventsPrologue = { - try await Task.sleep(for: .milliseconds(200)) - } - mockClient.sendEventsStub = ThrowingStub(defaultResult: .success(.init())) - - let instance = SystemUnderTest(client: mockClient, timeout: .milliseconds(100)) - - // exercise the test - instance.performActionWithTimeout() - - // expect timeout occurred before mock completed - try await Task.sleep(for: .milliseconds(150)) - await #expect(instance.didTimeout) } #### Pattern: Signaling Completion with Epilogue @Test - mutating func testEventHandlerLogsToTelemetry() async throws { + mutating func eventHandlerLogsToTelemetry() async throws { // set up the test with epilogue for coordination - let telemetryLogger = MockTelemetryEventLogger() - telemetryLogger.logEventStub = Stub() + let eventLogger = MockEventLogger() + eventLogger.logEventStub = Stub() let (signalStream, signaler) = AsyncStream.makeStream() - telemetryLogger.logEventEpilogue = { + eventLogger.logEventEpilogue = { signaler.yield() } @@ -587,7 +692,7 @@ execution timing. Prologues execute before the stub, epilogues execute after. Se await signalStream.first { _ in true } // expect the event was logged - #expect(telemetryLogger.logEventStub.calls.count == 1) + #expect(eventLogger.logEventStub.calls.count == 1) } #### Key Points diff --git a/README.md b/README.md index 444b9f9..1d31833 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ SwiftUI views do not currently have automated tests. To set up the development environment: - 1. Run `Scripts/install-git-hooks` to install pre-commit hooks that automatically check + 1. Run `Scripts/install-git-hooks` to install pre-push hooks that automatically check code formatting. 2. Use `Scripts/lint` to manually check code formatting at any time. 3. Use `Scripts/format` to automatically format code. diff --git a/Scripts/install-git-hooks b/Scripts/install-git-hooks index 670ea93..56d6935 100755 --- a/Scripts/install-git-hooks +++ b/Scripts/install-git-hooks @@ -6,45 +6,51 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Go to the repository root (one level up from Scripts) REPO_ROOT="$(dirname "$SCRIPT_DIR")" -# Check if we're in a git repository -if [ ! -d "$REPO_ROOT/.git" ]; then +# Check if we're in a git repository and get the git directory +# This works for both regular repos and worktrees +cd "$REPO_ROOT" +if ! git rev-parse --git-dir > /dev/null 2>&1; then echo "Error: Not in a git repository" exit 1 fi -mkdir -p "$REPO_ROOT/.git/hooks" +# Get the common git directory (shared across all worktrees) +GIT_COMMON_DIR="$(git rev-parse --git-common-dir)" +if [ ! -d "$GIT_COMMON_DIR" ]; then + echo "Error: Could not find git directory" + exit 1 +fi -# Function to install the pre-commit hook -install_pre_commit_hook() { - local pre_commit_hook="$REPO_ROOT/.git/hooks/pre-commit" +mkdir -p "$GIT_COMMON_DIR/hooks" - echo "Installing pre-commit hook..." +# Function to install the pre-push hook +install_pre_push_hook() { + local pre_push_hook="$GIT_COMMON_DIR/hooks/pre-push" - cat > "$pre_commit_hook" << 'EOF' -#!/bin/bash + echo "Installing pre-push hook..." -# Get the directory where this hook is located -HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + cat > "$pre_push_hook" << 'EOF' +#!/bin/bash -# Go to the repository root (two levels up from .git/hooks) -REPO_ROOT="$(dirname "$(dirname "$HOOK_DIR")")" +# Resolve the repository root using git, which correctly handles worktrees +REPO_ROOT="$(git rev-parse --show-toplevel)" # Run the lint script echo "Running lint check..." if ! "$REPO_ROOT/Scripts/lint"; then - echo "Lint check failed. Please fix formatting issues before committing." + echo "Lint check failed. Please fix formatting issues before pushing." exit 1 fi echo "Lint check passed." EOF - chmod +x "$pre_commit_hook" - echo "Pre-commit hook installed successfully!" + chmod +x "$pre_push_hook" + echo "Pre-push hook installed successfully!" } -# Install the pre-commit hook -install_pre_commit_hook +# Install the pre-push hook +install_pre_push_hook echo "All git hooks installed successfully!" -echo "The pre-commit hook will run 'Scripts/lint' before each commit." +echo "The pre-push hook will run 'Scripts/lint' before each push." diff --git a/Sources/DevConfiguration/Access Reporting/EventBusAccessReporter.swift b/Sources/DevConfiguration/Access Reporting/EventBusAccessReporter.swift index e589573..af5c8da 100644 --- a/Sources/DevConfiguration/Access Reporting/EventBusAccessReporter.swift +++ b/Sources/DevConfiguration/Access Reporting/EventBusAccessReporter.swift @@ -35,7 +35,7 @@ public struct EventBusAccessReporter: AccessReporter { ConfigVariableAccessSucceededEvent( key: event.metadata.key, value: configValue, - providerName: event.providerResults.first?.providerName + providerName: event.providerResults.first?.providerName, ) ) @@ -43,7 +43,7 @@ public struct EventBusAccessReporter: AccessReporter { eventBus.post( ConfigVariableAccessFailedEvent( key: event.metadata.key, - error: MissingValueError() + error: MissingValueError(), ) ) @@ -51,7 +51,7 @@ public struct EventBusAccessReporter: AccessReporter { eventBus.post( ConfigVariableAccessFailedEvent( key: event.metadata.key, - error: error + error: error, ) ) } diff --git a/Sources/DevConfiguration/Core/CodableValueRepresentation.swift b/Sources/DevConfiguration/Core/CodableValueRepresentation.swift index 3071f4c..e6a103a 100644 --- a/Sources/DevConfiguration/Core/CodableValueRepresentation.swift +++ b/Sources/DevConfiguration/Core/CodableValueRepresentation.swift @@ -75,7 +75,7 @@ public struct CodableValueRepresentation: Sendable { forKey key: ConfigKey, isSecret: Bool, fileID: String, - line: UInt + line: UInt, ) -> Data? { switch kind { case .string(let encoding): @@ -104,7 +104,7 @@ public struct CodableValueRepresentation: Sendable { forKey key: ConfigKey, isSecret: Bool, fileID: String, - line: UInt + line: UInt, ) async throws -> Data? { switch kind { case .string(let encoding): @@ -181,7 +181,7 @@ public struct CodableValueRepresentation: Sendable { isSecret: Bool, fileID: String, line: UInt, - onUpdate: @Sendable (Data?) -> Void + onUpdate: @Sendable (Data?) -> Void, ) async throws { switch kind { case .string(let encoding): diff --git a/Sources/DevConfiguration/Core/ConfigVariable.swift b/Sources/DevConfiguration/Core/ConfigVariable.swift index 26d7f44..2acddb2 100644 --- a/Sources/DevConfiguration/Core/ConfigVariable.swift +++ b/Sources/DevConfiguration/Core/ConfigVariable.swift @@ -80,7 +80,7 @@ public struct ConfigVariable: Sendable where Value: Sendable { /// - Returns: A copy of the `ConfigVariable` with the metadata value applied. public func metadata( _ keyPath: WritableKeyPath, - _ value: MetadataValue + _ value: MetadataValue, ) -> Self { var copy = self copy.metadata[keyPath: keyPath] = value @@ -296,7 +296,7 @@ extension ConfigVariable { key: key, defaultValue: defaultValue, content: .rawRepresentableCaseIterableString(), - isSecret: isSecret + isSecret: isSecret, ) } } @@ -382,7 +382,12 @@ extension ConfigVariable { /// - isSecret: Whether this variable's value should be treated as secret. Defaults to `false`. public init(key: ConfigKey, defaultValue: Value, isSecret: Bool = false) where Value: RawRepresentable & CaseIterable & Sendable, Value.RawValue == Int { - self.init(key: key, defaultValue: defaultValue, content: .rawRepresentableCaseIterableInt(), isSecret: isSecret) + self.init( + key: key, + defaultValue: defaultValue, + content: .rawRepresentableCaseIterableInt(), + isSecret: isSecret, + ) } } diff --git a/Sources/DevConfiguration/Core/ConfigVariableContent.swift b/Sources/DevConfiguration/Core/ConfigVariableContent.swift index 3a1bec5..989b8ab 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableContent.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableContent.swift @@ -98,7 +98,7 @@ extension ConfigVariableContent where Value == Bool { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -107,7 +107,7 @@ extension ConfigVariableContent where Value == Bool { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -117,7 +117,7 @@ extension ConfigVariableContent where Value == Bool { encode: { .bool($0) }, editorControl: .toggle, parse: { Bool($0).map { .bool($0) } }, - validate: nil + validate: nil, ) } } @@ -136,7 +136,7 @@ extension ConfigVariableContent where Value == [Bool] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -145,7 +145,7 @@ extension ConfigVariableContent where Value == [Bool] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -165,7 +165,7 @@ extension ConfigVariableContent where Value == [Bool] { } return .boolArray(values) }, - validate: nil + validate: nil, ) } } @@ -184,7 +184,7 @@ extension ConfigVariableContent where Value == Float64 { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -193,7 +193,7 @@ extension ConfigVariableContent where Value == Float64 { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -203,7 +203,7 @@ extension ConfigVariableContent where Value == Float64 { encode: { .double($0) }, editorControl: .decimalField, parse: { (try? Float64($0, format: .number, lenient: false)).map { .double($0) } }, - validate: nil + validate: nil, ) } } @@ -222,7 +222,7 @@ extension ConfigVariableContent where Value == [Float64] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -231,7 +231,7 @@ extension ConfigVariableContent where Value == [Float64] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -251,7 +251,7 @@ extension ConfigVariableContent where Value == [Float64] { } return .doubleArray(values) }, - validate: nil + validate: nil, ) } } @@ -270,7 +270,7 @@ extension ConfigVariableContent where Value == Int { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -279,7 +279,7 @@ extension ConfigVariableContent where Value == Int { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -297,7 +297,7 @@ extension ConfigVariableContent where Value == Int { } return .int(int) }, - validate: nil + validate: nil, ) } } @@ -316,7 +316,7 @@ extension ConfigVariableContent where Value == [Int] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -325,7 +325,7 @@ extension ConfigVariableContent where Value == [Int] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -348,7 +348,7 @@ extension ConfigVariableContent where Value == [Int] { } return .intArray(values) }, - validate: nil + validate: nil, ) } } @@ -367,7 +367,7 @@ extension ConfigVariableContent where Value == String { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -376,7 +376,7 @@ extension ConfigVariableContent where Value == String { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -386,7 +386,7 @@ extension ConfigVariableContent where Value == String { encode: { .string($0) }, editorControl: .textField, parse: { .string($0) }, - validate: nil + validate: nil, ) } } @@ -405,7 +405,7 @@ extension ConfigVariableContent where Value == [String] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -414,7 +414,7 @@ extension ConfigVariableContent where Value == [String] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -424,7 +424,7 @@ extension ConfigVariableContent where Value == [String] { encode: { .stringArray($0) }, editorControl: .textEditor, parse: { .stringArray($0.nonEmptyTrimmedLines) }, - validate: nil + validate: nil, ) } } @@ -443,7 +443,7 @@ extension ConfigVariableContent where Value == [UInt8] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -452,7 +452,7 @@ extension ConfigVariableContent where Value == [UInt8] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -462,7 +462,7 @@ extension ConfigVariableContent where Value == [UInt8] { encode: { .bytes($0) }, editorControl: nil, parse: nil, - validate: nil + validate: nil, ) } } @@ -478,7 +478,7 @@ extension ConfigVariableContent where Value == [[UInt8]] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -487,7 +487,7 @@ extension ConfigVariableContent where Value == [[UInt8]] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -496,7 +496,7 @@ extension ConfigVariableContent where Value == [[UInt8]] { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -506,7 +506,7 @@ extension ConfigVariableContent where Value == [[UInt8]] { encode: { .byteChunkArray($0) }, editorControl: nil, parse: nil, - validate: nil + validate: nil, ) } } @@ -526,7 +526,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -536,7 +536,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -546,7 +546,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -561,7 +561,7 @@ extension ConfigVariableContent { return false } return Value(rawValue: rawValue) != nil - } + }, ) } @@ -579,7 +579,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -589,7 +589,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -599,7 +599,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -611,12 +611,12 @@ extension ConfigVariableContent { options: Value.allCases.map { .init( label: $0.rawValue, - content: .string($0.rawValue) + content: .string($0.rawValue), ) } ), parse: nil, - validate: nil + validate: nil, ) } @@ -632,7 +632,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -642,7 +642,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -652,7 +652,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -667,7 +667,7 @@ extension ConfigVariableContent { return false } return strings.allSatisfy { Element(rawValue: $0) != nil } - } + }, ) } @@ -682,7 +682,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -692,7 +692,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -702,7 +702,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -717,7 +717,7 @@ extension ConfigVariableContent { return false } return Value(configString: string) != nil - } + }, ) } @@ -733,7 +733,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -743,7 +743,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -753,7 +753,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -768,7 +768,7 @@ extension ConfigVariableContent { return false } return strings.allSatisfy { Element(configString: $0) != nil } - } + }, ) } } @@ -788,7 +788,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -798,7 +798,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -808,7 +808,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -831,7 +831,7 @@ extension ConfigVariableContent { return false } return Value(rawValue: rawValue) != nil - } + }, ) } @@ -849,7 +849,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -859,7 +859,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -869,7 +869,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -881,12 +881,12 @@ extension ConfigVariableContent { options: Value.allCases.map { (value) in .init( label: "\(String(describing: value)) (\(value.rawValue))", - content: .int(value.rawValue) + content: .int(value.rawValue), ) } ), parse: nil, - validate: nil + validate: nil, ) } @@ -902,7 +902,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -912,7 +912,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -922,7 +922,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -950,7 +950,7 @@ extension ConfigVariableContent { return false } return ints.allSatisfy { Element(rawValue: $0) != nil } - } + }, ) } @@ -965,7 +965,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -975,7 +975,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -985,7 +985,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -1008,7 +1008,7 @@ extension ConfigVariableContent { return false } return Value(configInt: int) != nil - } + }, ) } @@ -1024,7 +1024,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, fetch: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -1034,7 +1034,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) }, startWatching: { (reader, key, isSecret, defaultValue, eventBus, fileID, line, continuation) in @@ -1044,7 +1044,7 @@ extension ConfigVariableContent { isSecret: isSecret, default: defaultValue, fileID: fileID, - line: line + line: line, ) { updates in for await value in updates { continuation.yield(value) @@ -1072,7 +1072,7 @@ extension ConfigVariableContent { return false } return ints.allSatisfy { Element(configInt: $0) != nil } - } + }, ) } } @@ -1090,14 +1090,14 @@ extension ConfigVariableContent { public static func json( representation: CodableValueRepresentation = .string(), decoder: JSONDecoder? = nil, - encoder: JSONEncoder? = nil + encoder: JSONEncoder? = nil, ) -> ConfigVariableContent where Value: Codable { return codable( representation: representation, decoder: decoder as (any TopLevelDecoder & Sendable)?, encoder: encoder as (any TopLevelEncoder & Sendable)?, editorControl: representation.supportsTextEditing ? .textEditor : nil, - parse: representation.supportsTextEditing ? { @Sendable in ConfigContent.string($0) } : nil + parse: representation.supportsTextEditing ? { @Sendable in ConfigContent.string($0) } : nil, ) } @@ -1111,14 +1111,14 @@ extension ConfigVariableContent { public static func propertyList( representation: CodableValueRepresentation = .data, decoder: PropertyListDecoder? = nil, - encoder: PropertyListEncoder? = nil + encoder: PropertyListEncoder? = nil, ) -> ConfigVariableContent where Value: Codable { codable( representation: representation, decoder: decoder as (any TopLevelDecoder & Sendable)?, encoder: encoder as (any TopLevelEncoder & Sendable)?, editorControl: nil, - parse: nil + parse: nil, ) } @@ -1129,7 +1129,7 @@ extension ConfigVariableContent { decoder: (any TopLevelDecoder & Sendable)?, encoder: (any TopLevelEncoder & Sendable)?, editorControl: EditorControl?, - parse: (@Sendable (_ input: String) -> ConfigContent?)? + parse: (@Sendable (_ input: String) -> ConfigContent?)?, ) -> ConfigVariableContent where Value: Codable { ConfigVariableContent( read: { (reader, key, isSecret, defaultValue, eventBus, fileID, line) in @@ -1139,7 +1139,7 @@ extension ConfigVariableContent { forKey: key, isSecret: isSecret, fileID: fileID, - line: line + line: line, ) else { return defaultValue @@ -1153,7 +1153,7 @@ extension ConfigVariableContent { ConfigVariableDecodingFailedEvent( key: AbsoluteConfigKey(key), targetType: Value.self, - error: error + error: error, ) ) return defaultValue @@ -1166,7 +1166,7 @@ extension ConfigVariableContent { forKey: key, isSecret: isSecret, fileID: fileID, - line: line + line: line, ) else { return defaultValue @@ -1180,7 +1180,7 @@ extension ConfigVariableContent { ConfigVariableDecodingFailedEvent( key: AbsoluteConfigKey(key), targetType: Value.self, - error: error + error: error, ) ) return defaultValue @@ -1194,7 +1194,7 @@ extension ConfigVariableContent { forKey: key, isSecret: isSecret, fileID: fileID, - line: line + line: line, ) { data in if let data { do { @@ -1205,7 +1205,7 @@ extension ConfigVariableContent { ConfigVariableDecodingFailedEvent( key: AbsoluteConfigKey(key), targetType: Value.self, - error: error + error: error, ) ) } @@ -1226,7 +1226,7 @@ extension ConfigVariableContent { return false } return (try? resolvedDecoder.decode(Value.self, from: data)) != nil - } + }, ) } } diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader.swift b/Sources/DevConfiguration/Core/ConfigVariableReader.swift index a24f0ec..f8d72a4 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableReader.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableReader.swift @@ -86,13 +86,13 @@ public final class ConfigVariableReader: Sendable { public convenience init( namedProviders: [NamedConfigProvider], eventBus: EventBus, - isEditorEnabled: Bool = false + isEditorEnabled: Bool = false, ) { self.init( namedProviders: namedProviders, accessReporter: EventBusAccessReporter(eventBus: eventBus), eventBus: eventBus, - isEditorEnabled: isEditorEnabled + isEditorEnabled: isEditorEnabled, ) } @@ -110,7 +110,7 @@ public final class ConfigVariableReader: Sendable { namedProviders: [NamedConfigProvider], accessReporter: any AccessReporter, eventBus: EventBus, - isEditorEnabled: Bool = false + isEditorEnabled: Bool = false, ) { var editorOverrideProvider: EditorOverrideProvider? var namedProviders = namedProviders @@ -127,7 +127,7 @@ public final class ConfigVariableReader: Sendable { self.accessReporter = accessReporter self.reader = ConfigReader( providers: namedProviders.map(\.provider), - accessReporter: accessReporter + accessReporter: accessReporter, ) self.namedProviders = namedProviders self.eventBus = eventBus @@ -178,7 +178,7 @@ extension ConfigVariableReader { destinationTypeName: String(describing: Value.self), editorControl: variable.content.editorControl, parse: variable.content.parse, - validate: variable.content.validate + validate: variable.content.validate, ) } } @@ -198,7 +198,7 @@ extension ConfigVariableReader { public func value( for variable: ConfigVariable, fileID: String = #fileID, - line: UInt = #line + line: UInt = #line, ) -> Value { variable.content.read(reader, variable.key, variable.isSecret, variable.defaultValue, eventBus, fileID, line) } @@ -214,7 +214,7 @@ extension ConfigVariableReader { public subscript( variable: ConfigVariable, fileID fileID: String = #fileID, - line line: UInt = #line + line line: UInt = #line, ) -> Value { value(for: variable, fileID: fileID, line: line) } @@ -230,7 +230,7 @@ extension ConfigVariableReader { public func fetchValue( for variable: ConfigVariable, fileID: String = #fileID, - line: UInt = #line + line: UInt = #line, ) async throws -> Value { try await variable.content.fetch( reader, @@ -239,7 +239,7 @@ extension ConfigVariableReader { variable.defaultValue, eventBus, fileID, - line + line, ) } @@ -259,7 +259,7 @@ extension ConfigVariableReader { for variable: ConfigVariable, fileID: String = #fileID, line: UInt = #line, - updatesHandler: @Sendable @escaping (AsyncStream) async throws -> Return + updatesHandler: @Sendable @escaping (AsyncStream) async throws -> Return, ) async throws -> Return where Return: Sendable { // Capture these locally so that the @Sendable task closures below don’t need to capture `self`. let configReader = reader @@ -285,7 +285,7 @@ extension ConfigVariableReader { eventBus, fileID, line, - continuation + continuation, ) return nil } diff --git a/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift index 2797213..e0fb7ac 100644 --- a/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift +++ b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift @@ -78,7 +78,7 @@ public struct RegisteredConfigVariable: Sendable { destinationTypeName: String, editorControl: EditorControl?, parse: (@Sendable (_ input: String) -> ConfigContent?)?, - validate: (@Sendable (_ content: ConfigContent) -> Bool)? + validate: (@Sendable (_ content: ConfigContent) -> Bool)?, ) { self.key = key self.defaultContent = defaultContent @@ -159,7 +159,7 @@ public struct RegisteredConfigVariable: Sendable { /// Finds the index of the closing `>` that matches the opening `<` whose content starts at `startIndex`. private static func findMatchingClosingAngleBracket( in string: String, - from startIndex: String.Index + from startIndex: String.Index, ) -> String.Index? { var depth = 1 var index = startIndex diff --git a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift index 1ac8267..8f54aa2 100644 --- a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift +++ b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift @@ -99,7 +99,7 @@ extension ConfigVariableDetailView { Spacer().layoutPriority(0) Picker( localizedStringResource("detailView.overrideSection.valuePicker"), - selection: $viewModel.overrideBool + selection: $viewModel.overrideBool, ) { Text(localized: "detailView.overridenSection.valuePickerFalse").tag(false) Text(localized: "detailView.overridenSection.valuePickerTrue").tag(true) @@ -110,7 +110,7 @@ extension ConfigVariableDetailView { case .picker(options: let pickerOptions): Picker( localizedStringResource("detailView.overrideSection.valuePicker"), - selection: $viewModel.overridePickerSelection + selection: $viewModel.overridePickerSelection, ) { ForEach(pickerOptions, id: \.content) { option in Text(option.label).tag(option.content) @@ -145,7 +145,7 @@ extension ConfigVariableDetailView { LabeledContent(localizedStringResource("detailView.overrideSection.valueLabel")) { TextField( localizedStringResource("detailView.overrideSection.valueTextField"), - text: $viewModel.overrideText + text: $viewModel.overrideText, ) .onSubmit { viewModel.commitOverrideText() } .textFieldStyle(.plain) @@ -193,7 +193,7 @@ extension ConfigVariableDetailView { ProviderBadge( providerName: providerValue.providerName, color: providerColor(at: providerValue.providerIndex), - isActive: providerValue.isActive + isActive: providerValue.isActive, ) .strikethrough(!providerValue.contentTypeMatches) } diff --git a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift index 47fe671..8513a4b 100644 --- a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift +++ b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift @@ -88,7 +88,7 @@ struct ConfigVariableListView: View { reader: ConfigVariableReader, customSectionTitle: LocalizedStringKey, @ViewBuilder customSection: () -> CustomSection, - onSave: @escaping ([RegisteredConfigVariable]) -> Void + onSave: @escaping ([RegisteredConfigVariable]) -> Void, ) { self.init( reader: reader, customSectionTitle: Text(customSectionTitle), customSection: customSection, - onSave: onSave + onSave: onSave, ) } @@ -78,7 +78,7 @@ public struct ConfigVariableEditor: View { reader: ConfigVariableReader, customSectionTitle: Text, @ViewBuilder customSection: () -> CustomSection, - onSave: @escaping ([RegisteredConfigVariable]) -> Void + onSave: @escaping ([RegisteredConfigVariable]) -> Void, ) { self.customSectionTitle = customSectionTitle self.customSection = customSection() @@ -91,7 +91,7 @@ public struct ConfigVariableEditor: View { ConfigVariableListView( viewModel: viewModel, customSectionTitle: customSectionTitle, - customSection: { customSection } + customSection: { customSection }, ) } } @@ -99,7 +99,7 @@ public struct ConfigVariableEditor: View { private static func makeViewModel( reader: ConfigVariableReader, - onSave: @escaping ([RegisteredConfigVariable]) -> Void + onSave: @escaping ([RegisteredConfigVariable]) -> Void, ) -> State { guard let editorOverrideProvider = reader.editorOverrideProvider else { return State(initialValue: nil) @@ -114,7 +114,7 @@ public struct ConfigVariableEditor: View { workingCopyDisplayName: localizedString("editorOverrideProvider.name"), namedProviders: namedProviders, registeredVariables: Array(reader.registeredVariables.values), - undoManager: UndoManager() + undoManager: UndoManager(), ) return State(initialValue: ConfigVariableListViewModel(document: document, onSave: onSave)) @@ -131,7 +131,7 @@ extension ConfigVariableEditor where CustomSection == EmptyView { /// - onSave: A closure called with the registered variables whose overrides changed when the user saves. public init( reader: ConfigVariableReader, - onSave: @escaping ([RegisteredConfigVariable]) -> Void + onSave: @escaping ([RegisteredConfigVariable]) -> Void, ) { self.customSectionTitle = Text(verbatim: "") self.customSection = EmptyView() diff --git a/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift b/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift index e02c9fb..a9d35a4 100644 --- a/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift +++ b/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift @@ -78,7 +78,7 @@ final class EditorDocument { workingCopyDisplayName: String, namedProviders: [NamedConfigProvider], registeredVariables: [RegisteredConfigVariable], - undoManager: UndoManager + undoManager: UndoManager, ) { self.editorOverrideProvider = editorOverrideProvider self.workingCopyDisplayName = workingCopyDisplayName @@ -107,7 +107,7 @@ final class EditorDocument { ProviderEditorSnapshot( displayName: namedProvider.displayName, index: index, - values: values + values: values, ) ) } @@ -123,7 +123,7 @@ final class EditorDocument { ProviderEditorSnapshot( displayName: localizedString("editor.defaultProviderName"), index: defaultIndex, - values: defaultValues + values: defaultValues, ) ) @@ -151,7 +151,7 @@ extension EditorDocument { /// Starts watching providers for snapshot changes. private func startWatching( namedProviders: [NamedConfigProvider], - registeredVariables: [RegisteredConfigVariable] + registeredVariables: [RegisteredConfigVariable], ) { guard !namedProviders.isEmpty else { return @@ -177,7 +177,7 @@ extension EditorDocument { for variable in registeredVariables { if let content = snapshot.configContent( forKey: variable.key, - preferredType: variable.defaultContent.configType + preferredType: variable.defaultContent.configType, ) { values[variable.key] = content } @@ -228,7 +228,7 @@ extension EditorDocument { return ResolvedValue( content: content, providerDisplayName: workingCopyDisplayName, - providerIndex: nil + providerIndex: nil, ) } @@ -238,7 +238,7 @@ extension EditorDocument { return ResolvedValue( content: content, providerDisplayName: snapshot.displayName, - providerIndex: snapshot.index + providerIndex: snapshot.index, ) } } @@ -272,7 +272,7 @@ extension EditorDocument { providerIndex: nil, isActive: isActive, valueString: content.displayString, - contentTypeMatches: content.configType == expectedType + contentTypeMatches: content.configType == expectedType, ) ) } @@ -287,7 +287,7 @@ extension EditorDocument { providerIndex: snapshot.index, isActive: isActive, valueString: content.displayString, - contentTypeMatches: content.configType == expectedType + contentTypeMatches: content.configType == expectedType, ) ) } diff --git a/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift b/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift index b04a2ac..0ee96d1 100644 --- a/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift +++ b/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift @@ -282,7 +282,7 @@ extension EditorOverrideProvider { private func addValueContinuation( _ continuation: AsyncStream.Continuation, id: UUID, - forKey key: ConfigKey + forKey key: ConfigKey, ) { mutableState.withLock { state in state.valueWatchers[key, default: [:]][id] = continuation @@ -305,7 +305,7 @@ extension EditorOverrideProvider { /// The continuation is immediately yielded the current snapshot. private func addSnapshotContinuation( _ continuation: AsyncStream.Continuation, - id: UUID + id: UUID, ) { mutableState.withLock { state in state.snapshotWatchers[id] = continuation diff --git a/Sources/DevConfiguration/Extensions/ConfigContent+Additions.swift b/Sources/DevConfiguration/Extensions/ConfigContent+Additions.swift index 17aaafb..80e5c2e 100644 --- a/Sources/DevConfiguration/Extensions/ConfigContent+Additions.swift +++ b/Sources/DevConfiguration/Extensions/ConfigContent+Additions.swift @@ -157,7 +157,7 @@ extension ConfigContent: @retroactive Codable { throw DecodingError.dataCorruptedError( forKey: .type, in: container, - debugDescription: "Unknown config type: \(typeString)" + debugDescription: "Unknown config type: \(typeString)", ) } diff --git a/Tests/DevConfigurationTests/Testing Support/RandomValueGenerating+DevConfiguration.swift b/Tests/DevConfigurationTests/Testing Support/RandomValueGenerating+DevConfiguration.swift index 927af92..5deb728 100644 --- a/Tests/DevConfigurationTests/Testing Support/RandomValueGenerating+DevConfiguration.swift +++ b/Tests/DevConfigurationTests/Testing Support/RandomValueGenerating+DevConfiguration.swift @@ -20,7 +20,7 @@ extension RandomValueGenerating { mutating func randomAccessEvent( key: AbsoluteConfigKey? = nil, result: Result? = nil, - providerResults: [AccessEvent.ProviderResult]? = nil + providerResults: [AccessEvent.ProviderResult]? = nil, ) -> AccessEvent { return AccessEvent( metadata: AccessEvent.Metadata( @@ -29,12 +29,12 @@ extension RandomValueGenerating { valueType: .string, sourceLocation: AccessEvent.Metadata.SourceLocation( fileID: randomAlphanumericString(), - line: random(UInt.self, in: .min ... .max) + line: random(UInt.self, in: .min ... .max), ), - accessTimestamp: randomDate() + accessTimestamp: randomDate(), ), providerResults: providerResults ?? [randomProviderResult()], - result: result ?? .success(randomConfigValue()) + result: result ?? .success(randomConfigValue()), ) } @@ -114,7 +114,7 @@ extension RandomValueGenerating { destinationTypeName: String? = nil, editorControl: EditorControl? = nil, parse: (@Sendable (_ input: String) -> ConfigContent?)? = nil, - validate: (@Sendable (_ content: ConfigContent) -> Bool)? = nil + validate: (@Sendable (_ content: ConfigContent) -> Bool)? = nil, ) -> RegisteredConfigVariable { RegisteredConfigVariable( key: key ?? randomConfigKey(), @@ -124,14 +124,14 @@ extension RandomValueGenerating { destinationTypeName: destinationTypeName ?? randomAlphanumericString(), editorControl: editorControl ?? .none, parse: parse, - validate: validate + validate: validate, ) } mutating func randomProviderResult( providerName: String? = nil, - result: Result? = nil + result: Result? = nil, ) -> AccessEvent.ProviderResult { let providerName = providerName ?? randomAlphanumericString() let result = result ?? .success(.init(encodedKey: randomAlphanumericString(), value: randomConfigValue())) diff --git a/Tests/DevConfigurationTests/Unit Tests/Access Reporting/EventBusAccessReporterTests.swift b/Tests/DevConfigurationTests/Unit Tests/Access Reporting/EventBusAccessReporterTests.swift index b8ccec1..c6e7292 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Access Reporting/EventBusAccessReporterTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Access Reporting/EventBusAccessReporterTests.swift @@ -54,7 +54,7 @@ struct EventBusAccessReporterTests: RandomValueGenerating { let accessEvent = randomAccessEvent( key: key, result: .success(configValue), - providerResults: providerResults + providerResults: providerResults, ) // set up a stream to receive the posted event @@ -87,7 +87,7 @@ struct EventBusAccessReporterTests: RandomValueGenerating { let key = randomAbsoluteConfigKey() let accessEvent = randomAccessEvent( key: key, - result: .success(nil) + result: .success(nil), ) // set up a stream to receive the posted event @@ -120,7 +120,7 @@ struct EventBusAccessReporterTests: RandomValueGenerating { let error = randomError() let accessEvent = randomAccessEvent( key: key, - result: .failure(error) + result: .failure(error), ) // set up a stream to receive the posted event diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift index a8bb46a..0d8d7f0 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderArrayTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -93,7 +93,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { provider.setValue( .init(.boolArray(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -159,7 +159,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { provider.setValue( .init(.intArray(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -225,7 +225,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { provider.setValue( .init(.doubleArray(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -291,7 +291,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { provider.setValue( .init(.stringArray(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -357,7 +357,7 @@ struct ConfigVariableReaderArrayTests: RandomValueGenerating { provider.setValue( .init(.byteChunkArray(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift index 268cc76..d1da928 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderCodableTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -172,7 +172,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { provider.setValue( .init(.string(updatedJSON), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -208,7 +208,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) setProviderValue(.string("not valid json"), forKey: key) @@ -230,7 +230,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable(key: key, defaultValue: defaultValue, content: .json()) setProviderValue(.string("not valid json"), forKey: key) @@ -256,11 +256,11 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let validValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let isSecret = randomBool() let provider = provider @@ -277,7 +277,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { provider.setValue( .init(.string(validJSON), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -292,11 +292,11 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let validValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let isSecret = randomBool() let provider = provider @@ -312,7 +312,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { provider.setValue( .init(.string(validJSON), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -329,16 +329,16 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let expectedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) + content: .propertyList(decoder: PropertyListDecoder()), ) let plistData = try! PropertyListEncoder().encode(expectedValue) setProviderValue(.bytes(Array(plistData)), forKey: key) @@ -357,16 +357,16 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let expectedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) + content: .propertyList(decoder: PropertyListDecoder()), ) let plistData = try! PropertyListEncoder().encode(expectedValue) setProviderValue(.bytes(Array(plistData)), forKey: key) @@ -385,22 +385,22 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { let key = randomConfigKey() let initialValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let updatedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let isSecret = randomBool() let provider = provider let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .propertyList(decoder: PropertyListDecoder()) + content: .propertyList(decoder: PropertyListDecoder()), ) let encoder = PropertyListEncoder() let initialPlist = try! encoder.encode(initialValue) @@ -416,7 +416,7 @@ struct ConfigVariableReaderCodableTests: RandomValueGenerating { provider.setValue( .init(.bytes(Array(updatedPlist)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift index 08cb7f2..275ae7b 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderConfigExpressionTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -93,7 +93,7 @@ struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { provider.setValue( .init(.string(updatedValue.description), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -173,7 +173,7 @@ struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { provider.setValue( .init(.stringArray(updatedValue.map(\.description)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -239,7 +239,7 @@ struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { provider.setValue( .init(.int(updatedValue.configInt), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -319,7 +319,7 @@ struct ConfigVariableReaderConfigExpressionTests: RandomValueGenerating { provider.setValue( .init(.intArray(updatedValue.map(\.configInt)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift index c7dd74d..a9036cd 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderDataRepresentationTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -44,16 +44,16 @@ struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { let key = randomConfigKey() let expectedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .json(representation: .data) + content: .json(representation: .data), ) let jsonData = try! JSONEncoder().encode(expectedValue) setProviderValue(.bytes(Array(jsonData)), forKey: key) @@ -72,16 +72,16 @@ struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { let key = randomConfigKey() let expectedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .json(representation: .data) + content: .json(representation: .data), ) let jsonData = try! JSONEncoder().encode(expectedValue) setProviderValue(.bytes(Array(jsonData)), forKey: key) @@ -100,22 +100,22 @@ struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { let key = randomConfigKey() let initialValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let updatedValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let defaultValue = MockCodableConfig( variant: randomAlphanumericString(), - count: randomInt(in: 1 ... 100) + count: randomInt(in: 1 ... 100), ) let isSecret = randomBool() let provider = provider let variable = ConfigVariable( key: key, defaultValue: defaultValue, - content: .json(representation: .data) + content: .json(representation: .data), ) let encoder = JSONEncoder() let initialJSON = try! encoder.encode(initialValue) @@ -131,7 +131,7 @@ struct ConfigVariableReaderDataRepresentationTests: RandomValueGenerating { provider.setValue( .init(.bytes(Array(updatedJSON)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderEditorTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderEditorTests.swift index fe775bb..2c0f629 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderEditorTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderEditorTests.swift @@ -21,7 +21,7 @@ struct ConfigVariableReaderEditorTests: RandomValueGenerating { // set up let reader = ConfigVariableReader( namedProviders: [.init(InMemoryProvider(values: [:]))], - eventBus: EventBus() + eventBus: EventBus(), ) // expect @@ -35,7 +35,7 @@ struct ConfigVariableReaderEditorTests: RandomValueGenerating { let reader = ConfigVariableReader( namedProviders: [.init(InMemoryProvider(values: [:]))], eventBus: EventBus(), - isEditorEnabled: false + isEditorEnabled: false, ) // expect @@ -60,7 +60,7 @@ struct ConfigVariableReaderEditorTests: RandomValueGenerating { let reader = ConfigVariableReader( namedProviders: [.init(otherProvider)], eventBus: EventBus(), - isEditorEnabled: true + isEditorEnabled: true, ) // expect @@ -84,13 +84,13 @@ struct ConfigVariableReaderEditorTests: RandomValueGenerating { let reader = ConfigVariableReader( namedProviders: [.init(otherProvider)], eventBus: EventBus(), - isEditorEnabled: true + isEditorEnabled: true, ) let variable = ConfigVariable( key: key, defaultValue: randomAlphanumericString(), - isSecret: false + isSecret: false, ) // Verify the provider value is returned before any override diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift index b79f0a2..a99d313 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRawRepresentableTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -127,7 +127,7 @@ struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { provider.setValue( .init(.string(updatedValue.rawValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -211,7 +211,7 @@ struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { provider.setValue( .init(.stringArray(updatedValue.map(\.rawValue)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -311,7 +311,7 @@ struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { provider.setValue( .init(.int(updatedValue.rawValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -395,7 +395,7 @@ struct ConfigVariableReaderRawRepresentableTests: RandomValueGenerating { provider.setValue( .init(.intArray(updatedValue.map(\.rawValue)), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift index 2855356..f2473d3 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift @@ -165,7 +165,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating { await #expect(processExitsWith: .failure) { let reader = ConfigVariableReader( namedProviders: [.init(InMemoryProvider(values: [:]))], - eventBus: EventBus() + eventBus: EventBus(), ) let variable1 = ConfigVariable(key: "duplicate.key", defaultValue: 1) let variable2 = ConfigVariable(key: "duplicate.key", defaultValue: 2) @@ -181,7 +181,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating { await #expect(processExitsWith: .failure) { let reader = ConfigVariableReader( namedProviders: [.init(InMemoryProvider(values: [:]))], - eventBus: EventBus() + eventBus: EventBus(), ) let variable = ConfigVariable( key: "encode.failure", @@ -193,13 +193,13 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating { encode: { _ in throw EncodingError.invalidValue( "", - .init(codingPath: [], debugDescription: "") + .init(codingPath: [], debugDescription: ""), ) }, editorControl: .none, parse: nil, - validate: nil - ) + validate: nil, + ), ) reader.register(variable) diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift index d20c5ae..e14b0e7 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderScalarTests.swift @@ -31,7 +31,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { provider.setValue( .init(content, isSecret: randomBool()), - forKey: .init(key) + forKey: .init(key), ) } @@ -123,7 +123,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { provider.setValue( .init(.bool(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -206,7 +206,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { provider.setValue( .init(.int(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -272,7 +272,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { provider.setValue( .init(.double(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -368,7 +368,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { provider.setValue( .init(.string(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() @@ -451,7 +451,7 @@ struct ConfigVariableReaderScalarTests: RandomValueGenerating { provider.setValue( .init(.bytes(updatedValue), isSecret: isSecret), - forKey: .init(key) + forKey: .init(key), ) let value2 = await iterator.next() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift index b915261..c9bc996 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift @@ -41,7 +41,7 @@ struct ConfigVariableReaderTests: RandomValueGenerating { let variable = ConfigVariable(key: key, defaultValue: !expectedValue) provider.setValue( .init(.bool(expectedValue), isSecret: randomBool()), - forKey: .init(variable.key) + forKey: .init(variable.key), ) let (eventStream, continuation) = AsyncStream.makeStream() diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift index 946fd39..51909fc 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift @@ -30,7 +30,7 @@ struct RegisteredConfigVariableTests: RandomValueGenerating { destinationTypeName: randomAlphanumericString(), editorControl: .none, parse: nil, - validate: nil + validate: nil, ) // expect @@ -63,7 +63,7 @@ struct RegisteredConfigVariableTests: RandomValueGenerating { ) mutating func initNormalizesDestinationTypeName( input: String, - expected: String + expected: String, ) { // set up let variable = RegisteredConfigVariable( @@ -74,7 +74,7 @@ struct RegisteredConfigVariableTests: RandomValueGenerating { destinationTypeName: input, editorControl: .none, parse: nil, - validate: nil + validate: nil, ) // expect @@ -93,7 +93,7 @@ struct RegisteredConfigVariableTests: RandomValueGenerating { destinationTypeName: randomAlphanumericString(), editorControl: .none, parse: nil, - validate: nil + validate: nil, ) // expect diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift index 1548882..c364f22 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift @@ -33,21 +33,21 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { mutating func makeDocument( namedProviders: [NamedConfigProvider] = [], - registeredVariables: [RegisteredConfigVariable] + registeredVariables: [RegisteredConfigVariable], ) -> EditorDocument { EditorDocument( editorOverrideProvider: editorOverrideProvider, workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables, - undoManager: undoManager + undoManager: undoManager, ) } mutating func makeViewModel( document: EditorDocument, - registeredVariable: RegisteredConfigVariable + registeredVariable: RegisteredConfigVariable, ) -> ConfigVariableDetailViewModel { ConfigVariableDetailViewModel(document: document, registeredVariable: registeredVariable) } @@ -69,7 +69,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { isSecret: isSecret, metadata: metadata, destinationTypeName: destinationTypeName, - editorControl: .textField + editorControl: .textField, ) let document = makeDocument(registeredVariables: [variable]) @@ -99,7 +99,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( metadata: metadata, - editorControl: .textField + editorControl: .textField, ) let document = makeDocument(registeredVariables: [variable]) @@ -177,7 +177,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: providerDisplayName)], - registeredVariables: [variable] + registeredVariables: [variable], ) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -233,7 +233,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: randomAlphanumericString())], - registeredVariables: [variable] + registeredVariables: [variable], ) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -351,7 +351,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), editorControl: .textField, - parse: { .string($0) } + parse: { .string($0) }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -390,7 +390,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), editorControl: .textField, - parse: { _ in nil } + parse: { _ in nil }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -412,7 +412,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { defaultContent: .string(randomAlphanumericString()), editorControl: .textField, parse: { .string($0) }, - validate: { _ in false } + validate: { _ in false }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -434,7 +434,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { defaultContent: .string(randomAlphanumericString()), editorControl: .textField, parse: { .string($0) }, - validate: { _ in true } + validate: { _ in true }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -470,7 +470,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), editorControl: .textField, - parse: { .string($0) } + parse: { .string($0) }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -489,7 +489,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { defaultContent: .string(randomAlphanumericString()), editorControl: .textField, parse: { .string($0) }, - validate: { _ in true } + validate: { _ in true }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -507,7 +507,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), editorControl: .textField, - parse: { _ in nil } + parse: { _ in nil }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) @@ -526,7 +526,7 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { defaultContent: .string(randomAlphanumericString()), editorControl: .textField, parse: { .string($0) }, - validate: { _ in false } + validate: { _ in false }, ) let document = makeDocument(registeredVariables: [variable]) let viewModel = makeViewModel(document: document, registeredVariable: variable) diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift index ac74310..ace3480 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift @@ -36,14 +36,14 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { mutating func makeDocument( namedProviders: [NamedConfigProvider] = [], - registeredVariables: [RegisteredConfigVariable]? = nil + registeredVariables: [RegisteredConfigVariable]? = nil, ) -> EditorDocument { EditorDocument( editorOverrideProvider: editorOverrideProvider, workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables ?? [randomRegisteredVariable()], - undoManager: undoManager + undoManager: undoManager, ) } @@ -84,7 +84,7 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { providerIndex: 0, isSecret: variable1.isSecret, hasOverride: false, - editorControl: variable1.editorControl + editorControl: variable1.editorControl, ), VariableListItem( key: variable2.key, @@ -94,7 +94,7 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { providerIndex: 0, isSecret: variable2.isSecret, hasOverride: false, - editorControl: variable2.editorControl + editorControl: variable2.editorControl, ), ] #expect(items == expected) @@ -123,7 +123,7 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { providerIndex: 0, isSecret: variable.isSecret, hasOverride: false, - editorControl: variable.editorControl + editorControl: variable.editorControl, ) ] #expect(items == expected) @@ -137,14 +137,14 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { metadata1.displayName = "ServerURL" let variable1 = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), - metadata: metadata1 + metadata: metadata1, ) var metadata2 = ConfigVariableMetadata() metadata2.displayName = "Timeout" let variable2 = randomRegisteredVariable( defaultContent: .int(randomInt(in: .min ... .max)), - metadata: metadata2 + metadata: metadata2, ) let document = makeDocument(registeredVariables: [variable1, variable2]) @@ -169,7 +169,7 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { let variable = randomRegisteredVariable( key: key, defaultContent: .string(randomAlphanumericString()), - metadata: metadata + metadata: metadata, ) let document = makeDocument(registeredVariables: [variable]) @@ -209,21 +209,21 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { metadataC.displayName = "Charlie" let variableC = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), - metadata: metadataC + metadata: metadataC, ) var metadataA = ConfigVariableMetadata() metadataA.displayName = "Alpha" let variableA = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), - metadata: metadataA + metadata: metadataA, ) var metadataB = ConfigVariableMetadata() metadataB.displayName = "Bravo" let variableB = randomRegisteredVariable( defaultContent: .string(randomAlphanumericString()), - metadata: metadataB + metadata: metadataB, ) // register in non-sorted order diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift index 84de741..2170080 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift @@ -34,14 +34,14 @@ struct EditorDocumentTests: RandomValueGenerating { mutating func makeDocument( editorOverrideProvider: EditorOverrideProvider? = nil, namedProviders: [NamedConfigProvider] = [], - registeredVariables: [RegisteredConfigVariable]? = nil + registeredVariables: [RegisteredConfigVariable]? = nil, ) -> EditorDocument { EditorDocument( editorOverrideProvider: editorOverrideProvider ?? self.editorOverrideProvider, workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables ?? [randomRegisteredVariable()], - undoManager: undoManager + undoManager: undoManager, ) } @@ -87,7 +87,7 @@ struct EditorDocumentTests: RandomValueGenerating { // exercise let document = makeDocument( namedProviders: [.init(provider, displayName: displayName)], - registeredVariables: [variable] + registeredVariables: [variable], ) // expect first snapshot has correct display name, index, and value @@ -109,7 +109,7 @@ struct EditorDocumentTests: RandomValueGenerating { // exercise let document = makeDocument( namedProviders: [.init(provider, displayName: randomAlphanumericString())], - registeredVariables: [variable] + registeredVariables: [variable], ) // expect last snapshot is "Default" with index = namedProviders.count and default values @@ -154,7 +154,7 @@ struct EditorDocumentTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: randomAlphanumericString())], - registeredVariables: [variable] + registeredVariables: [variable], ) let overrideContent = ConfigContent.string(randomAlphanumericString()) @@ -186,7 +186,7 @@ struct EditorDocumentTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: providerDisplayName)], - registeredVariables: [variable] + registeredVariables: [variable], ) // set a mismatched type in the working copy @@ -251,7 +251,7 @@ struct EditorDocumentTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: providerDisplayName)], - registeredVariables: [variable] + registeredVariables: [variable], ) let overrideContent = ConfigContent.string(randomAlphanumericString()) @@ -267,21 +267,21 @@ struct EditorDocumentTests: RandomValueGenerating { providerIndex: nil, isActive: true, valueString: overrideContent.displayString, - contentTypeMatches: true + contentTypeMatches: true, ), ProviderValue( providerName: providerDisplayName, providerIndex: 0, isActive: false, valueString: providerContent.displayString, - contentTypeMatches: true + contentTypeMatches: true, ), ProviderValue( providerName: localizedString("editor.defaultProviderName"), providerIndex: 1, isActive: false, valueString: defaultContent.displayString, - contentTypeMatches: true + contentTypeMatches: true, ), ] #expect(values == expected) @@ -304,7 +304,7 @@ struct EditorDocumentTests: RandomValueGenerating { let document = makeDocument( namedProviders: [.init(provider, displayName: providerDisplayName)], - registeredVariables: [variable] + registeredVariables: [variable], ) let overrideContent = ConfigContent.string(randomAlphanumericString()) @@ -320,21 +320,21 @@ struct EditorDocumentTests: RandomValueGenerating { providerIndex: nil, isActive: true, valueString: overrideContent.displayString, - contentTypeMatches: true + contentTypeMatches: true, ), ProviderValue( providerName: providerDisplayName, providerIndex: 0, isActive: false, valueString: mismatchedContent.displayString, - contentTypeMatches: false + contentTypeMatches: false, ), ProviderValue( providerName: localizedString("editor.defaultProviderName"), providerIndex: 1, isActive: false, valueString: defaultContent.displayString, - contentTypeMatches: true + contentTypeMatches: true, ), ] #expect(values == expected) From 95a832582a3d9ca504724dcee08ee885c3645ee4 Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Fri, 20 Mar 2026 10:42:43 -0400 Subject: [PATCH 2/5] Only support the editor on iOS for now --- .../DevConfiguration/Core/Localization.swift | 2 +- .../ConfigVariableDetailView.swift | 21 +++++-------------- .../ConfigVariableDetailViewModel.swift | 2 +- .../ConfigVariableDetailViewModeling.swift | 2 +- .../ConfigVariableListView.swift | 4 +--- .../ConfigVariableListViewModel.swift | 2 +- .../ConfigVariableListViewModeling.swift | 2 +- .../Editor/ConfigVariableEditor.swift | 2 +- .../Editor/Utilities/ProviderBadge.swift | 2 +- 9 files changed, 13 insertions(+), 26 deletions(-) diff --git a/Sources/DevConfiguration/Core/Localization.swift b/Sources/DevConfiguration/Core/Localization.swift index df1a8eb..cb31f5d 100644 --- a/Sources/DevConfiguration/Core/Localization.swift +++ b/Sources/DevConfiguration/Core/Localization.swift @@ -17,7 +17,7 @@ func localizedStringResource(_ keyAndValue: String.LocalizationValue) -> Localiz } -#if canImport(SwiftUI) +#if os(iOS) import SwiftUI extension Text { diff --git a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift index 8f54aa2..5540286 100644 --- a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift +++ b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift @@ -5,7 +5,7 @@ // Created by Prachi Gauriar on 3/8/2026. // -#if canImport(SwiftUI) +#if os(iOS) import Configuration import SwiftUI @@ -125,11 +125,8 @@ extension ConfigVariableDetailView { .frame(minHeight: 100) .border(viewModel.isOverrideTextValid ? Color.clear : Color.red) .autocorrectionDisabled() - - #if os(iOS) || os(visionOS) - .textInputAutocapitalization(.never) - .keyboardType(.asciiCapable) - #endif + .textInputAutocapitalization(.never) + .keyboardType(.asciiCapable) HStack { Spacer() @@ -155,16 +152,13 @@ extension ConfigVariableDetailView { RoundedRectangle(cornerRadius: 6) .stroke(viewModel.isOverrideTextValid ? Color.separator : Color.red) ) - #if os(iOS) || os(visionOS) .keyboardType(keyboardType) - #endif } } } } - #if os(iOS) || os(visionOS) private var keyboardType: UIKeyboardType { if viewModel.editorControl == .numberField { .numberPad @@ -174,7 +168,6 @@ extension ConfigVariableDetailView { .default } } - #endif private var providerValuesSection: some View { @@ -222,15 +215,11 @@ extension ConfigVariableDetailView { } } -#endif - extension Color { static var separator: Color { - #if canImport(UIKit) Color(UIColor.separator) - #elseif canImport(AppKit) - Color(NSColor.separatorColor) - #endif } } + +#endif diff --git a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModel.swift b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModel.swift index 8d19127..3a841bc 100644 --- a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModel.swift +++ b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModel.swift @@ -5,7 +5,7 @@ // Created by Prachi Gauriar on 3/9/2026. // -#if canImport(SwiftUI) +#if os(iOS) import Configuration import Foundation diff --git a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModeling.swift b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModeling.swift index 573c76c..c6cb2e7 100644 --- a/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModeling.swift +++ b/Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModeling.swift @@ -5,7 +5,7 @@ // Created by Prachi Gauriar on 3/9/2026. // -#if canImport(SwiftUI) +#if os(iOS) import Configuration import Foundation diff --git a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift index 8513a4b..5e2c1b5 100644 --- a/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift +++ b/Sources/DevConfiguration/Editor/Config Variable List/ConfigVariableListView.swift @@ -5,7 +5,7 @@ // Created by Prachi Gauriar on 3/8/2026. // -#if canImport(SwiftUI) +#if os(iOS) import Configuration import SwiftUI @@ -62,9 +62,7 @@ struct ConfigVariableListView Date: Fri, 20 Mar 2026 10:50:25 -0400 Subject: [PATCH 3/5] Add SPI yaml --- .spi.yml | 5 +++++ .../DevConfiguration/Core/NamedConfigProvider.swift | 2 +- .../Core/RegisteredConfigVariable.swift | 10 +++++----- .../Documentation.docc/Documentation.md | 7 +++++++ 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..4df8291 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [DevConfiguration] + swift_version: 6.2 diff --git a/Sources/DevConfiguration/Core/NamedConfigProvider.swift b/Sources/DevConfiguration/Core/NamedConfigProvider.swift index 6b9360e..10edb92 100644 --- a/Sources/DevConfiguration/Core/NamedConfigProvider.swift +++ b/Sources/DevConfiguration/Core/NamedConfigProvider.swift @@ -10,7 +10,7 @@ import Configuration /// A configuration provider paired with a human-readable display name. /// /// Use `NamedConfigProvider` when adding providers to a ``ConfigVariableReader`` to control how the provider's name -/// appears in the editor UI. If no display name is specified, the provider's ``ConfigProvider/providerName`` is used. +/// appears in the editor UI. If no display name is specified, the provider's `providerName` is used. /// /// let reader = ConfigVariableReader( /// providers: [ diff --git a/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift index e0fb7ac..0fbc5b6 100644 --- a/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift +++ b/Sources/DevConfiguration/Core/RegisteredConfigVariable.swift @@ -11,14 +11,14 @@ import Foundation /// A non-generic representation of a registered ``ConfigVariable``. /// /// `RegisteredConfigVariable` stores the type-erased information from a ``ConfigVariable`` so that registered variables -/// can be stored in homogeneous collections. It captures the variable's key, its default value as a ``ConfigContent``, -/// its secrecy setting, and any attached metadata. +/// can be stored in homogeneous collections. It captures the variable's key, its default value as a +/// ``Configuration/ConfigContent``, its secrecy setting, and any attached metadata. @dynamicMemberLookup public struct RegisteredConfigVariable: Sendable { /// The configuration key used to look up this variable's value. public let key: ConfigKey - /// The variable's default value represented as a ``ConfigContent``. + /// The variable's default value represented as a ``Configuration/ConfigContent``. public let defaultContent: ConfigContent /// Whether this variable's value should be treated as secret. @@ -31,9 +31,9 @@ public struct RegisteredConfigVariable: Sendable { /// /// This is captured at registration time via `String(describing: Value.self)` and may differ from the content type /// name when the variable uses a type that maps to a primitive content type (e.g., an `Int`-backed enum stored as - /// ``ConfigContent/int(_:)``). Standard generic types are normalized to use Swift shorthand syntax (e.g., + /// `ConfigContent.int(_:)`). Standard generic types are normalized to use Swift shorthand syntax, e.g., /// `Array` becomes `[Int]`, `Optional` becomes `String?`, and `Dictionary` becomes - /// `[String: Int]`). + /// `[String: Int]`. public let destinationTypeName: String /// A human-readable name for this variable's content type (e.g., `"Bool"`, `"[Int]"`). diff --git a/Sources/DevConfiguration/Documentation.docc/Documentation.md b/Sources/DevConfiguration/Documentation.docc/Documentation.md index e56ae2b..3a9ab6b 100644 --- a/Sources/DevConfiguration/Documentation.docc/Documentation.md +++ b/Sources/DevConfiguration/Documentation.docc/Documentation.md @@ -16,6 +16,10 @@ configuration management with extensible metadata, a variable management UI, and - ``ConfigVariable`` - ``ConfigVariableReader`` +### Editor Interface + +- ``ConfigVariableEditor`` + ### Variable Metadata - ``ConfigVariableMetadata`` @@ -32,3 +36,6 @@ configuration management with extensible metadata, a variable management UI, and - ``ConfigVariableContent`` - ``CodableValueRepresentation`` +- ``EditorControl`` +- ``NamedConfigProvider`` +- ``RegisteredConfigVariable`` From 1bd0f5ac7b9328cce005f6e3f5518adc5122a0f2 Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Fri, 20 Mar 2026 10:55:37 -0400 Subject: [PATCH 4/5] Disable view model tests for non-iOS platforms --- .../ConfigVariableDetailViewModelTests.swift | 3 ++- .../ConfigVariableListViewModelTests.swift | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift index c364f22..60f9950 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift @@ -5,6 +5,7 @@ // Created by Prachi Gauriar on 3/9/2026. // +#if os(iOS) import Configuration import DevTesting import Foundation @@ -52,7 +53,6 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { ConfigVariableDetailViewModel(document: document, registeredVariable: registeredVariable) } - // MARK: - init @Test @@ -604,3 +604,4 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { #expect(viewModel.overrideText == "one\ntwo\nthree") } } +#endif diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift index ace3480..1b6a157 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift @@ -5,6 +5,7 @@ // Created by Prachi Gauriar on 3/9/2026. // +#if os(iOS) import Configuration import DevTesting import Foundation @@ -446,3 +447,4 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { #expect(detailViewModel.key == variable.key) } } +#endif From 23e82f7a21a0daef885f18c676cf04374bb8d74e Mon Sep 17 00:00:00 2001 From: Prachi Gauriar Date: Fri, 20 Mar 2026 10:57:35 -0400 Subject: [PATCH 5/5] Disable non-iOS platforms for example app --- App/App.xcodeproj/project.pbxproj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/App/App.xcodeproj/project.pbxproj b/App/App.xcodeproj/project.pbxproj index 65cf054..3b30c34 100644 --- a/App/App.xcodeproj/project.pbxproj +++ b/App/App.xcodeproj/project.pbxproj @@ -279,11 +279,12 @@ REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 26.2; }; name = Debug; @@ -319,11 +320,12 @@ REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 26.2; }; name = Release;