Skip to content

Commit 5fdfb01

Browse files
committed
Add dual filtering with --fields (server-side) and --jq (client-side) flags
1 parent 71be452 commit 5fdfb01

13 files changed

Lines changed: 458 additions & 131 deletions

File tree

Cargo.lock

Lines changed: 229 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ serde_json = "1.0"
2222
serpapi = { version = "0.1.0", git = "https://github.com/serpapi/serpapi-rust.git", rev = "e65463152538433885fc1a4c66c8cd416a676715" }
2323
tokio = { version = "1.35", features = ["full"] }
2424
toml = "0.8"
25+
jaq-core = "1.2.1"
26+
jaq-interpret = "1.2.1"
27+
jaq-parse = "1.0.2"
28+
jaq-std = "1.2.1"
2529

2630
[package.metadata.docs.rs]
2731
targets = []

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,14 @@ Perform a search with any supported SerpApi engine.
4141
# Basic search
4242
serpapi --json search engine=google q=coffee
4343

44-
# With server-side field filtering (reduces token usage for AI agents)
45-
serpapi --json --jq "organic_results[].{title,link}" search engine=google q=coffee
44+
# With server-side field filtering (reduces response size at API level)
45+
serpapi --json --fields "organic_results[].{title,link}" search engine=google q=coffee
46+
47+
# With client-side jq filtering (like gh --jq)
48+
serpapi --json --jq ".organic_results[0:3] | [.[] | {title, link}]" search engine=google q=coffee
49+
50+
# Both: server-side reduces payload, then client-side refines
51+
serpapi --json --fields "organic_results" --jq ".organic_results[0:3] | [.[] | {title, link}]" search engine=google q=coffee
4652

4753
# Multiple parameters
4854
serpapi --json search engine=google q="coffee shops" location="Austin,TX"
@@ -84,7 +90,8 @@ serpapi login
8490
## Global Flags
8591

8692
- `--json` — Clean JSON output (no ANSI colors, for AI agents and pipelines)
87-
- `--jq <expr>` — Server-side field filtering (maps to SerpApi's `json_restrictor` parameter)
93+
- `--fields <expr>` — Server-side field filtering (maps to SerpApi's `json_restrictor` parameter)
94+
- `--jq <expr>` — Client-side jq filter applied to JSON output (same as `gh --jq`)
8895
- `--api-key <key>` — Override API key (takes priority over environment and config file)
8996

9097
**⚠️ Important: Flag Position**
@@ -94,7 +101,7 @@ Global flags must come **BEFORE** the subcommand, not after:
94101
```bash
95102
# ✅ Correct
96103
serpapi --json account
97-
serpapi --json --jq "organic_results[0:3]" search engine=google q=coffee
104+
serpapi --json --jq ".organic_results[0:3]" search engine=google q=coffee
98105

99106
# ❌ Incorrect (will fail with "unexpected argument")
100107
serpapi account --json
@@ -124,10 +131,17 @@ api_key = "your_serpapi_key_here"
124131
This CLI is optimized for consumption by AI agents (OpenClaw, Claude Code, Codex, etc.):
125132

126133
- **Use `--json` flag** for clean, parseable JSON output (no ANSI colors or formatting)
127-
- **Use `--jq` for server-side filtering** to reduce token usage:
128-
- Example: `--jq "organic_results[0:3]"` returns only first 3 results
134+
- **Use `--fields` for server-side filtering** to reduce token usage:
135+
- Example: `--fields "organic_results[0:3]"` returns only first 3 results
129136
- Filtering happens at the API level, saving bandwidth and context window tokens
130-
- Syntax follows JSONPath expressions
137+
- Syntax follows SerpApi's `json_restrictor` parameter
138+
- **Use `--jq` for client-side filtering** (same as `gh --jq`):
139+
- Example: `--jq ".organic_results | length"` counts results locally
140+
- Full jq expression support: pipes, array slicing, object construction, `select`, `map`, etc.
141+
- Runs after API response is received
142+
- **Combine both** for maximum efficiency:
143+
- `--fields` reduces the API response size (less bandwidth)
144+
- `--jq` refines the result further (less context window tokens)
131145
- **Exit codes**:
132146
- `0` = success
133147
- `1` = API error (invalid key, rate limit, etc.)

src/commands/account.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
use crate::error::CliError;
2-
use crate::output;
32
use serde_json::Value;
43
use serpapi::Client;
54
use std::collections::HashMap;
65

7-
pub async fn run(api_key: &str, json_mode: bool) -> Result<(), CliError> {
8-
// Create client with API key
6+
pub async fn run(api_key: &str) -> Result<Value, CliError> {
97
let mut client_params = HashMap::new();
108
client_params.insert("api_key".to_string(), api_key.to_string());
119
let client = Client::new(client_params);
12-
13-
// Call account API
1410
let result = client.account(&()).await
1511
.map_err(|e| CliError::NetworkError {
1612
message: format!("Network error: {}", e),
1713
})?;
18-
19-
// Check for error in response
2014
if let Value::Object(ref map) = result {
2115
if let Some(error_val) = map.get("error") {
2216
let error_msg = error_val.as_str()
@@ -27,11 +21,5 @@ pub async fn run(api_key: &str, json_mode: bool) -> Result<(), CliError> {
2721
});
2822
}
2923
}
30-
31-
// Output result
32-
output::print_json(&result, json_mode)
33-
.map_err(|e| CliError::ApiError {
34-
message: format!("Output error: {}", e),
35-
})?;
36-
Ok(())
24+
Ok(result)
3725
}

src/commands/archive.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
use crate::error::CliError;
2-
use crate::output;
32
use serde_json::Value;
43
use serpapi::Client;
54
use std::collections::HashMap;
65

7-
pub async fn run(id: &str, api_key: &str, json_mode: bool) -> Result<(), CliError> {
8-
// Create client with API key
6+
pub async fn run(id: &str, api_key: &str) -> Result<Value, CliError> {
97
let mut client_params = HashMap::new();
108
client_params.insert("api_key".to_string(), api_key.to_string());
119
let client = Client::new(client_params);
12-
13-
// Call search_archive API
1410
let result = client.search_archive(id).await
1511
.map_err(|e| CliError::NetworkError {
1612
message: format!("Network error: {}", e),
1713
})?;
18-
19-
// Check for error in response
2014
if let Value::Object(ref map) = result {
2115
if let Some(error_val) = map.get("error") {
2216
let error_msg = error_val.as_str()
@@ -27,10 +21,5 @@ pub async fn run(id: &str, api_key: &str, json_mode: bool) -> Result<(), CliErro
2721
});
2822
}
2923
}
30-
31-
// Output result
32-
output::print_json(&result, json_mode).map_err(|e| CliError::ApiError {
33-
message: format!("Output error: {}", e),
34-
})?;
35-
Ok(())
24+
Ok(result)
3625
}

src/commands/locations.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
use crate::error::CliError;
2-
use crate::output;
32
use crate::params::{self, Param};
43
use serde_json::Value;
54
use serpapi::Client;
65
use std::collections::HashMap;
76

8-
pub async fn run(params: Vec<Param>, json_mode: bool) -> Result<(), CliError> {
9-
// Build params HashMap
7+
pub async fn run(params: Vec<Param>) -> Result<Value, CliError> {
108
let params_map = params::params_to_hashmap(&params);
11-
12-
// Create client (no API key needed for locations)
139
let client = Client::new(HashMap::<String, String>::new());
14-
15-
// Call locations API
1610
let result = client.location(params_map).await
1711
.map_err(|e| CliError::NetworkError {
1812
message: format!("Network error: {}", e),
1913
})?;
20-
21-
// Check for error in response
2214
if let Value::Object(ref map) = result {
2315
if let Some(error_val) = map.get("error") {
2416
let error_msg = error_val.as_str()
@@ -29,11 +21,5 @@ pub async fn run(params: Vec<Param>, json_mode: bool) -> Result<(), CliError> {
2921
});
3022
}
3123
}
32-
33-
// Output result
34-
output::print_json(&result, json_mode)
35-
.map_err(|e| CliError::ApiError {
36-
message: format!("Output error: {}", e),
37-
})?;
38-
Ok(())
24+
Ok(result)
3925
}

src/commands/search.rs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::error::CliError;
2-
use crate::output;
32
use crate::params::{self, Param};
43
use serde_json::Value;
54
use serpapi::Client;
@@ -8,29 +7,19 @@ use std::collections::HashMap;
87
pub async fn run(
98
params: Vec<Param>,
109
api_key: &str,
11-
json_mode: bool,
12-
jq: Option<&str>,
13-
) -> Result<(), CliError> {
14-
// 1. Build params HashMap using params::params_to_hashmap
10+
fields: Option<&str>,
11+
) -> Result<Value, CliError> {
1512
let mut params_map = params::params_to_hashmap(&params);
16-
17-
// 2. Apply --jq if provided (inserts json_restrictor parameter)
18-
if let Some(jq_expr) = jq {
19-
params::apply_jq(&mut params_map, Some(jq_expr));
13+
if let Some(expr) = fields {
14+
params::apply_fields(&mut params_map, Some(expr));
2015
}
21-
22-
// 3. Create serpapi Client with API key
2316
let mut client_params = HashMap::new();
2417
client_params.insert("api_key".to_string(), api_key.to_string());
2518
let client = Client::new(client_params);
26-
27-
// 4. Call client.search(params).await
2819
let result = client.search(params_map).await
2920
.map_err(|e| CliError::NetworkError {
3021
message: format!("Network error: {}", e),
3122
})?;
32-
33-
// 5. Check for "error" key in response JSON
3423
if let Value::Object(ref map) = result {
3524
if let Some(error_val) = map.get("error") {
3625
let error_msg = error_val.as_str()
@@ -41,10 +30,5 @@ pub async fn run(
4130
});
4231
}
4332
}
44-
45-
// 6. Output result via output module
46-
output::print_json(&result, json_mode).map_err(|e| CliError::ApiError {
47-
message: format!("Output error: {}", e),
48-
})?;
49-
Ok(())
33+
Ok(result)
5034
}

src/jq.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
2+
use serde_json::Value;
3+
4+
pub fn apply(expression: &str, input: &Value) -> Result<Value, Box<dyn std::error::Error>> {
5+
let mut defs = ParseCtx::new(Vec::new());
6+
defs.insert_natives(jaq_core::core());
7+
defs.insert_defs(jaq_std::std());
8+
9+
let (filter, errs) = jaq_parse::parse(expression, jaq_parse::main());
10+
if !errs.is_empty() {
11+
return Err(format!("jq parse error: {:?}", errs).into());
12+
}
13+
14+
let filter = match filter {
15+
Some(f) => f,
16+
None => return Err("jq parse error: empty filter".into()),
17+
};
18+
19+
let filter = defs.compile(filter);
20+
if !defs.errs.is_empty() {
21+
return Err(format!("jq compile error: {} error(s)", defs.errs.len()).into());
22+
}
23+
24+
let inputs = RcIter::new(core::iter::empty());
25+
let out: Vec<Value> = filter
26+
.run((Ctx::new(Vec::new(), &inputs), Val::from(input.clone())))
27+
.map(|v| v.map(Value::from).map_err(|e| format!("jq error: {:?}", e)))
28+
.collect::<Result<Vec<_>, _>>()?;
29+
30+
match out.len() {
31+
0 => Ok(Value::Null),
32+
1 => Ok(out.into_iter().next().unwrap()),
33+
_ => Ok(Value::Array(out)),
34+
}
35+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pub mod config;
22
pub mod error;
33
pub mod output;
44
pub mod params;
5+
pub mod jq;

src/main.rs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod output;
66
mod error;
77
mod config;
88
mod params;
9+
mod jq;
910

1011
#[derive(Parser, Debug)]
1112
#[command(
@@ -21,6 +22,11 @@ struct Cli {
2122
#[arg(long)]
2223
json: bool,
2324

25+
/// Server-side field filtering (SerpApi json_restrictor parameter)
26+
#[arg(long)]
27+
fields: Option<String>,
28+
29+
/// Client-side jq filter applied to JSON output (like gh --jq)
2430
#[arg(long)]
2531
jq: Option<String>,
2632

@@ -48,7 +54,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4854
let cli = Cli::parse();
4955
let json_mode = cli.json;
5056

51-
match cli.command {
57+
let result = match cli.command {
5258
Command::Search { params } => {
5359
let api_key = config::resolve_api_key(
5460
cli.api_key.as_deref(),
@@ -57,47 +63,55 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5763
let parsed_params = params.iter()
5864
.map(|s| params::Param::from_str(s))
5965
.collect::<Result<Vec<_>, _>>()?;
60-
if let Err(e) = commands::search::run(parsed_params, &api_key, json_mode, cli.jq.as_deref()).await {
61-
error::print_error(&e);
62-
std::process::exit(error::exit_code(&e));
63-
}
66+
commands::search::run(parsed_params, &api_key, cli.fields.as_deref()).await
6467
},
6568
Command::Account => {
6669
let api_key = config::resolve_api_key(
6770
cli.api_key.as_deref(),
6871
std::env::var("SERPAPI_KEY").ok().as_deref()
6972
)?;
70-
if let Err(e) = commands::account::run(&api_key, json_mode).await {
71-
error::print_error(&e);
72-
std::process::exit(error::exit_code(&e));
73-
}
73+
commands::account::run(&api_key).await
7474
},
7575
Command::Locations { params } => {
7676
let parsed_params = params.iter()
7777
.map(|s| params::Param::from_str(s))
7878
.collect::<Result<Vec<_>, _>>()?;
79-
if let Err(e) = commands::locations::run(parsed_params, json_mode).await {
80-
error::print_error(&e);
81-
std::process::exit(error::exit_code(&e));
82-
}
79+
commands::locations::run(parsed_params).await
8380
},
8481
Command::Archive { id } => {
8582
let api_key = config::resolve_api_key(
8683
cli.api_key.as_deref(),
8784
std::env::var("SERPAPI_KEY").ok().as_deref()
8885
)?;
89-
if let Err(e) = commands::archive::run(&id, &api_key, json_mode).await {
90-
error::print_error(&e);
91-
std::process::exit(error::exit_code(&e));
92-
}
86+
commands::archive::run(&id, &api_key).await
9387
},
9488
Command::Login => {
9589
if let Err(e) = commands::login::run().await {
9690
error::print_error(&e);
9791
std::process::exit(error::exit_code(&e));
9892
}
93+
return Ok(());
9994
},
100-
}
95+
};
10196

97+
match result {
98+
Ok(value) => {
99+
let filtered = match cli.jq.as_deref() {
100+
Some(expr) => jq::apply(expr, &value)?,
101+
None => value,
102+
};
103+
output::print_json(&filtered, json_mode).map_err(|e| {
104+
let err = error::CliError::ApiError {
105+
message: format!("Output error: {}", e),
106+
};
107+
error::print_error(&err);
108+
std::process::exit(error::exit_code(&err));
109+
}).ok();
110+
}
111+
Err(e) => {
112+
error::print_error(&e);
113+
std::process::exit(error::exit_code(&e));
114+
}
115+
}
102116
Ok(())
103117
}

0 commit comments

Comments
 (0)