Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

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
}
}