OriginStamp Rust SDK Example
- 6 minutes read - 1143 wordsI wrote recently describing Python and Golang clients for OriginStamp based on OriginStamp’s API’s swagger spec. As a way to pursue learning rust, I’ve been forcing myself to write examples using rust. I’m honestly finding learning rust tough going and think I’m probably better to revert to the “Learning Rust” tutorials.
That said, herewith an explanation of building a rust client using an OpenAPI (!) generated SDK from OriginStamp’s swagger spec.
Swagger
I began this adventure using swagger-codegen
. I was unsuccessful. I became stuck trying to configure hyper
and hyper-tls
with the generated APIClient
. I stumbled upon this issue and switched to using openapi-generator
with reqwest
and have been succesful, ymmv.
Being somewhat reluctant to pollute my workstation with Java, I tend to use the excellent Google Cloud Shell. I installed (both swagger-codegen
and) openapi-generator
onto a Cloud Shell instance, generated the rust SDK and then copied the SDK to my workstation:
To ssh
into Cloud Shell from a shell, you can:
gcloud alpha cloud-shell ssh
Then, from the Cloud Shell:
git clone https://github.com/OpenAPITools/openapi-generator.git
cd openapi-generator
mvn clean package
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
--input-spec=../swagger.yaml \
--generator-name=rust \
--output=./rust-client-reqwest \
--library=reqwest
NB I’d copied the OriginStamp swagger spec to the Cloud Shell instance
gcloud alpha cloud-shell scp localhost:./swagger.yaml cloudshell:.
NB The
generate
command needs the flag--library=reqwest
to usereqwest
otherwise it defaults tohyper
Then:
gcloud alpha cloud-shell scp --recurse \
cloudshell:openapi-generator/rust-client-reqwest \
localhost:./rust-client-reqwest
NB the rust package name of the generated library will be
openapi
NB the documentation suggests placing the generated SDK in
./generated
but I prefer (as with Golang) to keep this more separate
Client
Assuming a hierarchy:
.
├── go-client
├── golang
├── python
├── rust
│ ├── Cargo.toml
│ ├── README.md
│ ├── src
│ └── target
├── rust-openapi-reqwest
│ ├── Cargo.toml
│ ├── README.md
│ ├── src
│ └── target
└── swagger.yaml
The rust client is in ./rust
and its Cargo.toml
contains:
[package]
name = "rust"
version = "0.1.0"
authors = ["DazWilkin <my@email.com>"]
edition = "2018"
[dependencies]
clap = "2.33.0"
openapi = { path = "../rust-openapi-reqwest" }
reqwest = "0.10.4"
rust-crypto = "0.2.36"
NB for now, see the package name
openapi
is referenced locally as../rust-openapi-reqwest
.
I’m (always) open to constructive feedback and guidance and especially with my faltering rust, so…
use clap::{App, Arg};
use crypto::digest::Digest;
use crypto::sha2::Sha256;
// use reqwest::{ClientBuilder, Error};
use openapi::{apis, models};
fn main() {
let matches = App::new("Rust OriginStamp Example")
.version("0.0.1")
.author("Daz Wilkin <my@email.com>")
.about("A simple Rust-client for OriginStamp generated using OpenAPI")
.arg(
Arg::with_name("api_key")
.short("k")
.long("api_key")
.takes_value(true)
.help("OriginStamp API Key"),
)
.get_matches();
let api_key = matches.value_of("api_key").unwrap();
let s = "Frederik Jack is a bubbly Border Collie";
let mut sha256 = Sha256::new();
sha256.input_str(s);
let h = sha256.result_str();
assert_eq!(
h,
"037e945cf8da5945acbcf2390c71a497c6edefdc364ada1f33d76a2b5f8b472b"
);
let api_client = apis::client::APIClient::new(apis::configuration::Configuration::new());
{
let rqst: models::TimestampRequest = models::TimestampRequest {
comment: Some("".to_string()),
hash: h,
notifications: None,
url: None,
};
let resp = api_client.timestamp_api().create_timestamp(api_key, rqst);
match resp {
Ok(r) => println!("{:?}", r),
Err(e) => println!("{:?}", e),
};
}
// `h` was moved into `TimeStampRequest`, duplicating...
let h = sha256.result_str();
{
let rqst: models::ProofRequest = models::ProofRequest {
currency: 0,
hash_string: h,
proof_type: 0,
};
let resp = api_client.proof_api().get_proof(api_key, rqst);
match resp {
Ok(ddlr) => match ddlr.data {
None => println!("No data"),
Some(dlr) => match dlr.download_url {
None => println!("URL not found!"),
Some(u) => println!("{:?}", u),
},
},
Err(e) => println!("{:?}", e),
};
}
}
I discovered clap
and it appears to be a good analog to Golang’s flag
module. The intent here is to obfuscate my OriginStamp API key and be able to pass this to the program when debugging in Visual Studio Code.
I’m using the rust language server in Visual Studio Code and added CodeLLDB
for rust debugging. This permits, the creation of a launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/target/debug/rust",
"args": [
"--api_key=[[YOUR-API-KEY]]",
],
"cwd": "${workspaceFolder}"
}
]
}
Or, of course, you may run everything from the command-line:
cargo run -- --api_key=${API_KEY}
I struggled to find documented examples of rust with swagger. The swagger-api
(sic.) repo had the most useful sample for petstore. So, my code is a translation of that.
We make 2 API calls against OriginStamp’s endpoint: create_timestamp
and get_proof
But, first, we need a client. Fortunately, this was super-simple (thanks to reqwest
):
let api_client = apis::client::APIClient::new(
apis::configuration::Configuration::new()
);
We can then create openapi::models::TimestampRequest
:
let rqst: models::TimestampRequest = models::TimestampRequest {
comment: Some("".to_string()),
hash: h,
notifications: None,
url: None,
};
and make the request against timestamp_api
:
let resp = api_client.timestamp_api().create_timestamp(api_key, rqst);
match resp {
Ok(r) => println!("{:?}", r),
Err(e) => println!("{:?}", e),
};
From Golang, rust’s Result
feels intuitive, matches the func do() (result, err)
and r,err := do(); if err then....
pattern albeit more stringent (which is good) and I like the pattern-matching (something I also enjoyed with Prolog and ML). I’m still slightly perplexed by the proliferation in ‘shortcuts’ with .unwrap()
and the seemingly new ?
operator. Perhaps this is just something I need to experience more to fully grok.
NB The comment
Some(String)
is created but not used and could have beenNone
too
The only interesting details here is the value of hash
This was created previously:
let s = "Frederik Jack is a bubbly Border Collie";
let mut sha256 = Sha256::new();
sha256.input_str(s);
let h = sha256.result_str();
assert_eq!(
h,
"037e945cf8da5945acbcf2390c71a497c6edefdc364ada1f33d76a2b5f8b472b"
);
It’s unclear to me why models::TimestampRequest
doesn’t borrow the value of e.g. hash
. This is a structure that does not (need to) change its properties’ values. Instead the generated structure requires that the value be moved. My naive (!?) hacky solution is to regenerate the hex-encoded string before using it again in the second call:
let h = sha256.result_str();
let rqst: models::ProofRequest = models::ProofRequest {
currency: 0,
hash_string: h,
proof_type: 0,
};
let resp = api_client.proof_api().get_proof(api_key, rqst);
match resp {
Ok(ddlr) => match ddlr.data {
None => println!("No data"),
Some(dlr) => match dlr.download_url {
None => println!("URL not found!"),
Some(u) => println!("{:?}", u),
},
},
Err(e) => println!("{:?}", e),
};
The match
nesting here is reminiscent of JavaScript callbacks. I believe there are some constructs in rust that help avoid this type of nesting. Another thing to learn.
Most importantly, it works.
DefaultTimestampResponse {
data: Some(TimestampResponse {
comment: None,
created: Some(false),
date_created: Some(1581969171289),
hash_string: Some("037e945cf8da5945acbcf2390c71a497c6edefdc364ada1f33d76a2b5f8b472b"),
timestamps: Some([
TimestampData {
currency_id: Some(0), # Bitcoin
private_key: Some("[[REDACTED]]"),
submit_status: Some(3),
timestamp: Some(1581986262000),
transaction: Some("89d145c41dd46e36b9772a853be800be37d32a2cf1b9facf4aee94c4583d99d5")
},
TimestampData {
currency_id: Some(1), # Ethereum
private_key: Some("[[REDACTED]]"),
submit_status: Some(3),
timestamp: Some(1581969676000),
transaction: Some("0x7850cc177e9362e7fbdc165b04aaa04c8625a3bb45ea44085114b0bb59c41f9d")
},
TimestampData {
currency_id: Some(2), # Aion
private_key: Some("[[REDACTED]]"),
submit_status: Some(3),
timestamp: Some(1581969611000),
transaction: Some("cb58a3b96d7593c7269eb2235284249b811e2d416f7087fbf129dfa9374e0cf7")
},
TimestampData {
currency_id: Some(100), # Südkurier (newspaper)
private_key: Some("[[REDACTED]]"),
submit_status: Some(3),
timestamp: Some(1582077600000),
transaction: Some("2020-02-19")
}
])
}),
error_code: Some(0),
error_message: None
}
"https://api.originstamp.org/v3/timestamp/proof/download?token=[[REDACTED]]&name=certificate.Bitcoin.037e945cf8da5945acbcf2390c71a497c6edefdc364ada1f33d76a2b5f8b472b.pdf"
I’ve annotated the currencies with their corresponding platform, e.g. 0 == Bitcoin and you can check Bitcoin for the transaction 89d145c41dd46e36b9772a853be800be37d32a2cf1b9facf4aee94c4583d99d5. Interestingly, the transactions are also published in a German newspaper (Südkurier). There’s an article about it here.
That’s all!