Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,7 @@ func addOutputFlag() {
dciCmd.PersistentFlags().IntP("table-max-col-width", "X", 0, "Maximum width per column when fitting or wrapping (0 = auto)")
dciCmd.PersistentFlags().StringP("customer-context", "D", "", "Override the active customer context for this command (e.g. acme.com)")
dciCmd.PersistentFlags().String("fields", "", "Comma-separated response fields to include")
dciCmd.PersistentFlags().String("exclude", "", "Comma-separated response fields to exclude")
dciCmd.PersistentFlags().String("exclude", "", "Comma-separated top-level fields to exclude from response items or wrappers")
dciCmd.PersistentFlags().Bool("full", false, "Return the full response without agent-oriented truncation")
dciCmd.PersistentFlags().Bool("no-truncate", false, "Disable long-value truncation")
dciCmd.PersistentFlags().Bool("yes", false, "Confirm a destructive operation")
Expand Down
183 changes: 141 additions & 42 deletions output_contract.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"strings"

"github.com/rest-sh/restish/cli"
Expand All @@ -13,7 +14,18 @@ func shapeResponseBody(body interface{}) interface{} {
fields := commaSeparatedValues(viper.GetString("agent-fields"))
excluded := commaSeparatedValues(viper.GetString("agent-exclude"))
if len(fields) > 0 {
body = projectResponseValue(body, fields)
matchedFields := make(map[string]bool, len(fields))
var hasComparableRows bool
body, hasComparableRows = projectResponseValue(body, fields, matchedFields)
missingFields := make([]string, 0, len(fields))
for _, field := range fields {
if !matchedFields[field] {
missingFields = append(missingFields, field)
}
}
if hasComparableRows && len(missingFields) > 0 && cli.Stderr != nil {
fmt.Fprintf(cli.Stderr, "warning: requested fields not present in the response: %s\n", strings.Join(missingFields, ", "))
}
}
if len(excluded) > 0 {
body = excludeResponseValue(body, excluded)
Expand Down Expand Up @@ -58,34 +70,38 @@ func commaSeparatedValues(value string) []string {
return result
}

func projectResponseValue(value interface{}, fields []string) interface{} {
func projectResponseValue(value interface{}, fields []string, matchedFields map[string]bool) (interface{}, bool) {
switch item := value.(type) {
case []interface{}:
return projectRows(item, fields)
return projectRows(item, fields, matchedFields)
case map[string]interface{}:
if result, ok := projectNestedRows(item, fields); ok {
return result
if result, hasComparableRows, ok := projectNestedRows(item, fields, matchedFields); ok {
return result, hasComparableRows
}
if key, rows, ok := listWrapperRows(item); ok {
result := copyObject(item)
result[key] = projectRows(rows, fields)
return result
projectedRows, hasComparableRows := projectRows(rows, fields, matchedFields)
result[key] = projectedRows
return result, hasComparableRows
}
return projectObject(item, fields)
return projectObject(item, fields, matchedFields)
default:
return value
return value, false
}
}

func projectRows(rows []interface{}, fields []string) []interface{} {
func projectRows(rows []interface{}, fields []string, matchedFields map[string]bool) ([]interface{}, bool) {
result := make([]interface{}, len(rows))
hasComparableRows := false
for index, row := range rows {
result[index] = projectObject(row, fields)
var rowComparable bool
result[index], rowComparable = projectObject(row, fields, matchedFields)
hasComparableRows = hasComparableRows || rowComparable
}
return result
return result, hasComparableRows
}

func projectNestedRows(root map[string]interface{}, fields []string) (map[string]interface{}, bool) {
func projectNestedRows(root map[string]interface{}, fields []string, matchedFields map[string]bool) (map[string]interface{}, bool, bool) {
for _, key := range []string{"result", "results"} {
container, ok := root[key].(map[string]interface{})
if !ok {
Expand All @@ -97,27 +113,32 @@ func projectNestedRows(root map[string]interface{}, fields []string) (map[string
}
result := copyObject(root)
projectedContainer := copyObject(container)
projectedContainer["rows"] = projectSchemaRows(rows, readReportSchemaColumnNames(container["schema"]), fields)
projectedRows, hasComparableRows := projectSchemaRows(rows, readReportSchemaColumnNames(container["schema"]), fields, matchedFields)
projectedContainer["rows"] = projectedRows
result[key] = projectedContainer
return result, true
return result, hasComparableRows, true
}
return nil, false
return nil, false, false
}

func projectSchemaRows(rows []interface{}, schema []string, fields []string) []interface{} {
func projectSchemaRows(rows []interface{}, schema []string, fields []string, matchedFields map[string]bool) ([]interface{}, bool) {
result := make([]interface{}, len(rows))
hasComparableRows := false
for index, row := range rows {
if cells, ok := row.([]interface{}); ok {
object := make(map[string]interface{}, len(cells))
for cellIndex, cell := range cells {
object[reportColumnName(schema, cellIndex)] = cell
}
result[index] = projectObject(object, fields)
result[index], _ = projectObject(object, fields, matchedFields)
hasComparableRows = true
continue
}
result[index] = projectObject(row, fields)
var rowComparable bool
result[index], rowComparable = projectObject(row, fields, matchedFields)
hasComparableRows = hasComparableRows || rowComparable
}
return result
return result, hasComparableRows
}

func listWrapperRows(object map[string]interface{}) (string, []interface{}, bool) {
Expand Down Expand Up @@ -163,53 +184,131 @@ func copyObject(object map[string]interface{}) map[string]interface{} {
return result
}

func projectObject(value interface{}, fields []string) interface{} {
func projectObject(value interface{}, fields []string, matchedFields map[string]bool) (interface{}, bool) {
object, ok := value.(map[string]interface{})
if !ok {
return value
return value, false
}
result := make(map[string]interface{}, len(fields))
for _, field := range fields {
if child, exists := object[field]; exists {
result[field] = child
matchedFields[field] = true
}
}
return result
return result, len(object) > 0
}

func excludeResponseValue(value interface{}, excluded []string) interface{} {
excludedSet := make(map[string]bool, len(excluded))
for _, field := range excluded {
excludedSet[field] = true
}
return transformResponseObjects(value, func(object map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{}, len(object))
for key, child := range object {
switch item := value.(type) {
case []interface{}:
return excludeRows(item, excludedSet)
case map[string]interface{}:
if result, ok := excludeNestedRows(item, excludedSet); ok {
return result
}
if key, rows, ok := listWrapperRows(item); ok {
Comment thread
chaim0m marked this conversation as resolved.
result := excludeObject(item, excludedSet).(map[string]interface{})
if !excludedSet[key] {
result[key] = child
result[key] = excludeRows(rows, excludedSet)
}
return result
}
return result
})
return excludeObject(item, excludedSet)
default:
return value
}
}

func transformResponseObjects(value interface{}, transform func(map[string]interface{}) map[string]interface{}) interface{} {
switch item := value.(type) {
case []interface{}:
result := make([]interface{}, len(item))
for index, child := range item {
result[index] = transformResponseObjects(child, transform)
func excludeNestedRows(root map[string]interface{}, excluded map[string]bool) (map[string]interface{}, bool) {
for _, key := range []string{"result", "results"} {
container, ok := root[key].(map[string]interface{})
if !ok {
continue
}
return result
case map[string]interface{}:
result := make(map[string]interface{}, len(item))
for key, child := range item {
result[key] = transformResponseObjects(child, transform)
rows, ok := container["rows"].([]interface{})
if !ok {
continue
}
return transform(result)
default:
result := excludeObject(root, excluded).(map[string]interface{})
if excluded[key] {
return result, true
}
filteredContainer := excludeObject(container, excluded).(map[string]interface{})
if !excluded["rows"] {
filteredRows, filteredSchema := excludeReportRows(rows, container["schema"], excluded)
filteredContainer["rows"] = filteredRows
if _, hasSchema := container["schema"]; hasSchema && !excluded["schema"] {
filteredContainer["schema"] = filteredSchema
}
}
result[key] = filteredContainer
return result, true
}
return nil, false
}

func excludeReportRows(rows []interface{}, schemaValue interface{}, excluded map[string]bool) ([]interface{}, interface{}) {
schema, ok := schemaValue.([]interface{})
columnNames := readReportSchemaColumnNames(schemaValue)
if !ok || len(columnNames) == 0 {
return excludeRows(rows, excluded), schemaValue
}

keptIndexes := make([]int, 0, len(columnNames))
filteredSchema := make([]interface{}, 0, len(schema))
for index, columnName := range columnNames {
if excluded[columnName] {
continue
}
keptIndexes = append(keptIndexes, index)
if index < len(schema) {
filteredSchema = append(filteredSchema, schema[index])
}
}

filteredRows := make([]interface{}, len(rows))
for rowIndex, row := range rows {
cells, ok := row.([]interface{})
if !ok {
filteredRows[rowIndex] = excludeObject(row, excluded)
continue
}
filteredCells := make([]interface{}, 0, len(keptIndexes))
for _, cellIndex := range keptIndexes {
if cellIndex < len(cells) {
filteredCells = append(filteredCells, cells[cellIndex])
}
}
filteredRows[rowIndex] = filteredCells
}
return filteredRows, filteredSchema
}

func excludeRows(rows []interface{}, excluded map[string]bool) []interface{} {
result := make([]interface{}, len(rows))
for index, row := range rows {
result[index] = excludeObject(row, excluded)
}
return result
}

func excludeObject(value interface{}, excluded map[string]bool) interface{} {
object, ok := value.(map[string]interface{})
if !ok {
return value
Comment thread
chaim0m marked this conversation as resolved.
}
result := make(map[string]interface{}, len(object))
for key, child := range object {
if !excluded[key] {
result[key] = child
}
}
return result
}

func truncateResponseValue(value interface{}, limit int) interface{} {
Expand Down
Loading
Loading