Skip to content

[#4253 2/8] feat(pe): PD validators (Validate + ValidateSelectionKeys)#4281

Draft
stevenvegt wants to merge 1 commit into
feature/4253-credential-selectionfrom
4253-2-validators
Draft

[#4253 2/8] feat(pe): PD validators (Validate + ValidateSelectionKeys)#4281
stevenvegt wants to merge 1 commit into
feature/4253-credential-selectionfrom
4253-2-validators

Conversation

@stevenvegt
Copy link
Copy Markdown
Member

Parent PRD

#4253

Item 2 of 8. Independent of item #1 (branches off the feature branch; can proceed in parallel).

Summary

Add two pure validators to vcr/pe: Validate (Policy 7, load-time PD consistency) and ValidateSelectionKeys (Policy 1, credential_selection key validation). Both are pure functions with typed errors and aggregate-all reporting. No caller is wired in this PR — the loader (policy/, discovery/) consumes Validate in item #7, and the API handlers consume ValidateSelectionKeys in item #6.

Implementation Spec

func Validate(pd PresentationDefinition) error — Policy 7

Semantic checks that JSON Schema cannot express. The PD has already passed schema validation (v2.Validate, which checks structure: required fields, types, additionalProperties, etc.) before this runs; Validate is a second, cross-field semantic pass on the parsed PresentationDefinition.

  • Id uniqueness within a constraints object. Within a single descriptor's Constraints.Fields, no two fields may share the same id (PEX requirement; not expressible in the JSON schema).
  • Same-id value-set consistency. For every field id appearing on two or more field-objects across descriptors, the value it binds to must satisfy every field's filter at once, so the intersection of the fields' allowed-value sets must be non-empty. Each filter is modelled as a value set:
    • const c{c}; enum [...] → that finite set; type-only / no filter → the universe of that type; pattern → a regex predicate.
    • Type agreement first: all declared Filter.Type for the id must be equal (mixed types can never agree; pins the type).
    • Finite-set intersection when any field for the id declares a const or enum: compute the intersection of all consts (as singletons) and all enum sets, then keep only values matching every declared pattern. Empty result → conflict. This catches differing consts, const-not-in-enum, disjoint enums, const-fails-pattern, and no-enum-value-matches-pattern.
    • Deferred — pattern vs pattern only. When an id carries only type + one or more patterns (no const/enum), the intersection is undecidable (regexp2 is not a regular language), so it is not checked at load. This degrades gracefully: at request time no credential can satisfy both patterns, the match fails, and the MatchReport from added baseline funcs from old repo #1 surfaces the no-eligible-candidate / binding conflict with the path and value.
    • Differing paths are always allowed (same id resolved from different JSONPaths on different credential types).
  • Aggregate-all: every conflicting id in the PD is reported in one error.

func ValidateSelectionKeys(selection map[string]string, pds ...PresentationDefinition) error — Policy 1

  • Builds the union of field ids across all supplied PDs (variadic: one PD for single-VP, two for two-VP).
  • Any selection key not in that union is unknown.
  • Validates key names only; values (empty or not) are ignored. This resolves the PRD open question on empty-value entries: an empty-string value does not change key validation.
  • Lists all unknown keys, sorted for determinism.

Typed errors

// Returned by ValidateSelectionKeys. nil when all keys are known.
type UnknownSelectionKeysError struct {
    Keys []string // sorted; every supplied key not present as a field id in any PD
}
func (e *UnknownSelectionKeysError) Error() string // "unknown credential_selection keys: a, b"

// Returned by Validate. nil when the PD is consistent.
type PDValidationError struct {
    Conflicts []FieldIDConflict
}
type FieldIDConflict struct {
    FieldID string
    Kind    string // "duplicate" | "type" | "unsatisfiable"
    Detail  string // e.g. `conflicting filter types: string vs number`; `no value satisfies all filters: const "Z" not in enum [X Y]`
}
func (e *PDValidationError) Error() string // aggregates all conflicts

Typed errors let item #6 build the OAuth invalid_request description (and #7 log structured detail) without string-parsing; the Error() strings stay human-readable for logs and operators.

Testing

Black-box validate_test.go, no mocks or fixtures.

  • Validate:
    • duplicate id within one constraints object;
    • conflicting filter types across same-id fields;
    • value-set conflicts: differing consts; const not in an enum; disjoint enums; const that fails another field's pattern; enum with no member matching another field's pattern;
    • allowed: path-only difference; no-filter / type-only field (no constraint); single-occurrence id (no-op); compatible const+enum (const in enum); pattern-vs-pattern with no const/enum (not flagged — deferred);
    • multiple conflicts all reported in one PDValidationError.
  • ValidateSelectionKeys: unknown key against a single PD; unknown key against a two-PD union; a key targeting only one side of a two-PD union passes; all-known passes; empty-string value validated by name; multiple unknown keys all listed and sorted.

Acceptance Criteria

  • Validate enforces id-uniqueness-within-constraints and same-id value-set non-empty intersection (type/const/enum + concrete-value-vs-pattern); defers only pattern-vs-pattern; ignores path differences.
  • ValidateSelectionKeys validates key names against the union of the supplied PDs; ignores values.
  • Both return the specified typed errors and aggregate all problems.
  • No callers wired (no behavior change outside vcr/pe); existing vcr/pe tests still green.
  • go build ./... and go test ./vcr/pe/... pass.

@qltysh
Copy link
Copy Markdown
Contributor

qltysh Bot commented May 27, 2026

Qlty


Coverage Impact

⬇️ Merging this pull request will decrease total coverage on feature/4253-credential-selection by 0.02%.

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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.

1 participant