|
| 1 | +# Editor Validation Implementation Plan |
| 2 | + |
| 3 | +This document tracks the implementation of input validation and enhanced editor controls |
| 4 | +for the config variable detail view. |
| 5 | + |
| 6 | + |
| 7 | +## Completed Work |
| 8 | + |
| 9 | +### Phase 1: `validate` Closure on `ConfigVariableContent` |
| 10 | + |
| 11 | + - Added `let validate: (@Sendable (_ content: ConfigContent) -> Bool)?` property |
| 12 | + - All 10 primitive factories pass `validate: nil` |
| 13 | + - `rawRepresentableString()` validates `Value(rawValue:) != nil` |
| 14 | + - `rawRepresentableInt()` validates `Value(rawValue:) != nil` |
| 15 | + - `expressibleByConfigString()` validates `Value(configString:) != nil` |
| 16 | + - `expressibleByConfigInt()` validates `Value(configInt:) != nil` |
| 17 | + - `codable()` attempts decode via `representation.data(from:)` then decoder |
| 18 | + |
| 19 | +### Phase 2: `EditorControl` Extensions + `RegisteredConfigVariable` |
| 20 | + |
| 21 | + - Added `EditorControl.PickerOption` nested struct (label + content) |
| 22 | + - Added `.textEditor` and `.picker(options:)` cases to `EditorControl.Kind` |
| 23 | + - Added `pickerOptions` computed property on `EditorControl` |
| 24 | + - Added `validate` property to `RegisteredConfigVariable`, threaded through init |
| 25 | + - Updated `ConfigVariableReader.register()` to pass `validate` |
| 26 | + - Updated all test call sites (RegisteredConfigVariableTests, random helper) |
| 27 | + |
| 28 | +### Phase 3: CaseIterable Content Factories |
| 29 | + |
| 30 | + - `rawRepresentableCaseIterableString()` — picker with `rawValue` labels |
| 31 | + - `rawRepresentableCaseIterableInt()` — picker with `String(describing:)` labels |
| 32 | + - Both use `parse: nil, validate: nil` (picker constrains input) |
| 33 | + |
| 34 | +### Phase 4: Codable JSON Editing |
| 35 | + |
| 36 | + - `json()` passes `.textEditor` + parse `{ .string($0) }` for string representations |
| 37 | + - `propertyList()` always passes `.none` + `nil` (not text-editable) |
| 38 | + - `codable()` private method accepts `editorControl` and `parse` parameters |
| 39 | + - Added `CodableValueRepresentation.supportsTextEditing` computed property |
| 40 | + - Added `CodableValueRepresentation.data(from:)` for extracting Data from |
| 41 | + ConfigContent |
| 42 | + |
| 43 | +### Phase 5: Array Editing (Newline-Separated) |
| 44 | + |
| 45 | + - All 8 array factories now use `.textEditor` |
| 46 | + - Primitive arrays: parse splits by newlines, no validate |
| 47 | + - RawRepresentable arrays: parse + validate checks each element |
| 48 | + - ExpressibleByConfig arrays: parse + validate checks each element |
| 49 | + - Added `String.nonEmptyTrimmedLines` helper extension |
| 50 | + - Added `ConfigContent.editableString` — newline-separated for arrays |
| 51 | + - View model uses `editableString` instead of `displayString` for `overrideText` |
| 52 | + |
| 53 | +### Phase 6: `ConfigVariable` Initializers for CaseIterable |
| 54 | + |
| 55 | + - Added init for `RawRepresentable<String> & CaseIterable` |
| 56 | + - Added init for `RawRepresentable<Int> & CaseIterable` |
| 57 | + - More specific constraints win overload resolution over existing inits |
| 58 | + |
| 59 | +### Phase 7: View Model Protocol + Implementation |
| 60 | + |
| 61 | + - Added `overridePickerSelection: ConfigContent { get set }` to protocol |
| 62 | + - Added `isOverrideTextValid: Bool { get }` to protocol |
| 63 | + - `ConfigVariableDetailViewModel.isOverrideTextValid` runs parse then validate |
| 64 | + - `commitOverrideText()` checks validate before committing |
| 65 | + - `overridePickerSelection` gets/sets override content directly |
| 66 | + |
| 67 | +### Phase 8: View Updates |
| 68 | + |
| 69 | + - **Toggle**: Unchanged |
| 70 | + - **Picker**: `Picker` bound to `overridePickerSelection`, iterates `pickerOptions` |
| 71 | + - **TextEditor**: Monospaced, `.autocorrectionDisabled()`, |
| 72 | + iOS `.textInputAutocapitalization(.never)` + `.keyboardType(.asciiCapable)`, |
| 73 | + red border on invalid, `@FocusState` for dismiss |
| 74 | + - TextEditor has "Apply" button (`.buttonStyle(.borderless)`, disabled when invalid, |
| 75 | + dismisses focus) |
| 76 | + - **TextField**: Red `foregroundStyle` when invalid |
| 77 | + - Added localized strings: `valuePicker`, `applyButton` |
| 78 | + |
| 79 | + |
| 80 | +## Files Modified (Source) |
| 81 | + |
| 82 | + - `Sources/DevConfiguration/Core/ConfigVariableContent.swift` |
| 83 | + - `Sources/DevConfiguration/Core/ConfigVariable.swift` |
| 84 | + - `Sources/DevConfiguration/Core/EditorControl.swift` |
| 85 | + - `Sources/DevConfiguration/Core/RegisteredConfigVariable.swift` |
| 86 | + - `Sources/DevConfiguration/Core/ConfigVariableReader.swift` |
| 87 | + - `Sources/DevConfiguration/Core/CodableValueRepresentation.swift` |
| 88 | + - `Sources/DevConfiguration/Extensions/ConfigContent+Additions.swift` |
| 89 | + - `Sources/DevConfiguration/Extensions/String+NonEmptyTrimmedLines.swift` (new) |
| 90 | + - `Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModeling.swift` |
| 91 | + - `Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailViewModel.swift` |
| 92 | + - `Sources/DevConfiguration/Editor/Config Variable Detail/ConfigVariableDetailView.swift` |
| 93 | + - `Sources/DevConfiguration/Resources/Localizable.xcstrings` |
| 94 | + |
| 95 | + |
| 96 | +## Files Modified (Tests) |
| 97 | + |
| 98 | + - `Tests/DevConfigurationTests/Unit Tests/Core/RegisteredConfigVariableTests.swift` |
| 99 | + - `Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift` |
| 100 | + - `Tests/DevConfigurationTests/Testing Support/RandomValueGenerating+DevConfiguration.swift` |
| 101 | + |
| 102 | + |
| 103 | +## Remaining: Phase 9 — Tests |
| 104 | + |
| 105 | +### 1. `String+NonEmptyTrimmedLines` Tests |
| 106 | + |
| 107 | +New file: |
| 108 | +`Tests/DevConfigurationTests/Unit Tests/Extensions/String+NonEmptyTrimmedLinesTests.swift` |
| 109 | + |
| 110 | + - Empty string returns empty array |
| 111 | + - Single line returns one trimmed element |
| 112 | + - Multiple lines returns trimmed elements |
| 113 | + - Blank/whitespace-only lines are filtered out |
| 114 | + - Leading/trailing whitespace on lines is trimmed |
| 115 | + |
| 116 | +### 2. `ConfigContent.editableString` Tests |
| 117 | + |
| 118 | +Add to existing: |
| 119 | +`Tests/DevConfigurationTests/Unit Tests/Extensions/ConfigContent+AdditionsTests.swift` |
| 120 | + |
| 121 | + - Scalar values (bool, int, double, string) return same as `displayString` |
| 122 | + - `boolArray` returns newline-separated (e.g., `"true\nfalse"`) |
| 123 | + - `intArray` returns newline-separated (e.g., `"1\n2\n3"`) |
| 124 | + - `doubleArray` returns newline-separated |
| 125 | + - `stringArray` returns newline-separated |
| 126 | + - `byteChunkArray` returns same as `displayString` |
| 127 | + |
| 128 | +### 3. `CodableValueRepresentation` Tests |
| 129 | + |
| 130 | +New file: |
| 131 | +`Tests/DevConfigurationTests/Unit Tests/Core/CodableValueRepresentationTests.swift` |
| 132 | + |
| 133 | + - `data(from:)` with string representation extracts string as UTF-8 data |
| 134 | + - `data(from:)` with string representation returns nil for non-string content |
| 135 | + - `data(from:)` with data representation extracts bytes as Data |
| 136 | + - `data(from:)` with data representation returns nil for non-bytes content |
| 137 | + - `supportsTextEditing` returns true for `.string()` |
| 138 | + - `supportsTextEditing` returns false for `.data` |
| 139 | + |
| 140 | +### 4. `EditorControl.pickerOptions` Tests |
| 141 | + |
| 142 | +New file: `Tests/DevConfigurationTests/Unit Tests/Core/EditorControlTests.swift` |
| 143 | + |
| 144 | + - `.picker(options:)` returns the options |
| 145 | + - `.toggle`, `.textField`, `.numberField`, `.decimalField`, `.textEditor`, `.none` |
| 146 | + all return nil |
| 147 | + |
| 148 | +### 5. `ConfigVariableDetailViewModel` Tests |
| 149 | + |
| 150 | +Add to existing: |
| 151 | +`Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift` |
| 152 | + |
| 153 | + - `isOverrideTextValid` returns true when `validate` is nil and parse succeeds |
| 154 | + - `isOverrideTextValid` returns true when both parse and validate succeed |
| 155 | + - `isOverrideTextValid` returns false when parse fails |
| 156 | + - `isOverrideTextValid` returns false when parse succeeds but validate fails |
| 157 | + - `commitOverrideText` sets override when valid |
| 158 | + - `commitOverrideText` does not set override when validation fails |
| 159 | + - `overridePickerSelection` getter returns override or default content |
| 160 | + - `overridePickerSelection` setter sets override on document |
| 161 | + - `editableString` is used for initial `overrideText` (verify array format) |
| 162 | + |
| 163 | +### 6. Registration Tests |
| 164 | + |
| 165 | +Update existing: |
| 166 | +`Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderRegistrationTests.swift` |
| 167 | + |
| 168 | + - Verify `validate` is captured for registered variables (e.g., register a |
| 169 | + `RawRepresentable<String>` variable and check `validate` is non-nil) |
0 commit comments