@@ -210,17 +210,56 @@ impl RobotFormatter {
210210 Self { config }
211211 }
212212
213- /// Format a value as output string
213+ /// Format a value as output string, applying field-mode filtering
214+ /// when the value is a search response with results.
214215 pub fn format < T : Serialize > ( & self , value : & T ) -> Result < String , serde_json:: Error > {
215- match self . config . format {
216- OutputFormat :: Json => serde_json:: to_string_pretty ( value) ,
217- OutputFormat :: Jsonl | OutputFormat :: Minimal => serde_json:: to_string ( value) ,
218- OutputFormat :: Table => {
219- // For table format, we still return JSON but it's not used
220- // The caller should handle table formatting separately
221- serde_json:: to_string_pretty ( value)
216+ let filtered = self . apply_fields ( value) ;
217+ let output = match self . config . format {
218+ OutputFormat :: Json => serde_json:: to_string_pretty ( & filtered) ,
219+ OutputFormat :: Jsonl | OutputFormat :: Minimal => serde_json:: to_string ( & filtered) ,
220+ OutputFormat :: Table => serde_json:: to_string_pretty ( & filtered) ,
221+ } ;
222+ output
223+ }
224+
225+ /// Apply field-mode filtering. For non-`Full` modes, drops fields
226+ /// from each item in `data.results` that are not in the allowed set.
227+ /// Returns the original value unchanged when the value is not a
228+ /// search response or serialization fails.
229+ fn apply_fields < T : Serialize > ( & self , value : & T ) -> serde_json:: Value {
230+ if matches ! ( self . config. fields, FieldMode :: Full ) {
231+ return serde_json:: to_value ( value) . unwrap_or ( serde_json:: Value :: Null ) ;
232+ }
233+ let mut v = match serde_json:: to_value ( value) {
234+ Ok ( v) => v,
235+ Err ( _) => return serde_json:: Value :: Null ,
236+ } ;
237+ if let Some ( results) = v. get_mut ( "data" ) . and_then ( |d| d. get_mut ( "results" ) ) {
238+ if let Some ( arr) = results. as_array_mut ( ) {
239+ let keep: Vec < & str > = match & self . config . fields {
240+ FieldMode :: Full => unreachable ! ( ) ,
241+ FieldMode :: Summary => vec ! [
242+ "rank" ,
243+ "id" ,
244+ "title" ,
245+ "url" ,
246+ "score" ,
247+ "preview" ,
248+ "source" ,
249+ "date" ,
250+ "preview_truncated" ,
251+ ] ,
252+ FieldMode :: Minimal => vec ! [ "rank" , "id" , "title" , "url" , "score" ] ,
253+ FieldMode :: Custom ( fields) => fields. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ,
254+ } ;
255+ for item in arr {
256+ if let Some ( obj) = item. as_object_mut ( ) {
257+ obj. retain ( |k, _| keep. contains ( & k. as_str ( ) ) ) ;
258+ }
259+ }
222260 }
223261 }
262+ v
224263 }
225264
226265 /// Format multiple values as JSONL
@@ -501,6 +540,85 @@ mod tests {
501540 assert ! ( was_truncated) ;
502541 assert ! ( truncated. starts_with( "abcde" ) ) ;
503542 }
543+
544+ #[ test]
545+ fn test_fields_full_includes_all ( ) {
546+ let config = RobotConfig :: new ( ) . with_fields ( FieldMode :: Full ) ;
547+ let formatter = RobotFormatter :: new ( config) ;
548+ let data = serde_json:: json!( {
549+ "data" : {
550+ "results" : [
551+ { "rank" : 1 , "id" : "a" , "title" : "T" , "url" : "u" , "score" : 0.9 , "preview" : "p" }
552+ ]
553+ }
554+ } ) ;
555+ let output = formatter. format ( & data) . unwrap ( ) ;
556+ assert ! ( output. contains( "\" preview\" " ) ) ;
557+ assert ! ( output. contains( "\" rank\" " ) ) ;
558+ }
559+
560+ #[ test]
561+ fn test_fields_minimal_excludes_preview ( ) {
562+ let config = RobotConfig :: new ( ) . with_fields ( FieldMode :: Minimal ) ;
563+ let formatter = RobotFormatter :: new ( config) ;
564+ let data = serde_json:: json!( {
565+ "data" : {
566+ "results" : [
567+ { "rank" : 1 , "id" : "a" , "title" : "T" , "url" : "u" , "score" : 0.9 , "preview" : "p" }
568+ ]
569+ }
570+ } ) ;
571+ let output = formatter. format ( & data) . unwrap ( ) ;
572+ assert ! ( !output. contains( "\" preview\" " ) ) ;
573+ assert ! ( output. contains( "\" rank\" " ) ) ;
574+ assert ! ( output. contains( "\" id\" " ) ) ;
575+ }
576+
577+ #[ test]
578+ fn test_fields_summary_includes_preview_excludes_body ( ) {
579+ let config = RobotConfig :: new ( ) . with_fields ( FieldMode :: Summary ) ;
580+ let formatter = RobotFormatter :: new ( config) ;
581+ let data = serde_json:: json!( {
582+ "data" : {
583+ "results" : [
584+ { "rank" : 1 , "id" : "a" , "title" : "T" , "preview" : "p" , "body" : "full text" }
585+ ]
586+ }
587+ } ) ;
588+ let output = formatter. format ( & data) . unwrap ( ) ;
589+ assert ! ( output. contains( "\" preview\" " ) ) ;
590+ assert ! ( !output. contains( "\" body\" " ) ) ;
591+ }
592+
593+ #[ test]
594+ fn test_fields_custom_only_keeps_named ( ) {
595+ let config =
596+ RobotConfig :: new ( ) . with_fields ( FieldMode :: Custom ( vec ! [ "rank" . into( ) , "id" . into( ) ] ) ) ;
597+ let formatter = RobotFormatter :: new ( config) ;
598+ let data = serde_json:: json!( {
599+ "data" : {
600+ "results" : [
601+ { "rank" : 1 , "id" : "a" , "title" : "T" , "url" : "u" , "score" : 0.9 }
602+ ]
603+ }
604+ } ) ;
605+ let output = formatter. format ( & data) . unwrap ( ) ;
606+ assert ! ( output. contains( "\" rank\" " ) ) ;
607+ assert ! ( output. contains( "\" id\" " ) ) ;
608+ assert ! ( !output. contains( "\" title\" " ) ) ;
609+ assert ! ( !output. contains( "\" url\" " ) ) ;
610+ assert ! ( !output. contains( "\" score\" " ) ) ;
611+ }
612+
613+ #[ test]
614+ fn test_fields_full_noop_on_non_search_response ( ) {
615+ let config = RobotConfig :: new ( ) . with_fields ( FieldMode :: Minimal ) ;
616+ let formatter = RobotFormatter :: new ( config) ;
617+ let data = serde_json:: json!( { "status" : "ok" , "msg" : "hello" } ) ;
618+ let output = formatter. format ( & data) . unwrap ( ) ;
619+ assert ! ( output. contains( "\" msg\" " ) ) ;
620+ assert ! ( output. contains( "\" status\" " ) ) ;
621+ }
504622}
505623
506624#[ cfg( test) ]
0 commit comments