use crate::io::config::ApplicationConfiguration;
use crate::io::database::{schema::Table, Database};
use crate::io::http::HttpMethod;
use crate::io::http::{delete, get, patch, post, put};
use crate::io::ApiResult;
use crate::param;
use crate::prelude::HashSet;
use crate::util::constants::{URL_ENCODED_CARAT, URL_ENCODED_SPACE};
use crate::util::{detect_json, detect_xml, Constant, Label, Searchable};
use crate::{Location, Repository, Scheme};
use async_trait::async_trait;
use axum::http::header::{HeaderName, HeaderValue};
use axum::http::HeaderMap;
use bon::Builder;
use color_eyre::eyre::{self, eyre};
use core::iter::once;
use core::{fmt, marker::PhantomData};
use derive_more::Display;
use fluent_uri::Uri;
use lazy_static::lazy_static;
use owo_colors::OwoColorize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use strum::EnumIs;
use tera::{Context, Tera};
use tracing::{trace, warn};
use validator::Validate;
pub mod citeas;
pub mod geonames;
pub mod github;
pub mod gitlab;
pub mod models_dev;
pub mod openai;
pub mod openapi;
pub mod orcid;
pub mod raid;
pub mod ror;
pub mod spdx;
lazy_static! {
pub static ref INCLUDED_ENDPOINTS: Vec<Endpoint> = Constant::json::<ApplicationConfiguration>("application").endpoints.unwrap_or_default();
}
pub trait Configuration {
fn from_env() -> Self;
fn with_body(self, value: impl Into<String>) -> Self;
fn with_domain(self, value: impl Into<String>) -> Self;
fn with_identifier(self, value: impl Into<String>) -> Self;
fn token(&self) -> &str;
fn domain(&self) -> &str;
fn identifier(&self) -> Option<&str>;
fn with_params(self, params: Vec<Param>) -> Self;
fn params(&self) -> &[Param];
}
#[async_trait]
pub trait DatabasePersistence {
async fn persist(self, database: Database<Table>) -> ApiResult<usize>;
}
pub trait IntoBody {
fn into_body(self) -> serde_json::Value;
}
pub trait IntoHeaders {
fn into_headers(self) -> HeaderMap;
}
pub trait QueryField: fmt::Display + for<'a> TryFrom<&'a str> {}
pub trait FallbackResponse {
fn into_error(content: &str) -> Option<eyre::Report>;
fn to_string(content: &str) -> Option<String> {
serde_json::from_str::<serde_json::Value>(content).ok().and_then(|value| match value {
| serde_json::Value::String(inner) => serde_json::from_str::<serde_json::Value>(&inner)
.ok()
.and_then(|nested| serde_json::to_string_pretty(&nested).ok())
.or_else(|| serde_json::to_string_pretty(&serde_json::Value::String(inner)).ok()),
| other => serde_json::to_string_pretty(&other).ok(),
})
}
}
#[async_trait]
pub trait RemoteResource {
type Query: QueryField + ValueValidator;
type Field: QueryField;
fn context(&self, params: Option<Vec<Param>>) -> Context {
self.context_with::<Self::Query, Self::Field>(params)
}
fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
where
Q: QueryField + ValueValidator,
F: QueryField;
fn handle<R>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
{
match response {
| Ok(content) => match content {
| ResponseContent::Json(content) => parse_json(&content),
| ResponseContent::Xml(content) => parse_xml(&content),
| ResponseContent::Yaml(content) => parse_yaml(&content),
| ResponseContent::Raw(content) => {
let raw = TextResponse { content };
serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
}
},
| Err(e) => Err(eyre!(e)),
}
}
fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
E: FallbackResponse;
async fn invoke(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>;
async fn invoke_with<Q, F>(&self, action: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
where
Q: QueryField + ValueValidator,
F: QueryField;
}
pub trait ValueValidator {
fn is_valid(&self, _value: &str) -> bool {
true
}
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
pub enum AuthenticationScheme {
#[default]
Bearer,
Basic,
ApiKey,
OAuth2,
AwsSignatureV4,
GoogleCloud,
Custom(String),
}
#[derive(Clone, Debug, Default, Deserialize, EnumIs, Serialize)]
pub enum ParamStyle {
#[default]
QueryPair,
QueryField,
FieldList,
KeyValuePair,
Header,
Body,
TemplateValue,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ResponseContent {
Json(String),
Raw(String),
Yaml(String),
Xml(String),
}
#[derive(Clone, Debug, Display, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
#[serde(rename_all = "lowercase")]
pub enum TreeEntryType {
#[display("tree")]
Tree,
#[display("blob")]
Blob,
}
pub struct Fallback<T>(PhantomData<T>);
pub struct NoFallback;
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
pub struct Authentication {
pub token: Option<String>,
#[builder(default)]
pub scheme: AuthenticationScheme,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct EmptyField(String);
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
#[builder(start_fn = at, on(String, into))]
pub struct Endpoint {
#[builder(start_fn)]
pub domain: String,
#[builder(default = String::new())]
pub name: String,
#[serde(default)]
pub scheme: Option<Scheme>,
pub port: Option<u16>,
pub authentication: Option<Authentication>,
pub root: Option<String>,
#[builder(default = vec![])]
pub resources: Vec<Resource>,
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = of_type, finish_fn = with_key, on(String, into))]
pub struct Param {
#[builder(start_fn)]
pub style: ParamStyle,
#[builder(finish_fn)]
pub name: String,
#[builder(
default = vec![],
with = |vecs: Vec<Vec<Option<&str>>>| {
vecs
.into_iter()
.map(|vec| vec.into_iter().map(|opt| opt.map(str::to_string)).collect())
.collect()
}
)]
pub values: Vec<Vec<Option<String>>>,
#[builder(default = false)]
pub required: bool,
}
pub struct Params(Vec<Param>);
#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
#[builder(start_fn = init, on(String, into))]
pub struct Resource {
pub name: String,
#[builder(with = |method: &str| HttpMethod::from(method))]
#[serde(default)]
pub method: HttpMethod,
pub template: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TextResponse {
pub content: String,
}
impl Endpoint {
pub fn base(&self) -> String {
let Self { domain, root, .. } = self;
let scheme = self.scheme.as_ref().map_or("https".to_string(), |s| s.to_string());
let port = self.port.map_or(String::new(), |port| format!(":{port}"));
let root = root.as_ref().map_or(String::new(), |root| format!("/{root}"));
format!("{scheme}://{domain}{port}{root}")
}
pub fn with_domain(&self, domain: impl Into<String>) -> Self {
Self {
domain: domain.into(),
..self.clone()
}
}
pub fn from_template(name: impl Into<String>) -> ApiResult<Self> {
let endpoint_name = name.into();
INCLUDED_ENDPOINTS
.find_by_name(&endpoint_name)
.ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration"))
}
}
impl Searchable<Endpoint> for Vec<Endpoint> {
fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> {
let name = value.into();
self.iter().find(|endpoint| endpoint.name.eq_ignore_ascii_case(&name)).cloned()
}
}
impl FallbackResponse for NoFallback {
fn into_error(_: &str) -> Option<eyre::Report> {
None
}
fn to_string(_: &str) -> Option<String> {
None
}
}
impl<T> FallbackResponse for Fallback<T>
where
T: for<'de> Deserialize<'de> + fmt::Debug,
{
fn into_error(content: &str) -> Option<eyre::Report> {
serde_json::from_str::<T>(content).ok().map(|why| {
let message = Self::to_string(content).unwrap_or_else(|| format!("{why:#?}"));
eyre!("{message}")
})
}
}
impl Searchable<Resource> for Vec<Resource> {
fn find_by_name(&self, value: impl Into<String>) -> Option<Resource> {
let name = value.into();
self.iter().find(|resource| resource.name.eq_ignore_ascii_case(&name)).cloned()
}
}
impl<T> QueryField for T where T: fmt::Display + for<'a> TryFrom<&'a str> {}
impl fmt::Display for AuthenticationScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
| AuthenticationScheme::Bearer => "Bearer",
| AuthenticationScheme::Basic => "Basic",
| AuthenticationScheme::ApiKey => "ApiKey",
| AuthenticationScheme::OAuth2 => "OAuth2",
| AuthenticationScheme::AwsSignatureV4 => "AWS Signature V4",
| AuthenticationScheme::GoogleCloud => "Google Cloud",
| AuthenticationScheme::Custom(scheme) => scheme,
}
)
}
}
impl fmt::Display for EmptyField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Display for TextResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.content)
}
}
impl Default for Endpoint {
fn default() -> Self {
Endpoint::at("https://example.com").build()
}
}
impl<'a> From<Uri<&'a str>> for Endpoint {
fn from(value: Uri<&'a str>) -> Self {
let domain: String = value
.authority()
.map(|auth| format!("{}://{}", value.scheme().as_str(), auth.host()))
.unwrap_or_default();
let port: Option<u16> = value.authority().and_then(|auth| auth.port_to_u16().ok()).flatten();
Endpoint::at(domain).maybe_port(port).build()
}
}
impl From<Location> for Endpoint {
fn from(value: Location) -> Self {
let domain = value.host().map(|h| format!("{}://{}", value.scheme(), h)).unwrap_or_default();
let port = value.port();
Endpoint::at(domain).maybe_port(port).build()
}
}
impl From<Repository> for Endpoint {
fn from(value: Repository) -> Self {
value.location().into()
}
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
| HttpMethod::Get => "GET",
| HttpMethod::Post => "POST",
| HttpMethod::Put => "PUT",
| HttpMethod::Patch => "PATCH",
| HttpMethod::Delete => "DELETE",
}
)
}
}
impl Param {
pub fn is_query(&self) -> bool {
self.style.is_query_pair() | self.style.is_query_field() | self.style.is_field_list()
}
pub fn to_query_string<Q: QueryField + ValueValidator, F: QueryField>(params: Vec<Param>) -> String {
let query = params
.iter()
.filter(|param| param.is_query() || param.style.is_key_value_pair())
.map(|param| param.to_string::<Q, F>())
.filter(|s| !s.is_empty())
.collect::<Vec<String>>()
.join("&");
if !query.is_empty() {
format!("?{query}")
} else {
String::new()
}
}
pub fn from_query_pair(key: &str, pairs: Vec<(&str, &str)>) -> Self {
Param::of_type(ParamStyle::QueryPair)
.values(pairs.into_iter().map(|(k, v)| vec![Some(k), Some(v)]).collect())
.with_key(key)
}
pub fn from_field_list(key: &str, fields: Vec<&str>) -> Self {
Param::of_type(ParamStyle::FieldList)
.values(fields.into_iter().map(|f| vec![Some(f)]).collect())
.with_key(key)
}
pub fn from_query_field(key: &str, fields: Vec<&str>) -> Self {
Param::of_type(ParamStyle::QueryField)
.values(fields.into_iter().map(|f| vec![Some(f)]).collect())
.with_key(key)
}
pub fn to_string<Q: QueryField + ValueValidator, F: QueryField>(&self) -> String {
let key = self.name.as_str();
let rendered: Option<String> = match self.style {
| ParamStyle::QueryPair => {
let separator = "+AND+";
let pairs: Vec<(&str, &str)> = self
.values
.iter()
.filter_map(
|vec| match (vec.first().and_then(|o| o.as_deref()), vec.get(1).and_then(|o| o.as_deref())) {
| (Some(k), Some(v)) => Some((k, v)),
| _ => None,
},
)
.collect();
param_from_query_pairs::<Q>(key, separator, pairs)
}
| ParamStyle::QueryField => {
let separator = URL_ENCODED_SPACE;
let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
param_from_query_fields::<Q>(key, separator, fields)
}
| ParamStyle::FieldList => {
let separator = ",";
let fields: Vec<&str> = self.values.iter().filter_map(|vec| vec.first().and_then(|o| o.as_deref())).collect();
param_from_field_list::<F>(key, separator, fields)
}
| ParamStyle::KeyValuePair => {
let value = self
.values
.iter()
.filter_map(|vec| vec.first().and_then(|o| o.as_deref()))
.collect::<String>();
param_from_key_value_pair::<Q>(key, &value)
}
| _ => None,
};
rendered.unwrap_or_default()
}
}
impl Default for Params {
fn default() -> Self {
Self::new()
}
}
impl Params {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn build(self) -> Vec<Param> {
self.0
}
pub fn with(self, param: Param) -> Self {
Self(self.0.into_iter().chain(once(param)).collect())
}
pub fn from_config(config: &impl Configuration) -> Self {
Self::new()
.with_auth(config.token(), None)
.with_template("identifier", config.identifier())
}
pub fn with_auth(self, token: &str, name: Option<&str>) -> Self {
let value = token.trim();
if !value.is_empty() {
let (header_name, header_value): (&str, String) = match name {
| None => ("Authorization", format!("Bearer {value}")),
| Some(name) => (name, value.to_string()),
};
self.with(param!(Header, header_name, header_value.as_str()))
} else {
self
}
}
pub fn with_template(self, key: &str, value: Option<&str>) -> Self {
match value {
| Some(v) if !v.is_empty() => self.with(param!(ParamStyle::TemplateValue, key, v)),
| _ => self,
}
}
pub fn with_keyvalue(self, key: &str, value: Option<&str>) -> Self {
match value {
| Some(v) if !v.is_empty() => self.with(param!(ParamStyle::KeyValuePair, key, v)),
| _ => self,
}
}
pub fn with_body(self, key: &str, value: &str) -> Self {
self.with(param!(ParamStyle::Body, key, value))
}
pub fn with_body_maybe(self, key: &str, value: Option<&str>) -> Self {
match value {
| Some(v) if !v.is_empty() => self.with(param!(ParamStyle::Body, key, v)),
| _ => self,
}
}
pub fn with_field(self, key: &str, value: &str) -> Self {
self.with(param!(ParamStyle::FieldList, key, value))
}
pub fn with_custom(self, custom: &[Param]) -> Self {
if custom.is_empty() {
self
} else {
Self(self.0.iter().chain(custom.iter()).cloned().collect())
}
}
}
impl IntoBody for Vec<Param> {
fn into_body(self) -> serde_json::Value {
let params: Vec<Param> = self.into_iter().filter(|Param { style, .. }| style.is_body()).collect();
match params.as_slice() {
| [Param { name, values, .. }] if name.is_empty() => {
let flattened: Vec<String> = values.iter().cloned().flat_map(|vec| vec.into_iter().flatten()).collect();
if flattened.len() == 1 {
let raw = flattened.into_iter().next().unwrap_or_default();
serde_json::from_str::<serde_json::Value>(&raw).unwrap_or(serde_json::Value::String(raw))
} else if flattened.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
}
}
| _ => {
let body = params
.into_iter()
.map(|param| {
let Param { name, values, .. } = param;
let flattened: Vec<String> = values.into_iter().flat_map(|vec| vec.into_iter().flatten()).collect();
let value = if flattened.len() == 1 {
#[allow(clippy::unwrap_used)]
serde_json::Value::String(flattened.into_iter().next().unwrap())
} else if flattened.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::Array(flattened.into_iter().map(serde_json::Value::String).collect())
};
(name, value)
})
.collect();
serde_json::Value::Object(body)
}
}
}
}
impl IntoHeaders for Vec<Param> {
fn into_headers(self) -> HeaderMap {
let mut headers = HeaderMap::new();
self.into_iter().filter(|Param { style, .. }| style.is_header()).for_each(|param| {
let Param { name, values, .. } = param;
if let Ok(header_name) = name.parse::<HeaderName>() {
values.into_iter().for_each(|vec| {
vec.into_iter().for_each(|opt_value| {
if let Some(raw) = opt_value {
if let Ok(mut header_value) = HeaderValue::from_str(&raw) {
header_value.set_sensitive(true);
headers.append(header_name.clone(), header_value);
}
}
});
});
}
});
headers
}
}
#[async_trait]
impl RemoteResource for Endpoint {
type Query = EmptyField;
type Field = EmptyField;
fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
where
Q: QueryField + ValueValidator,
F: QueryField,
{
let mut context = Context::new();
match data {
| Some(params) => {
let (query_params, other_params): (Vec<Param>, Vec<Param>) =
params.into_iter().partition(|param| param.is_query() || param.style.is_key_value_pair());
let query = Param::to_query_string::<Q, F>(query_params);
context.insert("query", &query);
other_params.into_iter().for_each(|Param { name, style, values, .. }| {
if style.is_template_value() {
values.into_iter().for_each(|vec| {
vec.into_iter().flatten().for_each(|value| {
let key = name.clone();
context.insert(&key, &value.clone());
});
});
}
});
}
| None => (),
}
context.insert("base", &self.base());
context
}
fn handle_or<R, E>(&self, response: ApiResult<ResponseContent>) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
E: FallbackResponse,
{
match response {
| Ok(content) => {
let raw_text = match &content {
| ResponseContent::Json(s) | ResponseContent::Xml(s) | ResponseContent::Yaml(s) | ResponseContent::Raw(s) => s.clone(),
};
let result: ApiResult<R> = match content {
| ResponseContent::Json(s) => parse_json(&s),
| ResponseContent::Xml(s) => parse_xml(&s),
| ResponseContent::Yaml(s) => parse_yaml(&s),
| ResponseContent::Raw(s) => {
let raw = TextResponse { content: s };
serde_json::to_string(&raw).map_err(|e| eyre!(e)).and_then(|json| parse_json(&json))
}
};
result.map_err(|err| E::into_error(&raw_text).unwrap_or(err))
}
| Err(why) => Err(eyre!(why)),
}
}
async fn invoke(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent> {
self.invoke_with::<Self::Query, Self::Field>(name, data).await
}
async fn invoke_with<Q, F>(&self, name: impl Into<String> + Clone + Send, data: Option<Vec<Param>>) -> ApiResult<ResponseContent>
where
Q: QueryField + ValueValidator,
F: QueryField,
{
let Self { resources, .. } = self;
let mut context = self.context_with::<Q, F>(data.clone());
let resource = resources.find_by_name(name);
match resource {
| Some(Resource { method, template, .. }) => {
let path = render(&template, &mut context);
let params = data.unwrap_or_default();
let headers = params.clone().into_headers();
let body = params.into_body();
let request = match method {
| HttpMethod::Delete => delete(path),
| HttpMethod::Get => get(path),
| HttpMethod::Patch => patch(path).json(&body),
| HttpMethod::Post => post(path).json(&body),
| HttpMethod::Put => put(path).json(&body),
};
warn!("=> {} {}", Label::run(), request.cyan());
match request.headers(headers).send().await {
| Ok(response) => match response.text().await {
| Ok(text) => {
trace!("=> {} Response {text}", Label::using());
let content = if detect_json(&text) {
ResponseContent::Json(text)
} else if detect_xml(&text) {
ResponseContent::Xml(text)
} else {
ResponseContent::Raw(text)
};
Ok(content)
}
| Err(why) => Err(eyre!(why)),
},
| Err(why) => Err(eyre!(why)),
}
}
| None => Err(eyre!("Resource not found")),
}
}
}
impl TryFrom<&str> for EmptyField {
type Error = String;
fn try_from(value: &str) -> eyre::Result<Self, Self::Error> {
Ok(EmptyField(value.to_string()))
}
}
impl ValueValidator for EmptyField {
fn is_valid(&self, _value: &str) -> bool {
true
}
}
pub(crate) fn extract_template_keys(template: &str) -> Vec<String> {
fn extract_key(expression: &str) -> Option<String> {
let trimmed = expression.trim().trim_matches('-');
trimmed
.split('|')
.next()
.map(str::trim)
.and_then(|base| base.split_whitespace().next().map(str::trim))
.and_then(|key| (!key.is_empty()).then(|| key.to_string()))
}
let mut seen = HashSet::new();
template
.split("{{")
.skip(1)
.filter_map(|segment| segment.split_once("}}").map(|(before, _)| before))
.filter_map(extract_key)
.filter(|key| seen.insert(key.clone()))
.collect()
}
pub(crate) fn param_from_key_value_pair<T: QueryField + ValueValidator>(key: &str, value: &str) -> Option<String> {
match T::try_from(key) {
| Ok(field) => {
if field.is_valid(value) {
Some(format!("{}={}", field, urlencoding::encode(value)))
} else {
warn!("=> {} Invalid key value ({}{})", Label::using(), format!("{key}=").dimmed(), value.red());
None
}
}
| Err(_) => {
warn!("=> {} Invalid key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
None
}
}
}
pub(crate) fn param_from_query_pairs<T: QueryField + ValueValidator>(key: &str, separator: &str, pairs: Vec<(&str, &str)>) -> Option<String> {
let values: Vec<String> = pairs
.into_iter()
.filter_map(|(k, v)| {
let key: &str = k;
let value: &str = v.trim();
match T::try_from(key) {
| Ok(field) => {
if field.is_valid(value) {
Some(format!("{}:{}", field, urlencoding::encode(value)))
} else {
warn!(
"=> {} Invalid query value ({}{})",
Label::using(),
format!("{key}=").dimmed(),
value.red()
);
None
}
}
| Err(_) => {
warn!("=> {} Invalid query key ({}{})", Label::using(), key.red(), format!("={value}").dimmed());
None
}
}
})
.collect();
if values.is_empty() {
None
} else {
Some(format!("{}={}", key, values.join(separator)))
}
}
pub(crate) fn param_from_field_list<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
let values: Vec<String> = fields
.into_iter()
.filter_map(|value: &str| {
let val = value;
match T::try_from(val) {
| Ok(column) => Some(column.to_string()),
| Err(_) => None,
}
})
.collect();
if values.is_empty() {
None
} else {
Some(format!("{key}={}", values.join(separator)))
}
}
pub(crate) fn param_from_query_fields<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
let valid_fields: Vec<T> = fields.into_iter().filter_map(|value| T::try_from(value).ok()).collect();
if valid_fields.is_empty() {
None
} else {
let count = valid_fields.len();
Some(format!(
"{}={}",
key,
valid_fields
.into_iter()
.enumerate()
.map(|(i, field)| format!("{}{URL_ENCODED_CARAT}{}.0", field, count.saturating_add(1).saturating_sub(i)))
.collect::<Vec<String>>()
.join(separator),
))
}
}
pub(crate) fn parse_json<R>(content: &str) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
{
match serde_json::from_str::<R>(content) {
| Ok(response) => Ok(response),
| Err(why) => Err(eyre!(why)),
}
}
pub(crate) fn parse_xml<R>(content: &str) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
{
match quick_xml::de::from_str::<R>(content) {
| Ok(response) => Ok(response),
| Err(why) => Err(eyre!(why)),
}
}
pub(crate) fn parse_yaml<R>(content: &str) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
{
match serde_norway::from_str::<R>(content) {
| Ok(response) => Ok(response),
| Err(why) => Err(eyre!(why)),
}
}
pub(crate) fn query_string<Q: QueryField + ValueValidator, F: QueryField>(
query_pairs: Vec<(&str, &str)>,
field_list: Vec<&str>,
query_fields: Vec<&str>,
) -> String {
let params = vec![
Param::from_query_pair("q", query_pairs),
Param::from_field_list("fl", field_list),
Param::from_query_field("qf", query_fields),
];
Param::to_query_string::<Q, F>(params)
}
pub(crate) fn render(template: &str, context: &mut Context) -> String {
let mut tera = Tera::default();
let keys = extract_template_keys(template);
keys.into_iter().for_each(|key| {
if !context.contains_key(&key) {
context.insert(&key, "");
}
});
tera.render_str(template, context).unwrap_or_default()
}
pub(crate) fn require_non_empty_secret(secret: &str, path: &str, names: &[&str]) -> ApiResult<String> {
let value = secret.trim();
if value.is_empty() {
let env_list = names.join(", ");
Err(eyre!("Missing required token for {path} request. Set one of: {env_list}"))
} else {
Ok(value.to_string())
}
}
#[cfg(test)]
mod tests;