...

Source file src/github.com/launchdarkly/go-jsonstream/v3/jreader/reader_object.go

Documentation: github.com/launchdarkly/go-jsonstream/v3/jreader

     1  package jreader
     2  
     3  // ObjectState is returned by Reader's Object and ObjectOrNull methods. Use it in conjunction with
     4  // Reader to iterate through a JSON object. To read the value of each object property, you will
     5  // still use the Reader's methods. Properties may appear in any order.
     6  //
     7  // This example reads an object whose values are strings; if there is a null instead of an object,
     8  // it behaves the same as for an empty object. Note that it is not necessary to check for an error
     9  // result before iterating over the ObjectState, or to break out of the loop if String causes an
    10  // error, because the ObjectState's Next method will return false if the Reader has had any errors.
    11  //
    12  //	values := map[string]string
    13  //	for obj := r.ObjectOrNull(); obj.Next(); {
    14  //	    key := string(obj.Name())
    15  //	    if s := r.String(); r.Error() == nil {
    16  //	        values[key] = s
    17  //	    }
    18  //	}
    19  //
    20  // The next example reads an object with two expected property names, "a" and "b". Any unrecognized
    21  // properties are ignored.
    22  //
    23  //	var result struct {
    24  //	    a int
    25  //	    b int
    26  //	}
    27  //	for obj := r.ObjectOrNull(); obj.Next(); {
    28  //	    switch string(obj.Name()) {
    29  //	    case "a":
    30  //	        result.a = r.Int()
    31  //	    case "b":
    32  //	        result.b = r.Int()
    33  //	    }
    34  //	}
    35  //
    36  // If the schema requires certain properties to always be present, the WithRequiredProperties method is
    37  // a convenient way to enforce this.
    38  type ObjectState struct {
    39  	r                     *Reader
    40  	afterFirst            bool
    41  	name                  []byte
    42  	requiredProps         []string
    43  	requiredPropsFound    []bool
    44  	requiredPropsPrealloc [20]bool // used as initial base array for requiredPropsFound to avoid allocation
    45  }
    46  
    47  // WithRequiredProperties adds a requirement that the specified JSON property name(s) must appear
    48  // in the JSON object at some point before it ends.
    49  //
    50  // This method returns a new, modified ObjectState. It should be called before the first time you
    51  // call Next. For instance:
    52  //
    53  //	requiredProps := []string{"key", "name"}
    54  //	for obj := reader.Object().WithRequiredProperties(requiredProps); obj.Next(); {
    55  //	    switch string(obj.Name()) { ... }
    56  //	}
    57  //
    58  // When the end of the object is reached (and Next() returns false), if one of the required
    59  // properties has not yet been seen, and no other error has occurred, the Reader's error state
    60  // will be set to a RequiredPropertyError.
    61  //
    62  // For efficiency, it is best to preallocate the list of property names globally rather than creating
    63  // it inline.
    64  func (obj ObjectState) WithRequiredProperties(requiredProps []string) ObjectState {
    65  	ret := obj
    66  	if len(requiredProps) > 0 {
    67  		ret.requiredProps = requiredProps
    68  	}
    69  	return ret
    70  }
    71  
    72  // IsDefined returns true if the ObjectState represents an actual object, or false if it was
    73  // parsed from a null value or was the result of an error. If IsDefined is false, Next will
    74  // always return false. The zero value ObjectState{} returns false for IsDefined.
    75  func (obj *ObjectState) IsDefined() bool {
    76  	return obj.r != nil
    77  }
    78  
    79  // Next checks whether an object property is available and returns true if so. It returns false
    80  // if the Reader has reached the end of the object, or if any previous Reader operation failed,
    81  // or if the object was empty or null.
    82  //
    83  // If Next returns true, you can then get the property name with Name, and use Reader methods
    84  // such as Bool or String to read the property value. If you do not care about the value, simply
    85  // calling Next again without calling a Reader method will discard the value, just as if you had
    86  // called SkipValue on the reader.
    87  //
    88  // See ObjectState for example code.
    89  func (obj *ObjectState) Next() bool {
    90  	if obj.r == nil || obj.r.err != nil {
    91  		return false
    92  	}
    93  	var isEnd bool
    94  	var err error
    95  	if !obj.afterFirst && len(obj.requiredProps) != 0 {
    96  		// Initialize the bool slice that we'll use to keep track of what properties we found.
    97  		// See comment on requiredPropsFoundSlice().
    98  		if len(obj.requiredProps) > len(obj.requiredPropsPrealloc) {
    99  			obj.requiredPropsFound = make([]bool, len(obj.requiredProps))
   100  		}
   101  	}
   102  
   103  	if obj.afterFirst {
   104  		if obj.r.awaitingReadValue {
   105  			if err := obj.r.SkipValue(); err != nil {
   106  				return false
   107  			}
   108  		}
   109  		isEnd, err = obj.r.tr.EndDelimiterOrComma('}')
   110  	} else {
   111  		obj.afterFirst = true
   112  		isEnd, err = obj.r.tr.Delimiter('}')
   113  	}
   114  	if err != nil {
   115  		obj.r.AddError(err)
   116  		return false
   117  	}
   118  	if isEnd {
   119  		obj.name = nil
   120  		if obj.requiredProps != nil {
   121  			found := obj.requiredPropsFoundSlice()
   122  			for i, requiredName := range obj.requiredProps {
   123  				if !found[i] {
   124  					obj.r.AddError(RequiredPropertyError{Name: requiredName, Offset: obj.r.tr.LastPos()})
   125  					break
   126  				}
   127  			}
   128  		}
   129  		return false
   130  	}
   131  	name, err := obj.r.tr.PropertyName()
   132  	if err != nil {
   133  		obj.r.AddError(err)
   134  		return false
   135  	}
   136  	obj.name = name
   137  	obj.r.awaitingReadValue = true
   138  	if obj.requiredProps != nil {
   139  		found := obj.requiredPropsFoundSlice()
   140  		for i, requiredName := range obj.requiredProps {
   141  			if requiredName == string(name) {
   142  				found[i] = true
   143  				break
   144  			}
   145  		}
   146  	}
   147  	return true
   148  }
   149  
   150  // Name returns the name of the current object property, or nil if there is no current property
   151  // (that is, if Next returned false or if Next was never called).
   152  //
   153  // For efficiency, to avoid allocating a string for each property name, the name is returned as a
   154  // byte slice which may refer directly to the source data. Casting this to a string within a simple
   155  // comparison expression or switch statement should not cause a string allocation; the Go compiler
   156  // optimizes these into direct byte-slice comparisons.
   157  func (obj *ObjectState) Name() []byte {
   158  	return obj.name
   159  }
   160  
   161  // This technique of using either a preallocated fixed-length array or a slice (where we have
   162  // only set the slice to a non-nil value if we determined that the array wasn't big enough) is a
   163  // way to avoid unnecessary heap allocations: if the ObjectState is on the stack, the fixed-length
   164  // array can stay on the stack too. In order for this to work, we *cannot* set the slice to refer
   165  // to the array (obj.requiredProps = obj.requiredPropsFound[0:len(obj.requiredProps)]); the Go
   166  // compiler can't prove that that's safe, so it will make everything escape to the heap. Instead
   167  // we have to conditionally reference one or the other here.
   168  func (obj *ObjectState) requiredPropsFoundSlice() []bool {
   169  	if obj.requiredPropsFound != nil {
   170  		return obj.requiredPropsFound
   171  	}
   172  	return obj.requiredPropsPrealloc[0:len(obj.requiredProps)]
   173  }
   174  

View as plain text