Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/rnag/smartsheet-rs

Async Smartsheet API implementation in Rust
https://github.com/rnag/smartsheet-rs

async hyper-rustls rust smartsheet smartsheet-api

Last synced: about 1 month ago
JSON representation

Async Smartsheet API implementation in Rust

Awesome Lists containing this project

README

        

# 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
// *[email protected], [email protected]*
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", "[email protected]")?,
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)