...

Source file src/github.com/qri-io/jsonpointer/pointer.go

Documentation: github.com/qri-io/jsonpointer

     1  // Package jsonpointer implements IETF rfc6901
     2  // JSON Pointers are a string syntax for
     3  // identifying a specific value within a JavaScript Object Notation
     4  // (JSON) document [RFC4627].  JSON Pointer is intended to be easily
     5  // expressed in JSON string values as well as Uniform Resource
     6  // Identifier (URI) [RFC3986] fragment identifiers.
     7  //
     8  // this package is intended to work like net/url from the go
     9  // standard library
    10  package jsonpointer
    11  
    12  import (
    13  	"fmt"
    14  	"net/url"
    15  	"strconv"
    16  	"strings"
    17  )
    18  
    19  const defaultPointerAllocationSize = 32
    20  
    21  // Parse parses str into a Pointer structure.
    22  // str may be a pointer or a url string.
    23  // If a url string, Parse will use the URL's fragment component
    24  // (the bit after the '#' symbol)
    25  func Parse(str string) (Pointer, error) {
    26  	// fast paths that skip url parse step
    27  	if len(str) == 0 || str == "#" {
    28  		return Pointer{}, nil
    29  	} else if str[0] == '/' {
    30  		return parse(str)
    31  	}
    32  
    33  	u, err := url.Parse(str)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	return parse(u.Fragment)
    38  }
    39  
    40  // IsEmpty is a utility function to check if the Pointer
    41  // is empty / nil equivalent
    42  func (p Pointer) IsEmpty() bool {
    43  	return len(p) == 0
    44  }
    45  
    46  // Head returns the root of the Pointer
    47  func (p Pointer) Head() *string {
    48  	if len(p) == 0 {
    49  		return nil
    50  	}
    51  	return &p[0]
    52  }
    53  
    54  // Tail returns everything after the Pointer head
    55  func (p Pointer) Tail() Pointer {
    56  	return Pointer(p[1:])
    57  }
    58  
    59  // The ABNF syntax of a JSON Pointer is:
    60  // json-pointer    = *( "/" reference-token )
    61  // reference-token = *( unescaped / escaped )
    62  // unescaped       = %x00-2E / %x30-7D / %x7F-10FFFF
    63  //    ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped'
    64  // escaped         = "~" ( "0" / "1" )
    65  //   ; representing '~' and '/', respectively
    66  func parse(str string) (Pointer, error) {
    67  	if len(str) == 0 {
    68  		return Pointer{}, nil
    69  	}
    70  
    71  	if str[0] != '/' {
    72  		return nil, fmt.Errorf("non-empty references must begin with a '/' character")
    73  	}
    74  	str = str[1:]
    75  
    76  	toks := strings.Split(str, separator)
    77  	for i, t := range toks {
    78  		toks[i] = unescapeToken(t)
    79  	}
    80  	return Pointer(toks), nil
    81  }
    82  
    83  // Pointer represents a parsed JSON pointer
    84  type Pointer []string
    85  
    86  // NewPointer creates a Pointer with a pre-allocated block of memory
    87  // to avoid repeated slice expansions
    88  func NewPointer() Pointer {
    89  	return make([]string, 0, defaultPointerAllocationSize)
    90  }
    91  
    92  // String implements the stringer interface for Pointer,
    93  // giving the escaped string
    94  func (p Pointer) String() (str string) {
    95  	for _, tok := range p {
    96  		str += "/" + escapeToken(tok)
    97  	}
    98  	return
    99  }
   100  
   101  // Eval evaluates a json pointer against a given root JSON document
   102  // Evaluation of a JSON Pointer begins with a reference to the root
   103  // value of a JSON document and completes with a reference to some value
   104  // within the document.  Each reference token in the JSON Pointer is
   105  // evaluated sequentially.
   106  func (p Pointer) Eval(data interface{}) (result interface{}, err error) {
   107  	result = data
   108  	for _, tok := range p {
   109  		if result, err = p.evalToken(tok, result); err != nil {
   110  			return nil, err
   111  		}
   112  	}
   113  	return
   114  }
   115  
   116  // Descendant returns a new pointer to a descendant of the current pointer
   117  // parsing the input path into components
   118  func (p Pointer) Descendant(path string) (Pointer, error) {
   119  	if !strings.HasPrefix(path, "/") {
   120  		path = "/" + path
   121  	}
   122  	dpath, err := parse(path)
   123  	if err != nil {
   124  		return p, err
   125  	}
   126  
   127  	if p.String() == "/" {
   128  		return dpath, nil
   129  	}
   130  
   131  	return append(p, dpath...), nil
   132  }
   133  
   134  // RawDescendant extends the pointer with 1 or more path tokens
   135  // The function itself is unsafe as it doesnt fully parse the input
   136  // and assumes the user is directly managing the pointer
   137  // This allows for much faster pointer management
   138  func (p Pointer) RawDescendant(path ...string) Pointer {
   139  	return append(p, path...)
   140  }
   141  
   142  // Evaluation of each reference token begins by decoding any escaped
   143  // character sequence.  This is performed by first transforming any
   144  // occurrence of the sequence '~1' to '/', and then transforming any
   145  // occurrence of the sequence '~0' to '~'.  By performing the
   146  // substitutions in this order, an implementation avoids the error of
   147  // turning '~01' first into '~1' and then into '/', which would be
   148  // incorrect (the string '~01' correctly becomes '~1' after
   149  // transformation).
   150  // The reference token then modifies which value is referenced according
   151  // to the following scheme:
   152  func (p Pointer) evalToken(tok string, data interface{}) (interface{}, error) {
   153  	switch ch := data.(type) {
   154  	case map[string]interface{}:
   155  		return ch[tok], nil
   156  	case []interface{}:
   157  		i, err := strconv.Atoi(tok)
   158  		if err != nil {
   159  			return nil, fmt.Errorf("invalid array index: %s", tok)
   160  		}
   161  		if i >= len(ch) {
   162  			return nil, fmt.Errorf("index %d exceeds array length of %d", i, len(ch))
   163  		}
   164  		return ch[i], nil
   165  	default:
   166  		return nil, fmt.Errorf("invalid JSON pointer: %s", p.String())
   167  	}
   168  }
   169  
   170  const (
   171  	separator        = "/"
   172  	escapedSeparator = "~1"
   173  	tilde            = "~"
   174  	escapedTilde     = "~0"
   175  )
   176  
   177  func unescapeToken(tok string) string {
   178  	tok = strings.Replace(tok, escapedSeparator, separator, -1)
   179  	return strings.Replace(tok, escapedTilde, tilde, -1)
   180  }
   181  
   182  func escapeToken(tok string) string {
   183  	tok = strings.Replace(tok, tilde, escapedTilde, -1)
   184  	return strings.Replace(tok, separator, escapedSeparator, -1)
   185  }
   186  

View as plain text