bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package output
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
"github.com/itchyny/gojq"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
)
|
|
|
|
// JqFilter applies a jq expression to data and writes the results to w.
|
|
// Scalar values are printed raw (no quotes for strings), matching jq -r behavior.
|
|
// Complex values (maps, arrays) are printed as indented JSON with Go's default
|
|
// HTML escaping (<, >, & → <, >, &).
|
|
func JqFilter(w io.Writer, data interface{}, expr string) error {
|
|
return jqFilter(w, data, expr, false)
|
|
}
|
|
|
|
// JqFilterRaw is like JqFilter but disables HTML escaping when re-marshaling
|
|
// complex jq results. Use it alongside OutRaw when the upstream envelope
|
|
// carries XML/HTML content that must survive --jq '.data.document' style
|
|
// projections without getting mangled into < escapes.
|
|
func JqFilterRaw(w io.Writer, data interface{}, expr string) error {
|
|
return jqFilter(w, data, expr, true)
|
|
}
|
|
|
|
func jqFilter(w io.Writer, data interface{}, expr string, raw bool) error {
|
|
query, err := gojq.Parse(expr)
|
|
if err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid jq expression: %s", err).WithCause(err)
|
|
}
|
|
code, err := gojq.Compile(query)
|
|
if err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid jq expression: %s", err).WithCause(err)
|
|
}
|
|
|
|
// Normalize data through toGeneric so typed structs become map[string]any.
|
|
normalized := toGeneric(data)
|
|
// Convert json.Number values to gojq-compatible types.
|
|
normalized = convertNumbers(normalized)
|
|
|
|
iter := code.Run(normalized)
|
|
for {
|
|
v, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if err, isErr := v.(error); isErr {
|
|
return errs.NewAPIError(errs.SubtypeUnknown, "jq error: %s", err).WithCause(err)
|
|
}
|
|
if err := writeJqValue(w, v, raw); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateJqFlags checks --jq flag compatibility with --output and --format flags,
|
|
// and validates the jq expression syntax. Returns nil if jqExpr is empty.
|
|
func ValidateJqFlags(jqExpr, outputFlag, format string) error {
|
|
if jqExpr == "" {
|
|
return nil
|
|
}
|
|
if outputFlag != "" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--jq and --output are mutually exclusive")
|
|
}
|
|
if format != "" && format != "json" {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--jq and --format %s are mutually exclusive", format)
|
|
}
|
|
return ValidateJqExpression(jqExpr)
|
|
}
|
|
|
|
// ValidateJqExpression checks whether a jq expression is syntactically valid.
|
|
func ValidateJqExpression(expr string) error {
|
|
query, err := gojq.Parse(expr)
|
|
if err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid jq expression: %s", err).WithCause(err)
|
|
}
|
|
_, err = gojq.Compile(query)
|
|
if err != nil {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid jq expression: %s", err).WithCause(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeJqValue writes a single jq result value to w.
|
|
// Scalars are printed raw; complex values as indented JSON.
|
|
// When raw is true, HTML escaping is disabled on complex values so that
|
|
// embedded XML/HTML content is preserved as-is.
|
|
func writeJqValue(w io.Writer, v interface{}, raw bool) error {
|
|
switch val := v.(type) {
|
|
case nil:
|
|
fmt.Fprintln(w, "null")
|
|
case bool:
|
|
fmt.Fprintln(w, val)
|
|
case int:
|
|
fmt.Fprintln(w, val)
|
|
case float64:
|
|
// Use %g to avoid trailing zeros, matching jq behavior.
|
|
fmt.Fprintf(w, "%g\n", val)
|
|
case *big.Int:
|
|
fmt.Fprintln(w, val.String())
|
|
case string:
|
|
// Raw output for strings (no quotes), matching jq -r.
|
|
fmt.Fprintln(w, val)
|
|
default:
|
|
// Complex value (map, array): indented JSON.
|
|
if raw {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(v); err != nil {
|
|
return errs.NewInternalError(errs.SubtypeSDKError, "failed to marshal jq result: %s", err).WithCause(err)
|
|
}
|
|
return nil
|
|
}
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return errs.NewInternalError(errs.SubtypeSDKError, "failed to marshal jq result: %s", err).WithCause(err)
|
|
}
|
|
fmt.Fprintln(w, string(b))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// convertNumbers recursively converts json.Number values to int or float64
|
|
// so that gojq can process them correctly.
|
|
func convertNumbers(v interface{}) interface{} {
|
|
switch val := v.(type) {
|
|
case json.Number:
|
|
if i, err := val.Int64(); err == nil {
|
|
return int(i)
|
|
}
|
|
if f, err := val.Float64(); err == nil {
|
|
return f
|
|
}
|
|
// Fallback: return as string (shouldn't happen for valid JSON numbers).
|
|
return val.String()
|
|
case map[string]interface{}:
|
|
for k, elem := range val {
|
|
val[k] = convertNumbers(elem)
|
|
}
|
|
return val
|
|
case []interface{}:
|
|
for i, elem := range val {
|
|
val[i] = convertNumbers(elem)
|
|
}
|
|
return val
|
|
default:
|
|
return v
|
|
}
|
|
}
|