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 }