From 42d2041a4fc8531693aaf892102f276d5b4a381a Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Fri, 29 Aug 2025 09:21:33 -0400 Subject: [PATCH] feat: implement serde_dynamo for all possible functions --- Cargo.lock | 12 ++++ Cargo.toml | 4 +- src/common/student_info.rs | 23 +++++-- src/pages/home_page.rs | 125 ++++++++++++------------------------- 4 files changed, 72 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 927122b..c94c8d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2958,6 +2958,7 @@ dependencies = [ "leptos_router", "macros", "serde", + "serde_dynamo", "tokio", "url", "uuid", @@ -3051,6 +3052,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_dynamo" +version = "4.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36c1b1792cfd9de29eb373ee6a4b74650369c096f55db7198ceb7b8921d1f7f" +dependencies = [ + "aws-sdk-dynamodb", + "base64 0.21.7", + "serde", +] + [[package]] name = "serde_json" version = "1.0.140" diff --git a/Cargo.toml b/Cargo.toml index 18588aa..f7d088a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,10 @@ leptos_axum = { version = "0.8", optional = true } leptos_meta = { version = "0.8" } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } wasm-bindgen = { version = "=0.2.100", optional = true } -aws-sdk-dynamodb = { version = "1.74.0", optional = true } +aws-sdk-dynamodb = { version = "1", optional = true } aws-config = { version = "1.6.2", optional = true } serde = "1.0.219" +serde_dynamo = { version = "4.2.14", features = ["aws-sdk-dynamodb+1"], optional = true } uuid = { version = "1.17.0", features = ["serde", "v4", "js"] } leptos_oidc = "0.9.0" url = "2.5.4" @@ -36,6 +37,7 @@ ssr = [ "dep:leptos_axum", "dep:aws-sdk-dynamodb", "dep:aws-config", + "dep:serde_dynamo", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/src/common/student_info.rs b/src/common/student_info.rs index 8f8a983..120eb70 100644 --- a/src/common/student_info.rs +++ b/src/common/student_info.rs @@ -1,10 +1,23 @@ use macros::Reactive; use serde::{Deserialize, Serialize}; +use std::default::Default; -#[derive(Serialize, Deserialize, Debug, Clone, Reactive)] +/// Represents information about a student. +#[derive(Serialize, Deserialize, Debug, Clone, Reactive, Default)] pub struct StudentInfo { - pub first_name: String, - pub last_name: String, - pub math_sat_score: i32, - pub student_email: String, + /// The email address of the student. This was used in the previous version of the database, + /// and should now be deprecated. + pub Email: String, + /// The student's first name. + #[serde(default)] + pub studentFirstName: String, + /// The student's last name. + #[serde(default)] + pub studentLastName: String, + /// The student's highest math score on the SAT. + #[serde(default)] + pub mathScoreSAT: i32, + /// The student's desired contact email. + #[serde(default)] + pub studentEmail: String, } diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 0ebb0d2..e630c8f 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,8 +1,15 @@ +// Server dependencies #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Client}; + +#[cfg(feature = "ssr")] +use serde_dynamo::{from_item, to_item}; + use crate::app::Unauthenticated; use crate::common::{StudentInfo, StudentInfoReactive, UserClaims}; -use crate::components::{ActionButton, Loading, OutlinedTextField, Select, CheckboxList, Panel, Row}; +use crate::components::{ + ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, Row, Select, +}; use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; @@ -24,38 +31,11 @@ pub async fn get_submission(id: String) -> Result { Ok(output) => { let item = output.item.unwrap(); - let first_name: String = item - .get("studentFirstName") - .and_then(|attr| attr.as_s().ok()) - .map(|s| s.to_owned()) - .unwrap_or_default(); + let info: StudentInfo = from_item(item).unwrap(); - let last_name: String = item - .get("studentLastName") - .and_then(|attr| attr.as_s().ok()) - .map(|s| s.to_owned()) - .unwrap_or_default(); + console_log(format!("Got info from API: {:?}", info).as_str()); - let math_sat_score = item - .get("mathSAT") - .and_then(|attr| attr.as_n().ok()) - .map(|n| n.parse::().unwrap_or(0)) - .unwrap_or_default(); - - let student_email = item - .get("studentEmail") - .and_then(|attr| attr.as_s().ok()) - .map(|s| s.to_owned()) - .unwrap_or_default(); - - console_log(format!("Got values from API: {} {}", first_name, last_name).as_str()); - - Ok(StudentInfo { - first_name, - last_name, - math_sat_score, - student_email, - }) + Ok(info) } Err(err) => { let msg = err.message().unwrap_or("Unknown error"); @@ -69,45 +49,19 @@ pub async fn create_sample_submission( student_info: StudentInfo, subject: String, ) -> Result<(), ServerFnError> { - use aws_sdk_dynamodb::{types::AttributeValue, Client}; + use aws_sdk_dynamodb::Client; let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let dbclient = Client::new(&config); - console_log( - format!( - "Creating sample submission with name {} {}, math score {}, and subject {}", - student_info.first_name, student_info.last_name, student_info.math_sat_score, subject - ) - .as_str(), - ); - + // The put_item action can create or update an item in a DynamoDB table. + // It will completely replace any existing item with the same primary key, + // which is the desired behavior. + let item = to_item(student_info).unwrap(); match dbclient - .update_item() - .table_name("student-applications") - .key("Email", AttributeValue::S(subject)) - .expression_attribute_values( - ":studentFirstName", - AttributeValue::S(student_info.first_name), - ) - .expression_attribute_values( - ":studentLastName", - AttributeValue::S(student_info.last_name), - ) - .expression_attribute_values( - ":mathScoreSAT", - AttributeValue::N(student_info.math_sat_score.to_string()), - ) - .expression_attribute_names("#studentFirstName", "studentFirstName") - .expression_attribute_names("#studentLastName", "studentLastName") - .expression_attribute_names("#mathScoreSAT", "mathScoreSAT") - .update_expression( - concat!("SET #studentFirstName = :studentFirstName, ", - "#studentLastName = :studentLastName, ", - "#mathScoreSAT = :mathScoreSAT"), - ) - .send() - .await + .put_item() + .set_item(Some(item)) + .send().await { Ok(_) => Ok(()), Err(err) => Err(ServerFnError::new(err.into_service_error().to_string())), @@ -133,21 +87,14 @@ pub fn HomePage() -> impl IntoView { let server_resource = Resource::new( move || user_claims.get().map(|claim| claim.claims.subject.clone()), async |opt_username| match opt_username { - Some(username) => get_submission(username).await.unwrap_or_else(|e| { - console_log(e.to_string().as_str()); - StudentInfo { - first_name: String::from("Error"), - last_name: String::new(), - math_sat_score: 0, - student_email: String::new() + Some(username) => get_submission(username) + .await + .unwrap_or_else(|e| { + console_log(e.to_string().as_str()); + StudentInfo::default() } - }), - None => StudentInfo { - first_name: String::new(), - last_name: String::new(), - math_sat_score: 0, - student_email: String::new() - }, + ), + None => StudentInfo::default() }, ); let submit_action = ServerAction::::new(); @@ -169,15 +116,21 @@ pub fn HomePage() -> impl IntoView { view! {
-

- "Testing paragraph! This panel should be the same size as the other." -

+ +

+ "Testing paragraph! This panel should be the same size as the other." +

+
+ +

"Current user's subject ID: "{reactive_info.Email}

+

"Current user's reported full name from the API: " - {reactive_info.first_name}" "{reactive_info.last_name} + {reactive_info.studentFirstName}" " + {reactive_info.studentLastName}

@@ -185,19 +138,19 @@ pub fn HomePage() -> impl IntoView { label="First Name:" placeholder="John" disabled=elements_disabled - value=reactive_info.first_name + value=reactive_info.studentFirstName />