Skip to content

Commit 36b5d63

Browse files
committed
clnrest: add xml, yaml, html and form-encoded response types
Changelog-Added: clnrest can now return successful responses as xml, yaml, html or form-encoded aswell as requested by the accept header
1 parent bd743d7 commit 36b5d63

File tree

3 files changed

+154
-8
lines changed

3 files changed

+154
-8
lines changed

Cargo.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/rest-plugin/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ bytes = "1"
1313
log = { version = "0.4", features = ['std'] }
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
16+
serde_yml = "0.0.12"
17+
quick-xml = { version = "0.37", features = ["serialize"] }
18+
serde_qs = "0.15"
1619
tokio-util = { version = "0.7", features = ["codec"] }
1720
tokio = { version="1", features = ['io-std', 'rt-multi-thread', 'sync', 'macros', 'io-util'] }
1821
axum = "0.8"

plugins/rest-plugin/src/handlers.rs

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub enum AppError {
2727
Forbidden(RpcError),
2828
NotFound(RpcError),
2929
InternalServerError(RpcError),
30+
NotAcceptable(RpcError),
3031
}
3132

3233
impl IntoResponse for AppError {
@@ -36,6 +37,7 @@ impl IntoResponse for AppError {
3637
AppError::Forbidden(err) => (StatusCode::FORBIDDEN, err),
3738
AppError::NotFound(err) => (StatusCode::NOT_FOUND, err),
3839
AppError::InternalServerError(err) => (StatusCode::INTERNAL_SERVER_ERROR, err),
40+
AppError::NotAcceptable(err) => (StatusCode::NOT_ACCEPTABLE, err),
3941
};
4042

4143
let body = Json(json!(error_message));
@@ -88,7 +90,9 @@ fn process_help_response(help_response: serde_json::Value) -> String {
8890
post,
8991
path = "/v1/{rpc_method}",
9092
responses(
91-
(status = 201, description = "Call rpc method", body = serde_json::Value),
93+
(status = 201, description = "Call rpc method", body = serde_json::Value,
94+
content(("application/json"),("application/yaml"),("application/xml"),
95+
("application/x-www-form-urlencoded"),("text/html"))),
9296
(status = 401, description = "Unauthorized", body = serde_json::Value),
9397
(status = 403, description = "Forbidden", body = serde_json::Value),
9498
(status = 404, description = "Not Found", body = serde_json::Value),
@@ -153,19 +157,109 @@ pub async fn call_rpc_method(
153157

154158
verify_rune(plugin.clone(), rune, &rpc_method, &rpc_params).await?;
155159

156-
match call_rpc(plugin, &rpc_method, rpc_params).await {
157-
Ok(result) => {
158-
let response_body = Json(result);
159-
let response = (StatusCode::CREATED, response_body).into_response();
160-
Ok(response)
161-
}
160+
let cln_result = match call_rpc(plugin, &rpc_method, rpc_params).await {
161+
Ok(result) => result,
162162
Err(err) => {
163163
if let Some(code) = err.code {
164164
if code == -32601 {
165165
return Err(AppError::NotFound(err));
166166
}
167167
}
168-
Err(AppError::InternalServerError(err))
168+
return Err(AppError::InternalServerError(err));
169+
}
170+
};
171+
172+
convert_response(headers, rpc_method, cln_result)
173+
}
174+
175+
fn convert_response(
176+
headers: axum::http::HeaderMap,
177+
rpc_method: String,
178+
cln_result: serde_json::Value,
179+
) -> Result<Response, AppError> {
180+
let accept = headers
181+
.get("accept")
182+
.and_then(|v| v.to_str().ok())
183+
.unwrap_or("application/json");
184+
185+
let format = match accept {
186+
a if a.contains("application/json") => "json",
187+
a if a.contains("application/yaml") => "yaml",
188+
a if a.contains("application/xml") => "xml",
189+
a if a.contains("application/x-www-form-urlencoded") => "form",
190+
a if a.contains("text/html") => "html",
191+
_ => {
192+
return Err(AppError::NotAcceptable(RpcError {
193+
code: None,
194+
data: None,
195+
message: format!("Unsupported accept header: {}", accept),
196+
}))
197+
}
198+
};
199+
200+
match format {
201+
"html" => match serde_json::to_string_pretty(&cln_result) {
202+
Ok(txt) => {
203+
let html = format!(
204+
"<html><body><h1>{} Result</h1><pre>{}</pre></body></html>",
205+
rpc_method, txt
206+
);
207+
Ok((StatusCode::CREATED, [("Content-Type", "text/html")], html).into_response())
208+
}
209+
Err(e) => Err(AppError::InternalServerError(RpcError {
210+
code: None,
211+
data: None,
212+
message: format!("Could not serialize to HTML: {}", e),
213+
})),
214+
},
215+
"yaml" => match serde_yml::to_string(&cln_result) {
216+
Ok(yaml) => Ok((
217+
StatusCode::CREATED,
218+
[("Content-Type", "application/yaml")],
219+
yaml,
220+
)
221+
.into_response()),
222+
Err(e) => Err(AppError::InternalServerError(RpcError {
223+
code: None,
224+
data: None,
225+
message: format!("Could not serialize to YAML: {}", e),
226+
})),
227+
},
228+
"xml" => match quick_xml::se::to_string_with_root(&rpc_method, &cln_result) {
229+
Ok(xml) => Ok((
230+
StatusCode::CREATED,
231+
[("Content-Type", "application/xml")],
232+
xml,
233+
)
234+
.into_response()),
235+
Err(e) => Err(AppError::InternalServerError(RpcError {
236+
code: None,
237+
data: None,
238+
message: format!("Could not serialize to XML: {}", e),
239+
})),
240+
},
241+
"form" => match serde_qs::to_string(&cln_result) {
242+
Ok(form) => Ok((
243+
StatusCode::CREATED,
244+
[("Content-Type", "application/x-www-form-urlencoded")],
245+
form,
246+
)
247+
.into_response()),
248+
Err(e) => Err(AppError::InternalServerError(RpcError {
249+
code: None,
250+
data: None,
251+
message: format!("Could not serialize to FORM-URLENCODED: {}", e),
252+
})),
253+
},
254+
_ => {
255+
let response_body = Json(cln_result);
256+
let response = (
257+
StatusCode::CREATED,
258+
[("Content-Type", "application/json")],
259+
response_body,
260+
)
261+
.into_response();
262+
Ok(response)
169263
}
170264
}
171265
}

0 commit comments

Comments
 (0)