@@ -6,80 +6,78 @@ import (
66 "strings"
77)
88
9- // defaultFillRateThreshold is the default proportion of items that must have a key for it to survive
10- const defaultFillRateThreshold = 0.1
9+ const (
10+ defaultFillRateThreshold = 0.1 // default proportion of items that must have a key for it to survive
11+ minFillRateRows = 3 // minimum number of items required to apply fill-rate filtering
12+ defaultMaxDepth = 2 // default nesting depth that flatten will recurse into
13+ )
14+
15+ // OptimizeListConfig controls the optimization pipeline behavior.
16+ type OptimizeListConfig struct {
17+ maxDepth int
18+ preservedFields map [string ]bool
19+ collectionExtractors map [string ][]string
20+ }
1121
12- // minFillRateRows is the minimum number of items required to apply fill-rate filtering
13- const minFillRateRows = 3
22+ type OptimizeListOption func (* OptimizeListConfig )
1423
15- // maxFlattenDepth is the maximum nesting depth that flatten will recurse into .
24+ // WithMaxDepth sets the maximum nesting depth for flattening .
1625// Deeper nested maps are silently dropped.
17- const maxFlattenDepth = 2
26+ func WithMaxDepth (d int ) OptimizeListOption {
27+ return func (c * OptimizeListConfig ) {
28+ c .maxDepth = d
29+ }
30+ }
1831
19- // preservedFields is a set of keys that are exempt from all destructive strategies except whitespace normalization.
32+ // WithPreservedFields sets keys that are exempt from all destructive strategies except whitespace normalization.
2033// Keys are matched against post-flatten map keys, so for nested fields like "user.html_url", the dotted key must be
21- // added explicitly. Empty collections are still dropped. Wins over collectionFieldExtractors .
22- var preservedFields = map [string ]bool {
23- "html_url" : true ,
24- "draft" : true ,
25- "prerelease" : true ,
34+ // added explicitly. Empty collections are still dropped. Wins over collectionExtractors .
35+ func WithPreservedFields ( fields map [string ]bool ) OptimizeListOption {
36+ return func ( c * OptimizeListConfig ) {
37+ c . preservedFields = fields
38+ }
2639}
2740
28- // collectionFieldExtractors controls how array fields are handled instead of being summarized as "[N items]".
41+ // WithCollectionExtractors controls how array fields are handled instead of being summarized as "[N items]".
2942// - 1 sub-field: comma-joined into a flat string ("bug, enhancement").
3043// - Multiple sub-fields: keep the array, but trim each element to only those fields.
3144//
3245// These are explicitly exempt from fill-rate filtering; if we asked for the extraction, it's likely important
3346// to preserve the data even if only one item has it.
34- var collectionFieldExtractors = map [string ][]string {
35- "labels" : { "name" },
36- "requested_reviewers" : { "login" },
37- "requested_teams" : { "name" },
47+ func WithCollectionExtractors ( extractors map [string ][]string ) OptimizeListOption {
48+ return func ( c * OptimizeListConfig ) {
49+ c . collectionExtractors = extractors
50+ }
3851}
3952
40- // MarshalItems is the single entry point for response optimization.
41- // Handles two shapes: plain JSON arrays and wrapped objects with metadata.
42- // An optional maxDepth controls how many nesting levels flatten will recurse
43- // into; it defaults to maxFlattenDepth when omitted.
44- func MarshalItems (data any , maxDepth ... int ) ([]byte , error ) {
45- depth := maxFlattenDepth
46- if len (maxDepth ) > 0 {
47- depth = maxDepth [0 ]
53+ // OptimizeList optimizes a list of items by applying flattening, URL removal, zero-value removal,
54+ // whitespace normalization, collection summarization, and fill-rate filtering.
55+ func OptimizeList [T any ](items []T , opts ... OptimizeListOption ) ([]byte , error ) {
56+ cfg := OptimizeListConfig {maxDepth : defaultMaxDepth }
57+ for _ , opt := range opts {
58+ opt (& cfg )
4859 }
4960
50- raw , err := json .Marshal (data )
61+ raw , err := json .Marshal (items )
5162 if err != nil {
5263 return nil , fmt .Errorf ("failed to marshal data: %w" , err )
5364 }
5465
55- switch raw [0 ] {
56- case '[' :
57- return optimizeArray (raw , depth )
58- case '{' :
59- return optimizeObject (raw , depth )
60- default :
61- return raw , nil
66+ var maps []map [string ]any
67+ if err := json .Unmarshal (raw , & maps ); err != nil {
68+ return nil , fmt .Errorf ("failed to unmarshal JSON: %w" , err )
6269 }
63- }
6470
65- // OptimizeItems runs the full optimization pipeline on a slice of items:
66- // flatten, remove URLs, remove zero-values, normalize whitespace,
67- // summarize collections, and fill-rate filtering.
68- func OptimizeItems (items []map [string ]any , depth int ) []map [string ]any {
69- if len (items ) == 0 {
70- return items
71+ for i , item := range maps {
72+ flattenedItem := flattenTo (item , cfg .maxDepth )
73+ maps [i ] = optimizeItem (flattenedItem , cfg )
7174 }
7275
73- for i , item := range items {
74- flattenedItem := flattenTo (item , depth )
75- items [i ] = optimizeItem (flattenedItem )
76+ if len (maps ) >= minFillRateRows {
77+ maps = filterByFillRate (maps , defaultFillRateThreshold , cfg )
7678 }
7779
78- if len (items ) >= minFillRateRows {
79- items = filterByFillRate (items , defaultFillRateThreshold )
80- }
81-
82- return items
80+ return json .Marshal (maps )
8381}
8482
8583// flattenTo recursively promotes values from nested maps into the parent
@@ -106,7 +104,7 @@ func flattenInto(item map[string]any, prefix string, result map[string]any, dept
106104
107105// filterByFillRate drops keys that appear on less than the threshold proportion of items.
108106// Preserved fields and extractor keys always survive.
109- func filterByFillRate (items []map [string ]any , threshold float64 ) []map [string ]any {
107+ func filterByFillRate (items []map [string ]any , threshold float64 , cfg OptimizeListConfig ) []map [string ]any {
110108 keyCounts := make (map [string ]int )
111109 for _ , item := range items {
112110 for key := range item {
@@ -117,8 +115,8 @@ func filterByFillRate(items []map[string]any, threshold float64) []map[string]an
117115 minCount := int (threshold * float64 (len (items )))
118116 keepKeys := make (map [string ]bool , len (keyCounts ))
119117 for key , count := range keyCounts {
120- _ , hasExtractor := collectionFieldExtractors [key ]
121- if count > minCount || preservedFields [key ] || hasExtractor {
118+ _ , hasExtractor := cfg . collectionExtractors [key ]
119+ if count > minCount || cfg . preservedFields [key ] || hasExtractor {
122120 keepKeys [key ] = true
123121 }
124122 }
@@ -136,55 +134,13 @@ func filterByFillRate(items []map[string]any, threshold float64) []map[string]an
136134 return items
137135}
138136
139- // optimizeArray is the entry point for optimizing a raw JSON array.
140- func optimizeArray (raw []byte , depth int ) ([]byte , error ) {
141- var items []map [string ]any
142- if err := json .Unmarshal (raw , & items ); err != nil {
143- return nil , fmt .Errorf ("failed to unmarshal JSON: %w" , err )
144- }
145- return json .Marshal (OptimizeItems (items , depth ))
146- }
147-
148- // optimizeObject is the entry point for optimizing a raw JSON object.
149- func optimizeObject (raw []byte , depth int ) ([]byte , error ) {
150- var wrapper map [string ]any
151- if err := json .Unmarshal (raw , & wrapper ); err != nil {
152- return nil , fmt .Errorf ("failed to unmarshal JSON: %w" , err )
153- }
154-
155- // find the first array field in the wrapper (data); rest is metadata to be preserved as is
156- // assumes exactly one array field exists; if multiple are present, only the first will be optimized
157- var dataKey string
158- for key , value := range wrapper {
159- if _ , ok := value .([]any ); ok {
160- dataKey = key
161- break
162- }
163- }
164- // if no data array found, just return the original response
165- if dataKey == "" {
166- return raw , nil
167- }
168-
169- rawItems := wrapper [dataKey ].([]any )
170- items := make ([]map [string ]any , 0 , len (rawItems ))
171- for _ , rawItem := range rawItems {
172- if m , ok := rawItem .(map [string ]any ); ok {
173- items = append (items , m )
174- }
175- }
176- wrapper [dataKey ] = OptimizeItems (items , depth )
177-
178- return json .Marshal (wrapper )
179- }
180-
181137// optimizeItem applies per-item strategies in a single pass: remove URLs,
182138// remove zero-values, normalize whitespace, summarize collections.
183139// Preserved fields skip everything except whitespace normalization.
184- func optimizeItem (item map [string ]any ) map [string ]any {
140+ func optimizeItem (item map [string ]any , cfg OptimizeListConfig ) map [string ]any {
185141 result := make (map [string ]any , len (item ))
186142 for key , value := range item {
187- preserved := preservedFields [key ]
143+ preserved := cfg . preservedFields [key ]
188144 if ! preserved && isURLKey (key ) {
189145 continue
190146 }
@@ -202,7 +158,7 @@ func optimizeItem(item map[string]any) map[string]any {
202158
203159 if preserved {
204160 result [key ] = value
205- } else if fields , ok := collectionFieldExtractors [key ]; ok {
161+ } else if fields , ok := cfg . collectionExtractors [key ]; ok {
206162 if len (fields ) == 1 {
207163 result [key ] = extractSubField (v , fields [0 ])
208164 } else {
0 commit comments