Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: type-safe builder for updates #12

Open
happylinks opened this issue Nov 16, 2023 · 5 comments
Open

Question: type-safe builder for updates #12

happylinks opened this issue Nov 16, 2023 · 5 comments

Comments

@happylinks
Copy link

happylinks commented Nov 16, 2023

I was wondering if you've thought about making the updates type-safe somehow.
Right now the updates are done like this:

let key = OrderKeyInput {
    user_name,
    order_id,
};

let expression = expr::Update::new("SET #status = :status")
    .name("#status", "status")
    .value(":status", status);

Order::update(key)
    .expression(expression)
    .execute(self)
    .await?;

But what if they could be done like:

let key = OrderKeyInput {
    user_name,
    order_id,
};

Order::update(key)
    .status(status)
    .execute(self)
    .await?;

OR

Order::update(key)
    .expression(Order::updateBuilder().status(status).build())
    .execute(self)
    .await?;

I'm not sure how this would work yet (still relatively new to rust), but from an api perspective this would be amazing. You'd abstract away the dynamodb syntax and prevent bugs due to typos there as well. For more specific expressions you could still use the current approach.

Curious about your opinion!

@neoeinstein
Copy link
Owner

neoeinstein commented Nov 17, 2023

I would love to see this too. For the first iteration, I didn't have a clear model of how to do it safely, especially with the myriad things that a single update expression can do at a time. If I were to make something, it would probably need to be a procedural macro that could parse out the various elements and then turn that into a structure that you could use.

@happylinks
Copy link
Author

I was looking into https://docs.rs/derive_builder/0.12.0/derive_builder/ a bit, seems like it could be relevant. It automatically generates a builder pattern for a struct. We'd want it to do this for a copy of the struct that has optional values for everything I think. Then we could use that struct to create the update expression and pass the names and values. Definitely not a complete picture yet, but wanted to share anyway :)

@happylinks
Copy link
Author

Also found this one: https://github.com/yanganto/struct-patch/tree/main

let mut patch = Story::new_empty_patch();
patch.archived = Some(true);
println!("patch: {:#?}", patch);
patch: StoryPatch {
    entity_type: None,
    story_id: None,
    name: None,
    creator: None,
    creator_org_id: None,
    anon_creator_id: None,
    archived: Some(
        true,
    ),
    link_scope: None,
    link_role: None,
    views: None,
    slug: None,
    created_at: None,
    updated_at: None,
    scene_ids: None,
    cta: None,
    captions_default_enabled: None,
    view_count_enabled: None,
    comments_enabled: None,
    dimensions: None,
    ttl: None,
}

@happylinks
Copy link
Author

happylinks commented Nov 19, 2023

Continuing that patch test a bit:

let mut patch = Story::new_empty_patch();
patch.archived = Some(true);
println!("patch: {:#?}", patch);

let json = serde_json::to_value(&patch).unwrap();
let non_none_keys: Vec<_> = json
    .as_object()
    .unwrap()
    .iter()
    .filter(|(_, v)| !v.is_null())
    .map(|(k, _)| k.to_string())
    .collect();

let update_expression = format!(
    "SET {}",
    non_none_keys
        .iter()
        .map(|key| { format!("#{} = :{}", key, key) })
        .collect::<Vec<String>>()
        .join(", ")
);
println!("update_expression: {:#?}", update_expression);

let mut expression = expr::Update::new(&update_expression);
for key in non_none_keys {
    expression = expression.name(&format!("#{}", key), &key);
    expression = expression.value(&format!(":{}", key), &json[key]);
}

println!("expression: {:#?}", expression);
patch: StoryPatch {
    entity_type: None,
    story_id: None,
    name: None,
    creator: None,
    creator_org_id: None,
    anon_creator_id: None,
    archived: Some(
        true,
    ),
    link_scope: None,
    link_role: None,
    views: None,
    slug: None,
    created_at: None,
    updated_at: None,
    scene_ids: None,
    cta: None,
    captions_default_enabled: None,
    view_count_enabled: None,
    comments_enabled: None,
    dimensions: None,
    ttl: None,
}
update_expression: "SET #archived = :archived"
expression: Update {
    expression: "SET #upd_archived = :upd_archived",
    names: [
        (
            "#upd_archived",
            "archived",
        ),
    ],
    values: [
        (
            ":upd_archived",
            Bool(
                true,
            ),
        ),
    ],
    sensitive_values: <0 values>,
}

Kind of cool! Definitely not ideal serialising it to json to figure out the non-None keys, and taking the value from the json, but maybe this will help us get into the right direction :)

@MinisculeGirraffe
Copy link

serde-rs/serde#984 (comment)

Throwing this out there as possibly relevant

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants