1use crate::io::{network_get_request, network_post_request, network_put_request};
6use crate::prelude::io::ErrorKind;
7use crate::prelude::Error;
8use crate::util::constants::{URL_ENCODED_CARAT, URL_ENCODED_SPACE};
9use crate::util::{detect_json, detect_xml};
10use crate::{Location, Repository, Scheme};
11use bon::Builder;
12use core::fmt;
13use derive_more::Display;
14use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
15use serde::{Deserialize, Serialize};
16use serde_with::skip_serializing_none;
17use tera::{Context, Tera};
18use uriparse::URI;
19use validator::Validate;
20
21pub mod citeas;
22pub mod github;
23pub mod gitlab;
24pub mod orcid;
25pub mod ror;
26pub mod spdx;
27
28pub trait EndpointSearch {
30 fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint>;
32}
33pub trait IntoHeaders {
35 fn into_headers(self) -> HeaderMap;
37}
38pub trait QueryField: fmt::Display + for<'a> TryFrom<&'a str> {}
40pub trait RestfulInterface {
43 type Query: QueryField + ValueValidator;
45 type Field: QueryField;
47
48 fn context(&self, _params: Option<Vec<Param>>) -> Context {
50 Context::new()
51 }
52 fn handle<R>(&self, response: Result<ResponseContent, Error>) -> Result<R, String>
54 where
55 R: for<'de> Deserialize<'de>;
56 fn invoke_sync(&self, action: impl Into<String> + Clone, data: Option<Vec<Param>>) -> Result<ResponseContent, Error>;
58 fn invoke_sync_with<Q, F>(&self, action: impl Into<String> + Clone, data: Option<Vec<Param>>) -> Result<ResponseContent, Error>
60 where
61 Q: QueryField + ValueValidator,
62 F: QueryField;
63 fn parse_json<R>(&self, content: &str) -> Result<R, String>
65 where
66 R: for<'de> Deserialize<'de>;
67 fn parse_xml<R>(&self, content: &str) -> Result<R, String>
69 where
70 R: for<'de> Deserialize<'de>;
71}
72pub trait ValueValidator {
74 fn is_valid(&self, _value: &str) -> bool {
76 true
77 }
78}
79#[derive(Clone, Debug, Default, Deserialize, Serialize)]
81pub enum AuthenticationScheme {
82 #[default]
84 Bearer,
85 Basic,
87 ApiKey,
89 OAuth2,
91 Custom(String),
93}
94#[derive(Clone, Debug, Default, Deserialize, Serialize)]
96#[serde(rename_all = "lowercase")]
97pub enum HttpMethod {
98 #[default]
100 Get,
101 Post,
103 Put,
105 Patch,
107 Delete,
109}
110#[derive(Clone, Debug, Default, Deserialize, Serialize)]
112pub enum ParamStyle {
113 #[default]
115 QueryPair,
116 QueryField,
118 FieldList,
120 Header,
122 Body,
124 TemplateValue,
126}
127#[derive(Clone, Debug, Deserialize, Serialize)]
129pub enum ResponseContent {
130 Json(String),
132 Raw(String),
134 Xml(String),
136}
137#[derive(Clone, Debug, Display, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
139#[serde(rename_all = "lowercase")]
140pub enum TreeEntryType {
141 #[display("tree")]
145 Tree,
146 #[display("blob")]
150 Blob,
151}
152#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
154#[builder(start_fn = init)]
155pub struct Authentication {
156 pub token: Option<String>,
159 #[builder(default)]
161 pub scheme: AuthenticationScheme,
162}
163#[derive(Clone, Debug, Deserialize, Serialize)]
165#[serde(rename_all = "kebab-case")]
166pub struct EmptyField(String);
167#[skip_serializing_none]
171#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
172#[builder(start_fn = at, on(String, into))]
173pub struct Endpoint {
174 #[builder(start_fn)]
176 pub domain: String,
177 #[builder(default = String::new())]
179 pub name: String,
180 #[serde(default)]
182 pub scheme: Option<Scheme>,
183 pub port: Option<u16>,
185 pub authentication: Option<Authentication>,
187 pub root: Option<String>,
191 #[builder(default = vec![])]
193 pub resources: Vec<Resource>,
194}
195#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
197#[builder(start_fn = of_type, finish_fn = with_key, on(String, into))]
198pub struct Param {
199 #[builder(start_fn)]
201 pub style: ParamStyle,
202 #[builder(finish_fn)]
204 pub name: String,
205 #[builder(
207 default = vec![],
208 with = |pairs: Vec<(Option<&str>, Option<&str>)>| {
209 pairs
210 .into_iter()
211 .map(|(k, v)| (k.map(str::to_string), v.map(str::to_string)))
212 .collect()
213 }
214 )]
215 pub values: Vec<(Option<String>, Option<String>)>,
216 #[builder(default = false)]
218 pub required: bool,
219}
220#[derive(Builder, Clone, Debug, Deserialize, Serialize, Validate)]
222#[builder(start_fn = init, on(String, into))]
223pub struct Resource {
224 pub name: String,
226 #[builder(with = |method: &str| HttpMethod::from(method))]
228 #[serde(default)]
229 pub method: HttpMethod,
230 pub template: String,
232}
233#[derive(Clone, Debug, Deserialize, Serialize)]
235pub struct TextResponse {
236 pub content: String,
238}
239impl Endpoint {
242 pub fn base(&self) -> String {
244 let Self { domain, root, .. } = self;
245 let scheme = self.scheme.as_ref().map_or("https".to_string(), |s| s.to_string());
246 let port = self.port.map_or(String::new(), |port| format!(":{port}"));
247 let root = root.as_ref().map_or(String::new(), |root| format!("/{root}"));
248 format!("{scheme}://{domain}{port}{root}")
249 }
250
251 pub fn context_with<Q, F>(&self, data: Option<Vec<Param>>) -> Context
253 where
254 Q: QueryField + ValueValidator,
255 F: QueryField,
256 {
257 let mut context = Context::new();
258 match data {
259 | Some(params) => {
260 let (query_params, other_params): (Vec<Param>, Vec<Param>) = params.into_iter().partition(|param| param.is_query());
261 let query = Param::to_query_string::<Q, F>(query_params);
262 context.insert("query", &query);
263 other_params.into_iter().for_each(|param| {
264 if param.is_template() {
265 param.values.into_iter().filter_map(|(k, v)| k.or(v)).for_each(|value| {
266 let key = param.name.clone();
267 context.insert(&key, &value.clone());
268 });
269 }
270 });
271 dbg!(&context);
273 }
274 | None => (),
275 }
276 context.insert("base", &self.base());
277 context
278 }
279}
280impl EndpointSearch for Vec<Endpoint> {
281 fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> {
282 let name = value.into();
283 self.iter().find(|endpoint| endpoint.name == name).cloned()
284 }
285}
286impl<T> QueryField for T where T: fmt::Display + for<'a> TryFrom<&'a str> {}
288impl fmt::Display for AuthenticationScheme {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(
291 f,
292 "{}",
293 match self {
294 | AuthenticationScheme::Bearer => "Bearer",
295 | AuthenticationScheme::Basic => "Basic",
296 | AuthenticationScheme::ApiKey => "ApiKey",
297 | AuthenticationScheme::OAuth2 => "OAuth2",
298 | AuthenticationScheme::Custom(scheme) => scheme,
299 }
300 )
301 }
302}
303impl fmt::Display for EmptyField {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 write!(f, "{}", self.0)
306 }
307}
308impl Default for Endpoint {
309 fn default() -> Self {
310 Endpoint::at("https://example.com").build()
311 }
312}
313impl From<&str> for HttpMethod {
314 fn from(value: &str) -> Self {
315 match value.to_uppercase().as_str() {
316 | "GET" => HttpMethod::Get,
317 | "POST" => HttpMethod::Post,
318 | "PUT" => HttpMethod::Put,
319 | "PATCH" => HttpMethod::Patch,
320 | "DELETE" => HttpMethod::Delete,
321 | _ => HttpMethod::Get,
322 }
323 }
324}
325impl<'a> From<URI<'a>> for Endpoint {
326 fn from(value: URI<'a>) -> Self {
327 let domain: String = value.host().map(|h| format!("{}://{}", value.scheme(), h)).unwrap_or_default();
328 let port: Option<u16> = value.authority().and_then(|auth| auth.port());
329 Endpoint::at(domain).maybe_port(port).build()
330 }
331}
332impl From<Location> for Endpoint {
333 fn from(value: Location) -> Self {
334 match value.uri() {
335 | Some(uri) => {
336 let endpoint: Endpoint = uri.into();
337 endpoint
338 }
339 | None => Endpoint::default(),
340 }
341 }
342}
343impl From<Repository> for Endpoint {
344 fn from(value: Repository) -> Self {
345 value.location().into()
346 }
347}
348impl Param {
349 pub fn is_body(&self) -> bool {
351 matches!(self.style, ParamStyle::Body)
352 }
353 pub fn is_query(&self) -> bool {
355 matches!(self.style, ParamStyle::QueryPair | ParamStyle::QueryField | ParamStyle::FieldList)
356 }
357 pub fn is_header(&self) -> bool {
359 matches!(self.style, ParamStyle::Header)
360 }
361 pub fn is_template(&self) -> bool {
363 matches!(self.style, ParamStyle::TemplateValue)
364 }
365 pub fn to_query_string<Q: QueryField + ValueValidator, F: QueryField>(params: Vec<Param>) -> String {
367 let query = params
368 .iter()
369 .filter(|param| param.is_query())
370 .map(|param| param.to_string::<Q, F>())
371 .filter(|s| !s.is_empty())
372 .collect::<Vec<String>>()
373 .join("&");
374 if !query.is_empty() {
375 format!("?{query}")
376 } else {
377 String::new()
378 }
379 }
380 pub fn from_query_pair(key: &str, pairs: Vec<(&str, &str)>) -> Self {
389 Param::of_type(ParamStyle::QueryPair)
390 .values(pairs.into_iter().map(|(k, v)| (Some(k), Some(v))).collect())
391 .with_key(key)
392 }
393 pub fn from_field_list(key: &str, fields: Vec<&str>) -> Self {
395 Param::of_type(ParamStyle::FieldList)
396 .values(fields.into_iter().map(|f| (Some(f), None)).collect())
397 .with_key(key)
398 }
399 pub fn from_query_field(key: &str, fields: Vec<&str>) -> Self {
401 Param::of_type(ParamStyle::QueryField)
402 .values(fields.into_iter().map(|f| (Some(f), None)).collect())
403 .with_key(key)
404 }
405
406 pub fn to_string<Q: QueryField + ValueValidator, F: QueryField>(&self) -> String {
410 let key = self.name.as_str();
411 let rendered: Option<String> = match self.style {
412 | ParamStyle::QueryPair => {
413 let separator = "+AND+";
414 let pairs: Vec<(&str, &str)> = self
415 .values
416 .iter()
417 .filter_map(|(key, value)| match (key.as_deref(), value.as_deref()) {
418 | (Some(k), Some(v)) => Some((k, v)),
419 | _ => None,
420 })
421 .collect();
422 param_from_query_pairs::<Q>(key, separator, pairs)
423 }
424 | ParamStyle::QueryField => {
425 let separator = URL_ENCODED_SPACE;
426 let fields: Vec<&str> = self
427 .values
428 .iter()
429 .filter_map(|(key, value)| key.as_deref().or(value.as_deref()))
430 .collect();
431 param_from_query_fields::<Q>(key, separator, fields)
432 }
433 | ParamStyle::FieldList => {
434 let separator = ",";
435 let fields: Vec<&str> = self
436 .values
437 .iter()
438 .filter_map(|(key, value)| key.as_deref().or(value.as_deref()))
439 .collect();
440 param_from_field_list::<F>(key, separator, fields)
441 }
442 | _ => None,
443 };
444 rendered.unwrap_or_default()
445 }
446}
447impl IntoHeaders for Vec<Param> {
448 fn into_headers(self) -> HeaderMap {
449 let mut headers = HeaderMap::new();
450 self.into_iter().filter(|param| param.is_header()).for_each(|param| {
451 let Param { name, values, .. } = param;
452 let name: HeaderName = match name.parse() {
453 | Ok(header_name) => header_name,
454 | Err(_) => return,
455 };
456 values.into_iter().for_each(|(_, value)| {
457 if let Some(raw) = value {
458 if let Ok(mut header_value) = HeaderValue::from_str(&raw) {
459 header_value.set_sensitive(true);
460 headers.append(name.clone(), header_value);
461 }
462 }
463 });
464 });
465 headers
466 }
467}
468impl RestfulInterface for Endpoint {
469 type Query = EmptyField;
470 type Field = EmptyField;
471
472 fn context(&self, data: Option<Vec<Param>>) -> Context {
473 self.context_with::<Self::Query, Self::Field>(data)
474 }
475 fn handle<R>(&self, response: Result<ResponseContent, Error>) -> Result<R, String>
476 where
477 R: for<'de> Deserialize<'de>,
478 {
479 match response {
480 | Ok(content) => match content {
481 | ResponseContent::Json(content) => self.parse_json(&content),
482 | ResponseContent::Xml(content) => self.parse_xml(&content),
483 | ResponseContent::Raw(content) => {
484 let raw = TextResponse { content };
485 serde_json::to_string(&raw)
486 .map_err(|e| e.to_string())
487 .and_then(|json| self.parse_json(&json))
488 }
489 },
490 | Err(e) => Err(e.to_string()),
491 }
492 }
493 fn invoke_sync(&self, name: impl Into<String> + Clone, data: Option<Vec<Param>>) -> Result<ResponseContent, Error> {
507 self.invoke_sync_with::<Self::Query, Self::Field>(name, data)
508 }
509 fn invoke_sync_with<Q, F>(&self, name: impl Into<String> + Clone, data: Option<Vec<Param>>) -> Result<ResponseContent, Error>
534 where
535 Q: QueryField + ValueValidator,
536 F: QueryField,
537 {
538 let Self { resources, .. } = self;
539 let mut tera = Tera::default();
540 let context = self.context_with::<Q, F>(data.clone());
541 let resource = resources.iter().find(|resource| resource.name == name.clone().into());
542 match resource {
543 | Some(Resource { method, template, .. }) => {
544 let path = tera.render_str(template, &context).unwrap_or_default();
545 let request = match method {
546 | HttpMethod::Get => network_get_request(path),
547 | HttpMethod::Post => network_post_request(path),
549 | HttpMethod::Put => network_put_request(path),
550 | _ => unimplemented!("Only GET, POST, and PUT methods are currently implemented"),
551 };
552 let headers = data.unwrap_or_default().into_headers();
553 match request.headers(headers).send() {
554 | Ok(response) => match response.text() {
555 | Ok(text) => {
556 let content = if detect_json(&text) {
557 ResponseContent::Json(text)
558 } else if detect_xml(&text) {
559 ResponseContent::Xml(text)
560 } else {
561 ResponseContent::Raw(text)
562 };
563 Ok(content)
564 }
565 | Err(why) => Err(Error::other(why.to_string())),
566 },
567 | Err(why) => Err(Error::other(why.to_string())),
568 }
569 }
570 | None => Err(Error::new(ErrorKind::NotFound, "Resource not found")),
571 }
572 }
573 fn parse_json<R>(&self, content: &str) -> Result<R, String>
574 where
575 R: for<'de> Deserialize<'de>,
576 {
577 match serde_json::from_str::<R>(content) {
578 | Ok(response) => Ok(response),
579 | Err(why) => Err(why.to_string()),
580 }
581 }
582 fn parse_xml<R>(&self, content: &str) -> Result<R, String>
583 where
584 R: for<'de> Deserialize<'de>,
585 {
586 match quick_xml::de::from_str::<R>(content) {
587 | Ok(response) => Ok(response),
588 | Err(why) => Err(why.to_string()),
589 }
590 }
591}
592impl TryFrom<&str> for EmptyField {
593 type Error = String;
594
595 fn try_from(value: &str) -> Result<Self, Self::Error> {
596 Ok(EmptyField(value.to_string()))
597 }
598}
599impl ValueValidator for EmptyField {
600 fn is_valid(&self, _value: &str) -> bool {
601 true
602 }
603}
604pub fn param_from_query_pairs<T: QueryField + ValueValidator>(key: &str, separator: &str, pairs: Vec<(&str, &str)>) -> Option<String> {
606 let values: Vec<String> = pairs
607 .into_iter()
608 .filter_map(|(k, v)| {
609 let key: &str = k;
610 let value: &str = v.trim();
611 match T::try_from(key) {
612 | Ok(field) => {
613 if field.is_valid(value) {
614 Some(format!("{}:{}", field, urlencoding::encode(value)))
615 } else {
616 None
617 }
618 }
619 | Err(_) => None,
620 }
621 })
622 .collect();
623 if values.is_empty() {
624 None
625 } else {
626 Some(format!("{}={}", key, values.join(separator)))
627 }
628}
629pub fn param_from_field_list<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
631 let values: Vec<String> = fields
632 .into_iter()
633 .filter_map(|value: &str| {
634 let val = value;
635 match T::try_from(val) {
636 | Ok(column) => Some(column.to_string()),
637 | Err(_) => None,
638 }
639 })
640 .collect();
641 if values.is_empty() {
642 None
643 } else {
644 Some(format!("{key}={}", values.join(separator)))
645 }
646}
647pub fn param_from_query_fields<T: QueryField>(key: &str, separator: &str, fields: Vec<&str>) -> Option<String> {
649 let valid_fields: Vec<T> = fields.into_iter().filter_map(|value| T::try_from(value).ok()).collect();
650 if valid_fields.is_empty() {
651 None
652 } else {
653 let count = valid_fields.len();
654 Some(format!(
655 "{}={}",
656 key,
657 valid_fields
658 .into_iter()
659 .enumerate()
660 .map(|(i, field)| format!("{}{URL_ENCODED_CARAT}{}.0", field, count + 1 - i))
661 .collect::<Vec<String>>()
662 .join(separator),
663 ))
664 }
665}
666pub fn parse<R>(content: &str) -> Result<R, String>
668where
669 R: for<'de> Deserialize<'de>,
670{
671 match quick_xml::de::from_str::<R>(content) {
672 | Ok(response) => Ok(response),
673 | Err(e) => Err(format!("Failed to parse ORCiD search response: {e}")),
674 }
675}
676pub fn query_string<Q: QueryField + ValueValidator, F: QueryField>(
687 query_pairs: Vec<(&str, &str)>,
688 field_list: Vec<&str>,
689 query_fields: Vec<&str>,
690) -> String {
691 let params = vec![
692 Param::from_query_pair("q", query_pairs),
693 Param::from_field_list("fl", field_list),
694 Param::from_query_field("qf", query_fields),
695 ];
696 Param::to_query_string::<Q, F>(params)
697}
698
699#[cfg(test)]
700mod tests;