From 13eb44f119f8292ea4e7ea8d5095255d4590a880 Mon Sep 17 00:00:00 2001 From: Michael Finson Date: Mon, 3 Aug 2026 20:08:24 +0300 Subject: [PATCH 1/3] fix: preserve response projection semantics --- output_contract.go | 138 ++++++++++++++++++++++++++++++++++------ output_contract_test.go | 54 ++++++++++++++++ 2 files changed, 171 insertions(+), 21 deletions(-) diff --git a/output_contract.go b/output_contract.go index f67fe36..52a7fa2 100644 --- a/output_contract.go +++ b/output_contract.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "github.com/rest-sh/restish/cli" @@ -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 { @@ -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 + } + 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 + } + } + return false +} + func projectResponseValue(value interface{}, fields []string) interface{} { switch item := value.(type) { case []interface{}: @@ -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 { + 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 + } + 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: diff --git a/output_contract_test.go b/output_contract_test.go index f1cc6cc..ec6cb37 100644 --- a/output_contract_test.go +++ b/output_contract_test.go @@ -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 From b5f90fd7a9aa4cba968c51dad88df5c0426059c1 Mon Sep 17 00:00:00 2001 From: Michael Finson Date: Tue, 4 Aug 2026 15:11:02 +0300 Subject: [PATCH 2/3] fix(output): address projection review feedback --- main.go | 2 +- output_contract.go | 189 ++++++++++++++++++++-------------------- output_contract_test.go | 112 ++++++++++++++++++++++-- 3 files changed, 204 insertions(+), 99 deletions(-) diff --git a/main.go b/main.go index 974550c..0f294e3 100644 --- a/main.go +++ b/main.go @@ -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") diff --git a/output_contract.go b/output_contract.go index 52a7fa2..3af58de 100644 --- a/output_contract.go +++ b/output_contract.go @@ -14,10 +14,18 @@ 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, ", ")) + matchedFields := make(map[string]bool, len(fields)) + var comparable bool + body, comparable = projectResponseValue(body, fields, matchedFields) + missingFields := make([]string, 0, len(fields)) + for _, field := range fields { + if !matchedFields[field] { + missingFields = append(missingFields, field) + } + } + if comparable && len(missingFields) > 0 && cli.Stderr != nil { + fmt.Fprintf(cli.Stderr, "warning: requested fields not present in the response: %s\n", strings.Join(missingFields, ", ")) } - body = projectResponseValue(body, fields) } if len(excluded) > 0 { body = excludeResponseValue(body, excluded) @@ -62,97 +70,38 @@ func commaSeparatedValues(value string) []string { return result } -func projectionFieldStatus(value interface{}, fields []string) (bool, bool) { +func projectResponseValue(value interface{}, fields []string, matchedFields map[string]bool) (interface{}, bool) { switch item := value.(type) { case []interface{}: - return projectionRowsFieldStatus(item, fields) + return projectRows(item, fields, matchedFields) 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 - } - 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 - } - } - return false -} - -func projectResponseValue(value interface{}, fields []string) interface{} { - switch item := value.(type) { - case []interface{}: - return projectRows(item, fields) - case map[string]interface{}: - if result, ok := projectNestedRows(item, fields); ok { - return result + if result, comparable, ok := projectNestedRows(item, fields, matchedFields); ok { + return result, comparable } if key, rows, ok := listWrapperRows(item); ok { result := copyObject(item) - result[key] = projectRows(rows, fields) - return result + projectedRows, comparable := projectRows(rows, fields, matchedFields) + result[key] = projectedRows + return result, comparable } - 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)) + comparable := false for index, row := range rows { - result[index] = projectObject(row, fields) + var rowComparable bool + result[index], rowComparable = projectObject(row, fields, matchedFields) + comparable = comparable || rowComparable } - return result + return result, comparable } -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 { @@ -164,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, comparable := projectSchemaRows(rows, readReportSchemaColumnNames(container["schema"]), fields, matchedFields) + projectedContainer["rows"] = projectedRows result[key] = projectedContainer - return result, true + return result, comparable, 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)) + comparable := 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) + comparable = true continue } - result[index] = projectObject(row, fields) + var rowComparable bool + result[index], rowComparable = projectObject(row, fields, matchedFields) + comparable = comparable || rowComparable } - return result + return result, comparable } func listWrapperRows(object map[string]interface{}) (string, []interface{}, bool) { @@ -230,18 +184,19 @@ 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{} { @@ -257,8 +212,10 @@ func excludeResponseValue(value interface{}, excluded []string) interface{} { return result } if key, rows, ok := listWrapperRows(item); ok { - result := copyObject(item) - result[key] = excludeRows(rows, excludedSet) + result := excludeObject(item, excludedSet).(map[string]interface{}) + if !excludedSet[key] { + result[key] = excludeRows(rows, excludedSet) + } return result } return excludeObject(item, excludedSet) @@ -277,15 +234,61 @@ func excludeNestedRows(root map[string]interface{}, excluded map[string]bool) (m if !ok { continue } - result := copyObject(root) - filteredContainer := copyObject(container) - filteredContainer["rows"] = excludeRows(rows, excluded) + 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 { diff --git a/output_contract_test.go b/output_contract_test.go index ec6cb37..6871192 100644 --- a/output_contract_test.go +++ b/output_contract_test.go @@ -52,7 +52,7 @@ func TestShapeResponseBodyProjectsExcludesAndTruncates(t *testing.T) { } } -func TestExcludePreservesWrapperMetadataAndNestedObjects(t *testing.T) { +func TestExcludeHonorsWrapperFieldsWithoutRecursingIntoNestedObjects(t *testing.T) { viper.Set("agent-exclude", "rowCount,amount") t.Cleanup(viper.Reset) @@ -70,8 +70,8 @@ func TestExcludePreservesWrapperMetadataAndNestedObjects(t *testing.T) { } shaped := shapeResponseBody(input).(map[string]interface{}) - if shaped["rowCount"] != 1 { - t.Fatalf("rowCount = %#v", shaped["rowCount"]) + if _, exists := shaped["rowCount"]; exists { + t.Fatal("explicitly excluded rowCount remains") } row := shaped["budgets"].([]interface{})[0].(map[string]interface{}) if _, exists := row["amount"]; exists { @@ -83,8 +83,69 @@ func TestExcludePreservesWrapperMetadataAndNestedObjects(t *testing.T) { } } +func TestExcludeHonorsListWrapperKey(t *testing.T) { + viper.Set("agent-exclude", "budgets") + t.Cleanup(viper.Reset) + + input := map[string]interface{}{ + "budgets": []interface{}{map[string]interface{}{"id": "budget-1"}}, + "rowCount": 1, + } + shaped := shapeResponseBody(input).(map[string]interface{}) + if _, exists := shaped["budgets"]; exists { + t.Fatal("explicitly excluded list wrapper remains") + } + if shaped["rowCount"] != 1 { + t.Fatalf("rowCount = %#v", shaped["rowCount"]) + } +} + +func TestExcludeFiltersReportSchemaAndPositionalRows(t *testing.T) { + viper.Set("agent-exclude", "cost") + t.Cleanup(viper.Reset) + + input := map[string]interface{}{ + "result": map[string]interface{}{ + "schema": []interface{}{ + map[string]interface{}{"name": "service"}, + map[string]interface{}{"name": "cost"}, + }, + "rows": []interface{}{[]interface{}{"BigQuery", 123.45}}, + }, + } + shaped := shapeResponseBody(input).(map[string]interface{}) + result := shaped["result"].(map[string]interface{}) + wantSchema := []interface{}{map[string]interface{}{"name": "service"}} + if !reflect.DeepEqual(result["schema"], wantSchema) { + t.Fatalf("schema = %#v, want %#v", result["schema"], wantSchema) + } + wantRows := []interface{}{[]interface{}{"BigQuery"}} + if !reflect.DeepEqual(result["rows"], wantRows) { + t.Fatalf("rows = %#v, want %#v", result["rows"], wantRows) + } +} + +func TestExcludeHonorsReportContainerFields(t *testing.T) { + viper.Set("agent-exclude", "schema") + t.Cleanup(viper.Reset) + + input := map[string]interface{}{ + "result": map[string]interface{}{ + "schema": []interface{}{map[string]interface{}{"name": "service"}}, + "rows": []interface{}{[]interface{}{"BigQuery"}}, + }, + } + result := shapeResponseBody(input).(map[string]interface{})["result"].(map[string]interface{}) + if _, exists := result["schema"]; exists { + t.Fatal("explicitly excluded schema remains") + } + if _, exists := result["rows"]; !exists { + t.Fatal("rows were removed with schema") + } +} + func TestUnknownProjectionFieldsWriteWarning(t *testing.T) { - viper.Set("agent-fields", "nosuchfield") + viper.Set("agent-fields", "id,nosuchfield") oldStderr := cli.Stderr var stderr bytes.Buffer cli.Stderr = &stderr @@ -98,14 +159,55 @@ func TestUnknownProjectionFieldsWriteWarning(t *testing.T) { "rowCount": 1, } shaped := shapeResponseBody(input).(map[string]interface{}) - if !strings.Contains(stderr.String(), "none of the requested fields exist") { + if !strings.Contains(stderr.String(), "requested fields not present in the response: nosuchfield") { t.Fatalf("stderr = %q", stderr.String()) } + if strings.Contains(stderr.String(), "response: id") { + t.Fatalf("matched field was reported missing: %q", stderr.String()) + } if shaped["rowCount"] != 1 { t.Fatalf("rowCount = %#v", shaped["rowCount"]) } } +func TestProjectionWarningUsesObjectReportRowFields(t *testing.T) { + viper.Set("agent-fields", "service") + oldStderr := cli.Stderr + var stderr bytes.Buffer + cli.Stderr = &stderr + t.Cleanup(func() { + cli.Stderr = oldStderr + viper.Reset() + }) + + input := map[string]interface{}{ + "result": map[string]interface{}{ + "schema": []interface{}{map[string]interface{}{"name": "colA"}}, + "rows": []interface{}{map[string]interface{}{"service": "BigQuery"}}, + }, + } + shapeResponseBody(input) + if stderr.Len() != 0 { + t.Fatalf("stderr = %q", stderr.String()) + } +} + +func TestEmptyProjectionDoesNotWarn(t *testing.T) { + viper.Set("agent-fields", "id") + oldStderr := cli.Stderr + var stderr bytes.Buffer + cli.Stderr = &stderr + t.Cleanup(func() { + cli.Stderr = oldStderr + viper.Reset() + }) + + shapeResponseBody([]interface{}{}) + if stderr.Len() != 0 { + t.Fatalf("stderr = %q", stderr.String()) + } +} + func TestShapeResponseBodyDefinitiveEmptyState(t *testing.T) { oldAgentMode := agentMode agentMode = true From 5c707bd27f2feed36c8485cf590d929bc827bc7b Mon Sep 17 00:00:00 2001 From: Michael Finson Date: Tue, 4 Aug 2026 15:12:04 +0300 Subject: [PATCH 3/3] refactor(output): clarify projection match tracking --- output_contract.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/output_contract.go b/output_contract.go index 3af58de..2916c8f 100644 --- a/output_contract.go +++ b/output_contract.go @@ -15,15 +15,15 @@ func shapeResponseBody(body interface{}) interface{} { excluded := commaSeparatedValues(viper.GetString("agent-exclude")) if len(fields) > 0 { matchedFields := make(map[string]bool, len(fields)) - var comparable bool - body, comparable = projectResponseValue(body, fields, matchedFields) + 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 comparable && len(missingFields) > 0 && cli.Stderr != nil { + 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, ", ")) } } @@ -75,14 +75,14 @@ func projectResponseValue(value interface{}, fields []string, matchedFields map[ case []interface{}: return projectRows(item, fields, matchedFields) case map[string]interface{}: - if result, comparable, ok := projectNestedRows(item, fields, matchedFields); ok { - return result, comparable + if result, hasComparableRows, ok := projectNestedRows(item, fields, matchedFields); ok { + return result, hasComparableRows } if key, rows, ok := listWrapperRows(item); ok { result := copyObject(item) - projectedRows, comparable := projectRows(rows, fields, matchedFields) + projectedRows, hasComparableRows := projectRows(rows, fields, matchedFields) result[key] = projectedRows - return result, comparable + return result, hasComparableRows } return projectObject(item, fields, matchedFields) default: @@ -92,13 +92,13 @@ func projectResponseValue(value interface{}, fields []string, matchedFields map[ func projectRows(rows []interface{}, fields []string, matchedFields map[string]bool) ([]interface{}, bool) { result := make([]interface{}, len(rows)) - comparable := false + hasComparableRows := false for index, row := range rows { var rowComparable bool result[index], rowComparable = projectObject(row, fields, matchedFields) - comparable = comparable || rowComparable + hasComparableRows = hasComparableRows || rowComparable } - return result, comparable + return result, hasComparableRows } func projectNestedRows(root map[string]interface{}, fields []string, matchedFields map[string]bool) (map[string]interface{}, bool, bool) { @@ -113,17 +113,17 @@ func projectNestedRows(root map[string]interface{}, fields []string, matchedFiel } result := copyObject(root) projectedContainer := copyObject(container) - projectedRows, comparable := projectSchemaRows(rows, readReportSchemaColumnNames(container["schema"]), fields, matchedFields) + projectedRows, hasComparableRows := projectSchemaRows(rows, readReportSchemaColumnNames(container["schema"]), fields, matchedFields) projectedContainer["rows"] = projectedRows result[key] = projectedContainer - return result, comparable, true + return result, hasComparableRows, true } return nil, false, false } func projectSchemaRows(rows []interface{}, schema []string, fields []string, matchedFields map[string]bool) ([]interface{}, bool) { result := make([]interface{}, len(rows)) - comparable := false + hasComparableRows := false for index, row := range rows { if cells, ok := row.([]interface{}); ok { object := make(map[string]interface{}, len(cells)) @@ -131,14 +131,14 @@ func projectSchemaRows(rows []interface{}, schema []string, fields []string, mat object[reportColumnName(schema, cellIndex)] = cell } result[index], _ = projectObject(object, fields, matchedFields) - comparable = true + hasComparableRows = true continue } var rowComparable bool result[index], rowComparable = projectObject(row, fields, matchedFields) - comparable = comparable || rowComparable + hasComparableRows = hasComparableRows || rowComparable } - return result, comparable + return result, hasComparableRows } func listWrapperRows(object map[string]interface{}) (string, []interface{}, bool) {