github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/output/stream.go (about)

     1  package output
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  	"sync"
    10  	"text/template"
    11  
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    13  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    14  )
    15  
    16  // OutputOptions allow more granular configuration of output details
    17  // TODO: This used to be "type OutputOptions = struct" (the '=' sign). Was that a typo or purposful? I couldn't embed it in the GlobalOptions data structure, so I removed the '='
    18  type OutputOptions struct {
    19  	// If set, hidden fields will be printed as well (depends on the format)
    20  	Verbose bool
    21  	// If set, the first line of "txt" and "csv" output will NOT (the keys) will squelched
    22  	NoHeader bool
    23  	// The format in which to print the output
    24  	Format string
    25  	// How to indent JSON output
    26  	JsonIndent string
    27  	// Chain name
    28  	Chain string
    29  	// Flag to check if we are in test mode
    30  	TestMode bool
    31  	// Output file name. If present, we will write output to this file
    32  	OutputFn string
    33  	// If true and OutputFn is non-empty, open OutputFn for appending (create if not present)
    34  	Append bool
    35  	// The writer
    36  	Writer io.Writer
    37  	// Extra options passed to model, for example command-specific output formatting flags
    38  	Extra map[string]any
    39  }
    40  
    41  var formatToSeparator = map[string]rune{
    42  	"csv": ',',
    43  	"txt": '\t',
    44  }
    45  
    46  // StreamWithTemplate executes a template `tmpl` over Model `model`
    47  func StreamWithTemplate(w io.Writer, model types.Model, tmpl *template.Template) error {
    48  	return tmpl.Execute(w, model.Data)
    49  }
    50  
    51  // StreamModel streams a single `Model`
    52  func StreamModel(w io.Writer, model types.Model, options OutputOptions) error {
    53  	if options.Format == "json" {
    54  		jw, ok := w.(*JsonWriter)
    55  		if !ok {
    56  			// This should never happen
    57  			panic("streaming JSON requires JsonWriter")
    58  		}
    59  		_, err := jw.WriteCompoundItem("", model.Data)
    60  		if err != nil {
    61  			return err
    62  		}
    63  		return nil
    64  	}
    65  
    66  	// Store map items as strings. All formats other than JSON need string data
    67  	strs := make([]string, 0, len(model.Order))
    68  	for _, key := range model.Order {
    69  		strs = append(strs, fmt.Sprint(model.Data[key]))
    70  	}
    71  
    72  	var separator rune
    73  	if len(options.Format) == 1 {
    74  		separator = rune(options.Format[0])
    75  	} else {
    76  		separator = formatToSeparator[options.Format]
    77  	}
    78  	if separator == 0 {
    79  		return fmt.Errorf("unknown format %s", options.Format)
    80  	}
    81  	outputWriter := csv.NewWriter(w)
    82  	outputWriter.Comma = rune(separator)
    83  	if !options.NoHeader { // notice double negative
    84  		_ = outputWriter.Write(model.Order)
    85  	}
    86  	_ = outputWriter.Write(strs)
    87  	// This Flushes for each printed item, but in the exchange the user gets
    88  	// the data printed as it comes
    89  	outputWriter.Flush()
    90  
    91  	err := outputWriter.Error()
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  func logErrors(errsToReport []string) {
   100  	for _, errMessage := range errsToReport {
   101  		logger.Error(errMessage)
   102  	}
   103  }
   104  
   105  type fetchDataFunc func(modelChan chan types.Modeler, errorChan chan error)
   106  
   107  // StreamMany outputs models as they are acquired
   108  func StreamMany(rCtx *RenderCtx, fetchData fetchDataFunc, options OutputOptions) error {
   109  	if rCtx.ModelChan != nil {
   110  		fetchData(rCtx.ModelChan, rCtx.ErrorChan)
   111  		return nil
   112  	}
   113  
   114  	errsToReport := make([]string, 0)
   115  
   116  	modelChan := make(chan types.Modeler)
   117  	errorChan := make(chan error)
   118  
   119  	first := true
   120  	go func() {
   121  		fetchData(modelChan, errorChan)
   122  		close(modelChan)
   123  		close(errorChan)
   124  	}()
   125  
   126  	isJson := options.Format == "json"
   127  	var jw *JsonWriter
   128  	if !isJson {
   129  		defer func() {
   130  			if len(errsToReport) == 0 {
   131  				return
   132  			}
   133  			logErrors(errsToReport)
   134  		}()
   135  	} else {
   136  		jw = options.Writer.(*JsonWriter)
   137  	}
   138  
   139  	// If user wants custom format, we have to prepare the template
   140  	customFormat := strings.Contains(options.Format, "{")
   141  	tmpl, err := template.New("").Parse(options.Format)
   142  	if customFormat && err != nil {
   143  		return err
   144  	}
   145  
   146  	errsMutex := sync.Mutex{}
   147  	for {
   148  		select {
   149  		case model, ok := <-modelChan:
   150  			if !ok {
   151  				return nil
   152  			}
   153  
   154  			// If the output is JSON and we are printing another item, put `,` in front of it
   155  			var err error
   156  			modelValue := model.Model(options.Chain, options.Format, options.Verbose, options.Extra)
   157  			if customFormat {
   158  				err = StreamWithTemplate(options.Writer, modelValue, tmpl)
   159  			} else {
   160  				err = StreamModel(options.Writer, modelValue, OutputOptions{
   161  					NoHeader:   !first || options.NoHeader,
   162  					Format:     options.Format,
   163  					JsonIndent: "  ",
   164  				})
   165  			}
   166  			if err != nil {
   167  				return err
   168  			}
   169  			first = false
   170  
   171  		case err, ok := <-errorChan:
   172  			if !ok {
   173  				continue
   174  			}
   175  			errsMutex.Lock()
   176  			if isJson {
   177  				jw.WriteError(err)
   178  			} else {
   179  				errsToReport = append(errsToReport, err.Error())
   180  			}
   181  			errsMutex.Unlock()
   182  
   183  		case <-rCtx.Ctx.Done():
   184  			err = rCtx.Ctx.Err()
   185  			if err == context.Canceled {
   186  				return nil
   187  			}
   188  			return err
   189  		}
   190  	}
   191  }