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