-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Rust code example standards
David Souther edited this page Jul 25, 2024
·
2 revisions
- One example crate per SDK service, with the same name as the published crate minus
aws-sdk-
. - Each crate has a
lib.rs
in the root, which uses pub mod for each module part.- Skipped if crates only have bin examples.
-
bin
dir for all binaries (CLI programs and Scenario runners).- Keep user interaction mainly in the bin areas, do not put user interaction in supporting libraries.
- Use the inquire crate for complex user input, and
stdin().read_line()
for simple user input.
- Decompose scenario actions into functions in
lib/[scenario_name].rs
, possibly as part of a struct with scenario information to manage the communication saga as en Epic. (See aurora scenario) - One client wrapper for integration test mocking, if necessary. (See aurora/src/rds.rs)
- Initialize client once, in main, using behavior_subject, and clone when necessary.
- When to prefer Loop vs Iterator:
- If there is a clear transformation from
T
toU
, prefer an iterator. - Do not nest control flow inside an iterator.
- Extract the logic to a dedicated function.
- Prefer a for loop if the function would be difficult to extract.
- Prefer an extracted function if the logic is nuanced and should be tested.
- If there is a clear transformation from
- How deep to go in nesting vs when to extract a new function?
- Two deep is fine, three deep is pushing it, four deep is probably too much.
- Prefer an extracted function if the logic is nuanced and should be tested.
- When to Trait vs bare functions?
- Examples rarely have enough complexity that a Trait is worth the mental overhead.
- bare functions or a struct to manage the Epic communication saga, if necessary.
- All operations are async.
- Use tokio for async runtime.
- List operations typically provide
.into_paginator()
as an async iterator. Use them whenever available, unless the example is specifically showing non-paginator pieces.let page_size = page_size.unwrap_or(10); let items: Result<Vec<_>, _> = client .scan() .table_name(table) .limit(page_size) .into_paginator() .items() .send() .collect() .await; println!("Items in table (up to {page_size}):"); for item in items? { println!(" {:?}", item); }
- Use builtin waters as
client.wait_until_...
whenever available. (Example in EC2)let wait_status_ok = client .wait_until_instance_status_ok() .instance_ids(id) .wait(Duration::from_secs(60)) .await; match wait_status_ok { Ok(_) => println!("Rebooted instance {id}, it is started with status OK."), Err(err) => return Err(err.into()), }
- Modeled errors for scenarios (see Aurora) (hand-modeled or thiserror).
#[derive(Debug, PartialEq, Eq)] pub struct ScenarioError { message: String, context: Option<MetadataError>, } impl ScenarioError { pub fn with(message: impl Into<String>) -> Self { ScenarioError { message: message.into(), context: None, } } pub fn new(message: impl Into<String>, err: &dyn ProvideErrorMetadata) -> Self { ScenarioError { message: message.into(), context: Some(MetadataError::from(err)), } } } impl std::error::Error for ScenarioError {} impl Display for ScenarioError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.context { Some(c) => write!(f, "{}: {}", self.message, c), None => write!(f, "{}", self.message), } } }
- Client::Error for one-off “scripts” or single-action examples. (See example in EC2).
use aws_sdk_ec2::{Client, Error}; async fn show_regions(client: &Client) -> Result<(), Error> { let rsp = client.describe_regions().send().await?; for region in rsp.regions() { // ... } Ok(()) }
- Anyhow and .into_service_error for things in the middle. (See example in SES).
let contacts: Vec<Contact> = match self .client .list_contacts() .contact_list_name(CONTACT_LIST_NAME) .send() .await { Ok(list_contacts_output) => { list_contacts_output.contacts.unwrap().into_iter().collect() } Err(e) => { return Err(anyhow!( "Error retrieving contact list {}: {}", CONTACT_LIST_NAME, e )) } };
- include_bytes! or include_str! for compile-time data.
- fs::read_to_string for glob load, fs::File for streaming, tie to SdkBody when needed (see example in s3).
- When showing examples that handle PII, use the secrets crate.
-
Unit tests in
#[cfg(test)] mod test
ortest.rs
, integration tests in the tests folder.- See fully worked example in Aurora
- Unit test
use crate::rds::MockRdsImpl; #[tokio::test] async fn test_scenario_set_engine_not_create() { let mut mock_rds = MockRdsImpl::default(); mock_rds .expect_create_db_cluster_parameter_group() .with( eq("RustSDKCodeExamplesDBParameterGroup"), eq("Parameter Group created by Rust SDK Code Example"), eq("aurora-mysql"), ) .return_once(|_, _, _| Ok(CreateDbClusterParameterGroupOutput::builder().build())); let mut scenario = AuroraScenario::new(mock_rds); let set_engine = scenario.set_engine("aurora-mysql", "aurora-mysql8.0").await; assert!(set_engine.is_err()); }
- Mocks
#[cfg(test)] use mockall::automock; #[cfg(test)] pub use MockRdsImpl as Rds; #[cfg(not(test))] pub use RdsImpl as Rds; pub struct RdsImpl { pub inner: RdsClient, } #[cfg_attr(test, automock)] impl RdsImpl { pub fn new(inner: RdsClient) -> Self { RdsImpl { inner } } pub async fn describe_db_engine_versions( &self, engine: &str, ) -> Result<DescribeDbEngineVersionsOutput, SdkError<DescribeDBEngineVersionsError>> { self.inner .describe_db_engine_versions() .engine(engine) .send() .await } // etc }
-
Coverage with cargo-llm-cov
- For command line examples, prefer using clap and command line args.
- For server and lambda examples, prefer using args.
- Use the actix-web crate for HTTP servers.
- For standalone scenarios, there are generally one or more “clean up” functions. These should be highly error resistant, logging but not stopping on any resource removal errors. The clean up function should always run, possibly by storing any error(s) from the main body and reporting them after attempting clean up.
- When implementing a scenario, try to start with the clean up.
- Scenarios requiring clean up may add a “—no-cleanup” or "—cleanup=false" flag to skip performing the cleanup step.
- Follow the spec for other cleanup decisions.
- About the AWS SDK Code Examples repo
- Code quality guidelines - testing and linting
- Code comment guidelines
- Sample files guidelines
- Cross-service example guidelines
- README templates
-
Code example standards
- General standards
- .NET code example standards
- C++ code example standards
- CLI/Bash code example standards
- Go code example standards
- Kotlin code example standards
- Java code example standards
- JavaScript code example standards
- PHP code example standards
- Python code example standards
- Swift code example standards
- SAP-ABAP code example standards
- Ruby code example standards
- Rust code example standards