Do It All In Rust #8

Open
djsweet wants to merge 4 commits from djsweet/get-it-started-in-rust into main
6 changed files with 1826 additions and 10 deletions

5
.gitignore vendored
View file

@ -1,3 +1,8 @@
.env
.envrc
.DS_Store
# Added by cargo
/target

1604
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "smsbb"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws-config = { version = "1.1.5", features = ["sso"] }
aws-sdk-dynamodb = "1.14.0"
aws-sdk-sns = "1.13.0"
aws-sdk-sqs = "1.13.0"
clap = "4.4.18"
tokio = { version = "1.36.0", features = ["full"] }

37
README.md Normal file
View file

@ -0,0 +1,37 @@
# smsbb 👼
✨ DIY sms announce lists for you and yours ✨
## norms for working on this repo
- work in a branch
- branch names should use the format `username/thing-you-are-doing`
- for example `git checkout -b maren/update-readme-3`
- don't worry about tidy commits! just keep your work in a branch
- open a PR and review with someone synchronously before merging to main
## getting started real fast
1. Make sure you have Rust (+ cargo) installed. The preferred way to do this
is with [rustup](https://rustup.rs).
2. Set up [an AWS Account](https://aws.amazon.com). Following the getting
started guide for the [AWS SDK For Rust](https://aws.amazon.com/sdk-for-rust/)
is a good way to get set up. We can confirm that this all works correctly
using Workforce Identity, i.e. establishing a profile in `~/.aws/config`,
setting the `AWS_PROFILE` environment variable appropriately, and using
`aws sso login` to get an access token.
3. With your preferred authentication mechanism set up (see above for
settings for Workforce Identity), run
```
cargo run
```
As of right this second, all the code does is set up DynamoDB tables.
The immediate future work here is:
1. Write code to provision AWS Pinpoint for each of smsbb's text campaigns
2. Figure out how to embed another Rust binary inside the main Rust binary
3. Use the embedded Rust binaries to deploy Lambda functions

View file

@ -1,10 +0,0 @@
# smsbb 👼
✨ DIY sms announce lists for you and yours ✨
## norms for working on this repo
- work in a branch
- branch names should use the format `username/thing-you-are-doing`
- for example `git checkout -b maren/update-readme-3`
- don't worry about tidy commits! just keep your work in a branch
- open a PR and review with someone synchronously before merging to main

166
src/main.rs Normal file
View file

@ -0,0 +1,166 @@
use aws_config::meta::region::RegionProviderChain;
use aws_config::BehaviorVersion;
use aws_sdk_dynamodb::operation::create_table::CreateTableError;
// use aws_sdk_sns::{Client as SnsClient, Error as SnsError};
use aws_sdk_dynamodb::Client as DynamoClient;
use aws_sdk_dynamodb::types::{AttributeDefinition, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType};
// const TOPIC_PREFIX: &'static str = "smsbb_";
const TABLE_PREFIX: &'static str = "smsbb_";
/*
fn full_topic_name(s: &str) -> String {
format!("{}{}", TOPIC_PREFIX, s)
}
*/
fn table_name_with_prefix(s: &str) -> String {
format!("{}{}", TABLE_PREFIX, s)
}
fn users_table_name() -> String {
table_name_with_prefix("users")
}
fn campaigns_table_name() -> String {
table_name_with_prefix("campaigns")
}
fn messages_table_name() -> String {
table_name_with_prefix("messages")
}
fn unsubscribes_table_name() -> String {
table_name_with_prefix("unsubscribes")
}
#[derive(Debug)]
enum SmsbbInternalError {
CreateTableFailed(String)
}
fn create_table_failed(table_name: &str) -> SmsbbInternalError {
SmsbbInternalError::CreateTableFailed(table_name.to_string())
}
struct DynamoKeySpecification<'a> {
hash_key: &'a str,
sort_key: Option<&'a str>
}
fn build_key_schema_element(attr_name: &str, key_type: KeyType) -> KeySchemaElement {
KeySchemaElement::builder()
.attribute_name(attr_name)
.key_type(key_type)
.build()
.unwrap()
}
fn string_attribute_definition(name: &str) -> AttributeDefinition {
AttributeDefinition::builder().attribute_name(name).attribute_type(ScalarAttributeType::S).build().unwrap()
}
fn atribute_definitions_for_key<'a>(primary_key: &DynamoKeySpecification) -> Option<Vec<AttributeDefinition>> {
let mut results = vec!(string_attribute_definition(primary_key.hash_key));
match primary_key.sort_key {
Some(sort_key) => {
results.push(string_attribute_definition(sort_key))
}
None => {}
}
Some(results)
}
async fn create_table_if_not_exists<'a>(
client: &DynamoClient,
table_name: &str,
primary_key: &DynamoKeySpecification<'a>
) -> Result<(), SmsbbInternalError> {
let primary_key_schema = build_key_schema_element(primary_key.hash_key, KeyType::Hash);
let mut table_builder = client.create_table()
.set_attribute_definitions(atribute_definitions_for_key(primary_key))
.table_name(table_name)
.key_schema(primary_key_schema)
.billing_mode(BillingMode::PayPerRequest);
match primary_key.sort_key {
Some(sort_key) => {
table_builder = table_builder.key_schema(build_key_schema_element(sort_key, KeyType::Range));
}
None => {}
}
let build_result = table_builder.send().await;
match build_result {
Ok(_) => {},
Err(e) => {
match e.as_service_error() {
None => {
dbg!("{}", e);
return Err(create_table_failed(table_name));
},
Some(cte) => {
match cte {
CreateTableError::ResourceInUseException(_) => {
}
problem => {
dbg!("{}", problem);
return Err(create_table_failed(table_name));
}
}
}
}
}
}
// We're not handling secondary indexes yet.
Ok(())
}
async fn ensure_tables(client: &DynamoClient) -> Result<(), SmsbbInternalError> {
let users_table = users_table_name();
let users_key = DynamoKeySpecification { hash_key: "email", sort_key: None };
create_table_if_not_exists(client, &users_table, &users_key).await?;
let campaign_table = campaigns_table_name();
let campaign_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("created_at") };
create_table_if_not_exists(client, &campaign_table, &campaign_key).await?;
let messages_table = messages_table_name();
let messages_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("message_id") };
create_table_if_not_exists(client, &messages_table, &messages_key).await?;
let unsubscribes_table = unsubscribes_table_name();
let unsubscribes_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("phone_number") };
create_table_if_not_exists(client, &unsubscribes_table, &unsubscribes_key).await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), SmsbbInternalError> {
let region_provider = RegionProviderChain::default_provider()
.or_else("us-east-1");
let config = aws_config::defaults(BehaviorVersion::latest())
.region(region_provider)
.load()
.await;
let dynamo_client = DynamoClient::new(&config);
let ensure_resp = ensure_tables(&dynamo_client).await;
match ensure_resp {
Err(sdk_error) => {
dbg!("{}", sdk_error);
return Err(SmsbbInternalError::CreateTableFailed("woof".to_string()));
},
_ => {}
}
Ok(())
}