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 huggingface;
39pub mod models_dev;
40pub mod openai;
41pub mod openapi;
42pub mod orcid;
43pub mod osti;
44pub mod raid;
45pub mod ror;
46pub mod spdx;
47
48lazy_static! {
49 pub static ref INCLUDED_ENDPOINTS: Vec<Endpoint> = Constant::json::<ApplicationConfiguration>("application").endpoints.unwrap_or_default();
51}
52pub fn first_header<'a>(headers: &'a HeaderMap, names: &[&str]) -> Option<&'a str> {
54 names.iter().filter_map(|name| headers.get(*name)).find_map(|value| value.to_str().ok())
55}
56pub fn sluggify(username: &str, user_id: u64) -> String {
58 let (slug, _) = username
59 .trim()
60 .to_ascii_lowercase()
61 .chars()
62 .fold((String::new(), false), |(mut value, separator), character| {
63 if character.is_ascii_alphanumeric() {
64 value.push(character);
65 (value, false)
66 } else if !value.is_empty() && !separator {
67 value.push('-');
68 (value, true)
69 } else {
70 (value, separator)
71 }
72 });
73 let slug = slug.trim_matches('-');
74 if slug.is_empty() {
75 format!("user-{user_id}")
76 } else {
77 slug.to_string()
78 }
79}
80pub trait Configuration {
82 fn from_env() -> Self;
84 fn with_body(self, value: impl Into<String>) -> Self;
86 fn with_domain(self, value: impl Into<String>) -> Self;
88 fn with_identifier(self, value: impl Into<String>) -> Self;
90 fn token(&self) -> &str;
92 fn domain(&self) -> &str;
94 fn identifier(&self) -> Option<&str>;
96 fn with_params(self, params: Vec<Param>) -> Self;
99 fn params(&self) -> &[Param];
101}
102#[async_trait]
104pub trait DatabasePersistence {
105 async fn persist(self, database: Database<Table>) -> ApiResult<usize>;
107}
108pub trait IntoBody {
110 fn into_body(self) -> serde_json::Value;
112}
113pub trait IntoHeaders {
115 fn into_headers(self) -> HeaderMap;
117}
118pub trait QueryField: fmt::Display + for<'a> TryFrom<&'a str> {}
120pub trait RepositoryFileMetadata {
122 fn path(&self) -> &str;
124 fn size(&self) -> Option<u64>;
126}
127pub trait FallbackResponse {
135 fn into_error(content: &str) -> Option<eyre::Report>;
138 fn to_string(content: &str) -> Option<String> {
140 serde_json::from_str::<serde_json::Value>(content).ok().and_then(|value| match value {
141 | serde_json::Value::String(inner) => serde_json::from_str::<serde_json::Value>(&inner)
142 .ok()
143 .and_then(|nested| serde_json::to_string_pretty(&nested).ok())
144 .or_else(|| serde_json::to_string_pretty(&serde_json::Value::String(inner)).ok()),
145 | other => serde_json::to_string_pretty(&other).ok(),
146 })
147 }
148}
149#[async_trait]
152pub trait RemoteResource {
153 type Query: QueryField + ValueValidator;
155 type Field: QueryField;
157
158 fn context(&self, params: Option<Vec<Param>>) -> Context {
160 self.context_with::<Self::Query, Self::Field>(params)
161 }
162 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
164 where
165 Q: QueryField + ValueValidator,
166 F: QueryField;
167 fn handle<R>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
169 where
170 R: for<'de> Deserialize<'de>,
171 {
172 match response {
173 | Ok(content) => match content {
174 | ResponseContent::Json(content) => parse_json(&content),
175 | ResponseContent::Xml(content) => parse_xml(&content),
176 | ResponseContent::Yaml(content) => parse_yaml(&content),
177 | ResponseContent::Raw(content) => {
178 let raw = TextResponse { content };
179 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
180 }
181 },
182 | Err(e) => Err(eyre!(e)),
183 }
184 }
185 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
188 where
189 R: for<'de> Deserialize<'de>,
190 E: FallbackResponse;
191 async fn invoke(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>;
193 async fn invoke_with<Q, F>(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
195 where
196 Q: QueryField + ValueValidator,
197 F: QueryField;
198}
199pub trait ValueValidator {
201 fn is_valid(&self, _value: &str) -> bool {
203 true
204 }
205}
206#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
208pub enum AuthenticationScheme {
209 #[default]
211 Bearer,
212 Basic,
214 ApiKey,
216 OAuth2,
218 AwsSignatureV4,
220 GoogleCloud,
222 Custom(String),
224}
225#[derive(Clone, Debug, Default, Deserialize, EnumIs, Serialize)]
227pub enum ParamStyle {
228 #[default]
230 QueryPair,
231 QueryField,
233 FieldList,
235 KeyValuePair,
237 Header,
239 Body,
241 TemplateValue,
243}
244#[derive(Clone, Debug, Deserialize, Serialize)]
246pub enum ResponseContent {
247 Json(String),
249 Raw(String),
251 Yaml(String),
253 Xml(String),
255}
256#[derive(Clone, Debug, Display, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
258#[serde(rename_all = "lowercase")]
259pub enum TreeEntryType {
260 #[serde(alias = "blob")]
262 #[display("file")]
263 File,
264 #[serde(alias = "tree")]
266 #[display("directory")]
267 Directory,
268}
269pub struct Fallback<T>(PhantomData<T>);
277pub struct NoFallback;
279#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
281#[builder(start_fn = init)]
282pub struct Authentication {
283 pub token: Option<String>,
286 #[builder(default)]
288 pub scheme: AuthenticationScheme,
289}
290#[derive(Clone, Debug, Deserialize, Serialize)]
292#[serde(rename_all = "kebab-case")]
293pub struct EmptyField(String);
294#[skip_serializing_none]
298#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
299#[builder(start_fn = at, on(String, into))]
300pub struct Endpoint {
301 #[builder(start_fn)]
303 pub domain: String,
304 #[builder(default = String::new())]
306 pub name: String,
307 #[serde(default)]
309 pub scheme: Option<Scheme>,
310 pub port: Option<u16>,
312 pub authentication: Option<Authentication>,
314 pub root: Option<String>,
318 #[builder(default = vec![])]
320 pub resources: Vec<Resource>,
321}
322#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
324pub struct Identifier<T> {
325 #[serde(rename = "id", alias = "iid")]
327 pub identifier: T,
328}
329#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
331#[builder(start_fn = of_type, finish_fn = with_key, on(String, into))]
332pub struct Param {
333 #[builder(start_fn)]
335 pub style: ParamStyle,
336 #[builder(finish_fn)]
338 pub name: String,
339 #[builder(
341 default = vec![],
342 with = |vecs: Vec<Vec<Option<&str>>>| {
343 vecs
344 .into_iter()
345 .map(|vec| vec.into_iter().map(|opt| opt.map(str::to_string)).collect())
346 .collect()
347 }
348 )]
349 pub values: Vec<Vec<Option<String>>>,
350 #[builder(default = false)]
352 pub required: bool,
353}
354pub struct Params(Vec<Param>);
370#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
372#[builder(start_fn = init, on(String, into))]
373pub struct Resource {
374 pub name: String,
376 #[builder(with = |method: &str| HttpMethod::from(method))]
378 #[serde(default)]
379 pub method: HttpMethod,
380 pub template: String,
382}
383#[derive(Clone, Debug, Deserialize, Serialize)]
385pub struct TextResponse {
386 pub content: String,
388}
389#[skip_serializing_none]
391#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
392pub struct TreeEntry {
393 pub path: String,
395 #[serde(rename = "type")]
397 pub entry_type: TreeEntryType,
398 pub size: Option<u64>,
400 pub id: Option<String>,
402 pub name: Option<String>,
404 pub mode: Option<String>,
406 pub sha: Option<String>,
408 pub url: Option<String>,
410}
411impl Endpoint {
412 pub fn from_parts(domain: String, scheme: Option<Scheme>, port: Option<u16>) -> Self {
416 Self {
417 domain,
418 scheme,
419 port,
420 ..Self::default()
421 }
422 }
423 pub fn base(&self) -> String {
425 let Self { domain, root, .. } = self;
426 let scheme = self.scheme.as_ref().map_or("https".to_string(), |s| s.to_string());
427 let port = self.port.map_or(String::new(), |port| format!(":{port}"));
428 let root = root.as_ref().map_or(String::new(), |root| format!("/{root}"));
429 format!("{scheme}://{domain}{port}{root}")
430 }
431 pub fn with_domain(&self, domain: impl Into<String>) -> Self {
433 let (domain, scheme, port) = Self::split_domain(domain.into().as_str());
434 Self {
435 domain,
436 scheme: scheme.or_else(|| self.scheme.clone()),
437 port: port.or(self.port),
438 ..self.clone()
439 }
440 }
441 fn split_domain(value: &str) -> (String, Option<Scheme>, Option<u16>) {
442 Uri::parse(value)
443 .ok()
444 .and_then(|uri| {
445 uri.authority().map(|authority| {
446 (
447 authority.host().to_string(),
448 Some(Scheme::from(uri.scheme().as_str())).filter(|scheme| *scheme != Scheme::Unsupported),
449 authority.port_to_u16().ok().flatten(),
450 )
451 })
452 })
453 .or_else(|| {
454 let authority = format!("//{}", value.trim());
455 UriRef::parse(authority.as_str()).ok().and_then(|uri| {
456 uri.authority()
457 .map(|parsed| (parsed.host().to_string(), None, parsed.port_to_u16().ok().flatten()))
458 })
459 })
460 .unwrap_or_else(|| (value.trim().to_string(), None, None))
461 }
462 pub fn from_template(name: impl Into<String>) -> ApiResult<Self> {
469 let endpoint_name = name.into();
470 INCLUDED_ENDPOINTS
471 .find_by_name(&endpoint_name)
472 .ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration"))
473 }
474}
475impl Searchable<Endpoint> for Vec<Endpoint> {
476 fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> {
477 let name = value.into();
478 self.iter().find(|endpoint| endpoint.name.eq_ignore_ascii_case(&name)).cloned()
479 }
480}
481impl FallbackResponse for NoFallback {
482 fn into_error(_: &str) -> Option<eyre::Report> {
483 None
484 }
485 fn to_string(_: &str) -> Option<String> {
486 None
487 }
488}
489impl<T> FallbackResponse for Fallback<T>
490where
491 T: for<'de> Deserialize<'de> + fmt::Debug,
492{
493 fn into_error(content: &str) -> Option<eyre::Report> {
494 serde_json::from_str::<T>(content).ok().map(|why| {
495 let message = Self::to_string(content).unwrap_or_else(|| format!("{why:#?}"));
496 eyre!("{message}")
497 })
498 }
499}
500impl Searchable<Resource> for Vec<Resource> {
501 fn find_by_name(&self, value: impl Into<String>) -> Option<Resource> {
502 let name = value.into();
503 self.iter().find(|resource| resource.name.eq_ignore_ascii_case(&name)).cloned()
504 }
505}
506impl<T> QueryField for T where T: fmt::Display + for<'a> TryFrom<&'a str> {}
508impl fmt::Display for AuthenticationScheme {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 write!(
511 f,
512 "{}",
513 match self {
514 | AuthenticationScheme::Bearer => "Bearer",
515 | AuthenticationScheme::Basic => "Basic",
516 | AuthenticationScheme::ApiKey => "ApiKey",
517 | AuthenticationScheme::OAuth2 => "OAuth2",
518 | AuthenticationScheme::AwsSignatureV4 => "AWS Signature V4",
519 | AuthenticationScheme::GoogleCloud => "Google Cloud",
520 | AuthenticationScheme::Custom(scheme) => scheme,
521 }
522 )
523 }
524}
525impl fmt::Display for EmptyField {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 write!(f, "{}", self.0)
528 }
529}
530impl fmt::Display for TextResponse {
531 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532 write!(f, "{}", self.content)
533 }
534}
535impl Default for Endpoint {
536 fn default() -> Self {
537 Endpoint::at("example.com").scheme(Scheme::default()).build()
538 }
539}
540impl<'a> From<Uri<&'a str>> for Endpoint {
541 fn from(value: Uri<&'a str>) -> Self {
542 let domain = value.authority().map(|auth| auth.host().to_string()).unwrap_or_default();
543 let port = value.authority().and_then(|auth| auth.port_to_u16().ok()).flatten();
544 Self::from_parts(
545 domain,
546 Some(Scheme::from(value.scheme().as_str())).filter(|scheme| *scheme != Scheme::Unsupported),
547 port,
548 )
549 }
550}
551impl From<Location> for Endpoint {
552 fn from(value: Location) -> Self {
553 let domain = value.host().unwrap_or_default();
554 let port = value.port();
555 Self::from_parts(domain, Some(value.scheme()), port)
556 }
557}
558impl From<Repository> for Endpoint {
559 fn from(value: Repository) -> Self {
560 value.location().into()
561 }
562}
563impl fmt::Display for HttpMethod {
564 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565 write!(
566 f,
567 "{}",
568 match self {
569 | HttpMethod::Get => "GET",
570 | HttpMethod::Post => "POST",
571 | HttpMethod::Put => "PUT",
572 | HttpMethod::Patch => "PATCH",
573 | HttpMethod::Delete => "DELETE",
574 }
575 )
576 }
577}
578impl Param {
579 pub fn is_query(&self) -> bool {
581 self.style.is_query_pair() | self.style.is_query_field() | self.style.is_field_list()
582 }
583 pub fn to_query_string<Q: QueryField + ValueValidator, F: QueryField>(params: Vec<Param>) -> String {
585 let query = params
586 .iter()
587 .filter(|param| param.is_query() || param.style.is_key_value_pair())
588 .map(|param| param.to_string::<Q, F>())
589 .filter(|s| !s.is_empty())
590 .collect::<Vec<String>>()
591 .join("&");
592 if !query.is_empty() {
593 format!("?{query}")
594 } else {
595 String::new()
596 }
597 }
598 pub fn from_query_pair(key: &str, pairs: Vec<(&str, &str)>) -> Self {
607 Param::of_type(ParamStyle::QueryPair)
608 .values(pairs.into_iter().map(|(k, v)| vec![Some(k), Some(v)]).collect())
609 .with_key(key)
610 }
611 pub fn from_field_list(key: &str, fields: Vec<&str>) -> Self {
613 Param::of_type(ParamStyle::FieldList)
614 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
615 .with_key(key)
616 }
617 pub fn from_query_field(key: &str, fields: Vec<&str>) -> Self {
619 Param::of_type(ParamStyle::QueryField)
620 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
621 .with_key(key)
622 }
623
624 pub fn to_string<Q: QueryField + ValueValidator, F: QueryField>(&self) -> String {
628 let key = self.name.as_str();
629 let rendered: Option<String> = match self.style {
630 | ParamStyle::QueryPair => {
631 let separator = "+AND+";
632 let pairs: Vec<(&str, &str)> = self
633 .values
634 .iter()
635 .filter_map(
636 |vec| match (vec.first().and_then(|o| o.as_deref()), vec.get(1).and_then(|o| o.as_deref())) {
637 | (Some(k), Some(v)) => Some((k, v)),
638 | _ => None,
639 },
640 )
641 .collect();
642 param_from_query_pairs::<Q>(key, separator, pairs)
643 }
644 | ParamStyle::QueryField => {
645 let separator = URL_ENCODED_SPACE;
646 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
647 param_from_query_fields::<Q>(key, separator, fields)
648 }
649 | ParamStyle::FieldList => {
650 let separator = ",";
651 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
652 param_from_field_list::<F>(key, separator, fields)
653 }
654 | ParamStyle::KeyValuePair => {
655 let value = self
656 .values
657 .iter()
658 .filter_map(|vec| vec.first().and_then(|o| o.as_deref()))
659 .collect::<String>();
660 param_from_key_value_pair::<Q>(key, &value)
661 }
662 | _ => None,
663 };
664 rendered.unwrap_or_default()
665 }
666}
667impl Default for Params {
668 fn default() -> Self {
669 Self::new()
670 }
671}
672impl Params {
673 pub fn new() -> Self {
675 Self(Vec::new())
676 }
677 pub fn build(self) -> Vec<Param> {
679 self.0
680 }
681 pub fn from_config(config: &impl Configuration) -> Self {
684 Self::new()
685 .with_auth(config.token(), None)
686 .with_template("identifier", config.identifier())
687 }
688 pub fn with(self, param: Param) -> Self {
690 Self(self.0.into_iter().chain(once(param)).collect())
691 }
692 pub fn with_auth(self, token: &str, name: Option<&str>) -> Self {
699 let value = token.trim();
700 if !value.is_empty() {
701 let (header_name, header_value): (&str, String) = match name {
702 | None => ("Authorization", format!("Bearer {value}")),
703 | Some(name) => (name, value.to_string()),
704 };
705 self.with(param!(Header, header_name, header_value.as_str()))
706 } else {
707 self
708 }
709 }
710 pub fn with_auth_maybe(self, token: Option<&str>, name: Option<&str>) -> Self {
714 match token.map(str::trim).filter(|value| !value.is_empty()) {
715 | Some(value) => self.with_auth(value, name),
716 | None => self,
717 }
718 }
719 pub fn with_template(self, key: &str, value: Option<&str>) -> Self {
721 match value {
722 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::TemplateValue, key, v)),
723 | _ => self,
724 }
725 }
726 pub fn with_keyvalue(self, key: &str, value: Option<&str>) -> Self {
728 match value {
729 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::KeyValuePair, key, v)),
730 | _ => self,
731 }
732 }
733 pub fn with_body(self, key: &str, value: &str) -> Self {
735 self.with(param!(ParamStyle::Body, key, value))
736 }
737 pub fn with_body_maybe(self, key: &str, value: Option<&str>) -> Self {
739 match value {
740 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::Body, key, v)),
741 | _ => self,
742 }
743 }
744 pub fn with_field(self, key: &str, value: &str) -> Self {
746 self.with(param!(ParamStyle::FieldList, key, value))
747 }
748 pub fn with_custom(self, custom: &[Param]) -> Self {
751 if custom.is_empty() {
752 self
753 } else {
754 Self(self.0.iter().chain(custom.iter()).cloned().collect())
755 }
756 }
757}
758impl IntoBody for Vec<Param> {
759 fn into_body(self) -> serde_json::Value {
760 let params: Vec<Param> = self.into_iter().filter(|Param { style, .. }| style.is_body()).collect();
761 match params.as_slice() {
762 | [Param { name, values, .. }] if name.is_empty() => {
763 let flattened: Vec<String> = values.iter().cloned().flat_map(|vec| vec.into_iter().flatten()).collect();
764 if flattened.len() == 1 {
765 let raw = flattened.into_iter().next().unwrap_or_default();
766 serde_json::from_str::<serde_json::Value>(&raw).unwrap_or(serde_json::Value::String(raw))
767 } else if flattened.is_empty() {
768 serde_json::Value::Null
769 } else {
770 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
771 }
772 }
773 | _ => {
774 let body = params
775 .into_iter()
776 .map(|param| {
777 let Param { name, values, .. } = param;
778 let flattened: Vec<String> = values.into_iter().flat_map(|vec| vec.into_iter().flatten()).collect();
779 let value = if flattened.len() == 1 {
780 #[allow(clippy::unwrap_used)]
781 serde_json::Value::String(flattened.into_iter().next().unwrap())
782 } else if flattened.is_empty() {
783 serde_json::Value::Null
784 } else {
785 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
786 };
787 (name, value)
788 })
789 .collect();
790 serde_json::Value::Object(body)
791 }
792 }
793 }
794}
795impl IntoHeaders for Vec<Param> {
796 fn into_headers(self) -> HeaderMap {
797 let mut headers = HeaderMap::new();
798 self.into_iter().filter(|Param { style, .. }| style.is_header()).for_each(|param| {
799 let Param { name, values, .. } = param;
800 if let Ok(header_name) = name.parse::<HeaderName>() {
801 values.into_iter().for_each(|vec| {
802 vec.into_iter().for_each(|opt_value| {
803 if let Some(raw) = opt_value {
804 if let Ok(mut header_value) = HeaderValue::from_str(&raw) {
805 header_value.set_sensitive(true);
806 headers.append(header_name.clone(), header_value);
807 }
808 }
809 });
810 });
811 }
812 });
813 headers
814 }
815}
816#[async_trait]
817impl RemoteResource for Endpoint {
818 type Query = EmptyField;
819 type Field = EmptyField;
820
821 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
822 where
823 Q: QueryField + ValueValidator,
824 F: QueryField,
825 {
826 let mut context = Context::new();
827 match data {
828 | Some(params) => {
829 let (query_params, other_params): (Vec<Param>, Vec<Param>) =
830 params.into_iter().partition(|param| param.is_query() || param.style.is_key_value_pair());
831 let query = Param::to_query_string::<Q, F>(query_params);
832 context.insert("query", &query);
833 other_params.into_iter().for_each(|Param { name, style, values, .. }| {
834 if style.is_template_value() {
835 values.into_iter().for_each(|vec| {
836 vec.into_iter().flatten().for_each(|value| {
837 let key = name.clone();
838 context.insert(&key, &value.clone());
839 });
840 });
841 }
842 });
843 }
844 | None => (),
845 }
846 context.insert("base", &self.base());
847 context
848 }
849 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
850 where
851 R: for<'de> Deserialize<'de>,
852 E: FallbackResponse,
853 {
854 match response {
855 | Ok(content) => {
856 let raw_text = match &content {
857 | ResponseContent::Json(s) | ResponseContent::Xml(s) | ResponseContent::Yaml(s) | ResponseContent::Raw(s) => s.clone(),
858 };
859 let result: ApiResult<R> = match content {
860 | ResponseContent::Json(s) => parse_json(&s),
861 | ResponseContent::Xml(s) => parse_xml(&s),
862 | ResponseContent::Yaml(s) => parse_yaml(&s),
863 | ResponseContent::Raw(s) => {
864 let raw = TextResponse { content: s };
865 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
866 }
867 };
868 result.map_err(|err| E::into_error(&raw_text).unwrap_or(err))
869 }
870 | Err(why) => Err(eyre!(why)),
871 }
872 }
873 async fn invoke(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent> {
887 self.invoke_with::<Self::Query, Self::Field>(name, data).await
888 }
889 async fn invoke_with<Q, F>(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
914 where
915 Q: QueryField + ValueValidator,
916 F: QueryField,
917 {
918 let Self { resources, .. } = self;
919 let context = self.context_with::<Q, F>(data.clone());
920 let resource = resources.find_by_name(name);
921 match resource {
922 | Some(Resource { method, template, .. }) => {
923 let path = render(&template, &context);
924 let params = data.unwrap_or_default();
925 let headers = params.clone().into_headers();
926 let body = params.into_body();
927 let request = match method {
928 | HttpMethod::Delete => delete(path),
929 | HttpMethod::Get => get(path),
930 | HttpMethod::Patch => patch(path).json(&body),
931 | HttpMethod::Post => post(path).json(&body),
932 | HttpMethod::Put => put(path).json(&body),
933 };
934 debug!("=> {} {}", Label::run(), request.cyan());
935 match request.headers(headers).send().await {
936 | Ok(response) => match response.text().await {
937 | Ok(text) => {
938 trace!("=> {} Response {text}", Label::using());
939 let content = if detect_json(&text) {
940 ResponseContent::Json(text)
941 } else if detect_xml(&text) {
942 ResponseContent::Xml(text)
943 } else {
944 ResponseContent::Raw(text)
945 };
946 Ok(content)
947 }
948 | Err(why) => Err(eyre!(why)),
949 },
950 | Err(why) => Err(eyre!(why)),
951 }
952 }
953 | None => Err(eyre!("Resource not found")),
954 }
955 }
956}
957impl TryFrom<&str> for EmptyField {
958 type Error = String;
959
960 fn try_from(value: &str) -> eyre::Result<Self, Self::Error> {
961 Ok(EmptyField(value.to_string()))
962 }
963}
964impl ValueValidator for EmptyField {
965 fn is_valid(&self, _value: &str) -> bool {
966 true
967 }
968}
969impl TreeEntry {
970 pub fn is_file(&self) -> bool {
972 self.entry_type == TreeEntryType::File
973 }
974 pub fn is_directory(&self) -> bool {
976 self.entry_type == TreeEntryType::Directory
977 }
978 pub fn path(self) -> String {
980 self.path
981 }
982}
983pub(crate) fn extract_template_keys(template: &str) -> Vec<String> {
984 fn extract_key(expression: &str) -> Option<String> {
985 let trimmed = expression.trim().trim_matches('-');
986 trimmed
987 .split('|')
988 .next()
989 .map(str::trim)
990 .and_then(|base| base.split_whitespace().next().map(str::trim))
991 .and_then(|key| (!key.is_empty()).then(|| key.to_string()))
992 }
993 template
994 .split("{{")
995 .skip(1)
996 .filter_map(|segment| segment.split_once("}}").map(|(before, _)| before))
997 .filter_map(extract_key)
998 .unique()
999 .collect()
1000}
1001pub(crate) fn param_from_key_value_pair<T: QueryField + ValueValidator>(key: &str, value: &str) -> Option<String> {
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!("=> {} Invalid key value ({}{})", Label::using(), format!("{key}=").dimmed(), value.red());
1009 None
1010 }
1011 }
1012 | Err(_) => {
1013 warn!("=> {} Invalid key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
1014 None
1015 }
1016 }
1017}
1018pub(crate) fn param_from_query_pairs<T: QueryField + ValueValidator>(key: &str, separator: &str, pairs: Vec<(&str, &str)>) -> Option<String> {
1020 let values: Vec<String> = pairs
1021 .into_iter()
1022 .filter_map(|(k, v)| {
1023 let key: &str = k;
1024 let value: &str = v.trim();
1025 match T::try_from(key) {
1026 | Ok(field) => {
1027 if field.is_valid(value) {
1028 Some(format!("{}:{}", field, urlencoding::encode(value)))
1029 } else {
1030 warn!(
1031 "=> {} Invalid query value ({}{})",
1032 Label::using(),
1033 format!("{key}=").dimmed(),
1034 value.red()
1035 );
1036 None
1037 }
1038 }
1039 | Err(_) => {
1040 warn!("=> {} Invalid query key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
1041 None
1042 }
1043 }
1044 })
1045 .collect();
1046 if values.is_empty() {
1047 None
1048 } else {
1049 Some(format!("{}={}", key, values.join(separator)))
1050 }
1051}
1052pub(crate) fn param_from_field_list<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
1054 let values: Vec<String> = fields
1055 .into_iter()
1056 .filter_map(|value: &str| {
1057 let val = value;
1058 match T::try_from(val) {
1059 | Ok(column) => Some(column.to_string()),
1060 | Err(_) => None,
1061 }
1062 })
1063 .collect();
1064 if values.is_empty() {
1065 None
1066 } else {
1067 Some(format!("{key}={}", values.join(separator)))
1068 }
1069}
1070pub(crate) fn param_from_query_fields<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
1072 let valid_fields: Vec<T> = fields.into_iter().filter_map(|value| T::try_from(value).ok()).collect();
1073 if valid_fields.is_empty() {
1074 None
1075 } else {
1076 let count = valid_fields.len();
1077 Some(format!(
1078 "{}={}",
1079 key,
1080 valid_fields
1081 .into_iter()
1082 .enumerate()
1083 .map(|(i, field)| format!("{}{URL_ENCODED_CARAT}{}.0", field, count.saturating_add(1).saturating_sub(i)))
1084 .collect::<Vec<String>>()
1085 .join(separator),
1086 ))
1087 }
1088}
1089pub(crate) fn parse_json<R>(content: &str) -> ApiResult<R>
1090where
1091 R: for<'de> Deserialize<'de>,
1092{
1093 match serde_json::from_str::<R>(content) {
1094 | Ok(response) => Ok(response),
1095 | Err(why) => Err(eyre!(why)),
1096 }
1097}
1098pub(crate) fn parse_xml<R>(content: &str) -> ApiResult<R>
1099where
1100 R: for<'de> Deserialize<'de>,
1101{
1102 match quick_xml::de::from_str::<R>(content) {
1103 | Ok(response) => Ok(response),
1104 | Err(why) => Err(eyre!(why)),
1105 }
1106}
1107pub(crate) fn parse_yaml<R>(content: &str) -> ApiResult<R>
1108where
1109 R: for<'de> Deserialize<'de>,
1110{
1111 match serde_norway::from_str::<R>(content) {
1112 | Ok(response) => Ok(response),
1113 | Err(why) => Err(eyre!(why)),
1114 }
1115}
1116pub(crate) fn query_string<Q: QueryField + ValueValidator, F: QueryField>(
1126 query_pairs: Vec<(&str, &str)>,
1127 field_list: Vec<&str>,
1128 query_fields: Vec<&str>,
1129) -> String {
1130 let params = vec![
1131 Param::from_query_pair("q", query_pairs),
1132 Param::from_field_list("fl", field_list),
1133 Param::from_query_field("qf", query_fields),
1134 ];
1135 Param::to_query_string::<Q, F>(params)
1136}
1137pub(crate) fn render(template: &str, context: &Context) -> String {
1138 let missing_values = extract_template_keys(template)
1139 .into_iter()
1140 .filter(|key| !context.contains_key(key))
1141 .map(|key| (key, serde_json::Value::String(String::new())));
1142 let merged = match context.clone().into_json() {
1143 | serde_json::Value::Object(existing) => serde_json::Value::Object(existing.into_iter().chain(missing_values).collect()),
1144 | _ => serde_json::Value::Object(missing_values.collect()),
1145 };
1146 Context::from_serialize(merged)
1147 .ok()
1148 .and_then(|context| Tera::one_off(template, &context, false).ok())
1149 .unwrap_or_default()
1150}
1151pub(crate) fn require_non_empty_secret(secret: &str, path: &str, names: &[&str]) -> ApiResult<String> {
1155 let value = secret.trim();
1156 if value.is_empty() {
1157 let env_list = names.join(", ");
1158 Err(eyre!("Missing required token for {path} request. Set one of: {env_list}"))
1159 } else {
1160 Ok(value.to_string())
1161 }
1162}
1163
1164#[cfg(test)]
1165mod tests;