Interaction functions

In this section, we will create a tool that stores location records to a database and query location points for a specific user. To represent a location in the code, we declare the following Location struct:

#[derive(Debug)]
struct Location {
user_id: String,
timestamp: String,
longitude: String,
latitude: String,
}

This struct keeps user_id, which represents the partition key, and timestamp, which represents the sort key.

DynamoDB is a key-value storage, where every record has a unique key. When you declare tables, you have to decide which attributes will be the key of a record. You can choose up to two keys. The first is required and represents a partition key that's used to distribute data across database partitions. The second key is optional and represents an attribute that's used to sort items in a table.

The rusoto_dynamodb crate contains an AttributeValue struct, which is used in queries and results to insert or extract data from tables. Since every record (that is, every item) of a table is a set of attribute names to attribute values, we will add the from_map method to convert the HashMap of attributes to our Location type:

impl Location {
fn from_map(map: HashMap<String, AttributeValue>) -> Result<Location, Error> {
let user_id = map
.get("Uid")
.ok_or_else(|| format_err!("No Uid in record"))
.and_then(attr_to_string)?;
let timestamp = map
.get("TimeStamp")
.ok_or_else(|| format_err!("No TimeStamp in record"))
.and_then(attr_to_string)?;
let latitude = map
.get("Latitude")
.ok_or_else(|| format_err!("No Latitude in record"))
.and_then(attr_to_string)?;
let longitude = map
.get("Longitude")
.ok_or_else(|| format_err!("No Longitude in record"))
.and_then(attr_to_string)?;
let location = Location { user_id, timestamp, longitude, latitude };
Ok(location)
}
}

We need four attributes: UidTimeStamp, Longitude, and Latitude. We extract every attribute from the map and convert it into a Location instance using the attr_to_string method:

fn attr_to_string(attr: &AttributeValue) -> Result<String, Error> {
if let Some(value) = &attr.s {
Ok(value.to_owned())
} else {
Err(format_err!("no string value"))
}
}

The AttributeValue struct contains multiple fields for different types of values:

  • b - A binary value represented by Vec<u8>
  • bool - A boolean value with the bool type
  • bs - A binary set, but represented as Vec<Vec<u8>>
  • l - A list of attributes of a Vec<AttributeValue> type
  • m - A map of attributes of a HashMap<String, AttributeValue> type
  • n - number stored as a String type to keep the exact value without any precision loss
  • ns - set of numbers as a Vec<String>
  • null - Used to represent a null value and stored as bool, which means the value is null
  • s - string, of the String type
  • ss - A set of strings, of the Vec<String> type

You might notice that there is no data type for timestamps. This is true, as DynamoDB uses strings for most types of data.

We use the s field to work with string values that we'll add with the add_location function:

fn add_location(conn: &DynamoDbClient, location: Location) -> Result<(), Error> {
let mut key: HashMap<String, AttributeValue> = HashMap::new();
key.insert("Uid".into(), s_attr(location.user_id));
key.insert("TimeStamp".into(), s_attr(location.timestamp));
let expression = format!("SET Latitude = :y, Longitude = :x");
let mut values = HashMap::new();
values.insert(":y".into(), s_attr(location.latitude));
values.insert(":x".into(), s_attr(location.longitude));
let update = UpdateItemInput {
table_name: "Locations".into(),
key,
update_expression: Some(expression),
expression_attribute_values: Some(values),
..Default::default()
};
conn.update_item(update)
.sync()
.map(drop)
.map_err(Error::from)
}

This function expects two parameters: a reference to a database client, and a Location instance to store. We have to prepare data manually to get it as an attributes map for storage, because DynamoDbClient takes values of the AttributeValue type only. The attributes included in the key are inserted into the HashMap, with values extracted from the Location instance and converted into AttributeValue using the s_attr function, which has the following declaration:

fn s_attr(s: String) -> AttributeValue {
AttributeValue {
s: Some(s),
..Default::default()
}
}

After we've filled the key map, we can set other attributes with expressions. To set attributes to an item, we have to specify them in DynamoDB syntax, along the lines of SET Longitude = :x, Latitude = :y. This expression means that we add two attributes with the names Longitude and Latitude. In the preceding expression, we used the placeholders of :x and :y, which will be replaced with real values that we pass in from the HashMap.

More information about expressions can be found here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.html.

When all of the data prepared, we fill the UpdateItemInput struct and set table_name to "Locations", because it requires this as an argument for the update_item method of DynamoDbClient.

The update_item method returns RusotoFuture, which implements the Future trait that we explored in Chapter 5Understanding Asynchronous Operations with Futures Crate. You can use the rusoto crate in asynchronous applications. Since we don't use a reactor or asynchronous operations in this example, we will call the sync method of RusotoFuture, which blocks the current thread and waits for Result.

We have implemented a method to create new data items to the table and now we need a function to retrieve data from this table. The following list_locations function gets a list of Location for a specific user from the Locations table:

fn list_locations(conn: &DynamoDbClient, user_id: String) -> Result<Vec<Location>, Error> {
let expression = format!("Uid = :uid");
let mut values = HashMap::new();
values.insert(":uid".into(), s_attr(user_id));
let query = QueryInput {
table_name: "Locations".into(),
key_condition_expression: Some(expression),
expression_attribute_values: Some(values),
..Default::default()
};
let items = conn.query(query).sync()?
.items
.ok_or_else(|| format_err!("No Items"))?;
let mut locations = Vec::new();
for item in items {
let location = Location::from_map(item)?;
locations.push(location);
}
Ok(locations)
}

The list_locations function expects a reference to the DynamoDbClient instance and a string with if of user. If there are items in the table for the requested user, they are returned as a Vec of items, converted into the Location type.

In this function, we use the query method of DynamoDbClient, which expects a QueryInput struct as an argument. We fill it with the name of the table, the condition of the key expression, and values to fill that expression. We use a simple Uid = :uid expression that queries items with the corresponding value of the Uid partition key. We use a :uid placeholder and create a HashMap instance with a :uid key and a user_id value, which is converted into AttributeValue with the s_attr function call.

Now, we have two functions to insert and query data. We will use them to implement a command-line tool to interact with DynamoDB. Let's start with parsing arguments for our tool.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.117.212