Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
138 changes: 117 additions & 21 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,6 +14,9 @@ func shapeResponseBody(body interface{}) interface{} {
fields := commaSeparatedValues(viper.GetString("agent-fields"))
excluded := commaSeparatedValues(viper.GetString("agent-exclude"))
if len(fields) > 0 {
if comparable, matched := projectionFieldStatus(body, fields); comparable && !matched && cli.Stderr != nil {
fmt.Fprintf(cli.Stderr, "warning: none of the requested fields exist in the response: %s\n", strings.Join(fields, ", "))
}
body = projectResponseValue(body, fields)
}
if len(excluded) > 0 {
Expand Down Expand Up @@ -58,6 +62,69 @@ func commaSeparatedValues(value string) []string {
return result
}

func projectionFieldStatus(value interface{}, fields []string) (bool, bool) {
switch item := value.(type) {
case []interface{}:
return projectionRowsFieldStatus(item, fields)
case map[string]interface{}:
for _, key := range []string{"result", "results"} {
container, ok := item[key].(map[string]interface{})
if !ok {
continue
}
rows, ok := container["rows"].([]interface{})
if !ok {
continue
}
schema := readReportSchemaColumnNames(container["schema"])
if len(schema) > 0 && len(rows) > 0 {
for _, field := range fields {
for _, column := range schema {
if field == column {
return true, true
}
}
}
return true, false
Comment thread
chaim0m marked this conversation as resolved.
Outdated
}
return projectionRowsFieldStatus(rows, fields)
}
if _, rows, ok := listWrapperRows(item); ok {
return projectionRowsFieldStatus(rows, fields)
}
if len(item) == 0 {
return false, false
}
return true, objectHasAnyField(item, fields)
default:
return false, false
}
}

func projectionRowsFieldStatus(rows []interface{}, fields []string) (bool, bool) {
comparable := false
for _, row := range rows {
object, ok := row.(map[string]interface{})
if !ok {
continue
}
comparable = true
if objectHasAnyField(object, fields) {
return true, true
}
}
return comparable, false
}

func objectHasAnyField(object map[string]interface{}, fields []string) bool {
for _, field := range fields {
if _, exists := object[field]; exists {
return true
Comment thread
chaim0m marked this conversation as resolved.
Outdated
}
}
return false
}

func projectResponseValue(value interface{}, fields []string) interface{} {
switch item := value.(type) {
case []interface{}:
Expand Down Expand Up @@ -182,36 +249,65 @@ func excludeResponseValue(value interface{}, excluded []string) interface{} {
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 {
if !excludedSet[key] {
result[key] = child
}
}
return result
})
}

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)
}
return result
return excludeRows(item, excludedSet)
case map[string]interface{}:
result := make(map[string]interface{}, len(item))
for key, child := range item {
result[key] = transformResponseObjects(child, transform)
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 := copyObject(item)
result[key] = excludeRows(rows, excludedSet)
return result
}
return transform(result)
return excludeObject(item, excludedSet)
default:
return value
}
}

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
}
rows, ok := container["rows"].([]interface{})
if !ok {
continue
}
result := copyObject(root)
filteredContainer := copyObject(container)
filteredContainer["rows"] = excludeRows(rows, excluded)
result[key] = filteredContainer
return result, true
}
return nil, false
}

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{} {
switch item := value.(type) {
case string:
Expand Down
54 changes: 54 additions & 0 deletions output_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,60 @@ func TestShapeResponseBodyProjectsExcludesAndTruncates(t *testing.T) {
}
}

func TestExcludePreservesWrapperMetadataAndNestedObjects(t *testing.T) {
viper.Set("agent-exclude", "rowCount,amount")
t.Cleanup(viper.Reset)

input := map[string]interface{}{
"budgets": []interface{}{
map[string]interface{}{
"id": "budget-1",
"amount": 1000,
"alertThresholds": []interface{}{
map[string]interface{}{"amount": 900, "percentage": 90},
},
},
},
"rowCount": 1,
}

shaped := shapeResponseBody(input).(map[string]interface{})
if shaped["rowCount"] != 1 {
t.Fatalf("rowCount = %#v", shaped["rowCount"])
}
row := shaped["budgets"].([]interface{})[0].(map[string]interface{})
if _, exists := row["amount"]; exists {
t.Fatal("top-level row amount remains")
}
threshold := row["alertThresholds"].([]interface{})[0].(map[string]interface{})
if threshold["amount"] != 900 {
t.Fatalf("nested amount = %#v", threshold["amount"])
}
}

func TestUnknownProjectionFieldsWriteWarning(t *testing.T) {
viper.Set("agent-fields", "nosuchfield")
oldStderr := cli.Stderr
var stderr bytes.Buffer
cli.Stderr = &stderr
t.Cleanup(func() {
cli.Stderr = oldStderr
viper.Reset()
})

input := map[string]interface{}{
"budgets": []interface{}{map[string]interface{}{"id": "budget-1"}},
"rowCount": 1,
}
shaped := shapeResponseBody(input).(map[string]interface{})
if !strings.Contains(stderr.String(), "none of the requested fields exist") {
t.Fatalf("stderr = %q", stderr.String())
}
if shaped["rowCount"] != 1 {
t.Fatalf("rowCount = %#v", shaped["rowCount"])
}
}

func TestShapeResponseBodyDefinitiveEmptyState(t *testing.T) {
oldAgentMode := agentMode
agentMode = true
Expand Down
Loading