Phase 4 (Waves 1–2): collection binding + validation engine#33
Conversation
- Add fixture interfaces for List/IList/ICollection/IReadOnlyList/IReadOnlyCollection<int> - Assert each materializes a List<int> from a delimited scalar - Add per-element DayOfWeek list test (element converter chain)
- Add IsListLike/IsCollectionShape predicates disjoint from IsEnumerable/IsArray - Add ListTypeConverter subclassing CollectionTypeConverter; materialize via cached per-element factory delegate (no warm-path reflection, S-4/A1) - Promote CollectionTypeConverter.Convert to virtual - Register ListTypeConverter after EnumerableTypeConverter, before EnumTypeConverter (disjoint, order preserved)
- Unbound int[] -> empty int[], unbound List<int> -> empty List<int> - Regression: unbound IEnumerable<int> -> empty int[] - B-3: two null-binds of List<int> return reference-distinct, mutation-isolated instances
- CreateNullResult branches IsArray -> IsEnumerable -> IsListLike -> value-type/null - Array/IEnumerable keep the shared cached empty array - List family binds a fresh empty List<T> per bind via a baked factory delegate (B-3), reusing the existing _nullResult slot so PropertyPlan[] layout is unchanged - PropertyConversion.Convert invokes the factory when _nullResult is Func<object>
…inder - child-section sequence binds string[]/int[]/List<string> elements - children win over coexisting scalar - comma-scalar regression guard (prod override) - whitespace scalar and empty sequence bind empty (no [" "] token) - RootSection prefix honored on the sequence path
…nces - grant InternalsVisibleTo to the Binders assembly so ConfigurationBinder reuses Core's internal IsCollectionShape predicate (single source of truth) - add GetChildren() branch: indexed sub-sections materialize a right-sized string[] via a manual two-pass walk (no LINQ on the bind path) - children win over a coexisting scalar; empty sequence falls through - whitespace/empty scalar on a collection target skips SetNewValue so COLL-02's empty-not-null default applies (avoids the [" "] token pitfall) - element chain and RootSection prefix inherited unchanged
- Convert_SecretInSequenceElement_DoesNotLeakValue: int[] secret in a later element is absent from the whole SettingsPropertyValueException.ToString() - first-element case (S-6): element position must not matter - List<int> case (S-6): redaction holds across the array and list converter shapes - drives the new ConfigurationBinder GetChildren() sequence path, not the scalar InMemoryBinder path
…ibute, aggregate exception - ISettingsValidator/ISettingValidation<T>.Validate return ValidationResult (drop Task<>) (D-07) - ValidationContext(object)/ValidationContext<T>(T) constructors carry the settings instance (D-08) - SettingsValidatorAttribute (AttributeTargets.Interface, Type ValidatorType) (D-11) - SettingsValidationException : SimpleSettingsException + static ThrowIfAny shared aggregate-and-throw (D-10/S-3) - Resources.SettingsValidationExceptionMessage composed only from author ValidationError text (D-12)
- object-level [SettingsValidator] and property-level [SettingsProperty(ValidatorType)] invocation - multi-error aggregation into one SettingsValidationException - cross-property rule observing the fully-populated instance - D-12 value-free exception message - B-2 validator-free short-circuit fast path
- thread object-level [SettingsValidator] and property-level [SettingsProperty(ValidatorType)] into the cached plan at build (D-11)
- SettingsPlan.HasValidators computed once at build; post-populate hook short-circuits before any allocation, error list allocated lazily (review B-2)
- validators run after the property-set loop so cross-property rules see the full instance (D-09)
- reflective dispatch selects the Validate overload via GetMethod(name, new[]{closedContextType}) + MakeGenericType context, avoiding AmbiguousMatchException (review S-2)
- aggregate-and-throw routed through shared SettingsValidationException.ThrowIfAny (review S-3/D-10)
…tests - AllowEmpty=false rejects "" and whitespace via SettingsPropertyNullException (value-free) - AllowEmpty=true still binds "" and whitespace as-is
…L-02) - Convert now throws value-free SettingsPropertyNullException for null or null-or-whitespace strings when AllowEmpty=false - AllowEmpty=true falls through to normal conversion (empty/whitespace bound as-is) - 04-01 list null-result factory dispatch preserved
| namespace ExistForAll.SimpleSettings | ||
| { | ||
| [AttributeUsage(AttributeTargets.Interface)] | ||
| public class SettingsValidatorAttribute : Attribute |
There was a problem hiding this comment.
But we have a section attribute we can reuse
There was a problem hiding this comment.
Done in b049f8d — SettingsValidatorAttribute is removed and the object-level validator now rides on SettingsSectionAttribute.ValidatorType (read at plan build in ValuesPopulator).
One consequence worth flagging: since [SettingsSection] is also the scan-discovery marker, attaching a validator now makes a type discoverable, so the deliberately-failing validation fixtures would be eagerly built by the whole-assembly ScanAssemblies tests. Making them internal to hide from the scan doesn't work — the Reflection.Emit proxy generator emits into a random-GUID dynamic assembly that can't implement an internal interface (verified: TypeLoadException). So the fixtures moved to a new never-scanned test library (ExistForAll.SimpleSettings.UnitTests.Fixtures), plus a regression test that the validator still runs under the scan path. Full suite 143/143.
|
|
||
| var closedContextType = ResolveContextType(validatorType); | ||
|
|
||
| var context = Activator.CreateInstance(closedContextType, target)!; |
There was a problem hiding this comment.
Done — it casts now. Dispatch is (ISettingsValidator)Activator.CreateInstance(validatorType) then validator.Validate(new ValidationContext(target)), with ISettingValidation<T> default-implementing the non-generic ISettingsValidator.Validate (the DIM bridge). No reflection / MakeGenericType / MethodInfo.Invoke anymore — see the current InvokeValidator in ValuesPopulator.cs.
…y elements - Enumerate the live config view once (a second GetChildren() pass could race a reload and overrun the count-sized array or inject null holes) — review MED-1. - Drop empty entries for parity with the comma-scalar split (RemoveEmptyEntries), so ["1","","3"] no longer diverges from "1,,3" and crashes int[]/List<int> — review MED-2. - PR #33 review comment 1.
- Consolidate the two null branches into one block (throw if required, else the null-result/factory), keeping the whitespace-string reject and the AllowEmpty=true fall-through intact. PR #33 review comment 2.
…idge; guard invocation value-free - ISettingValidation<T> default-implements the base Validate, forwarding to the generic overload, so ValuesPopulator dispatches through a plain ISettingsValidator cast — no MakeGenericType/GetMethod/Invoke reflection and no AmbiguousMatchException workaround (PR #33 comment 4). - Wrap validator invocation: a validator that throws is surfaced as the value-free SettingsValidatorInvocationException (never chains the inner, which may hold a secret) — review MED-3. - ValidationContext ctor accepts object? (drops the !); fixtures implement only the generic Validate; add property-validator positive/discriminating, object+property aggregation, and throwing-validator coverage.
…drop - Assert order-sensitive SequenceEqual on indexed-sequence cases (IsEquivalentTo was order-insensitive and would not catch an order regression) — review test-gap T1. - Add an interspersed-empty-element case proving parity with the comma-scalar — review MED-2.
|
Pushed review fixes — 5 commits ( Your inline comments
Also addressed (from the automated review)
Deferred to Phase 5
|
…ype (PR #33 comment #3) Reuse the existing section attribute for the object-level validator instead of a separate SettingsValidatorAttribute (now deleted; it was unreleased, so not a breaking change). ValuesPopulator reads the validator Type from SettingsSectionAttribute.ValidatorType; the dispatch and value-free exception wrapping are unchanged. Because [SettingsSection] is the scan-discovery marker, the deliberately-failing validation fixtures would now be eagerly built by the whole-assembly ScanAssemblies tests. Making them internal is not viable: the Reflection.Emit proxy generator emits into a random-GUID dynamic assembly that cannot implement an internal interface (verified: TypeLoadException). So the fixtures move to a new, never-scanned ExistForAll.SimpleSettings.UnitTests.Fixtures library, referenced by UnitTests but never passed to ScanAssemblies. Adds a regression test that the merged validator also runs under the assembly-scan path. Full suite 143/143 on net10; build clean on net8 + net10.
Phase 4 — Collection & Validation Binding (Waves 1–2)
Engine work for the client pre-beta requirements. This PR covers Waves 1–2 of Phase 4; Wave 3 (VAL-01 DI-resolved path + API-02) will be pushed to this same branch, so the diff will grow.
What's included
ListTypeConvertermaterializesList<T>/IList<T>/ICollection<T>/IReadOnlyList<T>/IReadOnlyCollection<T>from a delimited scalar via a cached per-element factory delegate (noActivator/MakeGenericTypeon the warm path). New disjointIsListLike/IsCollectionShapepredicates. Shape-awareCreateNullResult: an unset array/List/IEnumerable binds an empty collection, nevernull— with a fresh-per-bind emptyList<T>(arrays stay shared-cached).ConfigurationBinderbinds collections fromIConfigurationchild-section sequences (Section:Key:0,:1, …); children win over a scalar; the comma-scalar form still binds; empty/whitespace → empty. AnInternalsVisibleTogrant lets the Binders assembly reuse Core's shape predicate (one source of truth).[SettingsValidator]and property-level[SettingsProperty(ValidatorType)], including cross-property rules — aggregated into one value-freeSettingsValidationExceptionvia a sharedThrowIfAnyhelper. Validator-free types short-circuit with zero added allocation (SettingsPlan.HasValidatorsgate).[SettingsProperty(AllowEmpty=false)]rejects empty and whitespace strings (not justnull), reusing the value-freeSettingsPropertyNullException(no redaction-filter change).Security
ToString()chain (int[] first-element, int[] later-element, andList<int>). Formal security-auditor sign-off is pending the phase-level security gate.Verification
Notes
.planning/planning artifacts are intentionally excluded; thesrc/tree is byte-identical to the working phase branch.masterauto-publishes an-alpha.