Skip to content

Add VSS Http thin client implementation for get/put/listKeyVersions api's #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're definitely missing some crate-level and module-level docs here and below. Also an initial usage example would be nice-to-have on the landing page. Currently this is just blank:
Screenshot 2023-07-20 at 10 20 46

(Still think we should enable the missing_docs lint from the start)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's add that now. Didn't realize that needed to be explicit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As i understand, module docs just need 1-liner description, will add that.

As for usage doc at crate level, i think i would add it pre-release as we grow and add rest of the things.

Let me know if this sounds good:
client - Implements a thin-client[VssClient] to access a hosted instance of Versioned Storage Service (VSS).
error - Implements the error type[VssError] returned on interacting with [VSSClient]
types - Contains request/response types generated from API definition of VSS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now #6

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending of how many objects we expect to expose overall, we might want to consider exposing some part of the (important) objects from the crate root.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case maybe client should have been in lib ? (contrary to our previous discussion to move it out of that)
Let me know if its something else.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always re-export in that case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i think re-exporting makes sense 👍

pub mod error;
pub mod types;
Loading