Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions _examples/path_serialization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Path Serialization Example

This example demonstrates how the same validation error can be serialized using different path formats.

## Running the Example

```bash
go run _examples/path_serialization/app.go
```

## What It Shows

The example creates validation errors with various path structures and displays how each path is represented in different serialization formats:

1. **Default Format**: The original format used by `Path()` method (`/segment1/segment2` or `0/1`)
2. **JSON Pointer (RFC 6901)**: Standard JSON Pointer format (`/segment1/segment2/0`)
3. **JSONPath**: JSONPath format (`$.segment1.segment2[0]`)
4. **Dot Notation**: Dot notation format (`segment1.segment2[0]`)

## Example Output

The program demonstrates:
- Simple nested paths (e.g., `users.profile.name`)
- Paths with array indices (e.g., `users[0].emails[1]`)
- Complex mixed paths (e.g., `data.items[2].metadata.tags[0]`)
- Paths starting with indices (e.g., `[5].value`)
- Single segment paths (e.g., `username`)

Each example shows how the same error path is represented across all four serialization formats.
92 changes: 92 additions & 0 deletions _examples/path_serialization/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"context"
"fmt"

"proto.zip/studio/validate/pkg/errors"
"proto.zip/studio/validate/pkg/rulecontext"
)

func main() {
fmt.Println("=== Path Serialization Examples ===\n")

// Example 1: Simple nested path
fmt.Println("Example 1: Simple nested path (users.profile.name)")
example1()

// Example 2: Path with array indices
fmt.Println("\nExample 2: Path with array indices (users[0].emails[1])")
example2()

// Example 3: Complex mixed path
fmt.Println("\nExample 3: Complex mixed path (data.items[2].metadata.tags[0])")
example3()

// Example 4: Path starting with index
fmt.Println("\nExample 4: Path starting with index ([5].value)")
example4()

// Example 5: Single segment
fmt.Println("\nExample 5: Single segment (username)")
example5()
}

func example1() {
ctx := rulecontext.WithPathString(context.Background(), "users")
ctx = rulecontext.WithPathString(ctx, "profile")
ctx = rulecontext.WithPathString(ctx, "name")
err := errors.Errorf(errors.CodeMin, ctx, "below minimum", "must be at least %d", 10)

printAllFormats(err)
}

func example2() {
ctx := rulecontext.WithPathString(context.Background(), "users")
ctx = rulecontext.WithPathIndex(ctx, 0)
ctx = rulecontext.WithPathString(ctx, "emails")
ctx = rulecontext.WithPathIndex(ctx, 1)
err := errors.Errorf(errors.CodeMin, ctx, "below minimum", "must be at least %d", 10)

printAllFormats(err)
}

func example3() {
ctx := rulecontext.WithPathString(context.Background(), "data")
ctx = rulecontext.WithPathString(ctx, "items")
ctx = rulecontext.WithPathIndex(ctx, 2)
ctx = rulecontext.WithPathString(ctx, "metadata")
ctx = rulecontext.WithPathString(ctx, "tags")
ctx = rulecontext.WithPathIndex(ctx, 0)
err := errors.Errorf(errors.CodeMin, ctx, "below minimum", "must be at least %d", 10)

printAllFormats(err)
}

func example4() {
ctx := rulecontext.WithPathIndex(context.Background(), 5)
ctx = rulecontext.WithPathString(ctx, "value")
err := errors.Errorf(errors.CodeMin, ctx, "below minimum", "must be at least %d", 10)

printAllFormats(err)
}

func example5() {
ctx := rulecontext.WithPathString(context.Background(), "username")
err := errors.Errorf(errors.CodeMin, ctx, "below minimum", "must be at least %d", 10)

printAllFormats(err)
}

func printAllFormats(err errors.ValidationError) {
defaultSerializer := errors.DefaultPathSerializer{}
jsonPointerSerializer := errors.JSONPointerSerializer{}
jsonPathSerializer := errors.JSONPathSerializer{}
dotNotationSerializer := errors.DotNotationSerializer{}

fmt.Printf(" Default (Path()): %s\n", err.Path())
fmt.Printf(" Default (PathAs): %s\n", err.PathAs(defaultSerializer))
fmt.Printf(" JSON Pointer (RFC 6901): %s\n", err.PathAs(jsonPointerSerializer))
fmt.Printf(" JSONPath: %s\n", err.PathAs(jsonPathSerializer))
fmt.Printf(" Dot Notation: %s\n", err.PathAs(dotNotationSerializer))
}
21 changes: 21 additions & 0 deletions pkg/errors/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ func (collection ValidationErrorCollection) For(path string) ValidationErrorColl
return Collection(filteredErrors...)
}

// ForPathAs returns a new collection containing only errors for a specific path
// using the provided serializer to compare paths.
func (collection ValidationErrorCollection) ForPathAs(path string, serializer PathSerializer) ValidationErrorCollection {
if len(collection) == 0 {
return nil
}

var filteredErrors []ValidationError
for _, err := range collection {
if err.PathAs(serializer) == path {
filteredErrors = append(filteredErrors, err)
}
}

if len(filteredErrors) == 0 {
return nil
}

return Collection(filteredErrors...)
}

// Internal returns true if any error in the collection is an internal error.
// Internal errors are the most general classification and take precedence.
// Returns false for empty collections.
Expand Down
127 changes: 127 additions & 0 deletions pkg/errors/for_path_as_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package errors_test

import (
"context"
"testing"

"proto.zip/studio/validate/pkg/errors"
"proto.zip/studio/validate/pkg/rulecontext"
)

// TestForPathAs_WithDefaultSerializer tests:
// - ForPathAs method works with default serializer
func TestForPathAs_WithDefaultSerializer(t *testing.T) {
ctx1 := rulecontext.WithPathString(context.Background(), "a")
ctx1 = rulecontext.WithPathString(ctx1, "b")
err1 := errors.Errorf(errors.CodeMin, ctx1, "short", "message1")

ctx2 := rulecontext.WithPathString(context.Background(), "a")
ctx2 = rulecontext.WithPathString(ctx2, "c")
err2 := errors.Errorf(errors.CodeMin, ctx2, "short", "message2")

collection := errors.Collection(err1, err2)

serializer := errors.DefaultPathSerializer{}
filtered := collection.ForPathAs("/a/b", serializer)

if len(filtered) != 1 {
t.Errorf("Expected 1 error, got: %d", len(filtered))
}

if filtered[0].Error() != "message1" {
t.Errorf("Expected error message 'message1', got: '%s'", filtered[0].Error())
}
}

// TestForPathAs_WithJSONPointerSerializer tests:
// - ForPathAs method works with JSON Pointer serializer
func TestForPathAs_WithJSONPointerSerializer(t *testing.T) {
ctx1 := rulecontext.WithPathString(context.Background(), "a")
ctx1 = rulecontext.WithPathString(ctx1, "b")
err1 := errors.Errorf(errors.CodeMin, ctx1, "short", "message1")

ctx2 := rulecontext.WithPathString(context.Background(), "a")
ctx2 = rulecontext.WithPathString(ctx2, "c")
err2 := errors.Errorf(errors.CodeMin, ctx2, "short", "message2")

collection := errors.Collection(err1, err2)

serializer := errors.JSONPointerSerializer{}
filtered := collection.ForPathAs("/a/b", serializer)

if len(filtered) != 1 {
t.Errorf("Expected 1 error, got: %d", len(filtered))
}
}

// TestForPathAs_WithJSONPathSerializer tests:
// - ForPathAs method works with JSONPath serializer
func TestForPathAs_WithJSONPathSerializer(t *testing.T) {
ctx1 := rulecontext.WithPathString(context.Background(), "a")
ctx1 = rulecontext.WithPathString(ctx1, "b")
err1 := errors.Errorf(errors.CodeMin, ctx1, "short", "message1")

ctx2 := rulecontext.WithPathString(context.Background(), "a")
ctx2 = rulecontext.WithPathString(ctx2, "c")
err2 := errors.Errorf(errors.CodeMin, ctx2, "short", "message2")

collection := errors.Collection(err1, err2)

serializer := errors.JSONPathSerializer{}
filtered := collection.ForPathAs("$.a.b", serializer)

if len(filtered) != 1 {
t.Errorf("Expected 1 error, got: %d", len(filtered))
}
}

// TestForPathAs_WithDotNotationSerializer tests:
// - ForPathAs method works with dot notation serializer
func TestForPathAs_WithDotNotationSerializer(t *testing.T) {
ctx1 := rulecontext.WithPathString(context.Background(), "a")
ctx1 = rulecontext.WithPathString(ctx1, "b")
err1 := errors.Errorf(errors.CodeMin, ctx1, "short", "message1")

ctx2 := rulecontext.WithPathString(context.Background(), "a")
ctx2 = rulecontext.WithPathString(ctx2, "c")
err2 := errors.Errorf(errors.CodeMin, ctx2, "short", "message2")

collection := errors.Collection(err1, err2)

serializer := errors.DotNotationSerializer{}
filtered := collection.ForPathAs("a.b", serializer)

if len(filtered) != 1 {
t.Errorf("Expected 1 error, got: %d", len(filtered))
}
}

// TestForPathAs_NoMatches tests:
// - ForPathAs returns empty collection when no matches
func TestForPathAs_NoMatches(t *testing.T) {
ctx := rulecontext.WithPathString(context.Background(), "a")
ctx = rulecontext.WithPathString(ctx, "b")
err := errors.Errorf(errors.CodeMin, ctx, "short", "message")

collection := errors.Collection(err)

serializer := errors.DefaultPathSerializer{}
filtered := collection.ForPathAs("/nonexistent", serializer)

if len(filtered) != 0 {
t.Errorf("Expected empty collection, got: %d errors", len(filtered))
}
}

// TestForPathAs_EmptyCollection tests:
// - ForPathAs handles empty collection
func TestForPathAs_EmptyCollection(t *testing.T) {
collection := errors.Collection()

serializer := errors.DefaultPathSerializer{}
filtered := collection.ForPathAs("/a/b", serializer)

if len(filtered) != 0 {
t.Errorf("Expected nil or empty collection, got: %d errors", len(filtered))
}
}
85 changes: 85 additions & 0 deletions pkg/errors/path_as_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package errors_test

import (
"context"
"testing"

"proto.zip/studio/validate/pkg/errors"
"proto.zip/studio/validate/pkg/rulecontext"
)

// TestPathAs_WithDefaultSerializer tests:
// - PathAs method works with default serializer
func TestPathAs_WithDefaultSerializer(t *testing.T) {
ctx := rulecontext.WithPathString(context.Background(), "a")
ctx = rulecontext.WithPathString(ctx, "b")
err := errors.Errorf(errors.CodeMin, ctx, "short", "message")

serializer := errors.DefaultPathSerializer{}
path := err.PathAs(serializer)

if path == "" {
t.Error("Expected non-empty path")
}
}

// TestPathAs_WithJSONPointerSerializer tests:
// - PathAs method works with JSON Pointer serializer
func TestPathAs_WithJSONPointerSerializer(t *testing.T) {
ctx := rulecontext.WithPathString(context.Background(), "a")
ctx = rulecontext.WithPathString(ctx, "b")
err := errors.Errorf(errors.CodeMin, ctx, "short", "message")

serializer := errors.JSONPointerSerializer{}
path := err.PathAs(serializer)

expected := "/a/b"
if path != expected {
t.Errorf("Expected path to be '%s', got: '%s'", expected, path)
}
}

// TestPathAs_WithJSONPathSerializer tests:
// - PathAs method works with JSONPath serializer
func TestPathAs_WithJSONPathSerializer(t *testing.T) {
ctx := rulecontext.WithPathString(context.Background(), "a")
ctx = rulecontext.WithPathString(ctx, "b")
err := errors.Errorf(errors.CodeMin, ctx, "short", "message")

serializer := errors.JSONPathSerializer{}
path := err.PathAs(serializer)

expected := "$.a.b"
if path != expected {
t.Errorf("Expected path to be '%s', got: '%s'", expected, path)
}
}

// TestPathAs_WithDotNotationSerializer tests:
// - PathAs method works with dot notation serializer
func TestPathAs_WithDotNotationSerializer(t *testing.T) {
ctx := rulecontext.WithPathString(context.Background(), "a")
ctx = rulecontext.WithPathString(ctx, "b")
err := errors.Errorf(errors.CodeMin, ctx, "short", "message")

serializer := errors.DotNotationSerializer{}
path := err.PathAs(serializer)

expected := "a.b"
if path != expected {
t.Errorf("Expected path to be '%s', got: '%s'", expected, path)
}
}

// TestPathAs_EmptyPath tests:
// - PathAs returns empty string for errors without path
func TestPathAs_EmptyPath(t *testing.T) {
err := errors.Errorf(errors.CodeMin, context.Background(), "short", "message")

serializer := errors.DefaultPathSerializer{}
path := err.PathAs(serializer)

if path != "" {
t.Errorf("Expected empty path, got: '%s'", path)
}
}
30 changes: 30 additions & 0 deletions pkg/errors/path_serializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package errors

import (
"proto.zip/studio/validate/pkg/rulecontext"
)

// PathSerializer serializes an array of path segments into a string representation.
type PathSerializer interface {
Serialize(segments []rulecontext.PathSegment) string
}

// extractPathSegments extracts all segments from a PathSegment into an array,
// ordered from root to leaf (top to bottom).
// segment must not be nil (caller is responsible for checking).
func extractPathSegments(segment rulecontext.PathSegment) []rulecontext.PathSegment {
// First, collect all segments by traversing up to the root
var segments []rulecontext.PathSegment
current := segment
for current != nil {
segments = append(segments, current)
current = current.Parent()
}

// Reverse to get root-to-leaf order
for i, j := 0, len(segments)-1; i < j; i, j = i+1, j-1 {
segments[i], segments[j] = segments[j], segments[i]
}

return segments
}
Loading