1use crate::io::config::ApplicationConfiguration;
6use crate::io::database::{schema::Table, Database};
7use crate::io::http::HttpMethod;
8use crate::io::http::{delete, get, patch, post, put};
9use crate::io::ApiResult;
10use crate::param;
11use crate::util::constants::{URL_ENCODED_CARAT, URL_ENCODED_SPACE};
12use crate::util::{detect_json, detect_xml, Constant, Label, Searchable};
13use crate::{Location, Repository, Scheme};
14use async_trait::async_trait;
15use axum::http::header::{HeaderName, HeaderValue};
16use axum::http::HeaderMap;
17use bon::Builder;
18use color_eyre::eyre::{self, eyre};
19use core::iter::once;
20use core::{fmt, marker::PhantomData};
21use derive_more::Display;
22use fluent_uri::{Uri, UriRef};
23use itertools::Itertools;
24use lazy_static::lazy_static;
25use owo_colors::OwoColorize;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28use serde_with::skip_serializing_none;
29use strum::EnumIs;
30use tera::{Context, Tera};
31use tracing::{debug, trace, warn};
32use validator::Validate;
33
34pub mod citeas;
35pub mod geonames;
36pub mod github;
37pub mod gitlab;
38pub mod handle;
39pub mod huggingface;
40pub mod models_dev;
41pub mod openai;
42pub mod openapi;
43pub mod orcid;
44pub mod osti;
45pub mod raid;
46pub mod ror;
47pub mod spdx;
48pub mod swhid;
49
50lazy_static! {
51 pub static ref INCLUDED_ENDPOINTS: Vec<Endpoint> = Constant::json::<ApplicationConfiguration>("application").endpoints.unwrap_or_default();
53}
54pub trait Configuration {
56 fn from_env() -> Self;
58 fn with_body(self, value: impl Into<String>) -> Self;
60 fn with_domain(self, value: impl Into<String>) -> Self;
62 fn with_identifier(self, value: impl Into<String>) -> Self;
64 fn token(&self) -> &str;
66 fn domain(&self) -> &str;
68 fn identifier(&self) -> Option<&str>;
70 fn with_params(self, params: Vec<Param>) -> Self;
73 fn params(&self) -> &[Param];
75}
76#[async_trait]
78pub trait DatabasePersistence {
79 async fn persist(self, database: Database<Table>) -> ApiResult<usize>;
81}
82pub trait IntoBody {
84 fn into_body(self) -> serde_json::Value;
86}
87pub trait IntoHeaders {
89 fn into_headers(self) -> HeaderMap;
91}
92pub trait QueryField: fmt::Display + for<'a> TryFrom<&'a str> {}
94pub trait RepositoryFileMetadata {
96 fn path(&self) -> &str;
98 fn size(&self) -> Option<u64>;
100}
101pub trait FallbackResponse {
109 fn into_error(content: &str) -> Option<eyre::Report>;
112 fn to_string(content: &str) -> Option<String> {
114 serde_json::from_str::<serde_json::Value>(content).ok().and_then(|value| match value {
115 | serde_json::Value::String(inner) => serde_json::from_str::<serde_json::Value>(&inner)
116 .ok()
117 .and_then(|nested| serde_json::to_string_pretty(&nested).ok())
118 .or_else(|| serde_json::to_string_pretty(&serde_json::Value::String(inner)).ok()),
119 | other => serde_json::to_string_pretty(&other).ok(),
120 })
121 }
122}
123#[async_trait]
126pub trait RemoteResource {
127 type Query: QueryField + ValueValidator;
129 type Field: QueryField;
131
132 fn context(&self, params: Option<Vec<Param>>) -> Context {
134 self.context_with::<Self::Query, Self::Field>(params)
135 }
136 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
138 where
139 Q: QueryField + ValueValidator,
140 F: QueryField;
141 fn handle<R>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
143 where
144 R: for<'de> Deserialize<'de>,
145 {
146 match response {
147 | Ok(content) => match content {
148 | ResponseContent::Json(content) => parse_json(&content),
149 | ResponseContent::Xml(content) => parse_xml(&content),
150 | ResponseContent::Yaml(content) => parse_yaml(&content),
151 | ResponseContent::Raw(content) => {
152 let raw = TextResponse { content };
153 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
154 }
155 },
156 | Err(e) => Err(eyre!(e)),
157 }
158 }
159 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
162 where
163 R: for<'de> Deserialize<'de>,
164 E: FallbackResponse;
165 async fn invoke(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>;
167 async fn invoke_with<Q, F>(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
169 where
170 Q: QueryField + ValueValidator,
171 F: QueryField;
172}
173pub trait ValueValidator {
175 fn is_valid(&self, _value: &str) -> bool {
177 true
178 }
179}
180#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
182pub enum AuthenticationScheme {
183 #[default]
185 Bearer,
186 Basic,
188 ApiKey,
190 OAuth2,
192 AwsSignatureV4,
194 GoogleCloud,
196 Custom(String),
198}
199#[derive(Clone, Debug, Default, Deserialize, EnumIs, Serialize)]
201pub enum ParamStyle {
202 #[default]
204 QueryPair,
205 QueryField,
207 FieldList,
209 KeyValuePair,
211 Header,
213 Body,
215 TemplateValue,
217}
218#[derive(Clone, Debug, Deserialize, Serialize)]
220pub enum ResponseContent {
221 Json(String),
223 Raw(String),
225 Yaml(String),
227 Xml(String),
229}
230#[derive(Clone, Debug, Display, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
232#[serde(rename_all = "lowercase")]
233pub enum TreeEntryType {
234 #[serde(alias = "blob")]
236 #[display("file")]
237 File,
238 #[serde(alias = "tree")]
240 #[display("directory")]
241 Directory,
242}
243pub struct Fallback<T>(PhantomData<T>);
251pub struct NoFallback;
253#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
255#[builder(start_fn = init)]
256pub struct Authentication {
257 pub token: Option<String>,
260 #[builder(default)]
262 pub scheme: AuthenticationScheme,
263}
264#[derive(Clone, Debug, Deserialize, Serialize)]
266#[serde(rename_all = "kebab-case")]
267pub struct EmptyField(String);
268#[skip_serializing_none]
272#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
273#[builder(start_fn = at, on(String, into))]
274pub struct Endpoint {
275 #[builder(start_fn)]
277 pub domain: String,
278 #[builder(default = String::new())]
280 pub name: String,
281 #[serde(default)]
283 pub scheme: Option<Scheme>,
284 pub port: Option<u16>,
286 pub authentication: Option<Authentication>,
288 pub root: Option<String>,
292 #[builder(default = vec![])]
294 pub resources: Vec<Resource>,
295}
296#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
298pub struct Identifier<T> {
299 #[serde(rename = "id", alias = "iid")]
301 pub identifier: T,
302}
303#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
305#[builder(start_fn = of_type, finish_fn = with_key, on(String, into))]
306pub struct Param {
307 #[builder(start_fn)]
309 pub style: ParamStyle,
310 #[builder(finish_fn)]
312 pub name: String,
313 #[builder(
315 default = vec![],
316 with = |vecs: Vec<Vec<Option<&str>>>| {
317 vecs
318 .into_iter()
319 .map(|vec| vec.into_iter().map(|opt| opt.map(str::to_string)).collect())
320 .collect()
321 }
322 )]
323 pub values: Vec<Vec<Option<String>>>,
324 #[builder(default = false)]
326 pub required: bool,
327}
328pub struct Params(Vec<Param>);
344#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
346#[builder(start_fn = init, on(String, into))]
347pub struct Resource {
348 pub name: String,
350 #[builder(with = |method: &str| HttpMethod::from(method))]
352 #[serde(default)]
353 pub method: HttpMethod,
354 pub template: String,
356}
357#[derive(Clone, Debug, Deserialize, Serialize)]
359pub struct TextResponse {
360 pub content: String,
362}
363#[skip_serializing_none]
365#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
366pub struct TreeEntry {
367 pub path: String,
369 #[serde(rename = "type")]
371 pub entry_type: TreeEntryType,
372 pub size: Option<u64>,
374 pub id: Option<String>,
376 pub name: Option<String>,
378 pub mode: Option<String>,
380 pub sha: Option<String>,
382 pub url: Option<String>,
384}
385impl Endpoint {
386 pub fn from_parts(domain: String, scheme: Option<Scheme>, port: Option<u16>) -> Self {
390 Self {
391 domain,
392 scheme,
393 port,
394 ..Self::default()
395 }
396 }
397 pub fn base(&self) -> String {
399 let Self { domain, root, .. } = self;
400 let scheme = self.scheme.as_ref().map_or("https".to_string(), |s| s.to_string());
401 let port = self.port.map_or(String::new(), |port| format!(":{port}"));
402 let root = root.as_ref().map_or(String::new(), |root| format!("/{root}"));
403 format!("{scheme}://{domain}{port}{root}")
404 }
405 pub fn with_domain(&self, domain: impl Into<String>) -> Self {
407 let (domain, scheme, port) = Self::split_domain(domain.into().as_str());
408 Self {
409 domain,
410 scheme: scheme.or_else(|| self.scheme.clone()),
411 port: port.or(self.port),
412 ..self.clone()
413 }
414 }
415 fn split_domain(value: &str) -> (String, Option<Scheme>, Option<u16>) {
416 Uri::parse(value)
417 .ok()
418 .and_then(|uri| {
419 uri.authority().map(|authority| {
420 (
421 authority.host().to_string(),
422 Some(Scheme::from(uri.scheme().as_str())).filter(|scheme| *scheme != Scheme::Unsupported),
423 authority.port_to_u16().ok().flatten(),
424 )
425 })
426 })
427 .or_else(|| {
428 let authority = format!("//{}", value.trim());
429 UriRef::parse(authority.as_str()).ok().and_then(|uri| {
430 uri.authority()
431 .map(|parsed| (parsed.host().to_string(), None, parsed.port_to_u16().ok().flatten()))
432 })
433 })
434 .unwrap_or_else(|| (value.trim().to_string(), None, None))
435 }
436 pub fn from_template(name: impl Into<String>) -> ApiResult<Self> {
443 let endpoint_name = name.into();
444 INCLUDED_ENDPOINTS
445 .find_by_name(&endpoint_name)
446 .ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration"))
447 }
448}
449impl Searchable<Endpoint> for Vec<Endpoint> {
450 fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> {
451 let name = value.into();
452 self.iter().find(|endpoint| endpoint.name.eq_ignore_ascii_case(&name)).cloned()
453 }
454}
455impl FallbackResponse for NoFallback {
456 fn into_error(_: &str) -> Option<eyre::Report> {
457 None
458 }
459 fn to_string(_: &str) -> Option<String> {
460 None
461 }
462}
463impl<T> FallbackResponse for Fallback<T>
464where
465 T: for<'de> Deserialize<'de> + fmt::Debug,
466{
467 fn into_error(content: &str) -> Option<eyre::Report> {
468 serde_json::from_str::<T>(content).ok().map(|why| {
469 let message = Self::to_string(content).unwrap_or_else(|| format!("{why:#?}"));
470 eyre!("{message}")
471 })
472 }
473}
474impl Searchable<Resource> for Vec<Resource> {
475 fn find_by_name(&self, value: impl Into<String>) -> Option<Resource> {
476 let name = value.into();
477 self.iter().find(|resource| resource.name.eq_ignore_ascii_case(&name)).cloned()
478 }
479}
480impl<T> QueryField for T where T: fmt::Display + for<'a> TryFrom<&'a str> {}
482impl fmt::Display for AuthenticationScheme {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 write!(
485 f,
486 "{}",
487 match self {
488 | AuthenticationScheme::Bearer => "Bearer",
489 | AuthenticationScheme::Basic => "Basic",
490 | AuthenticationScheme::ApiKey => "ApiKey",
491 | AuthenticationScheme::OAuth2 => "OAuth2",
492 | AuthenticationScheme::AwsSignatureV4 => "AWS Signature V4",
493 | AuthenticationScheme::GoogleCloud => "Google Cloud",
494 | AuthenticationScheme::Custom(scheme) => scheme,
495 }
496 )
497 }
498}
499impl fmt::Display for EmptyField {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 write!(f, "{}", self.0)
502 }
503}
504impl fmt::Display for TextResponse {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 write!(f, "{}", self.content)
507 }
508}
509impl Default for Endpoint {
510 fn default() -> Self {
511 Endpoint::at("example.com").scheme(Scheme::default()).build()
512 }
513}
514impl<'a> From<Uri<&'a str>> for Endpoint {
515 fn from(value: Uri<&'a str>) -> Self {
516 let domain = value.authority().map(|auth| auth.host().to_string()).unwrap_or_default();
517 let port = value.authority().and_then(|auth| auth.port_to_u16().ok()).flatten();
518 Self::from_parts(
519 domain,
520 Some(Scheme::from(value.scheme().as_str())).filter(|scheme| *scheme != Scheme::Unsupported),
521 port,
522 )
523 }
524}
525impl From<Location> for Endpoint {
526 fn from(value: Location) -> Self {
527 let domain = value.host().unwrap_or_default();
528 let port = value.port();
529 Self::from_parts(domain, Some(value.scheme()), port)
530 }
531}
532impl From<Repository> for Endpoint {
533 fn from(value: Repository) -> Self {
534 value.location().into()
535 }
536}
537impl fmt::Display for HttpMethod {
538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539 write!(
540 f,
541 "{}",
542 match self {
543 | HttpMethod::Get => "GET",
544 | HttpMethod::Post => "POST",
545 | HttpMethod::Put => "PUT",
546 | HttpMethod::Patch => "PATCH",
547 | HttpMethod::Delete => "DELETE",
548 }
549 )
550 }
551}
552impl Param {
553 pub fn is_query(&self) -> bool {
555 self.style.is_query_pair() | self.style.is_query_field() | self.style.is_field_list()
556 }
557 pub fn to_query_string<Q: QueryField + ValueValidator, F: QueryField>(params: Vec<Param>) -> String {
559 let query = params
560 .iter()
561 .filter(|param| param.is_query() || param.style.is_key_value_pair())
562 .map(|param| param.to_string::<Q, F>())
563 .filter(|s| !s.is_empty())
564 .collect::<Vec<String>>()
565 .join("&");
566 if !query.is_empty() {
567 format!("?{query}")
568 } else {
569 String::new()
570 }
571 }
572 pub fn from_query_pair(key: &str, pairs: Vec<(&str, &str)>) -> Self {
581 Param::of_type(ParamStyle::QueryPair)
582 .values(pairs.into_iter().map(|(k, v)| vec![Some(k), Some(v)]).collect())
583 .with_key(key)
584 }
585 pub fn from_field_list(key: &str, fields: Vec<&str>) -> Self {
587 Param::of_type(ParamStyle::FieldList)
588 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
589 .with_key(key)
590 }
591 pub fn from_query_field(key: &str, fields: Vec<&str>) -> Self {
593 Param::of_type(ParamStyle::QueryField)
594 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
595 .with_key(key)
596 }
597
598 pub fn to_string<Q: QueryField + ValueValidator, F: QueryField>(&self) -> String {
602 let key = self.name.as_str();
603 let rendered: Option<String> = match self.style {
604 | ParamStyle::QueryPair => {
605 let separator = "+AND+";
606 let pairs: Vec<(&str, &str)> = self
607 .values
608 .iter()
609 .filter_map(
610 |vec| match (vec.first().and_then(|o| o.as_deref()), vec.get(1).and_then(|o| o.as_deref())) {
611 | (Some(k), Some(v)) => Some((k, v)),
612 | _ => None,
613 },
614 )
615 .collect();
616 param_from_query_pairs::<Q>(key, separator, pairs)
617 }
618 | ParamStyle::QueryField => {
619 let separator = URL_ENCODED_SPACE;
620 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
621 param_from_query_fields::<Q>(key, separator, fields)
622 }
623 | ParamStyle::FieldList => {
624 let separator = ",";
625 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
626 param_from_field_list::<F>(key, separator, fields)
627 }
628 | ParamStyle::KeyValuePair => {
629 let value = self
630 .values
631 .iter()
632 .filter_map(|vec| vec.first().and_then(|o| o.as_deref()))
633 .collect::<String>();
634 param_from_key_value_pair::<Q>(key, &value)
635 }
636 | _ => None,
637 };
638 rendered.unwrap_or_default()
639 }
640}
641impl Default for Params {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646impl Params {
647 pub fn new() -> Self {
649 Self(Vec::new())
650 }
651 pub fn build(self) -> Vec<Param> {
653 self.0
654 }
655 pub fn from_config(config: &impl Configuration) -> Self {
658 Self::new()
659 .with_auth(config.token(), None)
660 .with_template("identifier", config.identifier())
661 }
662 pub fn with(self, param: Param) -> Self {
664 Self(self.0.into_iter().chain(once(param)).collect())
665 }
666 pub fn with_auth(self, token: &str, name: Option<&str>) -> Self {
673 let value = token.trim();
674 if !value.is_empty() {
675 let (header_name, header_value): (&str, String) = match name {
676 | None => ("Authorization", format!("Bearer {value}")),
677 | Some(name) => (name, value.to_string()),
678 };
679 self.with(param!(Header, header_name, header_value.as_str()))
680 } else {
681 self
682 }
683 }
684 pub fn with_auth_maybe(self, token: Option<&str>, name: Option<&str>) -> Self {
688 match token.map(str::trim).filter(|value| !value.is_empty()) {
689 | Some(value) => self.with_auth(value, name),
690 | None => self,
691 }
692 }
693 pub fn with_template(self, key: &str, value: Option<&str>) -> Self {
695 match value {
696 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::TemplateValue, key, v)),
697 | _ => self,
698 }
699 }
700 pub fn with_keyvalue(self, key: &str, value: Option<&str>) -> Self {
702 match value {
703 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::KeyValuePair, key, v)),
704 | _ => self,
705 }
706 }
707 pub fn with_body(self, key: &str, value: &str) -> Self {
709 self.with(param!(ParamStyle::Body, key, value))
710 }
711 pub fn with_body_maybe(self, key: &str, value: Option<&str>) -> Self {
713 match value {
714 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::Body, key, v)),
715 | _ => self,
716 }
717 }
718 pub fn with_field(self, key: &str, value: &str) -> Self {
720 self.with(param!(ParamStyle::FieldList, key, value))
721 }
722 pub fn with_custom(self, custom: &[Param]) -> Self {
725 if custom.is_empty() {
726 self
727 } else {
728 Self(self.0.iter().chain(custom.iter()).cloned().collect())
729 }
730 }
731}
732impl IntoBody for Vec<Param> {
733 fn into_body(self) -> serde_json::Value {
734 let params: Vec<Param> = self.into_iter().filter(|Param { style, .. }| style.is_body()).collect();
735 match params.as_slice() {
736 | [Param { name, values, .. }] if name.is_empty() => {
737 let flattened: Vec<String> = values.iter().cloned().flat_map(|vec| vec.into_iter().flatten()).collect();
738 if flattened.len() == 1 {
739 let raw = flattened.into_iter().next().unwrap_or_default();
740 serde_json::from_str::<serde_json::Value>(&raw).unwrap_or(serde_json::Value::String(raw))
741 } else if flattened.is_empty() {
742 serde_json::Value::Null
743 } else {
744 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
745 }
746 }
747 | _ => {
748 let body = params
749 .into_iter()
750 .map(|param| {
751 let Param { name, values, .. } = param;
752 let flattened: Vec<String> = values.into_iter().flat_map(|vec| vec.into_iter().flatten()).collect();
753 let value = if flattened.len() == 1 {
754 #[allow(clippy::unwrap_used)]
755 serde_json::Value::String(flattened.into_iter().next().unwrap())
756 } else if flattened.is_empty() {
757 serde_json::Value::Null
758 } else {
759 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
760 };
761 (name, value)
762 })
763 .collect();
764 serde_json::Value::Object(body)
765 }
766 }
767 }
768}
769impl IntoHeaders for Vec<Param> {
770 fn into_headers(self) -> HeaderMap {
771 let mut headers = HeaderMap::new();
772 self.into_iter().filter(|Param { style, .. }| style.is_header()).for_each(|param| {
773 let Param { name, values, .. } = param;
774 if let Ok(header_name) = name.parse::<HeaderName>() {
775 values.into_iter().for_each(|vec| {
776 vec.into_iter().for_each(|opt_value| {
777 if let Some(raw) = opt_value {
778 if let Ok(mut header_value) = HeaderValue::from_str(&raw) {
779 header_value.set_sensitive(true);
780 headers.append(header_name.clone(), header_value);
781 }
782 }
783 });
784 });
785 }
786 });
787 headers
788 }
789}
790#[async_trait]
791impl RemoteResource for Endpoint {
792 type Query = EmptyField;
793 type Field = EmptyField;
794
795 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
796 where
797 Q: QueryField + ValueValidator,
798 F: QueryField,
799 {
800 let mut context = Context::new();
801 match data {
802 | Some(params) => {
803 let (query_params, other_params): (Vec<Param>, Vec<Param>) =
804 params.into_iter().partition(|param| param.is_query() || param.style.is_key_value_pair());
805 let query = Param::to_query_string::<Q, F>(query_params);
806 context.insert("query", &query);
807 other_params.into_iter().for_each(|Param { name, style, values, .. }| {
808 if style.is_template_value() {
809 values.into_iter().for_each(|vec| {
810 vec.into_iter().flatten().for_each(|value| {
811 let key = name.clone();
812 context.insert(&key, &value.clone());
813 });
814 });
815 }
816 });
817 }
818 | None => (),
819 }
820 context.insert("base", &self.base());
821 context
822 }
823 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
824 where
825 R: for<'de> Deserialize<'de>,
826 E: FallbackResponse,
827 {
828 match response {
829 | Ok(content) => {
830 let raw_text = match &content {
831 | ResponseContent::Json(s) | ResponseContent::Xml(s) | ResponseContent::Yaml(s) | ResponseContent::Raw(s) => s.clone(),
832 };
833 let result: ApiResult<R> = match content {
834 | ResponseContent::Json(s) => parse_json(&s),
835 | ResponseContent::Xml(s) => parse_xml(&s),
836 | ResponseContent::Yaml(s) => parse_yaml(&s),
837 | ResponseContent::Raw(s) => {
838 let raw = TextResponse { content: s };
839 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
840 }
841 };
842 result.map_err(|err| E::into_error(&raw_text).unwrap_or(err))
843 }
844 | Err(why) => Err(eyre!(why)),
845 }
846 }
847 async fn invoke(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent> {
861 self.invoke_with::<Self::Query, Self::Field>(name, data).await
862 }
863 async fn invoke_with<Q, F>(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
888 where
889 Q: QueryField + ValueValidator,
890 F: QueryField,
891 {
892 let Self { resources, .. } = self;
893 let context = self.context_with::<Q, F>(data.clone());
894 let resource = resources.find_by_name(name);
895 match resource {
896 | Some(Resource { method, template, .. }) => {
897 let path = render(&template, &context);
898 let params = data.unwrap_or_default();
899 let headers = params.clone().into_headers();
900 let body = params.into_body();
901 let request = match method {
902 | HttpMethod::Delete => delete(path),
903 | HttpMethod::Get => get(path),
904 | HttpMethod::Patch => patch(path).json(&body),
905 | HttpMethod::Post => post(path).json(&body),
906 | HttpMethod::Put => put(path).json(&body),
907 };
908 debug!("=> {} {}", Label::run(), request.cyan());
909 match request.headers(headers).send().await {
910 | Ok(response) => match response.text().await {
911 | Ok(text) => {
912 trace!("=> {} Response {text}", Label::using());
913 let content = if detect_json(&text) {
914 ResponseContent::Json(text)
915 } else if detect_xml(&text) {
916 ResponseContent::Xml(text)
917 } else {
918 ResponseContent::Raw(text)
919 };
920 Ok(content)
921 }
922 | Err(why) => Err(eyre!(why)),
923 },
924 | Err(why) => Err(eyre!(why)),
925 }
926 }
927 | None => Err(eyre!("Resource not found")),
928 }
929 }
930}
931impl TryFrom<&str> for EmptyField {
932 type Error = String;
933
934 fn try_from(value: &str) -> eyre::Result<Self, Self::Error> {
935 Ok(EmptyField(value.to_string()))
936 }
937}
938impl ValueValidator for EmptyField {
939 fn is_valid(&self, _value: &str) -> bool {
940 true
941 }
942}
943impl TreeEntry {
944 pub fn is_file(&self) -> bool {
946 self.entry_type == TreeEntryType::File
947 }
948 pub fn is_directory(&self) -> bool {
950 self.entry_type == TreeEntryType::Directory
951 }
952 pub fn path(self) -> String {
954 self.path
955 }
956}
957pub(crate) fn extract_template_keys(template: &str) -> Vec<String> {
958 fn extract_key(expression: &str) -> Option<String> {
959 let trimmed = expression.trim().trim_matches('-');
960 trimmed
961 .split('|')
962 .next()
963 .map(str::trim)
964 .and_then(|base| base.split_whitespace().next().map(str::trim))
965 .and_then(|key| (!key.is_empty()).then(|| key.to_string()))
966 }
967 template
968 .split("{{")
969 .skip(1)
970 .filter_map(|segment| segment.split_once("}}").map(|(before, _)| before))
971 .filter_map(extract_key)
972 .unique()
973 .collect()
974}
975pub fn first_header<'a>(headers: &'a HeaderMap, names: &[&str]) -> Option<&'a str> {
977 names.iter().filter_map(|name| headers.get(*name)).find_map(|value| value.to_str().ok())
978}
979pub(crate) fn param_from_key_value_pair<T: QueryField + ValueValidator>(key: &str, value: &str) -> Option<String> {
981 match T::try_from(key) {
982 | Ok(field) => {
983 if field.is_valid(value) {
984 Some(format!("{}={}", field, urlencoding::encode(value)))
985 } else {
986 warn!("=> {} Invalid key value ({}{})", Label::using(), format!("{key}=").dimmed(), value.red());
987 None
988 }
989 }
990 | Err(_) => {
991 warn!("=> {} Invalid key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
992 None
993 }
994 }
995}
996pub(crate) fn param_from_query_pairs<T: QueryField + ValueValidator>(key: &str, separator: &str, pairs: Vec<(&str, &str)>) -> Option<String> {
998 let values: Vec<String> = pairs
999 .into_iter()
1000 .filter_map(|(k, v)| {
1001 let key: &str = k;
1002 let value: &str = v.trim();
1003 match T::try_from(key) {
1004 | Ok(field) => {
1005 if field.is_valid(value) {
1006 Some(format!("{}:{}", field, urlencoding::encode(value)))
1007 } else {
1008 warn!(
1009 "=> {} Invalid query value ({}{})",
1010 Label::using(),
1011 format!("{key}=").dimmed(),
1012 value.red()
1013 );
1014 None
1015 }
1016 }
1017 | Err(_) => {
1018 warn!("=> {} Invalid query key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
1019 None
1020 }
1021 }
1022 })
1023 .collect();
1024 if values.is_empty() {
1025 None
1026 } else {
1027 Some(format!("{}={}", key, values.join(separator)))
1028 }
1029}
1030pub(crate) fn param_from_field_list<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
1032 let values: Vec<String> = fields
1033 .into_iter()
1034 .filter_map(|value: &str| {
1035 let val = value;
1036 match T::try_from(val) {
1037 | Ok(column) => Some(column.to_string()),
1038 | Err(_) => None,
1039 }
1040 })
1041 .collect();
1042 if values.is_empty() {
1043 None
1044 } else {
1045 Some(format!("{key}={}", values.join(separator)))
1046 }
1047}
1048pub(crate) fn param_from_query_fields<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
1050 let valid_fields: Vec<T> = fields.into_iter().filter_map(|value| T::try_from(value).ok()).collect();
1051 if valid_fields.is_empty() {
1052 None
1053 } else {
1054 let count = valid_fields.len();
1055 Some(format!(
1056 "{}={}",
1057 key,
1058 valid_fields
1059 .into_iter()
1060 .enumerate()
1061 .map(|(i, field)| format!("{}{URL_ENCODED_CARAT}{}.0", field, count.saturating_add(1).saturating_sub(i)))
1062 .collect::<Vec<String>>()
1063 .join(separator),
1064 ))
1065 }
1066}
1067pub(crate) fn parse_json<R>(content: &str) -> ApiResult<R>
1068where
1069 R: for<'de> Deserialize<'de>,
1070{
1071 match serde_json::from_str::<R>(content) {
1072 | Ok(response) => Ok(response),
1073 | Err(why) => Err(eyre!(why)),
1074 }
1075}
1076pub(crate) fn parse_xml<R>(content: &str) -> ApiResult<R>
1077where
1078 R: for<'de> Deserialize<'de>,
1079{
1080 match quick_xml::de::from_str::<R>(content) {
1081 | Ok(response) => Ok(response),
1082 | Err(why) => Err(eyre!(why)),
1083 }
1084}
1085pub(crate) fn parse_yaml<R>(content: &str) -> ApiResult<R>
1086where
1087 R: for<'de> Deserialize<'de>,
1088{
1089 match serde_norway::from_str::<R>(content) {
1090 | Ok(response) => Ok(response),
1091 | Err(why) => Err(eyre!(why)),
1092 }
1093}
1094pub(crate) fn query_string<Q: QueryField + ValueValidator, F: QueryField>(
1104 query_pairs: Vec<(&str, &str)>,
1105 field_list: Vec<&str>,
1106 query_fields: Vec<&str>,
1107) -> String {
1108 let params = vec![
1109 Param::from_query_pair("q", query_pairs),
1110 Param::from_field_list("fl", field_list),
1111 Param::from_query_field("qf", query_fields),
1112 ];
1113 Param::to_query_string::<Q, F>(params)
1114}
1115pub(crate) fn render(template: &str, context: &Context) -> String {
1116 let missing_values = extract_template_keys(template)
1117 .into_iter()
1118 .filter(|key| !context.contains_key(key))
1119 .map(|key| (key, serde_json::Value::String(String::new())));
1120 let merged = match context.clone().into_json() {
1121 | serde_json::Value::Object(existing) => serde_json::Value::Object(existing.into_iter().chain(missing_values).collect()),
1122 | _ => serde_json::Value::Object(missing_values.collect()),
1123 };
1124 Context::from_serialize(merged)
1125 .ok()
1126 .and_then(|context| Tera::one_off(template, &context, false).ok())
1127 .unwrap_or_default()
1128}
1129pub(crate) fn require_non_empty_secret(secret: &str, path: &str, names: &[&str]) -> ApiResult<String> {
1133 let value = secret.trim();
1134 if value.is_empty() {
1135 let env_list = names.join(", ");
1136 Err(eyre!("Missing required token for {path} request. Set one of: {env_list}"))
1137 } else {
1138 Ok(value.to_string())
1139 }
1140}
1141pub fn sluggify(username: &str, user_id: u64) -> String {
1143 let (slug, _) = username
1144 .trim()
1145 .to_ascii_lowercase()
1146 .chars()
1147 .fold((String::new(), false), |(mut value, separator), character| {
1148 if character.is_ascii_alphanumeric() {
1149 value.push(character);
1150 (value, false)
1151 } else if !value.is_empty() && !separator {
1152 value.push('-');
1153 (value, true)
1154 } else {
1155 (value, separator)
1156 }
1157 });
1158 let slug = slug.trim_matches('-');
1159 if slug.is_empty() {
1160 format!("user-{user_id}")
1161 } else {
1162 slug.to_string()
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests;