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::prelude::HashSet;
12use crate::util::constants::{URL_ENCODED_CARAT, URL_ENCODED_SPACE};
13use crate::util::{detect_json, detect_xml, Constant, Label, Searchable};
14use crate::{Location, Repository, Scheme};
15use async_trait::async_trait;
16use axum::http::header::{HeaderName, HeaderValue};
17use axum::http::HeaderMap;
18use bon::Builder;
19use color_eyre::eyre::{self, eyre};
20use core::iter::once;
21use core::{fmt, marker::PhantomData};
22use derive_more::Display;
23use fluent_uri::Uri;
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::{trace, warn};
32use validator::Validate;
33
34pub mod citeas;
35pub mod geonames;
36pub mod github;
37pub mod gitlab;
38pub mod models_dev;
39pub mod openai;
40pub mod openapi;
41pub mod orcid;
42pub mod raid;
43pub mod ror;
44pub mod spdx;
45
46lazy_static! {
47 pub static ref INCLUDED_ENDPOINTS: Vec<Endpoint> = Constant::json::<ApplicationConfiguration>("application").endpoints.unwrap_or_default();
49}
50pub trait Configuration {
52 fn from_env() -> Self;
54 fn with_body(self, value: impl Into<String>) -> Self;
56 fn with_domain(self, value: impl Into<String>) -> Self;
58 fn with_identifier(self, value: impl Into<String>) -> Self;
60 fn token(&self) -> &str;
62 fn domain(&self) -> &str;
64 fn identifier(&self) -> Option<&str>;
66 fn with_params(self, params: Vec<Param>) -> Self;
69 fn params(&self) -> &[Param];
71}
72#[async_trait]
74pub trait DatabasePersistence {
75 async fn persist(self, database: Database<Table>) -> ApiResult<usize>;
77}
78pub trait IntoBody {
80 fn into_body(self) -> serde_json::Value;
82}
83pub trait IntoHeaders {
85 fn into_headers(self) -> HeaderMap;
87}
88pub trait QueryField: fmt::Display + for<'a> TryFrom<&'a str> {}
90pub trait FallbackResponse {
98 fn into_error(content: &str) -> Option<eyre::Report>;
101 fn to_string(content: &str) -> Option<String> {
103 serde_json::from_str::<serde_json::Value>(content).ok().and_then(|value| match value {
104 | serde_json::Value::String(inner) => serde_json::from_str::<serde_json::Value>(&inner)
105 .ok()
106 .and_then(|nested| serde_json::to_string_pretty(&nested).ok())
107 .or_else(|| serde_json::to_string_pretty(&serde_json::Value::String(inner)).ok()),
108 | other => serde_json::to_string_pretty(&other).ok(),
109 })
110 }
111}
112#[async_trait]
115pub trait RemoteResource {
116 type Query: QueryField + ValueValidator;
118 type Field: QueryField;
120
121 fn context(&self, params: Option<Vec<Param>>) -> Context {
123 self.context_with::<Self::Query, Self::Field>(params)
124 }
125 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
127 where
128 Q: QueryField + ValueValidator,
129 F: QueryField;
130 fn handle<R>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
132 where
133 R: for<'de> Deserialize<'de>,
134 {
135 match response {
136 | Ok(content) => match content {
137 | ResponseContent::Json(content) => parse_json(&content),
138 | ResponseContent::Xml(content) => parse_xml(&content),
139 | ResponseContent::Yaml(content) => parse_yaml(&content),
140 | ResponseContent::Raw(content) => {
141 let raw = TextResponse { content };
142 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
143 }
144 },
145 | Err(e) => Err(eyre!(e)),
146 }
147 }
148 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
151 where
152 R: for<'de> Deserialize<'de>,
153 E: FallbackResponse;
154 async fn invoke(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>;
156 async fn invoke_with<Q, F>(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
158 where
159 Q: QueryField + ValueValidator,
160 F: QueryField;
161}
162pub trait ValueValidator {
164 fn is_valid(&self, _value: &str) -> bool {
166 true
167 }
168}
169#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
171pub enum AuthenticationScheme {
172 #[default]
174 Bearer,
175 Basic,
177 ApiKey,
179 OAuth2,
181 AwsSignatureV4,
183 GoogleCloud,
185 Custom(String),
187}
188#[derive(Clone, Debug, Default, Deserialize, EnumIs, Serialize)]
190pub enum ParamStyle {
191 #[default]
193 QueryPair,
194 QueryField,
196 FieldList,
198 KeyValuePair,
200 Header,
202 Body,
204 TemplateValue,
206}
207#[derive(Clone, Debug, Deserialize, Serialize)]
209pub enum ResponseContent {
210 Json(String),
212 Raw(String),
214 Yaml(String),
216 Xml(String),
218}
219#[derive(Clone, Debug, Display, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
221#[serde(rename_all = "lowercase")]
222pub enum TreeEntryType {
223 #[display("tree")]
227 Tree,
228 #[display("blob")]
232 Blob,
233}
234pub struct Fallback<T>(PhantomData<T>);
242pub struct NoFallback;
244#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
246#[builder(start_fn = init)]
247pub struct Authentication {
248 pub token: Option<String>,
251 #[builder(default)]
253 pub scheme: AuthenticationScheme,
254}
255#[derive(Clone, Debug, Deserialize, Serialize)]
257#[serde(rename_all = "kebab-case")]
258pub struct EmptyField(String);
259#[skip_serializing_none]
263#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
264#[builder(start_fn = at, on(String, into))]
265pub struct Endpoint {
266 #[builder(start_fn)]
268 pub domain: String,
269 #[builder(default = String::new())]
271 pub name: String,
272 #[serde(default)]
274 pub scheme: Option<Scheme>,
275 pub port: Option<u16>,
277 pub authentication: Option<Authentication>,
279 pub root: Option<String>,
283 #[builder(default = vec![])]
285 pub resources: Vec<Resource>,
286}
287#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
289#[builder(start_fn = of_type, finish_fn = with_key, on(String, into))]
290pub struct Param {
291 #[builder(start_fn)]
293 pub style: ParamStyle,
294 #[builder(finish_fn)]
296 pub name: String,
297 #[builder(
299 default = vec![],
300 with = |vecs: Vec<Vec<Option<&str>>>| {
301 vecs
302 .into_iter()
303 .map(|vec| vec.into_iter().map(|opt| opt.map(str::to_string)).collect())
304 .collect()
305 }
306 )]
307 pub values: Vec<Vec<Option<String>>>,
308 #[builder(default = false)]
310 pub required: bool,
311}
312pub struct Params(Vec<Param>);
328#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
330#[builder(start_fn = init, on(String, into))]
331pub struct Resource {
332 pub name: String,
334 #[builder(with = |method: &str| HttpMethod::from(method))]
336 #[serde(default)]
337 pub method: HttpMethod,
338 pub template: String,
340}
341#[derive(Clone, Debug, Deserialize, Serialize)]
343pub struct TextResponse {
344 pub content: String,
346}
347impl Endpoint {
348 pub fn base(&self) -> String {
350 let Self { domain, root, .. } = self;
351 let scheme = self.scheme.as_ref().map_or("https".to_string(), |s| s.to_string());
352 let port = self.port.map_or(String::new(), |port| format!(":{port}"));
353 let root = root.as_ref().map_or(String::new(), |root| format!("/{root}"));
354 format!("{scheme}://{domain}{port}{root}")
355 }
356 pub fn with_domain(&self, domain: impl Into<String>) -> Self {
358 Self {
359 domain: domain.into(),
360 ..self.clone()
361 }
362 }
363 pub fn from_template(name: impl Into<String>) -> ApiResult<Self> {
370 let endpoint_name = name.into();
371 INCLUDED_ENDPOINTS
372 .find_by_name(&endpoint_name)
373 .ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration"))
374 }
375}
376impl Searchable<Endpoint> for Vec<Endpoint> {
377 fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> {
378 let name = value.into();
379 self.iter().find(|endpoint| endpoint.name.eq_ignore_ascii_case(&name)).cloned()
380 }
381}
382impl FallbackResponse for NoFallback {
383 fn into_error(_: &str) -> Option<eyre::Report> {
384 None
385 }
386 fn to_string(_: &str) -> Option<String> {
387 None
388 }
389}
390impl<T> FallbackResponse for Fallback<T>
391where
392 T: for<'de> Deserialize<'de> + fmt::Debug,
393{
394 fn into_error(content: &str) -> Option<eyre::Report> {
395 serde_json::from_str::<T>(content).ok().map(|why| {
396 let message = Self::to_string(content).unwrap_or_else(|| format!("{why:#?}"));
397 eyre!("{message}")
398 })
399 }
400}
401impl Searchable<Resource> for Vec<Resource> {
402 fn find_by_name(&self, value: impl Into<String>) -> Option<Resource> {
403 let name = value.into();
404 self.iter().find(|resource| resource.name.eq_ignore_ascii_case(&name)).cloned()
405 }
406}
407impl<T> QueryField for T where T: fmt::Display + for<'a> TryFrom<&'a str> {}
409impl fmt::Display for AuthenticationScheme {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411 write!(
412 f,
413 "{}",
414 match self {
415 | AuthenticationScheme::Bearer => "Bearer",
416 | AuthenticationScheme::Basic => "Basic",
417 | AuthenticationScheme::ApiKey => "ApiKey",
418 | AuthenticationScheme::OAuth2 => "OAuth2",
419 | AuthenticationScheme::AwsSignatureV4 => "AWS Signature V4",
420 | AuthenticationScheme::GoogleCloud => "Google Cloud",
421 | AuthenticationScheme::Custom(scheme) => scheme,
422 }
423 )
424 }
425}
426impl fmt::Display for EmptyField {
427 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 write!(f, "{}", self.0)
429 }
430}
431impl fmt::Display for TextResponse {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 write!(f, "{}", self.content)
434 }
435}
436impl Default for Endpoint {
437 fn default() -> Self {
438 Endpoint::at("https://example.com").build()
439 }
440}
441impl<'a> From<Uri<&'a str>> for Endpoint {
442 fn from(value: Uri<&'a str>) -> Self {
443 let domain: String = value
444 .authority()
445 .map(|auth| format!("{}://{}", value.scheme().as_str(), auth.host()))
446 .unwrap_or_default();
447 let port: Option<u16> = value.authority().and_then(|auth| auth.port_to_u16().ok()).flatten();
448 Endpoint::at(domain).maybe_port(port).build()
449 }
450}
451impl From<Location> for Endpoint {
452 fn from(value: Location) -> Self {
453 let domain = value.host().map(|h| format!("{}://{}", value.scheme(), h)).unwrap_or_default();
454 let port = value.port();
455 Endpoint::at(domain).maybe_port(port).build()
456 }
457}
458impl From<Repository> for Endpoint {
459 fn from(value: Repository) -> Self {
460 value.location().into()
461 }
462}
463impl fmt::Display for HttpMethod {
464 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465 write!(
466 f,
467 "{}",
468 match self {
469 | HttpMethod::Get => "GET",
470 | HttpMethod::Post => "POST",
471 | HttpMethod::Put => "PUT",
472 | HttpMethod::Patch => "PATCH",
473 | HttpMethod::Delete => "DELETE",
474 }
475 )
476 }
477}
478impl Param {
479 pub fn is_query(&self) -> bool {
481 self.style.is_query_pair() | self.style.is_query_field() | self.style.is_field_list()
482 }
483 pub fn to_query_string<Q: QueryField + ValueValidator, F: QueryField>(params: Vec<Param>) -> String {
485 let query = params
486 .iter()
487 .filter(|param| param.is_query() || param.style.is_key_value_pair())
488 .map(|param| param.to_string::<Q, F>())
489 .filter(|s| !s.is_empty())
490 .collect::<Vec<String>>()
491 .join("&");
492 if !query.is_empty() {
493 format!("?{query}")
494 } else {
495 String::new()
496 }
497 }
498 pub fn from_query_pair(key: &str, pairs: Vec<(&str, &str)>) -> Self {
507 Param::of_type(ParamStyle::QueryPair)
508 .values(pairs.into_iter().map(|(k, v)| vec![Some(k), Some(v)]).collect())
509 .with_key(key)
510 }
511 pub fn from_field_list(key: &str, fields: Vec<&str>) -> Self {
513 Param::of_type(ParamStyle::FieldList)
514 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
515 .with_key(key)
516 }
517 pub fn from_query_field(key: &str, fields: Vec<&str>) -> Self {
519 Param::of_type(ParamStyle::QueryField)
520 .values(fields.into_iter().map(|f| vec![Some(f)]).collect())
521 .with_key(key)
522 }
523
524 pub fn to_string<Q: QueryField + ValueValidator, F: QueryField>(&self) -> String {
528 let key = self.name.as_str();
529 let rendered: Option<String> = match self.style {
530 | ParamStyle::QueryPair => {
531 let separator = "+AND+";
532 let pairs: Vec<(&str, &str)> = self
533 .values
534 .iter()
535 .filter_map(
536 |vec| match (vec.first().and_then(|o| o.as_deref()), vec.get(1).and_then(|o| o.as_deref())) {
537 | (Some(k), Some(v)) => Some((k, v)),
538 | _ => None,
539 },
540 )
541 .collect();
542 param_from_query_pairs::<Q>(key, separator, pairs)
543 }
544 | ParamStyle::QueryField => {
545 let separator = URL_ENCODED_SPACE;
546 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
547 param_from_query_fields::<Q>(key, separator, fields)
548 }
549 | ParamStyle::FieldList => {
550 let separator = ",";
551 let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
552 param_from_field_list::<F>(key, separator, fields)
553 }
554 | ParamStyle::KeyValuePair => {
555 let value = self
556 .values
557 .iter()
558 .filter_map(|vec| vec.first().and_then(|o| o.as_deref()))
559 .collect::<String>();
560 param_from_key_value_pair::<Q>(key, &value)
561 }
562 | _ => None,
563 };
564 rendered.unwrap_or_default()
565 }
566}
567impl Default for Params {
568 fn default() -> Self {
569 Self::new()
570 }
571}
572impl Params {
573 pub fn new() -> Self {
575 Self(Vec::new())
576 }
577 pub fn build(self) -> Vec<Param> {
579 self.0
580 }
581 pub fn with(self, param: Param) -> Self {
583 Self(self.0.into_iter().chain(once(param)).collect())
584 }
585 pub fn from_config(config: &impl Configuration) -> Self {
588 Self::new()
589 .with_auth(config.token(), None)
590 .with_template("identifier", config.identifier())
591 }
592 pub fn with_auth(self, token: &str, name: Option<&str>) -> Self {
599 let value = token.trim();
600 if !value.is_empty() {
601 let (header_name, header_value): (&str, String) = match name {
602 | None => ("Authorization", format!("Bearer {value}")),
603 | Some(name) => (name, value.to_string()),
604 };
605 self.with(param!(Header, header_name, header_value.as_str()))
606 } else {
607 self
608 }
609 }
610 pub fn with_template(self, key: &str, value: Option<&str>) -> Self {
612 match value {
613 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::TemplateValue, key, v)),
614 | _ => self,
615 }
616 }
617 pub fn with_keyvalue(self, key: &str, value: Option<&str>) -> Self {
619 match value {
620 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::KeyValuePair, key, v)),
621 | _ => self,
622 }
623 }
624 pub fn with_body(self, key: &str, value: &str) -> Self {
626 self.with(param!(ParamStyle::Body, key, value))
627 }
628 pub fn with_body_maybe(self, key: &str, value: Option<&str>) -> Self {
630 match value {
631 | Some(v) if !v.is_empty() => self.with(param!(ParamStyle::Body, key, v)),
632 | _ => self,
633 }
634 }
635 pub fn with_field(self, key: &str, value: &str) -> Self {
637 self.with(param!(ParamStyle::FieldList, key, value))
638 }
639 pub fn with_custom(self, custom: &[Param]) -> Self {
642 if custom.is_empty() {
643 self
644 } else {
645 Self(self.0.iter().chain(custom.iter()).cloned().collect())
646 }
647 }
648}
649impl IntoBody for Vec<Param> {
650 fn into_body(self) -> serde_json::Value {
651 let params: Vec<Param> = self.into_iter().filter(|Param { style, .. }| style.is_body()).collect();
652 match params.as_slice() {
653 | [Param { name, values, .. }] if name.is_empty() => {
654 let flattened: Vec<String> = values.iter().cloned().flat_map(|vec| vec.into_iter().flatten()).collect();
655 if flattened.len() == 1 {
656 let raw = flattened.into_iter().next().unwrap_or_default();
657 serde_json::from_str::<serde_json::Value>(&raw).unwrap_or(serde_json::Value::String(raw))
658 } else if flattened.is_empty() {
659 serde_json::Value::Null
660 } else {
661 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
662 }
663 }
664 | _ => {
665 let body = params
666 .into_iter()
667 .map(|param| {
668 let Param { name, values, .. } = param;
669 let flattened: Vec<String> = values.into_iter().flat_map(|vec| vec.into_iter().flatten()).collect();
670 let value = if flattened.len() == 1 {
671 #[allow(clippy::unwrap_used)]
672 serde_json::Value::String(flattened.into_iter().next().unwrap())
673 } else if flattened.is_empty() {
674 serde_json::Value::Null
675 } else {
676 serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
677 };
678 (name, value)
679 })
680 .collect();
681 serde_json::Value::Object(body)
682 }
683 }
684 }
685}
686impl IntoHeaders for Vec<Param> {
687 fn into_headers(self) -> HeaderMap {
688 let mut headers = HeaderMap::new();
689 self.into_iter().filter(|Param { style, .. }| style.is_header()).for_each(|param| {
690 let Param { name, values, .. } = param;
691 if let Ok(header_name) = name.parse::<HeaderName>() {
692 values.into_iter().for_each(|vec| {
693 vec.into_iter().for_each(|opt_value| {
694 if let Some(raw) = opt_value {
695 if let Ok(mut header_value) = HeaderValue::from_str(&raw) {
696 header_value.set_sensitive(true);
697 headers.append(header_name.clone(), header_value);
698 }
699 }
700 });
701 });
702 }
703 });
704 headers
705 }
706}
707#[async_trait]
708impl RemoteResource for Endpoint {
709 type Query = EmptyField;
710 type Field = EmptyField;
711
712 fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
713 where
714 Q: QueryField + ValueValidator,
715 F: QueryField,
716 {
717 let mut context = Context::new();
718 match data {
719 | Some(params) => {
720 let (query_params, other_params): (Vec<Param>, Vec<Param>) =
721 params.into_iter().partition(|param| param.is_query() || param.style.is_key_value_pair());
722 let query = Param::to_query_string::<Q, F>(query_params);
723 context.insert("query", &query);
724 other_params.into_iter().for_each(|Param { name, style, values, .. }| {
725 if style.is_template_value() {
726 values.into_iter().for_each(|vec| {
727 vec.into_iter().flatten().for_each(|value| {
728 let key = name.clone();
729 context.insert(&key, &value.clone());
730 });
731 });
732 }
733 });
734 }
735 | None => (),
736 }
737 context.insert("base", &self.base());
738 context
739 }
740 fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
741 where
742 R: for<'de> Deserialize<'de>,
743 E: FallbackResponse,
744 {
745 match response {
746 | Ok(content) => {
747 let raw_text = match &content {
748 | ResponseContent::Json(s) | ResponseContent::Xml(s) | ResponseContent::Yaml(s) | ResponseContent::Raw(s) => s.clone(),
749 };
750 let result: ApiResult<R> = match content {
751 | ResponseContent::Json(s) => parse_json(&s),
752 | ResponseContent::Xml(s) => parse_xml(&s),
753 | ResponseContent::Yaml(s) => parse_yaml(&s),
754 | ResponseContent::Raw(s) => {
755 let raw = TextResponse { content: s };
756 serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
757 }
758 };
759 result.map_err(|err| E::into_error(&raw_text).unwrap_or(err))
760 }
761 | Err(why) => Err(eyre!(why)),
762 }
763 }
764 async fn invoke(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent> {
778 self.invoke_with::<Self::Query, Self::Field>(name, data).await
779 }
780 async fn invoke_with<Q, F>(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
805 where
806 Q: QueryField + ValueValidator,
807 F: QueryField,
808 {
809 let Self { resources, .. } = self;
810 let mut context = self.context_with::<Q, F>(data.clone());
811 let resource = resources.find_by_name(name);
812 match resource {
813 | Some(Resource { method, template, .. }) => {
814 let path = render(&template, &mut context);
815 let params = data.unwrap_or_default();
816 let headers = params.clone().into_headers();
817 let body = params.into_body();
818 let request = match method {
819 | HttpMethod::Delete => delete(path),
820 | HttpMethod::Get => get(path),
821 | HttpMethod::Patch => patch(path).json(&body),
822 | HttpMethod::Post => post(path).json(&body),
823 | HttpMethod::Put => put(path).json(&body),
824 };
825 warn!("=> {} {}", Label::run(), request.cyan());
826 match request.headers(headers).send().await {
827 | Ok(response) => match response.text().await {
828 | Ok(text) => {
829 trace!("=> {} Response {text}", Label::using());
830 let content = if detect_json(&text) {
831 ResponseContent::Json(text)
832 } else if detect_xml(&text) {
833 ResponseContent::Xml(text)
834 } else {
835 ResponseContent::Raw(text)
836 };
837 Ok(content)
838 }
839 | Err(why) => Err(eyre!(why)),
840 },
841 | Err(why) => Err(eyre!(why)),
842 }
843 }
844 | None => Err(eyre!("Resource not found")),
845 }
846 }
847}
848impl TryFrom<&str> for EmptyField {
849 type Error = String;
850
851 fn try_from(value: &str) -> eyre::Result<Self, Self::Error> {
852 Ok(EmptyField(value.to_string()))
853 }
854}
855impl ValueValidator for EmptyField {
856 fn is_valid(&self, _value: &str) -> bool {
857 true
858 }
859}
860
861pub(crate) fn extract_template_keys(template: &str) -> Vec<String> {
862 fn extract_key(expression: &str) -> Option<String> {
863 let trimmed = expression.trim().trim_matches('-');
864 trimmed
865 .split('|')
866 .next()
867 .map(str::trim)
868 .and_then(|base| base.split_whitespace().next().map(str::trim))
869 .and_then(|key| (!key.is_empty()).then(|| key.to_string()))
870 }
871 let mut seen = HashSet::new();
872 template
873 .split("{{")
874 .skip(1)
875 .filter_map(|segment| segment.split_once("}}").map(|(before, _)| before))
876 .filter_map(extract_key)
877 .filter(|key| seen.insert(key.clone()))
878 .collect()
879}
880pub(crate) fn param_from_key_value_pair<T: QueryField + ValueValidator>(key: &str, value: &str) -> Option<String> {
882 match T::try_from(key) {
883 | Ok(field) => {
884 if field.is_valid(value) {
885 Some(format!("{}={}", field, urlencoding::encode(value)))
886 } else {
887 warn!("=> {} Invalid key value ({}{})", Label::using(), format!("{key}=").dimmed(), value.red());
888 None
889 }
890 }
891 | Err(_) => {
892 warn!("=> {} Invalid key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
893 None
894 }
895 }
896}
897pub(crate) fn param_from_query_pairs<T: QueryField + ValueValidator>(key: &str, separator: &str, pairs: Vec<(&str, &str)>) -> Option<String> {
899 let values: Vec<String> = pairs
900 .into_iter()
901 .filter_map(|(k, v)| {
902 let key: &str = k;
903 let value: &str = v.trim();
904 match T::try_from(key) {
905 | Ok(field) => {
906 if field.is_valid(value) {
907 Some(format!("{}:{}", field, urlencoding::encode(value)))
908 } else {
909 warn!(
910 "=> {} Invalid query value ({}{})",
911 Label::using(),
912 format!("{key}=").dimmed(),
913 value.red()
914 );
915 None
916 }
917 }
918 | Err(_) => {
919 warn!("=> {} Invalid query key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
920 None
921 }
922 }
923 })
924 .collect();
925 if values.is_empty() {
926 None
927 } else {
928 Some(format!("{}={}", key, values.join(separator)))
929 }
930}
931pub(crate) fn param_from_field_list<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
933 let values: Vec<String> = fields
934 .into_iter()
935 .filter_map(|value: &str| {
936 let val = value;
937 match T::try_from(val) {
938 | Ok(column) => Some(column.to_string()),
939 | Err(_) => None,
940 }
941 })
942 .collect();
943 if values.is_empty() {
944 None
945 } else {
946 Some(format!("{key}={}", values.join(separator)))
947 }
948}
949pub(crate) fn param_from_query_fields<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
951 let valid_fields: Vec<T> = fields.into_iter().filter_map(|value| T::try_from(value).ok()).collect();
952 if valid_fields.is_empty() {
953 None
954 } else {
955 let count = valid_fields.len();
956 Some(format!(
957 "{}={}",
958 key,
959 valid_fields
960 .into_iter()
961 .enumerate()
962 .map(|(i, field)| format!("{}{URL_ENCODED_CARAT}{}.0", field, count.saturating_add(1).saturating_sub(i)))
963 .collect::<Vec<String>>()
964 .join(separator),
965 ))
966 }
967}
968pub(crate) fn parse_json<R>(content: &str) -> ApiResult<R>
969where
970 R: for<'de> Deserialize<'de>,
971{
972 match serde_json::from_str::<R>(content) {
973 | Ok(response) => Ok(response),
974 | Err(why) => Err(eyre!(why)),
975 }
976}
977pub(crate) fn parse_xml<R>(content: &str) -> ApiResult<R>
978where
979 R: for<'de> Deserialize<'de>,
980{
981 match quick_xml::de::from_str::<R>(content) {
982 | Ok(response) => Ok(response),
983 | Err(why) => Err(eyre!(why)),
984 }
985}
986pub(crate) fn parse_yaml<R>(content: &str) -> ApiResult<R>
987where
988 R: for<'de> Deserialize<'de>,
989{
990 match serde_norway::from_str::<R>(content) {
991 | Ok(response) => Ok(response),
992 | Err(why) => Err(eyre!(why)),
993 }
994}
995pub(crate) fn query_string<Q: QueryField + ValueValidator, F: QueryField>(
1005 query_pairs: Vec<(&str, &str)>,
1006 field_list: Vec<&str>,
1007 query_fields: Vec<&str>,
1008) -> String {
1009 let params = vec![
1010 Param::from_query_pair("q", query_pairs),
1011 Param::from_field_list("fl", field_list),
1012 Param::from_query_field("qf", query_fields),
1013 ];
1014 Param::to_query_string::<Q, F>(params)
1015}
1016pub(crate) fn render(template: &str, context: &mut Context) -> String {
1017 let mut tera = Tera::default();
1018 let keys = extract_template_keys(template);
1019 keys.into_iter().for_each(|key| {
1020 if !context.contains_key(&key) {
1021 context.insert(&key, "");
1022 }
1023 });
1024 tera.render_str(template, context).unwrap_or_default()
1025}
1026pub(crate) fn require_non_empty_secret(secret: &str, path: &str, names: &[&str]) -> ApiResult<String> {
1030 let value = secret.trim();
1031 if value.is_empty() {
1032 let env_list = names.join(", ");
1033 Err(eyre!("Missing required token for {path} request. Set one of: {env_list}"))
1034 } else {
1035 Ok(value.to_string())
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests;