package afdata
import (
"errors"
"fmt"
"io"
"os"
"strings"
"syscall"
)
type OutputFormat string
const (
OutputFormatJson OutputFormat = "json"
OutputFormatYaml OutputFormat = "yaml"
OutputFormatPlain OutputFormat = "plain"
)
func CliParseOutput(s string) (OutputFormat, error) {
switch s {
case "json":
return OutputFormatJson, nil
case "yaml":
return OutputFormatYaml, nil
case "plain":
return OutputFormatPlain, nil
default:
return "", fmt.Errorf("invalid --output format %q: expected json, yaml, or plain", s)
}
}
type LogFilters struct {
filters []string
}
func CliParseLogFilters(entries []string) LogFilters {
var out []string
for _, entry := range entries {
s := strings.ToLower(strings.TrimSpace(entry))
if s == "" {
continue
}
duplicate := false
for _, existing := range out {
if existing == s {
duplicate = true
break
}
}
if !duplicate {
out = append(out, s)
}
}
return LogFilters{filters: out}
}
func (lf LogFilters) Enabled(event string) bool {
if len(lf.filters) == 0 {
return false
}
lower := strings.ToLower(event)
for _, filter := range lf.filters {
if filter == "all" {
return true
}
if strings.HasPrefix(lower, filter) {
return true
}
}
return false
}
func (lf LogFilters) IsEmpty() bool {
return len(lf.filters) == 0
}
func (lf LogFilters) Values() []string {
return append([]string(nil), lf.filters...)
}
func Render(value any, format OutputFormat, options OutputOptions) string {
switch format {
case OutputFormatYaml:
return renderYaml(value, options)
case OutputFormatPlain:
return renderPlain(value, options)
default:
return renderJSON(value, options)
}
}
type OutputTo string
const (
OutputToSplit OutputTo = "split"
OutputToStdout OutputTo = "stdout"
OutputToStderr OutputTo = "stderr"
)
func ParseOutputTo(value string) (OutputTo, error) {
switch value {
case "split":
return OutputToSplit, nil
case "stdout":
return OutputToStdout, nil
case "stderr":
return OutputToStderr, nil
default:
return "", fmt.Errorf("unsupported --output-to `%s`; expected split, stdout, or stderr", value)
}
}
type CliEmitter struct {
writer io.Writer
diagnostic io.Writer
format OutputFormat
outputOptions OutputOptions
terminalEmitted bool
logFieldsFunc func() map[string]any
}
func NewCliEmitter(writer io.Writer, format OutputFormat) *CliEmitter {
return NewCliEmitterWithOptions(writer, format, OutputOptions{})
}
func NewCliEmitterWithOptions(writer io.Writer, format OutputFormat, outputOptions OutputOptions) *CliEmitter {
return &CliEmitter{
writer: writer,
format: format,
outputOptions: outputOptions,
}
}
func NewCliEmitterFinite(resultWriter, diagnostic io.Writer, format OutputFormat) *CliEmitter {
return NewCliEmitterFiniteWithOptions(resultWriter, diagnostic, format, OutputOptions{})
}
func NewCliEmitterFiniteWithOptions(resultWriter, diagnostic io.Writer, format OutputFormat, outputOptions OutputOptions) *CliEmitter {
return &CliEmitter{
writer: resultWriter,
diagnostic: diagnostic,
format: format,
outputOptions: outputOptions,
}
}
func NewCliEmitterFromOutputTo(selector OutputTo, format OutputFormat) *CliEmitter {
return NewCliEmitterFromOutputToWithOptions(selector, format, OutputOptions{})
}
func NewCliEmitterFromOutputToWithOptions(selector OutputTo, format OutputFormat, outputOptions OutputOptions) *CliEmitter {
switch selector {
case OutputToStdout:
return NewCliEmitterWithOptions(os.Stdout, format, outputOptions)
case OutputToStderr:
return NewCliEmitterWithOptions(os.Stderr, format, outputOptions)
default: return NewCliEmitterFiniteWithOptions(os.Stdout, os.Stderr, format, outputOptions)
}
}
func (e *CliEmitter) WithLogFields(provider func() map[string]any) *CliEmitter {
e.logFieldsFunc = provider
return e
}
func (e *CliEmitter) Emit(event Event) error {
return e.writeEvent(event.Value())
}
func (e *CliEmitter) EmitValidatedValue(value any) error {
if err := ValidateProtocolEvent(value, true); err != nil {
return err
}
envelope := value.(map[string]any)
return e.writeEvent(envelope)
}
func (e *CliEmitter) writeEvent(envelope map[string]any) error {
kind, _ := envelope["kind"].(string)
switch kind {
case "log", "progress":
if e.terminalEmitted {
return fmt.Errorf("cannot emit non-terminal event after terminal event")
}
case "result", "error":
if e.terminalEmitted {
return fmt.Errorf("cannot emit duplicate terminal event")
}
default:
return fmt.Errorf("unsupported event kind %q", kind)
}
sink := e.writer
if e.diagnostic != nil && kind != "result" {
sink = e.diagnostic
}
_, err := io.WriteString(sink, Render(envelope, e.format, e.outputOptions)+"\n")
if err != nil {
return fmt.Errorf("failed to write CLI event: %w", err)
}
if kind == "result" || kind == "error" {
e.terminalEmitted = true
}
return nil
}
func (e *CliEmitter) EmitResult(payload any) error {
event := NewJSONResult(payload).Build()
return e.Emit(event)
}
func (e *CliEmitter) EmitError(code string, message string) error {
event, err := NewJSONError(code, message).Build()
if err != nil {
return err
}
return e.Emit(event)
}
func (e *CliEmitter) EmitProgress(message string) error {
event := NewJSONProgress(map[string]any{"message": message}).Build()
return e.Emit(event)
}
func (e *CliEmitter) EmitLog(level LogLevel, message string) error {
payload := map[string]any{"level": string(level), "message": message}
if e.logFieldsFunc != nil {
providerFields := e.logFieldsFunc()
for k, v := range providerFields {
if _, alreadySet := payload[k]; !alreadySet {
payload[k] = v
}
}
}
event := NewJSONLog(payload).Build()
return e.Emit(event)
}
func (e *CliEmitter) Finish(event Event, successCode int) int {
err := e.Emit(event)
switch {
case err == nil:
return successCode
case errors.Is(err, syscall.EPIPE):
return 0
default:
return 4
}
}
func (e *CliEmitter) FinishResult(payload any) int {
return e.Finish(NewJSONResult(payload).Build(), 0)
}
func BuildCliVersion(name, displayName, version, build string) map[string]any {
result := map[string]any{
"code": "version",
"name": name,
"version": version,
}
if displayName != "" {
result["display_name"] = displayName
}
if build != "" {
result["build"] = build
}
return map[string]any{
"kind": "result",
"result": result,
"trace": map[string]any{},
}
}
func CliRenderVersion(name, displayName, version, build string, format OutputFormat) string {
rendered := Render(BuildCliVersion(name, displayName, version, build), format, OutputOptions{})
return strings.TrimRight(rendered, "\n") + "\n"
}
func splitVersionFlag(arg string) (name string, value string, hasInline bool) {
if !strings.HasPrefix(arg, "-") || arg == "-" {
return "", "", false
}
body := arg
if idx := strings.IndexByte(arg, '='); idx >= 0 {
body = arg[:idx]
value = arg[idx+1:]
hasInline = true
}
return strings.TrimLeft(body, "-"), value, hasInline
}
func CliHandleVersionOrContinue(args []string, valueFlags []string, name, displayName, version, build string, defaultFormats ...OutputFormat) (out string, handled bool, err error) {
valueFlagSet := make(map[string]struct{}, len(valueFlags))
for _, flag := range valueFlags {
valueFlagSet[strings.TrimLeft(flag, "-")] = struct{}{}
}
versionRequested := false
outputFormat := OutputFormat("")
outputExplicit := false
for i := 0; i < len(args); {
arg := args[i]
if arg == "--" {
break
}
if !strings.HasPrefix(arg, "-") {
break
}
if arg == "--version" {
versionRequested = true
i++
continue
}
flagName, inlineValue, hasInline := splitVersionFlag(arg)
if flagName == "output-to" {
if !hasInline && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
i += 2
} else {
i++
}
continue
}
if flagName == "output" {
var value string
haveValue := false
switch {
case hasInline:
value = inlineValue
haveValue = true
i++
case i+1 < len(args) && !strings.HasPrefix(args[i+1], "-"):
value = args[i+1]
haveValue = true
i += 2
default:
i++
}
if !haveValue {
err = fmt.Errorf("missing value for --output: expected json, yaml, or plain")
continue
}
parsed, parseErr := CliParseOutput(value)
if parseErr != nil {
err = parseErr
} else if outputExplicit && outputFormat != parsed {
err = fmt.Errorf("conflicting output formats: --output %s conflicts with previous output format", value)
} else {
outputFormat = parsed
outputExplicit = true
}
continue
}
_, isValueFlag := valueFlagSet[flagName]
if isValueFlag && !hasInline && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
i += 2
} else {
i++
}
}
if !versionRequested {
return "", false, nil
}
if err != nil {
return "", true, err
}
format := outputFormat
if !outputExplicit {
switch len(defaultFormats) {
case 0:
format = OutputFormatJson
case 1:
format = defaultFormats[0]
default:
return "", true, fmt.Errorf("expected at most one default version output format")
}
}
return CliRenderVersion(name, displayName, version, build, format), true, nil
}