Skip to content

Commit 7de469d

Browse files
authored
Added API wrapping and Markdown rendering examples (#25)
* Added API wrapping and Markdown rendering examples * Simplified code * Removed possible panics
1 parent 33551fe commit 7de469d

File tree

5 files changed

+398
-0
lines changed

5 files changed

+398
-0
lines changed

Cargo.lock

+63
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/api-wrapper/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "switch-toggle"
3+
version = "0.2.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
fastedge = { path = "../../" }
8+
serde = "1.0"
9+
serde_json = "1.0"
10+
url = "2.5"
11+
12+
[lib]
13+
crate-type = ["cdylib"]
14+

examples/api-wrapper/src/lib.rs

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
Example app to demonstrate how to wrap several API calls into single edge app.
3+
This example gets device status from SmartThings API and toggles it
4+
App needs following env vars to be set:
5+
PASSWORD - password to check user's permissions, simplest form of authentication
6+
DEVICE - device ID in SmartThings
7+
TOKEN - SmartThings API token
8+
*/
9+
10+
use std::env;
11+
use fastedge::{
12+
body::Body,
13+
http::{header, Error, Method, Request, Response, StatusCode},
14+
};
15+
use url::Url;
16+
use serde_json::{Value, from_str};
17+
18+
const API_BASE: &str = "https://api.smartthings.com/v1/devices/";
19+
20+
#[fastedge::http]
21+
fn main(req: Request<Body>) -> Result<Response<Body>, Error> {
22+
match req.method() {
23+
&Method::GET | &Method::HEAD => (),
24+
_ => return Response::builder().status(StatusCode::METHOD_NOT_ALLOWED).header(header::ALLOW, "GET, HEAD").body(Body::from("This method is not allowed\n"))
25+
};
26+
27+
let Ok(expected_pass) = env::var("PASSWORD") else {
28+
return Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("Misconfigured app\n"));
29+
};
30+
let provided_pass = match req.headers().get(header::AUTHORIZATION) {
31+
None => return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("No auth header\n")),
32+
Some(h) => match h.to_str() {
33+
Ok(v) => v,
34+
Err(_) => return Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("cannot process auth header"))
35+
}
36+
};
37+
if expected_pass != provided_pass {
38+
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::empty());
39+
}
40+
41+
let Ok(device) = env::var("DEVICE") else {
42+
return Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("Misconfigured app\n"))
43+
};
44+
let Ok(token) = env::var("TOKEN") else {
45+
return Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("Misconfigured app\n"))
46+
};
47+
48+
let wanted_status = match get_device_status(&token, &device) {
49+
Err(status) => {
50+
println!("cannot get device's current status");
51+
return Response::builder().status(status).body(Body::empty())
52+
},
53+
Ok(s) => match s.as_str() {
54+
"off" => "on",
55+
"on" => "off",
56+
_ => return Response::builder().status(StatusCode::NOT_FOUND).body(Body::from("Unsupported device status\n")),
57+
}
58+
};
59+
60+
let res = match send_device_command(&token, &device, wanted_status) {
61+
Err(status) => status,
62+
Ok(s) => match s.as_str() {
63+
"ACCEPTED" => StatusCode::NO_CONTENT,
64+
_ => StatusCode::NOT_FOUND
65+
}
66+
};
67+
68+
Response::builder().status(res).body(Body::empty())
69+
}
70+
71+
fn get_device_status(token: &str, device: &str) -> Result<String, StatusCode> {
72+
let req = Request::builder()
73+
.method(Method::GET)
74+
.header(header::ACCEPT, "application/json")
75+
.header(header::AUTHORIZATION, "Bearer ".to_string() + token)
76+
.header(header::PRAGMA, "no-cache")
77+
.uri(API_BASE.to_string() + device + "/status")
78+
.body(Body::empty())
79+
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
80+
81+
let rsp = match request(req) {
82+
Err(status_code) => return Err(status_code),
83+
Ok(r) => r,
84+
};
85+
86+
let json: Value = match from_str(String::from_utf8(rsp.body().to_vec()).or(Err(StatusCode::INTERNAL_SERVER_ERROR))?.as_str()) {
87+
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
88+
Ok(j) => j
89+
};
90+
let status = json.get(&"components").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
91+
.get(&"main").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
92+
.get(&"switch").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
93+
.get(&"switch").ok_or(StatusCode::INTERNAL_SERVER_ERROR)? // this is correct, "switch" two times, this is the structuire of this JSON schema
94+
.get(&"value").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?.to_string();
95+
96+
Ok(status.trim_matches('"').to_string())
97+
}
98+
99+
fn send_device_command(token: &str, device: &str, command: &str) -> Result<String, StatusCode> {
100+
let req = Request::builder()
101+
.method(Method::POST)
102+
.header(header::ACCEPT, "application/json")
103+
.header(header::CONTENT_TYPE, "application/json")
104+
.header(header::AUTHORIZATION, "Bearer ".to_string() + token)
105+
.uri(API_BASE.to_string() + device + "/commands")
106+
.body(Body::from("{\"commands\": [{\"capability\": \"switch\", \"command\": \"".to_string() + command + "\"}]}"))
107+
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
108+
109+
let rsp = match request(req) {
110+
Err(status_code) => return Err(status_code),
111+
Ok(r) => r,
112+
};
113+
114+
let json: Value = from_str(String::from_utf8(rsp.body().to_vec()).or(Err(StatusCode::INTERNAL_SERVER_ERROR))?.as_str())
115+
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
116+
let status = json.get(&"results").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
117+
.as_array().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?[0]
118+
.get(&"status").ok_or(StatusCode::INTERNAL_SERVER_ERROR)?.to_string();
119+
120+
Ok(status.trim_matches('"').to_string())
121+
}
122+
123+
fn request(req: Request<Body>) -> Result<Response<Body>, StatusCode> {
124+
let rsp = match fastedge::send_request(req) {
125+
Err(error) => {
126+
let status_code = match error {
127+
fastedge::Error::UnsupportedMethod(_) => StatusCode::METHOD_NOT_ALLOWED,
128+
fastedge::Error::BindgenHttpError(_) => StatusCode::INTERNAL_SERVER_ERROR,
129+
fastedge::Error::HttpError(_) => StatusCode::INTERNAL_SERVER_ERROR,
130+
fastedge::Error::InvalidBody => StatusCode::BAD_REQUEST,
131+
fastedge::Error::InvalidStatusCode(_) => StatusCode::BAD_REQUEST
132+
};
133+
return Err(status_code);
134+
}
135+
Ok(r) => r,
136+
};
137+
138+
let status = rsp.status();
139+
if is_redirect(status) {
140+
if let Some(location) = rsp.headers().get(header::LOCATION) {
141+
let new_url = Url::parse(
142+
location.to_str().or(Err(StatusCode::INTERNAL_SERVER_ERROR))?)
143+
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
144+
145+
let loc = new_url.as_str();
146+
let host = new_url.host().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?.to_string();
147+
println!("Redirect to {}", loc);
148+
let sub_req = Request::builder()
149+
.method(Method::GET)
150+
.header(header::HOST, host)
151+
.uri(loc)
152+
.body(Body::empty())
153+
.or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
154+
155+
return request(sub_req);
156+
}
157+
}
158+
if status == StatusCode::OK {
159+
return Ok(rsp);
160+
}
161+
162+
Err(status)
163+
}
164+
165+
// List of acceptible 300-series redirect codes.
166+
const REDIRECT_CODES: &[StatusCode] = &[
167+
StatusCode::MOVED_PERMANENTLY,
168+
StatusCode::FOUND,
169+
StatusCode::SEE_OTHER,
170+
StatusCode::TEMPORARY_REDIRECT,
171+
StatusCode::PERMANENT_REDIRECT,
172+
];
173+
174+
fn is_redirect(status_code: StatusCode) -> bool {
175+
return REDIRECT_CODES.contains(&status_code)
176+
}

examples/markdown-render/Cargo.toml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "md_render"
3+
version = "0.2.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
fastedge = { path = "../../" }
8+
mime = "0.3.17"
9+
pulldown-cmark = "0.11"
10+
url = "2.5"
11+
12+
[lib]
13+
crate-type = ["cdylib"]

0 commit comments

Comments
 (0)