generated from okp4/template-rust
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(logic): implements logic cosmwasm URI handling
- Loading branch information
Showing
4 changed files
with
299 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use thiserror::Error; | ||
use url::ParseError; | ||
|
||
#[derive(Error, Debug, PartialEq, Eq)] | ||
pub enum CosmwasmUriError { | ||
#[error("{0}")] | ||
ParseURI(#[from] ParseError), | ||
|
||
#[error("{0}")] | ||
ParseQuery(String), | ||
|
||
#[error("{0}")] | ||
SerializeQuery(String), | ||
|
||
#[error("Malformed URI: {0}")] | ||
Malformed(String), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
use crate::error::CosmwasmUriError; | ||
use serde::{de, ser}; | ||
use std::collections::HashMap; | ||
use url::Url; | ||
|
||
const COSMWASM_SCHEME: &'static str = "cosmwasm"; | ||
const COSMWASM_QUERY_PARAM: &'static str = "query"; | ||
|
||
/// Represents a file system URI used to load files from the logic module dedicated to the resolution | ||
/// of data coming from a CosmWasm smart contract query. The URI having the form: | ||
/// | ||
/// `cosmwasm:{contract_name}:{contract_address}?query={contract_query}` | ||
/// | ||
/// Where: | ||
/// - `{contract_name}`: Only informative, represents the corresponding smart contract name or type (e.g. `cw-storage`); | ||
/// - `{contract_address}`: The address of the smart contract to query; | ||
/// - `{contract_query}`: The JSON query to perform on the targeted smart contract, URL encoded; | ||
#[derive(Clone, Debug, PartialEq, Eq)] | ||
pub struct CosmwasmUri { | ||
pub contract_name: Option<String>, | ||
pub contract_address: String, | ||
pub raw_query: String, | ||
} | ||
|
||
impl CosmwasmUri { | ||
pub fn try_new<T>( | ||
contract_name: Option<String>, | ||
contract_address: String, | ||
query: &T, | ||
) -> Result<CosmwasmUri, CosmwasmUriError> | ||
where | ||
T: ser::Serialize + ?Sized, | ||
{ | ||
serde_json_wasm::to_string(query) | ||
.map_err(|e| CosmwasmUriError::SerializeQuery(e.to_string())) | ||
.map(|raw_query| CosmwasmUri { | ||
contract_name, | ||
contract_address, | ||
raw_query, | ||
}) | ||
} | ||
|
||
pub fn into_query<T>(self) -> Result<T, CosmwasmUriError> | ||
where | ||
T: de::DeserializeOwned, | ||
{ | ||
serde_json_wasm::from_str(self.raw_query.as_str()) | ||
.map_err(|e| CosmwasmUriError::ParseQuery(e.to_string())) | ||
} | ||
|
||
fn encode_query(self) -> String { | ||
return form_urlencoded::Serializer::new(String::new()) | ||
.append_pair(COSMWASM_QUERY_PARAM, self.raw_query.as_str()) | ||
.finish(); | ||
} | ||
} | ||
|
||
impl TryFrom<String> for CosmwasmUri { | ||
type Error = CosmwasmUriError; | ||
|
||
fn try_from(value: String) -> Result<Self, Self::Error> { | ||
Url::parse(value.as_str()) | ||
.map_err(|e| CosmwasmUriError::ParseURI(e)) | ||
.and_then(|uri: Url| { | ||
if uri.scheme() != COSMWASM_SCHEME { | ||
return Err(CosmwasmUriError::Malformed("wrong scheme".to_string())); | ||
} | ||
|
||
let path = uri.path().to_string(); | ||
let mut path_parts = path.split(':').map(String::from).collect::<Vec<String>>(); | ||
let (contract_name, contract_address) = | ||
match (path_parts.pop(), path_parts.pop(), path_parts.pop()) { | ||
(Some(address), Some(name), None) if !address.is_empty() => { | ||
Ok((Some(name), address)) | ||
} | ||
(Some(address), None, None) if !address.is_empty() => Ok((None, address)), | ||
_ => Err(CosmwasmUriError::Malformed("wrong path".to_string())), | ||
}?; | ||
|
||
let queries = uri | ||
.query_pairs() | ||
.into_owned() | ||
.collect::<HashMap<String, String>>(); | ||
|
||
match queries.get(COSMWASM_QUERY_PARAM) { | ||
Some(raw_query) => Ok(CosmwasmUri { | ||
contract_name, | ||
contract_address, | ||
raw_query: raw_query.clone(), | ||
}), | ||
_ => Err(CosmwasmUriError::Malformed( | ||
"missing 'query' query parameter".to_string(), | ||
)), | ||
} | ||
}) | ||
} | ||
} | ||
|
||
impl ToString for CosmwasmUri { | ||
fn to_string(&self) -> String { | ||
let encoded_query = self.clone().encode_query(); | ||
match self.contract_name.clone() { | ||
Some(name) => [ | ||
COSMWASM_SCHEME, | ||
":", | ||
name.as_str(), | ||
":", | ||
self.contract_address.as_str(), | ||
"?", | ||
encoded_query.as_str(), | ||
] | ||
.join(""), | ||
_ => [ | ||
COSMWASM_SCHEME, | ||
":", | ||
self.contract_address.as_str(), | ||
"?", | ||
encoded_query.as_str(), | ||
] | ||
.join(""), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use serde::{Deserialize, Serialize}; | ||
use url::ParseError; | ||
|
||
#[test] | ||
fn serde_success() { | ||
let cases = vec![ | ||
( | ||
CosmwasmUri{ | ||
contract_name: Some("name".to_string()), | ||
contract_address: "address".to_string(), | ||
raw_query: "".to_string() | ||
}, | ||
"cosmwasm:name:address?query=".to_string(), | ||
), | ||
( | ||
CosmwasmUri{ | ||
contract_name: Some("name".to_string()), | ||
contract_address: "address".to_string(), | ||
raw_query: "{\"object_data\":{\"id\":\"1a88ca1632c7323c0aa594000cda26ed9f48b36351c29c3d1e35e0a0474e862e\"}}".to_string() | ||
}, | ||
"cosmwasm:name:address?query=%7B%22object_data%22%3A%7B%22id%22%3A%221a88ca1632c7323c0aa594000cda26ed9f48b36351c29c3d1e35e0a0474e862e%22%7D%7D".to_string(), | ||
), | ||
( | ||
CosmwasmUri{ | ||
contract_name: None, | ||
contract_address: "address".to_string(), | ||
raw_query: "\"data\"".to_string() | ||
}, | ||
"cosmwasm:address?query=%22data%22".to_string(), | ||
), | ||
]; | ||
|
||
for case in cases { | ||
assert_eq!(case.0.clone().to_string(), case.1); | ||
let res = CosmwasmUri::try_from(case.1); | ||
assert!(res.is_ok()); | ||
assert_eq!(res.unwrap(), case.0); | ||
} | ||
} | ||
|
||
#[test] | ||
fn parse_uri_error() { | ||
let cases = vec![ | ||
( | ||
"cosmwasm".to_string(), | ||
CosmwasmUriError::ParseURI(ParseError::RelativeUrlWithoutBase), | ||
), | ||
( | ||
"cw:name:address?query=".to_string(), | ||
CosmwasmUriError::Malformed("wrong scheme".to_string()), | ||
), | ||
( | ||
"cw:address?query=".to_string(), | ||
CosmwasmUriError::Malformed("wrong scheme".to_string()), | ||
), | ||
( | ||
"cosmwasm:too_much:name:address?query=".to_string(), | ||
CosmwasmUriError::Malformed("wrong path".to_string()), | ||
), | ||
( | ||
"cosmwasm:?query=".to_string(), | ||
CosmwasmUriError::Malformed("wrong path".to_string()), | ||
), | ||
( | ||
"cosmwasm:name:address?".to_string(), | ||
CosmwasmUriError::Malformed("missing 'query' query parameter".to_string()), | ||
), | ||
( | ||
"cosmwasm:name:address".to_string(), | ||
CosmwasmUriError::Malformed("missing 'query' query parameter".to_string()), | ||
), | ||
]; | ||
|
||
for case in cases { | ||
let res = CosmwasmUri::try_from(case.0); | ||
assert!(res.is_err()); | ||
assert_eq!(res.err().unwrap(), case.1); | ||
} | ||
} | ||
|
||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
struct TestQuery { | ||
pub content: String, | ||
} | ||
|
||
#[test] | ||
fn try_new() { | ||
let cases = vec![ | ||
( | ||
Some("name".to_string()), | ||
"address".to_string(), | ||
TestQuery { | ||
content: "content".to_string(), | ||
}, | ||
"{\"content\":\"content\"}", | ||
), | ||
( | ||
None, | ||
"address".to_string(), | ||
TestQuery { | ||
content: "content".to_string(), | ||
}, | ||
"{\"content\":\"content\"}", | ||
), | ||
]; | ||
|
||
for case in cases { | ||
let res = CosmwasmUri::try_new(case.0.clone(), case.1.clone(), &case.2); | ||
|
||
assert!(res.is_ok()); | ||
let uri = res.unwrap(); | ||
assert_eq!(uri.contract_name, case.0); | ||
assert_eq!(uri.contract_address, case.1); | ||
assert_eq!(uri.raw_query, case.3); | ||
} | ||
} | ||
|
||
#[test] | ||
fn into_query() { | ||
let cases = vec![ | ||
( | ||
CosmwasmUri { | ||
contract_name: None, | ||
contract_address: "address".to_string(), | ||
raw_query: "{\"content\":\"content\"}".to_string(), | ||
}, | ||
Ok(TestQuery { | ||
content: "content".to_string(), | ||
}), | ||
), | ||
( | ||
CosmwasmUri { | ||
contract_name: None, | ||
contract_address: "address".to_string(), | ||
raw_query: "".to_string(), | ||
}, | ||
Err(CosmwasmUriError::ParseQuery( | ||
"EOF while parsing a JSON value.".to_string(), | ||
)), | ||
), | ||
]; | ||
|
||
for case in cases { | ||
let res = case.0.into_query::<TestQuery>(); | ||
assert_eq!(res, case.1); | ||
} | ||
} | ||
} |