An idiomatic pattern for Condition / Filters / Query Builders for API?

⚓ Rust    📅 2025-12-04    👤 surdeus    👁️ 1      

surdeus

I have the below rust code:

pub struct IdFilter {
    pub eq: Option<uuid::Uuid>,
    pub contained_in: Option<Vec<uuid::Uuid>>,
}

pub struct NullableIdFilter {
    pub eq: Option<uuid::Uuid>,
    pub contained_in: Option<Vec<uuid::Uuid>>,
    pub is_null: Option<bool>,
}

pub struct NullableDateTimeFilter {
    pub gt: Option<DateTime<Utc>>,
    pub gte: Option<DateTime<Utc>>,
    pub lt: Option<DateTime<Utc>>,
    pub lte: Option<DateTime<Utc>>,
    pub is_null: Option<bool>,
}

pub struct NullableStringFilter {
    pub eq: Option<String>,
    pub similar_to: Option<String>,
    pub starts_with: Option<String>,
    pub ends_with: Option<String>,
    pub contained_in: Option<Vec<String>>,
    pub is_null: Option<bool>,
}

pub struct PlayerQueryInput {
    pub filters: Option<Box<PlayerFilters>>,
    pub paging: Option<Paging>,
    pub sort_by: Option<Vec<PlayerSortBy>>,
}

pub struct PlayerFilters {
    pub and: Vec<Self>,
    pub or: Vec<Self>,
    pub not: Option<Box<Self>>,

    pub id: Option<IdFilter>,
    pub created_time: Option<DateTimeFilter>,
    pub creator: Option<NullableIdFilter>,
    pub modified_time: Option<NullableDateTimeFilter>,
    pub modifier: Option<NullableIdFilter>,
    pub status: Option<PlayerStatus>,
    pub jersey_number: Option<NumberFilter<i64>>,
    pub start_date: Option<DateTimeFilter>,
    pub end_date: Option<DateTimeFilter>,
    pub is_part_time: Option<BoolFilter>,
    pub weekend_game: Option<BoolFilter>,
    pub team_reference: Option<NullableStringFilter>,
    pub salary: Option<DecimalFilter>,
    pub bonus: Option<DecimalFilter>,
    pub tax: Option<DecimalFilter>,
    pub team_id: Option<IdFilter>,
    pub contract_id: Option<IdFilter>,
    pub position_id: Option<IdFilter>,
    pub equipment_id: Option<NullableIdFilter>,
    pub vehicle_id: Option<NullableIdFilter>,
    pub tool_id: Option<NullableIdFilter>,
    pub permit_id: Option<NullableIdFilter>,
    pub material_id: Option<NullableIdFilter>,
    pub extra_feature_id: Option<NullableIdFilter>,
    pub invoice_id: Option<NullableIdFilter>,
    pub transfer_list_id: Option<NullableIdFilter>,

    // Here all the "with_" and "query_" fields: these are all Player's related objects (entities)
    pub with_equipment: Option<Box<EquipmentFilters>>,
    pub with_permit: Option<Box<PermitFilters>>,
    pub with_vehicle: Option<Box<VehicleFilters>>,
    pub with_container: Option<Box<ContainerFilters>>,
    pub with_transfer_list: Option<Box<PlayerTransferListFilters>>,
    pub with_material: Option<Box<MaterialFilters>>,
    pub with_extra_feature: Option<Box<ExtraFeatureFilters>>,
    pub with_position: Option<Box<PositionFilters>>,
    pub with_expenses: Option<Box<PlayerExpenseQueryInput>>,
    pub query_expenses_with: Option<Box<PlayerExpenseQueryInput>>,
    pub with_routes: Option<Box<PlayerRouteQueryInput>>,
    pub query_routes_with: Option<Box<PlayerRouteQueryInput>>,
    pub with_team: Option<Box<TeamFilters>>,
    pub with_documents: Option<Box<PlayerDocumentQueryInput>>,
    pub with_letters: Option<Box<PlayerLetterQueryInput>>,
    pub query_letters_with: Option<Box<PlayerLetterQueryInput>>,
    pub with_bonuses: Option<Box<PlayerBonusQueryInput>>,
    pub query_bonuses_with: Option<Box<PlayerBonusQueryInput>>,
    pub with_free_items: Option<Box<PlayerFreeItemQueryInput>>,
    pub query_free_items_with: Option<Box<PlayerFreeItemQueryInput>>,
    pub with_skill: Option<Box<PlayerSkillQueryInput>>,
    pub query_skill_with: Option<Box<PlayerSkillQueryInput>>,
    pub with_invoice: Option<Box<InvoiceFilters>>,
    pub with_contract: Option<Box<ContractFilters>>,

    // this is for Player's related objects "skills", not a native field (is not a bool on the Player struct)
    pub has_skill: Option<bool>,
}

As you can imagine this can be a very big struct: this for player is just an example, real ones can be huge: a lot of KB!

So far this pattern worked well because using graphql with async-graphql the generated graphql schema was fine and I could use a very convenient and typed query like this:

const filters = {
    status: PlayerStatus.SOME,
    team_reference: { eq: "some" },
    and: [{filters}, {filters}],
    or: [{filters}, {filters}],
    // and even:
    and: [
        {
            is_part_time: { is_null: true },
            or: [{filters}]
        }
    ]
}

as you can see is very useful.

But now for a new project I'm dropping graphql and I would like to find another way of creating filters, something less huge. I'm using classic REST APIs. But this is not related to specific technique I use, I think.

A StackOverflow's user suggested this:

use chrono::{DateTime, Utc};

enum SingleFilter {
    Id (u32),
    CreatedAt (DateTime<Utc>),
    CreatedBy (u32),
    Position (i16),
}

fn main() {
    let mut player_filters = vec![];

    player_filters.push (SingleFilter::CreatedBy(42));

    println!("{player_filters:?}")
}

But what about AND, OR, NOT and other aggregation operators?

What about fields like with_invoice or query_skill_with?

I need to serialize and deserialize automatically from frontend.

What do you think about?

Is there an idomatic way of doing this?

1 post - 1 participant

Read full topic

🏷️ Rust_feed