diff --git a/ast/term.go b/ast/term.go index 9117da8f80..b3b61a9291 100644 --- a/ast/term.go +++ b/ast/term.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/json" "fmt" + "net/url" "regexp" "sort" "strconv" @@ -805,6 +806,27 @@ func EmptyRef() Ref { return Ref([]*Term{}) } +// PtrRef returns a new reference against the head for the pointer +// s. Path components in the pointer are unescaped. +func PtrRef(head *Term, s string) (Ref, error) { + s = strings.Trim(s, "/") + if s == "" { + return Ref{head}, nil + } + parts := strings.Split(s, "/") + ref := make(Ref, len(parts)+1) + ref[0] = head + for i := 0; i < len(parts); i++ { + var err error + parts[i], err = url.PathUnescape(parts[i]) + if err != nil { + return nil, err + } + ref[i+1] = StringTerm(parts[i]) + } + return ref, nil +} + // RefTerm creates a new Term with a Ref value. func RefTerm(r ...*Term) *Term { return &Term{Value: Ref(r)} @@ -967,6 +989,21 @@ func (ref Ref) IsNested() bool { return false } +// Ptr returns a slash-separated path string for this ref. If the ref +// contains non-string terms this function returns an error. Path +// components are escaped. +func (ref Ref) Ptr() (string, error) { + parts := make([]string, 0, len(ref)-1) + for _, term := range ref[1:] { + if str, ok := term.Value.(String); ok { + parts = append(parts, url.PathEscape(string(str))) + } else { + return "", fmt.Errorf("invalid path value type") + } + } + return strings.Join(parts, "/"), nil +} + var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$") func (ref Ref) String() string { diff --git a/ast/term_test.go b/ast/term_test.go index 2c586a6d57..970882487f 100644 --- a/ast/term_test.go +++ b/ast/term_test.go @@ -419,6 +419,49 @@ func TestRefConcat(t *testing.T) { } } +func TestRefPtr(t *testing.T) { + cases := []string{ + "", + "a", + "a/b", + "/a/b", + "/a/b/", + "a%2Fb", + } + + for _, tc := range cases { + ref, err := PtrRef(DefaultRootDocument.Copy(), tc) + if err != nil { + t.Fatal("Unexpected error:", err) + } + + ptr, err := ref.Ptr() + if err != nil { + t.Fatal("Unexpected error:", err) + } + + roundtrip, err := PtrRef(DefaultRootDocument.Copy(), ptr) + if err != nil { + t.Fatal("Unexpected error:", err) + } + + if !ref.Equal(roundtrip) { + t.Fatalf("Expected roundtrip of %q to be equal but got %v and %v", tc, ref, roundtrip) + } + } + + if _, err := PtrRef(DefaultRootDocument.Copy(), "2%"); err == nil { + t.Fatalf("Expected error from %q", "2%") + } + + ref := Ref{VarTerm("x"), IntNumberTerm(1)} + + if _, err := ref.Ptr(); err == nil { + t.Fatal("Expected error from x[1]") + } + +} + func TestSetEqual(t *testing.T) { tests := []struct { a string