...

Source file src/github.com/launchdarkly/go-sdk-common/v3/ldattr/ref.go

Documentation: github.com/launchdarkly/go-sdk-common/v3/ldattr

     1  package ldattr
     2  
     3  import (
     4  	"encoding/json"
     5  	"strings"
     6  
     7  	"github.com/launchdarkly/go-sdk-common/v3/lderrors"
     8  
     9  	"github.com/launchdarkly/go-jsonstream/v3/jreader"
    10  )
    11  
    12  // Ref is an attribute name or path expression identifying a value within a Context.
    13  //
    14  // This type is mainly intended to be used internally by LaunchDarkly SDK and service code, where
    15  // efficiency is a major concern so it's desirable to do any parsing or preprocessing just once.
    16  // Applications are unlikely to need to use the Ref type directly.
    17  //
    18  // It can be used to retrieve a value with Context.GetValueForRef, or to identify an attribute or
    19  // nested value that should be considered private with Builder.PrivateRef (the SDK configuration
    20  // can also have a list of private attribute references).
    21  //
    22  // Parsing and validation are done at the time that the [NewRef] or [NewLiteralRef] constructor is called.
    23  // If a Ref instance was created from an invalid string, or if it is an uninitialized Ref{}, it is
    24  // considered invalid and its [Ref.Err] method will return a non-nil error.
    25  //
    26  // # Syntax
    27  //
    28  // The string representation of an attribute reference in LaunchDarkly JSON data uses the following
    29  // syntax:
    30  //
    31  // If the first character is not a slash, the string is interpreted literally as an attribute name.
    32  // An attribute name can contain any characters, but must not be empty.
    33  //
    34  // If the first character is a slash, the string is interpreted as a slash-delimited path where the
    35  // first path component is an attribute name, and each subsequent path component is the name of a
    36  // property in a JSON object. Any instances of the characters "/" or "~" in a path component are
    37  // escaped as "~1" or "~0" respectively. This syntax deliberately resembles JSON Pointer, but no JSON
    38  // Pointer behaviors other than those mentioned here are supported.
    39  //
    40  // # Examples
    41  //
    42  // Suppose there is a context whose JSON implementation looks like this:
    43  //
    44  //	{
    45  //	  "kind": "user",
    46  //	  "key": "value1",
    47  //	  "address": {
    48  //	    "street": {
    49  //	      "line1": "value2",
    50  //	      "line2": "value3"
    51  //	    },
    52  //	    "city": "value4"
    53  //	  },
    54  //	  "good/bad": "value5"
    55  //	}
    56  //
    57  // The attribute references "key" and "/key" would both point to "value1".
    58  //
    59  // The attribute reference "/address/street/line1" would point to "value2".
    60  //
    61  // The attribute references "good/bad" and "/good~1bad" would both point to "value5".
    62  type Ref struct {
    63  	err                 error
    64  	rawPath             string
    65  	singlePathComponent string
    66  	components          []string
    67  }
    68  
    69  // NewRef creates a Ref from a string. For the supported syntax and examples, see [Ref].
    70  //
    71  // This constructor always returns a Ref that preserves the original string, even if validation fails,
    72  // so that calling [Ref.String] (or serializing the Ref to JSON) will produce the original string. If
    73  // validation fails, [Ref.Err] will return a non-nil error and any SDK method that takes this Ref as a
    74  // parameter will consider it invalid.
    75  func NewRef(referenceString string) Ref {
    76  	if referenceString == "" || referenceString == "/" {
    77  		return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: referenceString}
    78  	}
    79  	if referenceString[0] != '/' {
    80  		// When there is no leading slash, this is a simple attribute reference with no character escaping.
    81  		return Ref{singlePathComponent: referenceString, rawPath: referenceString}
    82  	}
    83  	path := referenceString[1:]
    84  	if !strings.Contains(path, "/") {
    85  		// There's only one segment, so this is still a simple attribute reference. However, we still may
    86  		// need to unescape special characters.
    87  		if unescaped, ok := unescapePath(path); ok {
    88  			return Ref{singlePathComponent: unescaped, rawPath: referenceString}
    89  		}
    90  		return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString}
    91  	}
    92  	parts := strings.Split(path, "/")
    93  	ret := Ref{rawPath: referenceString, components: make([]string, 0, len(parts))}
    94  	for _, p := range parts {
    95  		if p == "" {
    96  			ret.err = lderrors.ErrAttributeExtraSlash{}
    97  			return ret
    98  		}
    99  		unescaped, ok := unescapePath(p)
   100  		if !ok {
   101  			return Ref{err: lderrors.ErrAttributeInvalidEscape{}, rawPath: referenceString}
   102  		}
   103  		ret.components = append(ret.components, unescaped)
   104  	}
   105  	return ret
   106  }
   107  
   108  // NewLiteralRef is similar to [NewRef] except that it always interprets the string as a literal
   109  // attribute name, never as a slash-delimited path expression. There is no escaping or unescaping,
   110  // even if the name contains literal '/' or '~' characters. Since an attribute name can contain
   111  // any characters, this method always returns a valid Ref unless the name is empty.
   112  //
   113  // For example: ldattr.NewLiteralRef("name") is exactly equivalent to ldattr.NewRef("name").
   114  // ldattr.NewLiteralRef("a/b") is exactly equivalent to ldattr.NewRef("a/b") (since the syntax
   115  // used by NewRef treats the whole string as a literal as long as it does not start with a slash),
   116  // or to ldattr.NewRef("/a~1b").
   117  func NewLiteralRef(attrName string) Ref {
   118  	if attrName == "" {
   119  		return Ref{err: lderrors.ErrAttributeEmpty{}, rawPath: attrName}
   120  	}
   121  	if attrName[0] != '/' {
   122  		// When there is no leading slash, this is a simple attribute reference with no character escaping.
   123  		return Ref{singlePathComponent: attrName, rawPath: attrName}
   124  	}
   125  	// If there is a leading slash, then the attribute name actually starts with a slash. To represent it
   126  	// as an Ref, it'll need to be escaped.
   127  	escapedPath := "/" + strings.ReplaceAll(strings.ReplaceAll(attrName, "~", "~0"), "/", "~1")
   128  	return Ref{singlePathComponent: attrName, rawPath: escapedPath}
   129  }
   130  
   131  // IsDefined returns true if the Ref has a value, meaning that it is not an uninitialized Ref{}.
   132  // That does not guarantee that the value is valid; use [Ref.Err] to test that.
   133  func (a Ref) IsDefined() bool {
   134  	return a.rawPath != "" || a.err != nil
   135  }
   136  
   137  // Equal returns true if the two Ref instances have the same value.
   138  //
   139  // You cannot compare Ref instances with the == operator, because the struct may contain a slice;
   140  // [reflect.DeepEqual] will work, but is less efficient.
   141  func (a Ref) Equal(other Ref) bool {
   142  	if a.err != other.err || a.rawPath != other.rawPath || a.singlePathComponent != other.singlePathComponent {
   143  		return false
   144  	}
   145  	return true
   146  	// We don't need to check the components slice, because it's impossible for the components to be different
   147  	// if rawPath is the same.
   148  }
   149  
   150  // Err returns nil for a valid Ref, or a non-nil error value for an invalid Ref.
   151  //
   152  // A Ref is invalid if the input string is empty, or starts with a slash but is not a valid
   153  // slash-delimited path, or starts with a slash and contains an invalid escape sequence. For a list of
   154  // the possible validation errors, see the [lderrors] package.
   155  //
   156  // Otherwise, the Ref is valid, but that does not guarantee that such an attribute exists in any
   157  // given Context. For instance, NewRef("name") is a valid Ref, but a specific Context might or might
   158  // not have a name.
   159  //
   160  // See comments on the Ref type for more details of the attribute reference syntax.
   161  func (a Ref) Err() error {
   162  	if a.err == nil && a.rawPath == "" {
   163  		return lderrors.ErrAttributeEmpty{}
   164  	}
   165  	return a.err
   166  }
   167  
   168  // Depth returns the number of path components in the Ref.
   169  //
   170  // For a simple attribute reference such as "name" with no leading slash, this returns 1.
   171  //
   172  // For an attribute reference with a leading slash, it is the number of slash-delimited path
   173  // components after the initial slash. For instance, NewRef("/a/b").Depth() returns 2.
   174  func (a Ref) Depth() int {
   175  	if a.err != nil || (a.singlePathComponent == "" && a.components == nil) {
   176  		return 0
   177  	}
   178  	if a.components == nil {
   179  		return 1
   180  	}
   181  	return len(a.components)
   182  }
   183  
   184  // Component retrieves a single path component from the attribute reference.
   185  //
   186  // For a simple attribute reference such as "name" with no leading slash, if index is zero,
   187  // Component returns the attribute name.
   188  //
   189  // For an attribute reference with a leading slash, if index is non-negative and less than
   190  // a.Depth(), Component returns the path component.
   191  //
   192  // If index is out of range, it returns "".
   193  //
   194  //	NewRef("a").Component(0)      // returns "a"
   195  //	NewRef("/a/b").Component(1)   // returns "b"
   196  func (a Ref) Component(index int) string {
   197  	if index == 0 && len(a.components) == 0 {
   198  		return a.singlePathComponent
   199  	}
   200  	if index < 0 || index >= len(a.components) {
   201  		return ""
   202  	}
   203  	return a.components[index]
   204  }
   205  
   206  // String returns the attribute reference as a string, in the same format used by NewRef().
   207  // If the Ref was created with [NewRef], this value is identical to the original string. If it
   208  // was created with [NewLiteralRef], the value may be different due to unescaping (for instance,
   209  // an attribute whose name is "/a" would be represented as "~1a".
   210  func (a Ref) String() string {
   211  	return a.rawPath
   212  }
   213  
   214  // MarshalJSON produces a JSON representation of the Ref. If it is an uninitialized Ref{}, this
   215  // is a JSON null token. Otherwise, it is a JSON string using the same value returned by [Ref.String].
   216  func (a Ref) MarshalJSON() ([]byte, error) {
   217  	if !a.IsDefined() {
   218  		return []byte(`null`), nil
   219  	}
   220  	return json.Marshal(a.String())
   221  }
   222  
   223  // UnmarshalJSON parses a Ref from a JSON value. If the value is null, the result is an
   224  // uninitialized Ref(). If the value is a string, it is passed to [NewRef]. Any other type
   225  // causes an error.
   226  //
   227  // A valid JSON string that is not valid as a Ref path (such as "" or "///") does not cause
   228  // UnmarshalJSON to return an error; instead, it stores the string in the Ref and the error
   229  // can be obtained from [Ref.Err]. This is deliberate, so that the LaunchDarkly SDK will be
   230  // able to parse a set of feature flag data even if one of the flags contains an invalid Ref.
   231  func (a *Ref) UnmarshalJSON(data []byte) error {
   232  	r := jreader.NewReader(data)
   233  	s, nonNull := r.StringOrNull()
   234  	if err := r.Error(); err != nil {
   235  		return err
   236  	}
   237  	if nonNull {
   238  		*a = NewRef(s)
   239  	} else {
   240  		*a = Ref{}
   241  	}
   242  	return nil
   243  }
   244  
   245  // Performs unescaping of attribute reference path components:
   246  //
   247  //   - "~1" becomes "/"
   248  //   - "~0" becomes "~"
   249  //   - "~" followed by any character other than "0" or "1" is invalid
   250  //
   251  // The second return value is true if successful, or false if there was an invalid escape sequence.
   252  func unescapePath(path string) (string, bool) {
   253  	// If there are no tildes then there's definitely nothing to do
   254  	if !strings.Contains(path, "~") {
   255  		return path, true
   256  	}
   257  	out := make([]byte, 0, 100) // arbitrary preallocated size - path components will almost always be shorter than this
   258  	for i := 0; i < len(path); i++ {
   259  		ch := path[i]
   260  		if ch != '~' {
   261  			out = append(out, ch)
   262  			continue
   263  		}
   264  		i++
   265  		if i >= len(path) {
   266  			return "", false
   267  		}
   268  		var unescaped byte
   269  		switch path[i] {
   270  		case '0':
   271  			unescaped = '~'
   272  		case '1':
   273  			unescaped = '/'
   274  		default:
   275  			return "", false
   276  		}
   277  		out = append(out, unescaped)
   278  	}
   279  	return string(out), true
   280  }
   281  

View as plain text