Skip to content

Add EachKey validator for validating array keys#1793

Merged
henriquemoody merged 1 commit into
Respect:mainfrom
henriquemoody:each_key
Jun 25, 2026
Merged

Add EachKey validator for validating array keys#1793
henriquemoody merged 1 commit into
Respect:mainfrom
henriquemoody:each_key

Conversation

@henriquemoody

Copy link
Copy Markdown
Member

Each validates array values but there is no built-in way to validate their keys. The only workaround is transforming the input first with v::after('array_keys', v::each(...)), which is indirect and loses the composability that makes the library expressive. Without EachKey, validating something as common as array<string, int> requires an awkward transformation instead of a readable composition.

EachKey fills that gap by validating each key in an iterable against an inner validator, mirroring how Each validates values. This makes array-key constraints a first-class concern: v::eachKey(v::stringType()) is immediately readable, and combining it with Each via AllOf gives a clean expression for typed arrays like
v::allOf(v::eachKey(v::stringType()), v::each(v::intType())).

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.42%. Comparing base (e30d333) to head (b228af0).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##               main    #1793   +/-   ##
=========================================
  Coverage     99.42%   99.42%           
- Complexity     1034     1042    +8     
=========================================
  Files           195      196    +1     
  Lines          2416     2444   +28     
=========================================
+ Hits           2402     2430   +28     
  Misses           14       14           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new EachKey validator to make array/iterable key validation composable (mirroring how Each validates values), plus the supporting fluent-builder API surface, documentation, and tests.

Changes:

  • Introduces Respect\Validation\Validators\EachKey with templates and short-circuit support.
  • Adds unit + feature coverage for validation behavior, error messaging, naming/templating, and short-circuit mode.
  • Updates docs and fluent mixin interfaces/builders so v::eachKey(...) and related chain variants are discoverable, and includes the validator in smoke tests and docs index.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/Validators/EachKeyTest.php Unit coverage for EachKey validation + short-circuit behavior.
tests/feature/Validators/EachKeyTest.php Feature coverage for rendered messages/templates, naming, and short-circuit output.
tests/src/SmokeTestProvider.php Adds EachKey to the smoke-test validator inventory.
src/Validators/EachKey.php Implements the new validator and its templates/short-circuit evaluation.
src/Mixins/UndefOrChain.php Adds undefOrEachKey(...) to fluent chain interface.
src/Mixins/UndefOrBuilder.php Adds undefOrEachKey(...) to fluent static builder interface.
src/Mixins/PropertyChain.php Adds propertyEachKey(...) to fluent chain interface.
src/Mixins/PropertyBuilder.php Adds propertyEachKey(...) to fluent static builder interface.
src/Mixins/NullOrChain.php Adds nullOrEachKey(...) to fluent chain interface.
src/Mixins/NullOrBuilder.php Adds nullOrEachKey(...) to fluent static builder interface.
src/Mixins/NotChain.php Adds notEachKey(...) to fluent chain interface.
src/Mixins/NotBuilder.php Adds notEachKey(...) to fluent static builder interface.
src/Mixins/KeyChain.php Adds keyEachKey(...) to fluent chain interface.
src/Mixins/KeyBuilder.php Adds keyEachKey(...) to fluent static builder interface.
src/Mixins/Chain.php Adds eachKey(...) to the core fluent chain interface (v::...).
src/Mixins/Builder.php Adds eachKey(...) to the core fluent static builder interface.
src/Mixins/AllChain.php Adds allEachKey(...) to fluent chain interface.
src/Mixins/AllBuilder.php Adds allEachKey(...) to fluent static builder interface.
docs/validators/EachKey.md New validator documentation and examples.
docs/validators/Each.md Cross-links to EachKey in “See Also”.
docs/validators.md Adds EachKey to category lists and alphabetical index.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Validators/EachKey.php Outdated
Comment thread docs/validators/EachKey.md

@alganet alganet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few nits:

  • The messages are effort sensitive. If it makes it too hard to use the actual keys instead of the .dot representation we can make a compromise.
  • The wording (switching "in" to "of" in the main message, dropping "of" in the items) is my opinion. I suggest passing the messages through an LLM for grammar checking.
  • Non-scalar keys (in advanced iterators) are niche and I understand if that complicates too much the implementation. If we decide to not do it, perhaps we should think of making the IterableType check more stricter (raw ArrayType that would block those advanced iterators from being used or equivalent).

Comment thread tests/feature/Validators/EachKeyTest.php Outdated
Comment thread tests/feature/Validators/EachKeyTest.php
@henriquemoody

henriquemoody commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

The problem is that some validators (like each(), key() , property()) set a path path on child results, and by not creating a path on EachKey it will inherit the parent path and become indistinguishable:

Using actual keys (no ->withPath())

getMessage():

Scenario Output
Standalone The key "a" must be an integer
Inside key(data) The key .data must be an integer ❌ both keys identical
Deep key(user, key(settings)) The key .user.settings must be an integer ❌ same

getFullMessage():

- Each key of `.data` must be valid
  - The key `.data` must be an integer
  - The key `.data` must be an integer

So using actual keys isn't viable when composed inside key(). The compromise I'd suggest is keeping the .dot representation but keeping "The key of":

"The key of" (current) — getMessage()

Scenario Output
Inside key(data) The key of .data.a must be an integer
Deep key(user, key(settings)) The key of .user.settings.theme must be an integer

"The key" — getMessage()

Scenario Output
Inside key(data) The key .data.a must be an integer
Deep key(user, key(settings)) The key .user.settings.theme must be an integer

"The key .data.a" reads as if .data.a is a key name — but a is the key and .data is just the parent path.

"The key of .data.a" separates "key" from the path — .data.a is the location, not the key's name. The root message already scopes it ("Each key of .data must be valid"), so the child just needs to say which location and what failed.

I think "The key of" is the best compromise here.

@alganet

alganet commented Jun 25, 2026

Copy link
Copy Markdown
Member

@henriquemoody what about dropping the entire prefix?

".data.a must be an integer"
".user.settings.theme must be an integer"

Just that, no "The key" nor "The key of".

Edit: in retrospect, not using the word "key" might make the message opaque and misleading (user thinks it's a value or something).

I'm ok with the compromise.

@henriquemoody

Copy link
Copy Markdown
Member Author

How about something like that?

Full message:
- Every key in `["a", "b", "c"]` must be valid
  - Key `.0` must be a string
  - Key `.1` must be a string
  - Key `.2` must be a string

Main message:
Key `.0` must be a string

@henriquemoody henriquemoody force-pushed the each_key branch 3 times, most recently from 8ef8f3a to a9a7380 Compare June 25, 2026 07:13
Each validates array values but there is no built-in way to validate
their keys. The only workaround is transforming the input first with
v::after('array_keys', v::each(...)), which is indirect and loses the
composability that makes the library expressive. Without EachKey,
validating something as common as array<string, int> requires an
awkward transformation instead of a readable composition.

EachKey fills that gap by validating each key in an iterable against an
inner validator, mirroring how Each validates values. This makes
array-key constraints a first-class concern: v::eachKey(v::stringType())
is immediately readable, and combining it with Each via AllOf gives a
clean expression for typed arrays like
v::allOf(v::eachKey(v::stringType()), v::each(v::intType())).
@henriquemoody henriquemoody merged commit b228af0 into Respect:main Jun 25, 2026
7 checks passed
@henriquemoody henriquemoody deleted the each_key branch June 25, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants