8 releases (5 breaking)

0.6.2 Feb 6, 2023
0.6.1 Apr 5, 2022
0.5.0 Feb 27, 2022
0.4.0 Feb 25, 2022
0.1.0 Feb 7, 2022

#918 in Web programming

43 downloads per month

MIT license

150KB
2.5K SLoC

smartsheet-rs

github crates.io docs.rs build status

smartsheet-rs is a rust crate which provides an async wrapper API that lets you easily interact with the Smartsheet API 2.0.

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 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 in the Smartsheet API Documentation.

  2. Add these dependencies to your Cargo.toml:

    [dependencies]
    smartsheet-rs = "0.6.2"
    tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
    
  3. Add some usage to your application:

    use smartsheet_rs::SmartsheetApi;
    
    #[tokio::main]
    async fn main() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
        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 have been implemented currently:

You can check out sample usage of these API methods in the 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:

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<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[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.

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.

Here is the relevant part of the code which demonstrates the ideal way of processing MULTI_CONTACT cell data for a given row:

// 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 <john@example.com>*
let names = contacts.name_addrs();

For the full code, check out the cell_multi_contact example in the project repo.

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 Rows where a Cell from a column does not have a specified value.

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<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[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 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.

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<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[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:

[dependencies]
smartsheet-rs = { version = "0.6.2", default-features = false, features = ["native-tls", "logging", "serde-std"] }

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.

License

This project is proudly licensed under the MIT 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

Dependencies

~5–20MB
~279K SLoC