Skip to content

Commit

Permalink
Add VSS Http thin client implementation for get/put/listKeyVersions a…
Browse files Browse the repository at this point in the history
…pi's
  • Loading branch information
G8XSU committed Jul 20, 2023
1 parent fe4f654 commit 0031a6b
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 278 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/target
/vss-accessor/src/proto/
/src/proto/
22 changes: 18 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
[workspace]
members = [
"vss-accessor",
]
[package]
name = "vss-client"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
prost = "0.11.9"
reqwest = { version = "0.11.13", features = ["rustls-tls"] }

[dev-dependencies]

[build-dependencies]
prost-build = { version = "0.11.3" }
reqwest = { version = "0.11.13", features = ["blocking"] }

[features]
genproto = []
11 changes: 5 additions & 6 deletions vss-accessor/build.rs → build.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#[cfg(feature = "genproto")]
extern crate prost_build;

use std::fs::File;
use std::path::Path;
use std::{env, fs};
#[cfg(feature = "genproto")]
use std::{env, fs, fs::File, path::Path};

/// To generate updated proto objects:
/// 1. Place `vss.proto` file in `src/proto/`
Expand All @@ -20,8 +19,8 @@ fn generate_protos() {
).unwrap();

prost_build::compile_protos(&["src/proto/vss.proto"], &["src/"]).unwrap();
let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("org.vss.rs");
fs::copy(from_path, "src/generated-src/org.vss.rs").unwrap();
let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("vss.rs");
fs::copy(from_path, "src/types.rs").unwrap();
}

#[cfg(feature = "genproto")]
Expand Down
81 changes: 81 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use ::prost::Message;
use reqwest;
use reqwest::Client;

use crate::error::VssError;
use crate::types::{
GetObjectRequest, GetObjectResponse, ListKeyVersionsRequest, ListKeyVersionsResponse, PutObjectRequest,
PutObjectResponse,
};

/// Thin-client to access a hosted instance of Versioned Storage Service (VSS).
/// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API.
pub struct VssClient {
base_url: String,
client: Client,
}

impl VssClient {
/// Constructs a [`VssClient`] using `base_url` as the VSS server endpoint.
pub fn new(base_url: &str) -> Self {
let client = Client::new();
Self { base_url: String::from(base_url), client }
}

/// Fetches a value against a given `key` in `request`.
/// Makes a service call to the `GetObject` endpoint of the VSS server.
/// For API contract/usage, refer to docs for [`GetObjectRequest`] and [`GetObjectResponse`].
pub async fn get_object(&self, request: &GetObjectRequest) -> Result<GetObjectResponse, VssError> {
let url = format!("{}/getObject", self.base_url);

let raw_response = self.client.post(url).body(request.encode_to_vec()).send().await?;
let status = raw_response.status();
let payload = raw_response.bytes().await?;

if status.is_success() {
let response = GetObjectResponse::decode(&payload[..])?;
Ok(response)
} else {
Err(VssError::new(status, payload))
}
}

/// Writes multiple [`PutObjectRequest::transaction_items`] as part of a single transaction.
/// Makes a service call to the `PutObject` endpoint of the VSS server, with multiple items.
/// Items in the `request` are written in a single all-or-nothing transaction.
/// For API contract/usage, refer to docs for [`PutObjectRequest`] and [`PutObjectResponse`].
pub async fn put_object(&self, request: &PutObjectRequest) -> Result<PutObjectResponse, VssError> {
let url = format!("{}/putObjects", self.base_url);

let response_raw = self.client.post(url).body(request.encode_to_vec()).send().await?;
let status = response_raw.status();
let payload = response_raw.bytes().await?;

if status.is_success() {
let response = PutObjectResponse::decode(&payload[..])?;
Ok(response)
} else {
Err(VssError::new(status, payload))
}
}

/// Lists keys and their corresponding version for a given [`ListKeyVersionsRequest::store_id`].
/// Makes a service call to the `ListKeyVersions` endpoint of the VSS server.
/// For API contract/usage, refer to docs for [`ListKeyVersionsRequest`] and [`ListKeyVersionsResponse`].
pub async fn list_key_versions(
&self, request: &ListKeyVersionsRequest,
) -> Result<ListKeyVersionsResponse, VssError> {
let url = format!("{}/listKeyVersions", self.base_url);

let response_raw = self.client.post(url).body(request.encode_to_vec()).send().await?;
let status = response_raw.status();
let payload = response_raw.bytes().await?;

if status.is_success() {
let response = ListKeyVersionsResponse::decode(&payload[..])?;
Ok(response)
} else {
Err(VssError::new(status, payload))
}
}
}
78 changes: 78 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::types::{ErrorCode, ErrorResponse};
use prost::bytes::Bytes;
use prost::{DecodeError, Message};
use reqwest::StatusCode;
use std::error::Error;
use std::fmt::{Display, Formatter};

/// When there is an error while writing to VSS storage, the response contains a relevant error code.
/// A mapping from a VSS server error codes. Refer to [`ErrorResponse`] docs for more
/// information regarding each error code and corresponding use-cases.
#[derive(Debug)]
pub enum VssError {
InvalidRequestError(String),
ConflictError(String),
InternalServerError(String),
InternalError(String),
}

impl VssError {
/// Create new instance of `VssError`
pub fn new(status: StatusCode, payload: Bytes) -> VssError {
match ErrorResponse::decode(&payload[..]) {
Ok(error_response) => VssError::from(error_response),
Err(e) => {
let message =
format!("Unable to decode ErrorResponse from server, HttpStatusCode: {}, DecodeErr: {}", status, e);
VssError::InternalError(message)
}
}
}
}

impl Display for VssError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
VssError::InvalidRequestError(message) => {
write!(f, "Request sent to VSS Storage was invalid: {}", message)
}
VssError::ConflictError(message) => {
write!(f, "Potential version conflict in write operation: {}", message)
}
VssError::InternalServerError(message) => {
write!(f, "InternalServerError: {}", message)
}
VssError::InternalError(message) => {
write!(f, "InternalError: {}", message)
}
}
}
}

impl Error for VssError {}

impl From<ErrorResponse> for VssError {
fn from(error_response: ErrorResponse) -> Self {
match error_response.error_code() {
ErrorCode::InvalidRequestException => VssError::InvalidRequestError(error_response.message),
ErrorCode::ConflictException => VssError::ConflictError(error_response.message),
ErrorCode::InternalServerException => VssError::InternalServerError(error_response.message),
_ => VssError::InternalError(format!(
"VSS responded with an unknown error code: {}, message: {}",
error_response.error_code, error_response.message
)),
}
}
}

impl From<DecodeError> for VssError {
fn from(err: DecodeError) -> Self {
VssError::InternalError(err.to_string())
}
}

impl From<reqwest::Error> for VssError {
fn from(err: reqwest::Error) -> Self {
VssError::InternalError(err.to_string())
}
}
4 changes: 4 additions & 0 deletions vss-accessor/src/lib.rs → src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)]

pub mod client;
pub mod error;
pub mod types;
Loading

0 comments on commit 0031a6b

Please sign in to comment.