# smartsheet-rs [github](https://github.com/rnag/smartsheet-rs) [crates.io](https://crates.io/crates/smartsheet-rs) [docs.rs](https://docs.rs/smartsheet-rs) [build status](https://github.com/rnag/smartsheet-rs/actions?query=branch%3Amain) **smartsheet-rs** is a *rust crate* which provides an `async` wrapper API that lets you easily interact with the [Smartsheet API 2.0](https://smartsheet-platform.github.io/api-docs/). This is an *unofficial* SDK I have made to learn Rust a little, but I hope you have fun with it -- I know that I certainly had quite a bit of fun in writing out the implementation for this crate. ## Table of Contents * [Getting Started](#getting-started) * [Implemented Methods](#implemented-methods) * [A Larger Example](#a-larger-example) * [Cells](#cells) * [Retrieve Cells](#retrieve-cells) * [Multi-Contact Cells](#multi-contact-cells) * [Rows](#rows) * [Retrieve Rows](#retrieve-rows) * [Create Rows](#create-rows) * [Dependencies and Features](#dependencies-and-features) * [Contributing](#contributing) * [License](#license) * [Authors](#authors) ## Getting Started Getting started with the `smartsheet-rs` library is easy: 1. Set **SMARTSHEET_ACCESS_TOKEN** in your environment; you can also use the `SmartsheetApi::from_token` constructor to explicitly set the token value. Find out more about [Authentication and Access Tokens](https://smartsheet-platform.github.io/api-docs/#authentication-and-access-tokens) in the Smartsheet API Documentation. 3. Add these dependencies to your `Cargo.toml`: ```toml [dependencies] smartsheet-rs = "0.6.2" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` 3. Add some usage to your application: ```rust use smartsheet_rs::SmartsheetApi; #[tokio::main] async fn main() -> std::result::Result<(), Box> { let smart = SmartsheetApi::from_env()?; println!("Created a Smartsheet API client"); let sheets = smart.list_sheets().await?; println!("Printing sheet names:"); for sheet in sheets.data { println!(" - {}", sheet.name); } Ok(()) } ``` ## Implemented Methods The following API methods from the [official documentation](https://smartsheet-platform.github.io/api-docs) have been implemented currently: - [List Sheets](https://smartsheet-platform.github.io/api-docs/#list-sheets) - [List Columns](https://smartsheet-platform.github.io/api-docs/#list-columns) - [List Attachments](https://smartsheet-platform.github.io/api-docs/#list-attachments) - [Get Sheet](https://smartsheet-platform.github.io/api-docs/#get-sheet) - [Get Column](https://smartsheet-platform.github.io/api-docs/#get-column) - [Get Attachment](https://smartsheet-platform.github.io/api-docs/#get-attachment) - [Get Row](https://smartsheet-platform.github.io/api-docs/#get-row) - [Add Rows](https://smartsheet-platform.github.io/api-docs/#add-rows) - [Update Rows](https://smartsheet-platform.github.io/api-docs/#update-rows) - [Delete Rows](https://smartsheet-platform.github.io/api-docs/#delete-rows) You can check out sample usage of these API methods in the [examples/](https://github.com/rnag/smartsheet-rs/tree/main/examples) folder in the project repo on GitHub. ## A Larger Example When working with rows and cells in the SmartSheet API, one thing that stands out is that the API purposefully identifies columns by their *ID*, rather than their title or *column name*. However, as humans it's much more natural and convenient to refer to *column names* when working with the data. Towards that end, the **smartsheet-rs** crate provides helper *struct* implementations such as the `ColumnMapper`, `CellGetter`, and `RowGetter` in order to simplify interaction with the Smartsheet API. ### Cells #### Retrieve Cells To retrieve an individual cell from a row by its associated *column id*, you can simply use `Row::get_cell_by_id`. To instead retrieve a single `Cell` by its *column name*, you can first build out a mapping of *Column Name to Id* with a `ColumnMapper`, and then pair that with `CellGetter` in order to retrieve a Cell from a Row. Here's a quick example of how that would work: ```rust use smartsheet_rs::{CellGetter, ColumnMapper, SmartsheetApi}; // TODO update these values as needed const SHEET_ID: u64 = 1234567890; const COLUMN_NAME: &str = "My Column"; // A simple type alias so as to DRY. type Result = std::result::Result>; #[tokio::main] async fn main() -> Result<()> { let smart = SmartsheetApi::from_env()?; let sheet = smart.get_sheet(SHEET_ID).await?; // Create interchangeable name <-> id mappings for columns in the row let cols = ColumnMapper::new(&sheet.columns); // Create a `CellGetter` helper to find cells in a row by *Column Name* let get_cell = CellGetter::new(&cols); // Get the first row in the sheet. We could also access // a row by index, like `&sheet.rows[i]` for example. let first_row = sheet.rows.first().unwrap(); // Try to find a cell in the row by it's column name match get_cell.by_name(first_row, COLUMN_NAME) { Ok(cell) => println!("Here's the cell: {:#?}", *cell), Err(e) => println!("Error: {}", e), } Ok(()) } ``` The `CellGetter::by_name` method works by iterating over each cell in the row, and then returning the first `Cell` where the *Column Id* for the cell matches the specified *column name*. If the need arises to retrieve *multiple* `Cell` objects from a `Row` by their column names, it might be a better idea to first build out a mapping of each *column name* to the `Cell` object in the row for that column. The method `CellGetter::name_to_cell` can be used for this purpose, as shown below. ```rust let column_name_to_cell = get_cell.name_to_cell(row); println!("{:#?}", column_name_to_cell); // Prints: // { // "Column 1": Cell {...}, // "Column 2": Cell {...}, // ... ``` #### Multi-Contact Cells When working with more [complex objects] such as cells for a `MULTI_CONTACT` column type, the helper method `Cell::contacts` can be used to extract the contact info from the cell. Note that to retrieve the emails for each contact, it's necessary to pass the `include=objectValue` query parameter, along with the corresponding `level` parameter, in order to gather the full *Multi-contact* details. [complex objects]: https://smartsheet.redoc.ly/#section/API-Basics/Multi-contact-or-Multi-picklist:-Working-with-Complex-Objects Here is the relevant part of the code which demonstrates the ideal way of processing `MULTI_CONTACT` cell data for a given row: ```rust // Retrieve the sheet with `MULTI_CONTACT` info included, such as emails. let sheet = smart.get_sheet_with_multi_contact_info(sheet_id).await?; // Let's assume we retrieve the cell for the specified column from the first row. let cell = get_cell.by_name(&sheet.rows[0], "My Multi-Contact Column")?; // Now we create a list of `Contact` objects from the cell details. let contacts = cell.contacts()?; // Get the contact emails, as a comma-delimited string in the format // *john1@example.com, john2@example.com* let emails = contacts.addrs_str(); // Get a list of contact name addresses, where each one as indicated // in the RFC will be in the format `[display-name] angle-addr` -- // that is, for example, *John Doe * let names = contacts.name_addrs(); ``` For the full code, check out the [`cell_multi_contact`] example in the project repo. [`cell_multi_contact`]: https://github.com/rnag/smartsheet-rs/blob/main/examples/cell_multi_contact.rs ### Rows #### Retrieve Rows To retrieve an individual row from a sheet by its associated *row id*, you can simply use `Sheet::get_row_by_id`. If the goal is to find one or more rows that match a specified condition from a list of rows, you can use the `RowGetter` helper to make the task much more convenient. Here's a simple example to find the **first** `Row` where a `Cell` from a column has a particular value, and find **all** `Row`s where a `Cell` from a column does *not* have a specified value. ```rust use serde_json::to_string_pretty; use smartsheet_rs::{ColumnMapper, RowGetter, SmartsheetApi}; // TODO update these values as needed const SHEET_ID: u64 = 1234567890; // A simple type alias so as to DRY. type Result = std::result::Result>; #[tokio::main] async fn main() -> Result<()> { let smart = SmartsheetApi::from_env()?; let sheet = smart.get_sheet(SHEET_ID).await?; let cols = ColumnMapper::from(&sheet); // Create a `RowGetter` helper to find rows in a sheet by a condition // based on a *Column Name* and *Column Value*. let get_row = RowGetter::new(&sheet.rows, &cols); let row = get_row // Note: "My Value" can be either a String, Number, or Boolean. .where_eq("Column 1", "My Value")? // Only want to get the first row which matches the condition. .first()?; let rows = get_row // Retrieve *all* rows that *do not* match the specified cell value. .where_ne("Column 2", 123.45)? .find_all()?; // Print the match for the first query println!("Here's the first result: {:#?}", *row); // Print the list of rows that match the second query println!("Found {} Rows that match the second condition:", rows.len()); println!("{}", to_string_pretty(&rows)?); Ok(()) } ``` Similar to the example of retrieving *multiple* `Cell` objects from a `Row`, the `Sheet::id_to_row` method can be used to build out a mapping of each *row id* to its associated `Row` object. This can be useful when searching for multiple `Row` objects by their *row id* value. #### Create Rows To add or update rows, it's necessary to build out a list of cells to update the values for, and then add the cells to the row. The helper *struct* `CellFactory` can be used to construct `Cell` objects to add to a `Row`. Note that to *add* rows, we need to pass in a [location-specifier](https://smartsheet.redoc.ly/#section/Specify-Row-Location) attribute. To *update* rows, we only need to set the *Row Id* for each row. An example of *adding* a new `Row` to a sheet is shown below. Here we set the location specifier `to_top` to send the new row to the top of the sheet. ```rust use serde_json::to_string_pretty; use smartsheet_rs::models::{Decision, LightPicker, Row, RowLocationSpecifier}; use smartsheet_rs::{CellFactory, ColumnMapper, SmartsheetApi}; // TODO update these values as needed const SHEET_ID: u64 = 1234567890; // A simple type alias so as to DRY. type Result = std::result::Result>; #[tokio::main] async fn main() -> Result<()> { let smart = SmartsheetApi::from_env()?; let index_result = smart.list_columns(SHEET_ID).await?; let cols = ColumnMapper::from(&index_result); // Create a `CellFactory` helper to build out a list of cells to create // a `Row` from. let make = CellFactory::new(&cols); // Create the `Cell` objects to add here. let cells = [ make.cell("Text/Number Column", 123.45)?, make.cell("Symbol Column #1", LightPicker::Yellow)?, make.cell("Symbol Column #2", Decision::Hold)?, make.cell("Checkbox Column", true)?, make.contact_cell("Assigned To", "user1.email@smartsheet.com")?, make.url_hyperlink_cell("Link to Page", "Rust Homepage", "https://rust-lang.org")?, make.multi_picklist_cell( "Multi Dropdown Column", &["Hello, world!", "Testing", "1 2 3"], )?, ]; // Create a new `Row` from the list of `Cell` objects. let row_to_add = Row::from(&cells); println!("Input Object: {}", to_string_pretty(&row_to_add)?); // Add the Rows to the Sheet let _ = smart.add_rows(SHEET_ID, [row_to_add].to_top(true)).await?; Ok(()) } ``` ## Dependencies and Features This library uses only the minimum required dependencies, in order to keep the overall size small. This crate uses [`hyper`][] and [`hyper-rustls`][] internally, to make HTTPS requests to the Smartsheet API. While `hyper-rustls` was chosen as the default TLS implementation because it works without issue when cross-compiling for the **x86_64-unknown-linux-musl** target as is common for [AWS Lambda][] deployments, it is still possible to instead use the native [`hyper-tls`][] implementation, which relies on OpenSSL. To do this, disable the default "rust-tls" feature and enable the "native-tls" feature: ```toml [dependencies] smartsheet-rs = { version = "0.6.2", default-features = false, features = ["native-tls", "logging", "serde-std"] } ``` [`hyper`]: https://docs.rs/hyper [`hyper-rustls`]: https://docs.rs/hyper-rustls [`hyper-tls`]: https://docs.rs/hyper-tls [AWS Lambda]: https://docs.aws.amazon.com/sdk-for-rust/latest/dg/lambda.html ## Contributing Contributions are welcome! Open a pull request to fix a bug, or [open an issue][] to discuss a new feature or change. Check out the [Contributing][] section in the docs for more info. [Contributing]: CONTRIBUTING.md [open an issue]: https://github.com/rnag/smartsheet-rs/issues ## License This project is proudly licensed under the MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT). `smartsheet-rs` can be distributed according to the MIT license. Contributions will be accepted under the same license. ## Authors * [Ritvik Nag](https://github.com/rnag)