Refactor frontend validation: declarative constraints + interpreter#807
Refactor frontend validation: declarative constraints + interpreter#807alistair3149 wants to merge 19 commits intomasterfrom
Conversation
The 'maxItems' name is self-documenting about what's being capped (item count) and matches the JSON Schema convention for array-length bounds, distinguishing it from value-bound constraints like Number's minimum/maximum.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Wikimedia ESLint config (curly rule) requires braces around all if-statement bodies. Apply to the early-return guards in isValueEmpty and the value-type guards in interpretConstraints.
All six Property Type subclasses now implement getConstraints. Lifting the method from non-abstract-with-default to abstract removes the dead fallback path and makes the contract explicit.
PropertyTypeAdapter wraps external PropertyTypeRegistration plug-ins and overrides validate directly, so getConstraints is never invoked through it — but the abstract method still must be implemented to satisfy the TS contract. Returning [] is correct. The ConstraintInterpreter test that constructed a hand-rolled StringValue used a string literal type that didn't match the ValueType enum; switch to the enum value with an explicit StringValue type annotation.
… test - Export Constraint and ConstraintInterpreter from public-api.ts so external TS extensions subclassing BasePropertyType can satisfy the abstract getConstraints contract. - Comment PropertyTypeAdapter.getConstraints to flag that it exists only to satisfy the abstract contract since validate() is overridden. - Add DateTime regression test for required + empty StringValue. The refactor unified empty-value handling via isValueEmpty (now matching Text and Select behavior); the old DateTime.validate only flagged required when value was undefined, missing the empty-StringValue case. New test pins the unified behavior.
TypeScript narrowing handles the discriminated-union refinement through the ValueType.String guard, so the (value as StringValue) casts were defensive overkill. Remove them. Add a comment near the cardinality emit explaining that the 'single-value-only' code is hardcoded for maxItems === 1 (today's only use case via Select). Constraint is now exported, so external plug-ins could construct cardinality records with maxItems > 1; the comment flags where to branch the error code when that lands.
|
I looked at this a bit, not a super deep dive yet, and suspect this is net-negative It reminds me of what I did with Validator/ParamProcessor for SMW and some other extensions like Maps. That was a mistake. The declarative system seems appealing, but the complexity/indirection cost is real, making understanding/maintenance costs notably higher than having some imperative lines. My concern is with the declarative constraints and interpreter. The PR does a bunch of other stuff that seems nice and could be merged. List with the help of CC:
Beyond that we could implement backend validation fairly straightforward as one of the next steps, though ADR needs to still be created, which I will do ASAP. |
|
More explicitly: my concern is that the declarative+interpreter approach makes maintenance and evolution of the system more difficult, not easier, due to the complexity and indirection insufficiently paying for itself. I am NOT sure it's net-negative but would put 60/40 odds on it. |
You're right — staying imperative with shared utilities wasn't seriously considered in our brainstorm. The indirection cost likely outweighs the payoff at this scale. The five wins you listed (
How does this pivot direction sit with you? |
Follows-up to #631.
Summary
validate()into declarative constraint metadata + a generic interpreter, with a small per-typevalidateValuehook for intrinsic format checks (URL syntax, ISO-8601 parse, SubjectId validity, DateTime min/max).SubjectValidator.validatereturns a structuredSubjectValidationError[]instead ofboolean; adds i18n messageneowiki-field-label-required.severityfield onConstraintandValueValidationError. No Property Type emits severity yet; activation is a one-line change per type when the severity-levels feature is decided.Details
Constraintdiscriminated union with six kinds:required,minLength,maxLength,uniqueItems,cardinality(withmaxItems: number),enum(withallowedValues: string[]). Each variant carries optionalseverity?: 'error' | 'warning'.interpretConstraints(constraints, value)pure function indomain/ConstraintInterpreter.ts. Switches onconstraint.kind; type-mismatched constraints (e.g.minLengthon aNumberValue) are skipped silently.isValueEmpty(value)helper indomain/Value.tscentralizing the per-Value-Type "empty" notion.BasePropertyType.validatebecomes concrete and delegates[...validateValue(value, property), ...interpretConstraints(getConstraints(property), value)]. ThevalidateValue-first ordering preserves the existing per-type test ordering.getConstraintsis abstract;validateValuedefaults to[].getConstraintsonly). Number, Url, Relation, DateTime each contribute a smallvalidateValuehook (3-8 lines) for the type-intrinsic format check.Constraint,ConstraintInterpreter,Severityexported frompublic-api.tsso external TS extensions subclassingBasePropertyTypecan satisfy the abstract contract.RelationType.validate(which had none) before migrating it.requirednow fires for emptyStringValue(parts.length === 0), not onlyundefined. This matches Text and Select; the old DateTime code only flaggedundefined. Net effect is a small correctness improvement.Out of scope
// TODO: check property.multiplegaps — preserved verbatim; closing them is a separate follow-up.Manual Browser Check
Professional Wiki) and click Edit.not-a-url. Confirm:cdx-text-input--status-errorclass).not-a-urlwithhttps://example.com. Confirm the error state clears and Save changes re-enables.Berlin) and click Edit.Germany. Confirm Save changes re-enables.Local URLs: http://localhost:8484/index.php/Professional_Wiki and http://localhost:8484/index.php/Berlin
Test plan
make tscigreen (build + 819 tests across 79 files + lint)make tsciafter pulling