use crate::{entity_path, TableEntity};
use serde::Serialize;
use serde_json;
const BATCH_MAX_SIZE: usize = 100;
quick_error! {
#[derive(Debug)]
pub enum BatchError {
UnexpectedPartitionKey {
display("Batch operation cannot be executed in multiple partitions")
}
TooManyOperations {
display("Batch operation size limit reached")
}
JSONError(err: serde_json::Error) {
from()
display("json error: {}", err)
cause(err)
}
}
}
pub enum BatchOperation {
Delete {
row_key: String,
etag: Option<String>,
},
Insert {
row_key: String,
payload: String,
},
Update {
row_key: String,
payload: String,
etag: Option<String>,
},
}
impl BatchOperation {
fn into_payload(&self, uri_prefix: &str, table: &str, partition_key: &str, body: &mut String) {
match *self {
BatchOperation::Insert { ref payload, .. } => {
body.push_str("POST ");
body.push_str(uri_prefix);
body.push_str(table);
body.push_str(" HTTP/1.1\n");
body.push_str("Accept: application/json;odata=nometadata\n");
body.push_str("Content-Type: application/json\n\n");
body.push_str(&payload);
body.push_str("\n");
}
BatchOperation::Update {
ref row_key,
ref etag,
ref payload,
} => {
body.push_str("PUT ");
body.push_str(uri_prefix);
body.push_str(&entity_path(table, partition_key, row_key));
body.push_str(" HTTP/1.1\n");
body.push_str("Accept: application/json;odata=nometadata\n");
body.push_str("Content-Type: application/json\n");
if let Some(etag) = etag {
body.push_str("If-Match: \"");
body.push_str(etag);
body.push_str("\"\n\n");
} else {
body.push_str("If-Match: *\n\n");
}
body.push_str(&payload);
body.push_str("\n");
}
BatchOperation::Delete {
ref row_key,
ref etag,
} => {
body.push_str("DELETE ");
body.push_str(uri_prefix);
body.push_str(&entity_path(table, partition_key, row_key));
body.push_str(" HTTP/1.1\n");
body.push_str("Accept: application/json;odata=nometadata\n");
body.push_str("Content-Type: application/json\n");
if let Some(etag) = etag {
body.push_str("If-Match: \"");
body.push_str(etag);
body.push_str("\"\n");
} else {
body.push_str("If-Match: *\n");
}
body.push_str("\n");
}
}
}
}
pub struct Batch {
partition_key: String,
items: Vec<BatchOperation>,
}
#[derive(Serialize)]
struct InsertPayload<'a, T> {
#[serde(rename = "RowKey")]
row_key: &'a str,
#[serde(rename = "PartitionKey")]
partition_key: &'a str,
#[serde(flatten)]
payload: &'a T,
}
impl Batch {
pub fn new(partition_key: String) -> Batch {
Batch {
partition_key: partition_key,
items: vec![],
}
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_full(&self) -> bool {
self.items.len() >= BATCH_MAX_SIZE
}
pub fn add_operation(&mut self, op: BatchOperation) -> Result<&mut Self, BatchError> {
self.items.push(op);
if self.is_full() {
Err(BatchError::TooManyOperations)
} else {
Ok(self)
}
}
pub fn add_insert<T>(&mut self, row_key: String, data: &T) -> Result<&mut Self, BatchError>
where
T: Serialize,
{
let payload = serde_json::to_string(&InsertPayload {
partition_key: &self.partition_key,
row_key: &row_key,
payload: data,
})?;
self.add_operation(BatchOperation::Insert { row_key, payload })
}
pub fn add_insert_entity<T>(&mut self, entity: TableEntity<T>) -> Result<&mut Self, BatchError>
where
T: Serialize,
{
if entity.partition_key != self.partition_key {
Err(BatchError::UnexpectedPartitionKey)
} else {
self.add_insert(entity.row_key, &entity.payload)
}
}
pub fn add_update<T>(
&mut self,
row_key: String,
data: &T,
etag: Option<String>,
) -> Result<&mut Self, BatchError>
where
T: Serialize,
{
self.add_operation(BatchOperation::Update {
row_key: row_key.to_owned(),
payload: serde_json::to_string(data)?,
etag: etag,
})
}
pub fn add_update_entity<T>(&mut self, entity: TableEntity<T>) -> Result<&mut Self, BatchError>
where
T: Serialize,
{
if entity.partition_key != self.partition_key {
Err(BatchError::UnexpectedPartitionKey)
} else {
self.add_update(entity.row_key, &entity.payload, entity.etag)
}
}
pub fn add_delete(
&mut self,
row_key: String,
etag: Option<String>,
) -> Result<&mut Self, BatchError> {
self.add_operation(BatchOperation::Delete {
row_key: row_key.to_owned(),
etag: etag,
})
}
pub fn add_delete_entity<T>(
&mut self,
entity: TableEntity<T>,
) -> Result<&mut Self, BatchError> {
if entity.partition_key != self.partition_key {
Err(BatchError::UnexpectedPartitionKey)
} else {
self.add_delete(entity.row_key, entity.etag)
}
}
pub(crate) fn into_payload(self, uri_prefix: &str, table: &str) -> String {
let mut payload = String::default();
payload.push_str("--batch_a1e9d677-b28b-435e-a89e-87e6a768a431\n");
payload.push_str("Content-Type: multipart/mixed; boundary=changeset_8a28b620-b4bb-458c-a177-0959fb14c977\n\n");
for item in self.items {
payload.push_str("--changeset_8a28b620-b4bb-458c-a177-0959fb14c977\n");
payload.push_str("Content-Type: application/http\n");
payload.push_str("Content-Transfer-Encoding: binary\n\n");
item.into_payload(uri_prefix, table, &self.partition_key, &mut payload);
payload.push_str("\n");
}
payload.push_str("--changeset_8a28b620-b4bb-458c-a177-0959fb14c977--\n");
payload.push_str("--batch_a1e9d677-b28b-435e-a89e-87e6a768a431\n");
println!("{}", payload);
payload
}
}