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  }