Add backend validation foundation#822
Open
alistair3149 wants to merge 19 commits into
Open
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd through serializer
Three tests broke once mediawiki_read auth was provisioned and Neo4j-dependent test code actually ran: - testNoWriteOccurs called SubjectLabel::__toString() which doesn't exist; use the public ->text property instead. - testInvalidConstraintReturnsViolation wrote the schema JSON with maximum nested inside constraints; NumberProperty::fromPartialJson reads it at top level, so the constraint never took effect. Flattened the JSON. - testSubjectWithMissingSchemaReturnsSchemaNotFoundViolation cannot be set up with the current fixture harness: a Subject with a never-existing Schema cannot be projected into Neo4j, so the handler returns 404 before the schema-not-found path. Marked skipped with the reasoning; the defensive schema-not-found code path remains and is documented in ValidationCodes.md.
SubjectLabel::createForValidation was removed in 8a5ff5b but the helper in SubjectValidatorTest still referenced it, causing CI to fail with 'Call to undefined method'. The full unfiltered phpunit suite was not run locally before the prior push, so this was missed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the foundation tier of ADR 21: Add Backend Validation. Adds a
SubjectValidatorservice, extends thePropertyTypeplugin contract with avalidate()method, and exposes two new REST endpoints that return constraint violations without performing a write.Follows-up to #819 (re-uses the post-PUT-refactor patterns for
Application/use cases andEntryPoints/REST/adapters).Summary
POST /neowiki/v0/subject/validate(create-shape body) andPOST /neowiki/v0/subject/{subjectId}/validate(update-shape body). Both return200 {violations: [...]}on well-formed input — violations are not HTTP errors.PropertyTypeinterface now requiresvalidate(NeoValue, PropertyDefinition): Violation[]. Implemented on all six built-in types and on RedHerb'sColorType(canonical extension example).docs/ValidationCodes.mdis the shared-codes contract between PHP and TS, including known gaps.Scope
In: PHP-side validator + endpoints + plugin contract change. Foundation tier only.
Out (deferred): Edit endpoints unchanged (no validator on
POST/PUT /subject); no enforcement config; no pre-existing-vs-new violation differentiation; TS top-level validator still returns boolean (not violations); no UI surface for the new endpoints.Wire-level changes
POST /neowiki/v0/subject/validateBody matches
POST /subjectcreate:{ "schema": "Company", "label": "ACME", "statements": { ... } }200 {violations: [...]}on well-formed input;400for missing/malformed body fields;404for non-existentschema.POST /neowiki/v0/subject/{subjectId}/validateBody matches
PUT /subject/{subjectId}:{ "label": "New", "statements": { ... }, "comment": "ignored" }200 {violations: [...]}on well-formed input;400for invalid subject-id format;404for non-existent Subject;200with aschema-not-foundviolation when the Subject's stored Schema has gone missing (asymmetry with the create endpoint is intentional and explained in the handler).Response shape
{ "violations": [ { "propertyName": "Websites", "code": "invalid-url", "args": [], "valuePartIndex": 1 }, { "propertyName": null, "code": "label-required", "args": [] } ] }propertyNameis the string form (ornullfor Subject-level violations).argsis always present.valuePartIndexis omitted when not applicable. The stable code set is documented indocs/ValidationCodes.md.Code structure
src/Domain/Validation/Violation.php— immutable VO withwithPropertyName().SubjectValidatoruses the builder to attach the property name (which PropertyType plugins don't know themselves).src/Application/Validation/SubjectValidator.php— pure orchestration. Algorithm mirrors the TSSubjectValidator: label check → iterate statements → skip unknown-to-schema properties (schema-drift accommodation) → delegate toPropertyType::validate().src/Domain/PropertyType/PropertyType.php— interface gainsvalidate(NeoValue, PropertyDefinition): Violation[]. Built-in implementations port the rules fromresources/ext.neowiki/src/domain/propertyTypes/*.ts.src/Domain/Subject/SubjectLabel.php— gainscreateForValidation(string)static that bypasses the empty-rejection constructor via reflection. The closure-->call($instance)alternative was rejected by PHPCS; reflection is the equivalent that passes the standard. Used only by the validate path; the regular constructor still rejects empty labels for write paths.src/Domain/Schema/Property/DateTimeProperty.php— gains public staticparseStrictDateTime(string): ?DateTimeImmutablesoDateTimeType::validatecan reuse the existing strict-ISO-8601 + calendar-overflow logic.src/Application/SelectStatementResolver.php— gainsresolveOrLeave(), a non-throwing variant used only by the validate handlers. Unresolvable select values pass through as-is;SelectType::validatethen surfaces them asinvalid-optionviolations rather than the handler erroring out. The throwingresolve()is unchanged for write paths.src/EntryPoints/REST/ValidateSubjectApi.php+ValidateSubjectUpdateApi.php— thin adapters.needsWriteAccess()returns false. No CSRF (read auth).extension.json— two newRestRoutesentries;ModuleSpecHandlerpicks them up into the OpenAPI spec automatically.tests/RedHerb/src/ColorType.php— real hex-color validation replacing the stub, as the canonical plugin-contract example.Pre-1.0 stability —
docs/RestApi.mdalready disclaims that/neowiki/v0/*is not stable. No back-compat shims.Test plan
ViolationTest,SubjectValidatorTest, the six*TypeValidateTestclasses,DateTimePropertyTest,SubjectLabelTest,ColorTypeValidateTest,SelectStatementResolverTest.make phpunit filter=ValidateSubjectApiTest— 9/9.make phpunit filter=ValidateSubjectUpdateApiTest— 9/10 (1 skipped:testSubjectWithMissingSchemaReturnsSchemaNotFoundViolation— fixture cannot create a findable Subject with a never-existing Schema; the defensive code path is documented).make phpunit filter=EntryPoints— 249/249 (1 skip).make phpunit filter=ModuleSpecHandlerNeoWikiTest— both new routes appear in the OpenAPI spec (assertion count rose from 376 to 407).make csclean (phpcs + phpstan).c734dd96).Manual API Check
Demo data needs to be present first:
make import-demo-data. Replacelocalhost:8484with your wiki URL.Create-shape endpoint
Update-shape endpoint
Find a subject ID first (e.g. via
curl http://localhost:8484/rest.php/neowiki/v0/page/{pageId}/subjects), then:Plugin-contract breaking change
The
PropertyTypeinterface gains a requiredvalidate()method. Extension authors maintaining custom Property Types must implement it. The RedHerbColorTypeis updated in this PR as the canonical example.Known limitations
Documented in
docs/ValidationCodes.md. Intentional gaps in the foundation tier:required— mirrors the TS behavior. To be fixed when the TS top-level validator is upgraded.StatementListBuilderdrops empty-valued Statements before validation; same fix as above.TextTypemin-length / max-length. PHPTextPropertydoesn't yet expose those fields; TS does.RelationTypeinvalid-subject-id. PHPSubjectIdvalidates at construction, soRelationValuecannot hold an invalid target; PHP doesn't emit this code.