package afdata
import (
"fmt"
"io"
"strings"
)
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 CliEmitter struct {
writer 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 (e *CliEmitter) WithLogFields(provider func() map[string]any) *CliEmitter {
e.logFieldsFunc = provider
return e
}
func (e *CliEmitter) Emit(event Event) error {
envelope := event.Value()
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)
}
_, err := io.WriteString(e.writer, 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) EmitValidatedValue(value any) error {
if err := ValidateProtocolEvent(value, true); err != nil {
return err
}
envelope := value.(map[string]any)
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)
}
_, err := io.WriteString(e.writer, 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 BuildCliVersion(version string) map[string]any {
return map[string]any{
"kind": "result",
"result": map[string]any{
"code": "version",
"version": version,
},
"trace": map[string]any{},
}
}
func CliRenderVersion(name string, version string, format OutputFormat) string {
var rendered string
if format == "" {
rendered = fmt.Sprintf("%s %s", name, version)
} else {
rendered = Render(BuildCliVersion(version), format, OutputOptions{})
}
return strings.TrimRight(rendered, "\n") + "\n"
}
func CliHandleVersionOrContinue(args []string, name string, version string) (out string, handled bool, err error) {
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" || arg == "-V" {
versionRequested = true
i++
continue
}
if arg == "--json" {
if outputExplicit && outputFormat != OutputFormatJson {
err = fmt.Errorf("conflicting output formats: --json conflicts with previous output format")
} else {
outputFormat = OutputFormatJson
outputExplicit = true
}
i++
continue
}
if arg == "--output" || strings.HasPrefix(arg, "--output=") {
var value string
if strings.HasPrefix(arg, "--output=") {
value = strings.TrimPrefix(arg, "--output=")
i++
} else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
value = args[i+1]
i += 2
} else {
err = fmt.Errorf("missing value for --output: expected json, yaml, or plain")
i++
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
}
i++
}
if !versionRequested {
return "", false, nil
}
if err != nil {
return "", true, err
}
if outputExplicit {
return CliRenderVersion(name, version, outputFormat), true, nil
}
return CliRenderVersion(name, version, ""), true, nil
}