Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
50 changes: 50 additions & 0 deletions src/commands/events.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
types::request::{EventByIdRequest, EventBySlugRequest, EventTagsRequest, EventsRequest},
};
use rust_decimal::Decimal;

use super::is_numeric_id;
use crate::output::OutputFormat;
Expand Down Expand Up @@ -47,6 +49,38 @@ pub enum EventsCommand {
/// Filter by tag slug (e.g. "politics", "crypto")
#[arg(long)]
tag: Option<String>,

/// Minimum trading volume (e.g. 1000000)
#[arg(long)]
volume_min: Option<Decimal>,

/// Maximum trading volume
#[arg(long)]
volume_max: Option<Decimal>,

/// Minimum liquidity
#[arg(long)]
liquidity_min: Option<Decimal>,

/// Maximum liquidity
#[arg(long)]
liquidity_max: Option<Decimal>,

/// Only events starting after this date (e.g. 2026-03-01T00:00:00Z)
#[arg(long)]
start_date_min: Option<DateTime<Utc>>,

/// Only events starting before this date
#[arg(long)]
start_date_max: Option<DateTime<Utc>>,

/// Only events ending after this date
#[arg(long)]
end_date_min: Option<DateTime<Utc>>,

/// Only events ending before this date
#[arg(long)]
end_date_max: Option<DateTime<Utc>>,
},

/// Get a single event by ID or slug
Expand All @@ -72,6 +106,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
order,
ascending,
tag,
volume_min,
volume_max,
liquidity_min,
liquidity_max,
start_date_min,
start_date_max,
end_date_min,
end_date_max,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

Expand All @@ -83,6 +125,14 @@ pub async fn execute(client: &gamma::Client, args: EventsArgs, output: OutputFor
.maybe_tag_slug(tag)
// EventsRequest::order is Vec<String>; into_iter on Option yields 0 or 1 items.
.order(order.into_iter().collect())
.maybe_volume_min(volume_min)
.maybe_volume_max(volume_max)
.maybe_liquidity_min(liquidity_min)
.maybe_liquidity_max(liquidity_max)
.maybe_start_date_min(start_date_min)
.maybe_start_date_max(start_date_max)
.maybe_end_date_min(end_date_min)
.maybe_end_date_max(end_date_max)
.build();

let events = client.events(&request).await?;
Expand Down
56 changes: 56 additions & 0 deletions src/commands/markets.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand};
use polymarket_client_sdk::gamma::{
self,
Expand All @@ -10,6 +11,7 @@ use polymarket_client_sdk::gamma::{
response::Market,
},
};
use rust_decimal::Decimal;

use super::is_numeric_id;
use crate::output::OutputFormat;
Expand Down Expand Up @@ -49,6 +51,42 @@ pub enum MarketsCommand {
/// Sort ascending instead of descending
#[arg(long)]
ascending: bool,

/// Minimum trading volume (e.g. 1000000)
#[arg(long)]
volume_min: Option<Decimal>,

/// Maximum trading volume
#[arg(long)]
volume_max: Option<Decimal>,

/// Minimum liquidity
#[arg(long)]
liquidity_min: Option<Decimal>,

/// Maximum liquidity
#[arg(long)]
liquidity_max: Option<Decimal>,

/// Only markets starting after this date (e.g. 2026-03-01T00:00:00Z)
#[arg(long)]
start_date_min: Option<DateTime<Utc>>,

/// Only markets starting before this date
#[arg(long)]
start_date_max: Option<DateTime<Utc>>,

/// Only markets ending after this date
#[arg(long)]
end_date_min: Option<DateTime<Utc>>,

/// Only markets ending before this date
#[arg(long)]
end_date_max: Option<DateTime<Utc>>,

/// Filter by tag ID
#[arg(long)]
tag: Option<String>,
},

/// Get a single market by ID or slug
Expand Down Expand Up @@ -87,6 +125,15 @@ pub async fn execute(
offset,
order,
ascending,
volume_min,
volume_max,
liquidity_min,
liquidity_max,
start_date_min,
start_date_max,
end_date_min,
end_date_max,
tag,
} => {
let resolved_closed = closed.or_else(|| active.map(|a| !a));

Expand All @@ -96,6 +143,15 @@ pub async fn execute(
.maybe_offset(offset)
.maybe_order(order)
.ascending(ascending)
.maybe_volume_num_min(volume_min)
.maybe_volume_num_max(volume_max)
.maybe_liquidity_num_min(liquidity_min)
.maybe_liquidity_num_max(liquidity_max)
.maybe_start_date_min(start_date_min)
.maybe_start_date_max(start_date_max)
.maybe_end_date_min(end_date_min)
.maybe_end_date_max(end_date_max)
.maybe_tag_id(tag)
.build();

let markets = client.markets(&request).await?;
Expand Down
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub(crate) struct Cli {
/// Signature type: eoa, proxy, or gnosis-safe
#[arg(long, global = true)]
signature_type: Option<String>,

/// Comma-separated list of fields to include in JSON output (e.g. question,volume_num,slug)
#[arg(long, global = true, value_delimiter = ',')]
fields: Option<Vec<String>>,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -81,6 +85,9 @@ async fn main() -> ExitCode {

#[allow(clippy::too_many_lines)]
pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> {
// set (or clear) the JSON field filter for this invocation
output::set_json_fields(cli.fields);

// Lazy-init so we only pay for the client we actually use.
let gamma = std::cell::LazyCell::new(polymarket_client_sdk::gamma::Client::default);
let data = std::cell::LazyCell::new(polymarket_client_sdk::data::Client::default);
Expand Down
73 changes: 72 additions & 1 deletion src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ pub(crate) mod series;
pub(crate) mod sports;
pub(crate) mod tags;

use std::sync::RwLock;

use chrono::{DateTime, Utc};
use polymarket_client_sdk::types::Decimal;
use rust_decimal::prelude::ToPrimitive;
use serde_json::Value;
use tabled::Table;
use tabled::settings::object::Columns;
use tabled::settings::{Modify, Style, Width};

/// field names to keep in JSON output; updated per invocation via --fields
static JSON_FIELDS: RwLock<Option<Vec<String>>> = RwLock::new(None);

pub(crate) fn set_json_fields(fields: Option<Vec<String>>) {
if let Ok(mut guard) = JSON_FIELDS.write() {
*guard = fields;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent failure in set_json_fields hides broken --fields

Low Severity

set_json_fields silently swallows a RwLock write failure with if let Ok(...), meaning the --fields flag is quietly ignored and the previous filter value persists. Meanwhile, print_json calls .unwrap() on the read lock, which would panic on the same poisoned-lock condition. The writer silently no-ops while the reader crashes — these two call sites need consistent error handling. If the write silently fails, users get unfiltered output with no indication that --fields was ignored.

Additional Locations (1)
Fix in Cursor Fix in Web


pub(crate) const DASH: &str = "—";

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
Expand Down Expand Up @@ -63,10 +75,37 @@ pub(crate) fn active_status(closed: Option<bool>, active: Option<bool>) -> &'sta
}

pub(crate) fn print_json(data: &(impl serde::Serialize + ?Sized)) -> anyhow::Result<()> {
println!("{}", serde_json::to_string_pretty(data)?);
let guard = JSON_FIELDS.read().unwrap();
if let Some(fields) = guard.as_deref() {
// only convert to Value when filtering — preserves key order in the common case
let value = serde_json::to_value(data)?;
println!(
"{}",
serde_json::to_string_pretty(&filter_fields(value, fields))?
);
} else {
println!("{}", serde_json::to_string_pretty(data)?);
}
Ok(())
}

/// keep only the requested keys from an object or each object in an array
fn filter_fields(value: Value, fields: &[String]) -> Value {
match value {
Value::Array(arr) => {
Value::Array(arr.into_iter().map(|v| filter_fields(v, fields)).collect())
}
Value::Object(map) => {
let filtered = map
.into_iter()
.filter(|(k, _)| fields.iter().any(|f| f == k))
.collect();
Value::Object(filtered)
}
other => other,
}
}

pub(crate) fn print_error(error: &anyhow::Error, format: OutputFormat) {
match format {
OutputFormat::Json => {
Expand Down Expand Up @@ -185,4 +224,36 @@ mod tests {
fn format_decimal_just_below_million_uses_k() {
assert_eq!(format_decimal(dec!(999_999)), "$1000.0K");
}

#[test]
fn filter_fields_keeps_only_requested_keys() {
let obj = serde_json::json!({"a": 1, "b": 2, "c": 3});
let fields = vec!["a".into(), "c".into()];
let result = filter_fields(obj, &fields);
assert_eq!(result, serde_json::json!({"a": 1, "c": 3}));
}

#[test]
fn filter_fields_applies_to_each_array_element() {
let arr = serde_json::json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}]);
let fields = vec!["a".into()];
let result = filter_fields(arr, &fields);
assert_eq!(result, serde_json::json!([{"a": 1}, {"a": 3}]));
}

#[test]
fn filter_fields_returns_empty_object_when_no_match() {
let obj = serde_json::json!({"a": 1, "b": 2});
let fields = vec!["z".into()];
let result = filter_fields(obj, &fields);
assert_eq!(result, serde_json::json!({}));
}

#[test]
fn filter_fields_passes_through_non_object_values() {
let val = serde_json::json!(42);
let fields = vec!["a".into()];
let result = filter_fields(val, &fields);
assert_eq!(result, serde_json::json!(42));
}
}