Skip to content
Open
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
13 changes: 11 additions & 2 deletions compose.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package lenspath

// this package provides ways to compose lenspath

// Compose creates a new Lenspath by appending additional lens segments to this Lenspath.
// The original Lenspath is not modified; a new instance is returned.
//
// This is useful for building up complex paths programmatically or reusing common path prefixes.
//
// Examples:
//
// base, _ := Create([]string{"users", "0"})
// namePath, _ := base.Compose([]Lens{"name"}) // users[0].name
// emailPath, _ := base.Compose([]Lens{"email"}) // users[0].email
// // base remains unchanged as []string{"users", "0"}
func (lp *Lenspath) Compose(lens []Lens) (*Lenspath, error) {
newlens := append(lp.lens, lens...)
copylens := make([]Lens, len(newlens))
Expand Down
26 changes: 20 additions & 6 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,37 @@ package lenspath

import "fmt"

// EmptyLensPathErr is returned when attempting to create a Lenspath with an empty lens slice.
type EmptyLensPathErr struct{}

func (e *EmptyLensPathErr) Error() string {
return "lenspath: must have at least one lens"
}

// InvalidLensPathErr is returned when a path cannot be traversed due to type mismatches
// or structural issues in the data.
type InvalidLensPathErr struct {
index int
errType InvalidLensPathErrType
}

// InvalidLensPathErrType describes the kind of path traversal error.
type InvalidLensPathErrType string

const (
ArrayExpectedErr InvalidLensPathErrType = "expected array (*)"
LensPathStoppedErr = "could not navigate further, end of structure reached"
CannotSetFieldErr = "cannot set field"
PathContainsArrErr = "path contains *; use Getter/Setter() instead"
PathDoesntContainsArrErr = "path does not contain *; use Get() instead"
// ArrayExpectedErr indicates a non-wildcard, non-numeric segment was used on an array/slice
ArrayExpectedErr InvalidLensPathErrType = "expected array (*)"
// LensPathStoppedErr indicates the path ended prematurely in the data structure
LensPathStoppedErr = "could not navigate further, end of structure reached"
// CannotSetFieldErr indicates a struct field cannot be modified
CannotSetFieldErr = "cannot set field"
// PathContainsArrErr indicates Get/Set was called on a path with wildcards (use Getter/Setter)
PathContainsArrErr = "path contains *; use Getter/Setter() instead"
// PathDoesntContainsArrErr indicates the path doesn't contain wildcards when expected
PathDoesntContainsArrErr = "path does not contain *; use Get() instead"
)

// NewInvalidLensPathErr creates a new InvalidLensPathErr with the given context.
func NewInvalidLensPathErr(index int, errType InvalidLensPathErrType) *InvalidLensPathErr {
return &InvalidLensPathErr{index, errType}
}
Expand All @@ -31,16 +41,20 @@ func (e *InvalidLensPathErr) Error() string {
return fmt.Sprintf("lenspath: %s; lens index: %d", e.errType, e.index)
}

// Is implements error matching for InvalidLensPathErr.
func (e *InvalidLensPathErr) Is(err error) bool {
_, ok := err.(*InvalidLensPathErr)
return ok
}

// InvalidSetParamErr is returned when setter parameters don't match expectations.
type InvalidSetParamErr string

const (
// ArrayParamExpectedErr indicates an array was expected for the set value
ArrayParamExpectedErr InvalidSetParamErr = "expected array for set value"
ParamSizeMismatchErr InvalidSetParamErr = "array param and structure field array length should match"
// ParamSizeMismatchErr indicates array parameter size doesn't match structure field array length
ParamSizeMismatchErr InvalidSetParamErr = "array param and structure field array length should match"
)

func (e InvalidSetParamErr) Error() string {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/covalenthq/lenspath
module github.com/loganpowell/lenspath

go 1.19
202 changes: 202 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package lenspath_test

import (
"encoding/json"
"testing"

"github.com/loganpowell/lenspath"
)

// parseDotPath converts a dot-separated path string into path segments.
// Example: "delivery_items.0.description" -> []string{"delivery_items", "0", "description"}
func parseDotPath(path string) []string {
result := []string{}
current := ""

for _, char := range path {
if char == '.' {
if current != "" {
result = append(result, current)
current = ""
}
} else {
current += string(char)
}
}

if current != "" {
result = append(result, current)
}

return result
}

// setValueAtPath is a helper that combines path parsing and value setting.
func setValueAtPath(data map[string]any, path string, value any) error {
pathParts := parseDotPath(path)
lens, err := lenspath.Create(pathParts)
if err != nil {
return err
}
return lens.Set(data, value)
}

// TestIntegrationDotPathSyntax tests building complex nested structures using dot-path syntax.
// This validates that the library can handle real-world scenarios of building data structures
// from scratch with mixed maps, arrays, and nested objects.
func TestIntegrationDotPathSyntax(t *testing.T) {
// Start with empty map
result := make(map[string]any)

// Set array values
if err := setValueAtPath(result, "delivery_items.0.description", "Test Item"); err != nil {
t.Fatalf("Failed to set delivery_items.0.description: %v", err)
}
if err := setValueAtPath(result, "delivery_items.0.quantity", 10); err != nil {
t.Fatalf("Failed to set delivery_items.0.quantity: %v", err)
}
if err := setValueAtPath(result, "delivery_items.0.hs_code", "123456"); err != nil {
t.Fatalf("Failed to set delivery_items.0.hs_code: %v", err)
}
if err := setValueAtPath(result, "delivery_items.1.description", "Second Item"); err != nil {
t.Fatalf("Failed to set delivery_items.1.description: %v", err)
}
if err := setValueAtPath(result, "delivery_items.1.quantity", 20); err != nil {
t.Fatalf("Failed to set delivery_items.1.quantity: %v", err)
}

// Set nested object values
if err := setValueAtPath(result, "delivery_detail.invoice_number", "INV-001"); err != nil {
t.Fatalf("Failed to set delivery_detail.invoice_number: %v", err)
}
if err := setValueAtPath(result, "delivery_detail.ship_from_city", "TestCity"); err != nil {
t.Fatalf("Failed to set delivery_detail.ship_from_city: %v", err)
}

// Set top-level values
if err := setValueAtPath(result, "pid_id", "PID-123"); err != nil {
t.Fatalf("Failed to set pid_id: %v", err)
}
if err := setValueAtPath(result, "vat_rate", 15.0); err != nil {
t.Fatalf("Failed to set vat_rate: %v", err)
}

// Verify structure - top-level keys
if result["pid_id"] != "PID-123" {
t.Errorf("Expected pid_id to be %q, got %v", "PID-123", result["pid_id"])
}
if result["vat_rate"] != 15.0 {
t.Errorf("Expected vat_rate to be %v, got %v", 15.0, result["vat_rate"])
}

// Verify delivery_items array
deliveryItems, ok := result["delivery_items"].([]any)
if !ok {
t.Fatalf("Expected delivery_items to be []any, got %T", result["delivery_items"])
}
if len(deliveryItems) != 2 {
t.Fatalf("Expected delivery_items length 2, got %d", len(deliveryItems))
}

// Verify first item
item0, ok := deliveryItems[0].(map[string]any)
if !ok {
t.Fatalf("Expected delivery_items[0] to be map, got %T", deliveryItems[0])
}
if item0["description"] != "Test Item" {
t.Errorf("Expected description %q, got %v", "Test Item", item0["description"])
}
if item0["quantity"] != 10 {
t.Errorf("Expected quantity %v, got %v", 10, item0["quantity"])
}
if item0["hs_code"] != "123456" {
t.Errorf("Expected hs_code %q, got %v", "123456", item0["hs_code"])
}

// Verify second item
item1, ok := deliveryItems[1].(map[string]any)
if !ok {
t.Fatalf("Expected delivery_items[1] to be map, got %T", deliveryItems[1])
}
if item1["description"] != "Second Item" {
t.Errorf("Expected description %q, got %v", "Second Item", item1["description"])
}
if item1["quantity"] != 20 {
t.Errorf("Expected quantity %v, got %v", 20, item1["quantity"])
}

// Verify delivery_detail
deliveryDetail, ok := result["delivery_detail"].(map[string]any)
if !ok {
t.Fatalf("Expected delivery_detail to be map, got %T", result["delivery_detail"])
}
if deliveryDetail["invoice_number"] != "INV-001" {
t.Errorf("Expected invoice_number %q, got %v", "INV-001", deliveryDetail["invoice_number"])
}
if deliveryDetail["ship_from_city"] != "TestCity" {
t.Errorf("Expected ship_from_city %q, got %v", "TestCity", deliveryDetail["ship_from_city"])
}

// Optional: Log the JSON for visual verification
jsonBytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
t.Logf("Result:\n%s", string(jsonBytes))
}

// TestParseDotPath verifies the dot path parsing helper function.
func TestParseDotPath(t *testing.T) {
tests := []struct {
input string
want []string
}{
{
input: "simple",
want: []string{"simple"},
},
{
input: "one.two",
want: []string{"one", "two"},
},
{
input: "delivery_items.0.description",
want: []string{"delivery_items", "0", "description"},
},
{
input: "a.b.c.d.e",
want: []string{"a", "b", "c", "d", "e"},
},
{
input: "",
want: []string{},
},
{
input: "trailing.",
want: []string{"trailing"},
},
{
input: ".leading",
want: []string{"leading"},
},
{
input: "multiple..dots",
want: []string{"multiple", "dots"},
},
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := parseDotPath(tt.input)
if len(got) != len(tt.want) {
t.Errorf("parseDotPath(%q) length = %d, want %d", tt.input, len(got), len(tt.want))
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("parseDotPath(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
})
}
}
Loading