From 7df209106f3901a4cbf1a6560109b82758de91f0 Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Fri, 15 Feb 2019 07:17:13 -0800 Subject: [PATCH] ast: Add helper to convert ref to pointer string In the past we've written a bunch of ad-hoc code to convert refs to slash-separated/JSON Pointer-esque strings. These just add a helper to the ref struct to convert to/from that kind of string. Signed-off-by: Torin Sandall --- ast/term.go | 37 +++++++++++++++++++++++++++++++++++++ ast/term_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) 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