github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/output/json_writer.go (about) 1 package output 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "strings" 9 10 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types" 11 ) 12 13 type FieldType int 14 15 var comma = "," 16 17 const ( 18 FieldArray FieldType = iota 19 FieldObject 20 ) 21 22 type position int 23 24 const ( 25 positionEmpty position = iota 26 positionInRoot 27 positionInArray 28 positionInObject 29 positionRootClosed 30 ) 31 32 type state = struct { 33 position 34 children int 35 } 36 37 type DefaultField struct { 38 Key string 39 FieldType 40 } 41 42 // JsonWriter can write JSON object in portions. 43 type JsonWriter struct { 44 // current state of the JSON object (are we inside of 45 // an array or object? are there any children?) 46 state state 47 // previous state is helpful when closing an object or 48 // array, so we will keep it as a stack 49 previousStates []state 50 // the writer that we will output to 51 outputWriter io.Writer 52 indentLevel int 53 indentString string 54 // errors to output 55 errs []string 56 // function to get meta data 57 GetMeta func() (*types.MetaData, error) 58 // flag indicating if we should output `meta` object as 59 // well 60 ShouldWriteMeta bool 61 // Should writer write newline after `Close` 62 ShouldWriteNewline bool 63 DefaultField 64 } 65 66 // NewJsonWriter creates JsonWriter with some useful defaults 67 func NewJsonWriter(w io.Writer) *JsonWriter { 68 return &JsonWriter{ 69 outputWriter: w, 70 indentString: " ", 71 GetMeta: func() (*types.MetaData, error) { 72 return &types.MetaData{}, nil 73 }, 74 } 75 } 76 77 func NewDefaultJsonWriter(w io.Writer, shouldWriteNewline bool) *JsonWriter { 78 jw := NewJsonWriter(w) 79 jw.DefaultField = DefaultField{ 80 Key: "data", 81 FieldType: FieldArray, 82 } 83 jw.ShouldWriteNewline = shouldWriteNewline 84 return jw 85 } 86 87 // popState removes the last item from state stack and returns it 88 func (w *JsonWriter) popState() (prevState state) { 89 lastIndex := len(w.previousStates) - 1 90 prevState = w.previousStates[lastIndex] 91 w.previousStates = w.previousStates[:lastIndex] 92 return 93 } 94 95 // writeBase writes directly to `outputWriter` without performing any 96 // additional operations. It is a foundation for all other Write* 97 // functions 98 func (w *JsonWriter) writeBase(toWrite []byte) (n int, err error) { 99 return w.outputWriter.Write(toWrite) 100 } 101 102 func (w *JsonWriter) writeNewline() (n int, err error) { 103 return w.writeBase([]byte("\n")) 104 } 105 106 // writeErrors writes `errors` array 107 func (w *JsonWriter) writeErrors() (n int, err error) { 108 return w.WriteCompoundItem("errors", w.errs) 109 } 110 111 // openRoot prints the beginning "{" 112 func (w *JsonWriter) openRoot() (n int, err error) { 113 // You can only open root once 114 if w.state.position != positionEmpty { 115 err = errors.New("root object is already opened") 116 return 117 } 118 119 n, err = w.writeBase([]byte("{")) 120 w.indentLevel = 1 121 w.previousStates = append(w.previousStates, w.state) 122 w.state.position = positionInRoot 123 w.state.children = 0 124 125 if w.DefaultField.Key != "" { 126 dn, err := w.OpenField(w.DefaultField.Key, w.DefaultField.FieldType) 127 n += dn 128 if err != nil { 129 return n, err 130 } 131 } 132 133 return 134 } 135 136 // GetPrefixForLevel returns indent string for given intent level 137 func (w *JsonWriter) GetPrefixForLevel(level int) (prefix string) { 138 if level == 0 { 139 return 140 } 141 return strings.Repeat(w.indentString, level) 142 } 143 144 // GetCurrentPrefix returns indent string for current indent level. 145 // The returned string can be passed to json.MarshalIndent as `prefix` 146 // to match this writer's indentation. 147 func (w *JsonWriter) GetCurrentPrefix() (prefix string) { 148 return w.GetPrefixForLevel(w.indentLevel) 149 } 150 151 // Indent writes indentation string 152 func (w *JsonWriter) Indent() { 153 prefix := w.GetCurrentPrefix() 154 _, _ = w.writeBase([]byte(prefix)) 155 } 156 157 // Write writes bytes p, adding indentation and comma before if needed. 158 // In most cases, you should use `WriteItem` instead. 159 func (w *JsonWriter) Write(p []byte) (n int, err error) { 160 if w.state.position == positionEmpty { 161 n, err = w.openRoot() 162 } 163 if err != nil { 164 return 165 } 166 if shouldInsertComma(w.state) { 167 bw, cerr := w.writeBase([]byte(comma)) 168 n += bw 169 err = cerr 170 if err != nil { 171 return 172 } 173 } 174 bw, err := w.writeNewline() 175 n += bw 176 if err != nil { 177 return 178 } 179 w.Indent() 180 if w.state.position == positionInArray || w.state.position == positionInObject || w.state.position == positionInRoot { 181 w.state.children++ 182 } 183 return w.writeBase(p) 184 } 185 186 // WriteItem writes `value` under the key `key` 187 func (w *JsonWriter) WriteItem(key string, value string) (n int, err error) { 188 if key == "" { 189 return w.Write([]byte(value)) 190 } 191 return w.Write([]byte(fmt.Sprintf(`"%s": %s`, key, value))) 192 } 193 194 // WriteError saves error to be written when the writer is `Close`d 195 func (w *JsonWriter) WriteError(err error) { 196 w.errs = append(w.errs, err.Error()) 197 } 198 199 // WriteCompoundItem makes it easier to write an object or array. 200 func (w *JsonWriter) WriteCompoundItem(key string, obj any) (n int, err error) { 201 if w.state.position == positionEmpty { 202 _, _ = w.openRoot() 203 } 204 prefix := w.GetPrefixForLevel(w.indentLevel) 205 marshalled, err := json.MarshalIndent(obj, prefix, w.indentString) 206 if err != nil { 207 return 208 } 209 return w.WriteItem(key, string(marshalled)) 210 } 211 212 // Close writes errors and meta data (if requested) and then the ending "}" 213 func (w *JsonWriter) Close() error { 214 defer func() { 215 if !w.ShouldWriteNewline { 216 return 217 } 218 _, _ = w.writeNewline() 219 }() 220 // If we didn't write anything, but there is default key, we need 221 // to write it 222 if w.state.position == positionEmpty && w.DefaultField.Key != "" { 223 _, _ = w.openRoot() 224 } 225 // CloseField if in field 226 if w.state.position == positionInArray { 227 _, _ = w.CloseField(FieldArray) 228 } 229 if w.state.position == positionInObject { 230 _, _ = w.CloseField(FieldObject) 231 } 232 // Print meta, if any 233 if w.ShouldWriteMeta { 234 meta, err := w.GetMeta() 235 if err != nil { 236 w.WriteError(err) 237 } 238 _, _ = w.WriteCompoundItem("meta", meta) 239 } 240 // Print errors, if any 241 if len(w.errs) > 0 { 242 _, _ = w.writeErrors() 243 } 244 // Print closing bracket 245 if w.state.position == positionEmpty && w.DefaultField.Key == "" { 246 _, err := w.writeBase([]byte("{}")) 247 return err 248 } 249 _, _ = w.CloseField(FieldObject) 250 return nil 251 } 252 253 // OpenField writes opening "[" or "{", depending on `state.position` 254 func (w *JsonWriter) OpenField(key string, fieldType FieldType) (n int, err error) { 255 if w.state.position == positionEmpty { 256 n, err = w.openRoot() 257 } 258 if err != nil { 259 return 260 } 261 262 if shouldInsertComma(w.state) { 263 bw, err := w.writeBase([]byte(comma)) 264 n += bw 265 if err != nil { 266 return n, err 267 } 268 } 269 270 _, _ = w.writeNewline() 271 w.Indent() 272 273 bracket := "[" 274 newPosition := positionInArray 275 if fieldType == FieldObject { 276 bracket = "{" 277 newPosition = positionInObject 278 } 279 280 if key == "" { 281 n, err = w.writeBase([]byte(bracket)) 282 } else { 283 n, err = w.writeBase([]byte(fmt.Sprintf(`"%s": %s`, key, bracket))) 284 } 285 if err != nil { 286 return 287 } 288 289 w.indentLevel++ 290 w.state.children++ 291 w.previousStates = append(w.previousStates, w.state) 292 w.state = state{ 293 position: newPosition, 294 children: 0, 295 } 296 return 297 } 298 299 // CloseField writes closing "]" or "}", depending on `state.position` 300 func (w *JsonWriter) CloseField(fieldType FieldType) (n int, err error) { 301 indentMod := -1 302 bracket := "]" 303 if fieldType == FieldObject { 304 bracket = "}" 305 } 306 w.indentLevel = w.indentLevel + indentMod 307 if w.state.children > 0 { 308 _, _ = w.writeNewline() 309 w.Indent() 310 } 311 n, err = w.writeBase([]byte(bracket)) 312 if err != nil { 313 return 314 } 315 previousState := w.popState() 316 w.state = previousState 317 318 return 319 } 320 321 func (w *JsonWriter) GetOutputWriter() *io.Writer { 322 return &w.outputWriter 323 } 324 325 // Helpers 326 327 func shouldInsertComma(s state) bool { 328 return s.children > 0 329 }