Skip to main content

mzdata/io/
proxi.rs

1use std::{
2    cmp::Ordering,
3    fmt::{self, Display},
4    marker::PhantomData,
5    str::FromStr,
6};
7
8use mzdata_spectrum::IsolationWindowBuilder;
9use num_traits::AsPrimitive;
10use serde::{de::SeqAccess, Deserialize, Deserializer, Serialize};
11
12use crate::{
13    curie,
14    io::usi::USI,
15    params::{ControlledVocabulary, Param, ParamCow, Value, CURIE},
16    prelude::*,
17    spectrum::{
18        ArrayType, BinaryArrayMap, BinaryDataArrayType, DataArray,
19        MultiLayerSpectrum, Precursor, ScanPolarity, SignalContinuity, SpectrumDescription,
20    },
21};
22
23/// The possible PROXI backends
24#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
25pub enum PROXIBackend {
26    PeptideAtlas,
27    MassIVE,
28    Pride,
29    Jpost,
30    ProteomeXchange,
31    /// A custom backend with the given PROXI url, the pattern `{USI}` will be replaced with the actual usi in building the final request.
32    ///
33    /// For example the custom url `http://yourdomain.rs/proxi/v0.1/spectra?usi={USI}&resultType=full` will map to `http://yourdomain.rs/proxi/v0.1/spectra?usi=mzspec:ID:FILE:scan:SCAN&resultType=full`.
34    Custom(String),
35}
36
37impl std::fmt::Display for PROXIBackend {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(
40            f,
41            "{}",
42            match self {
43                Self::PeptideAtlas => "PeptideAtlas",
44                Self::MassIVE => "MassIVE",
45                Self::Pride => "PRIDE",
46                Self::Jpost => "jPOST",
47                Self::ProteomeXchange => "ProteomeXchange",
48                Self::Custom(_) => "Custom",
49            }
50        )
51    }
52}
53
54impl PROXIBackend {
55    const ALL: &[Self] = &[
56        Self::ProteomeXchange,
57        Self::PeptideAtlas,
58        Self::MassIVE,
59        Self::Pride,
60        Self::Jpost,
61    ];
62
63    /// The PROXI server base url which needs concatenating of the USI at the end
64    fn proxi_url(&self, usi: &str) -> String {
65        match self {
66            Self::PeptideAtlas => format!("http://www.peptideatlas.org/api/proxi/v0.1/spectra?resultType=full&usi={usi}") ,
67            Self::MassIVE => format!("http://massive.ucsd.edu/ProteoSAFe/proxi/v0.1/spectra?resultType=full&usi={usi}"),
68            Self::Pride => format!("https://www.ebi.ac.uk/pride/proxi/archive/v0.1/spectra?resultType=full&usi={usi}"),
69            Self::Jpost => format!("https://repository.jpostdb.org/proxi/spectra?resultType=full&usi={usi}"),
70            Self::ProteomeXchange => format!("https://proteomecentral.proteomexchange.org/api/proxi/v0.1/spectra?resultType=full&usi={usi}"),
71            Self::Custom(url) => url.replace("{USI}", usi),
72        }
73    }
74}
75
76impl USI {
77    /// Download this USI using the [PROXI](https://github.com/HUPO-PSI/proxi-schemas/) API from
78    /// the given PROXI backend. If no PROXI backend is indicated it will try all known backends
79    /// sequentially and return the first successful spectrum. The result contains the backend that
80    /// ultimately succeeded as well as the list of returned spectra. This is a list because the
81    /// USI could contain the stem of the filename and the backend could have multiple formats for
82    /// that file in which case the data from each file format is returned.
83    ///
84    /// A [`reqwest::blocking::Client`] can be provided to create the requests, if no client is
85    /// provided a default client will be used. This is useful to reuse a client over multiple
86    /// requests for performance or if needed to use proxies to download the data.
87    ///
88    /// # Errors
89    /// It returns a [`PROXIError::IO`] when the network request or the parsing of the answer
90    /// failed. It returns [`PROXIError::Error`] if the backend returned an error. It returns
91    /// [`PROXIError::PeakUnavailable`] if all returned spectra have [`Status::PeakUnavailable`].
92    /// It returns [`PROXIError::NotFound`] if no backend gave any result (also no error) in the
93    /// case of the aggregate PROXI calling, this is a major error and should not occur readily.
94    ///
95    /// This function is only available with the feature `proxi`.
96    pub fn download_spectrum_blocking(
97        &self,
98        backend: Option<PROXIBackend>,
99        client: Option<reqwest::blocking::Client>,
100    ) -> Result<(PROXIBackend, Vec<PROXISpectrum>), PROXIError> {
101        backend.map_or_else(
102            || {
103                let client = client.unwrap_or_default();
104                let mut last_error = None;
105                PROXIBackend::ALL
106                    .iter()
107                    .find_map(|backend| {
108                        let response = client
109                                .get(backend.proxi_url(&self.to_string()))
110                                .send();
111                        match response {
112                            Ok(resp) => {
113                                match resp.text() {
114                                    Ok(text) => {
115                                        match serde_json::from_str(&text) {
116                                            Ok(val) => {
117                                                transform_response(backend.clone(), Ok(val)).map_err(|err| {
118                                                    last_error = Some(err);
119                                                }).ok()
120                                            },
121                                            Err(e) => {
122                                                log::error!("Error: {e} on backend {backend:?} while retrieving {self}");
123                                                log::trace!("PROXI response body: {text}");
124                                                None
125                                            }
126                                        }
127                                    },
128                                    Err(e) => {
129                                        transform_response(backend.clone(), Err(e)).map_err(|err| {
130                                            last_error = Some(err);
131                                        }).ok()
132                                    },
133                                }
134                            },
135                            Err(e) => {
136                                transform_response(backend.clone(), Err(e)).map_err(|err| {
137                                    last_error = Some(err);
138                                }).ok()
139                            },
140                        }
141                    })
142                    .ok_or(last_error.unwrap_or(PROXIError::NotFound))
143            },
144            |backend| {
145                transform_response(
146                    backend.clone(),
147                    reqwest::blocking::get(backend.proxi_url(&self.to_string()))
148                        .and_then(reqwest::blocking::Response::json),
149                )
150            },
151        )
152    }
153
154    /// Download this USI using the [PROXI](https://github.com/HUPO-PSI/proxi-schemas/) API from
155    /// the given PROXI backend. If no PROXI backend is indicated it will try all known backends
156    /// concurrently and return the first successful spectrum. The result contains the backend that
157    /// ultimately succeeded as well as the list of returned spectra. This is a list because the
158    /// USI could contain the stem of the filename and the backend could have multiple formats for
159    /// that file in which case the data from each file format is returned.
160    ///
161    /// A [`reqwest::Client`] can be provided to create the requests, if no client is
162    /// provided a default client will be used. This is useful to reuse a client over multiple
163    /// requests for performance or if needed to use proxies to download the data.
164    ///
165    /// # Errors
166    /// It returns a [`PROXIError::IO`] when the network request or the parsing of the answer
167    /// failed. It returns [`PROXIError::Error`] if the backend returned an error. It returns
168    /// [`PROXIError::PeakUnavailable`] if all returned spectra have [`Status::PeakUnavailable`].
169    /// It returns [`PROXIError::NotFound`] if no backend gave any result (also no error) in the
170    /// case of the aggregate PROXI calling, this is a major error and should not occur readily.
171    ///
172    /// This function is only available with the feature `proxi-async`.
173    #[cfg(feature = "proxi-async")]
174    pub async fn download_spectrum_async(
175        &self,
176        backend: Option<PROXIBackend>,
177        client: Option<reqwest::Client>,
178    ) -> Result<(PROXIBackend, Vec<PROXISpectrum>), PROXIError> {
179        async fn get_response(
180            client: &reqwest::Client,
181            backend: PROXIBackend,
182            usi: &str,
183        ) -> Result<(PROXIBackend, Vec<PROXISpectrum>), PROXIError> {
184            transform_response(
185                backend.clone(),
186                match client.get(backend.proxi_url(usi)).send().await {
187                    Ok(r) => r.json::<PROXIResponse>().await,
188                    Err(e) => Err(e),
189                },
190            )
191        }
192
193        let client = client.unwrap_or_default();
194        let usi = self.to_string();
195        if let Some(backend) = backend {
196            get_response(&client, backend, &usi).await
197        } else {
198            use futures::StreamExt;
199
200            let mut requests = futures::stream::FuturesUnordered::new();
201            let mut last_error = None;
202            for backend in PROXIBackend::ALL {
203                requests.push(get_response(&client, backend.clone(), &usi));
204            }
205
206            while let Some(res) = requests.next().await {
207                match res {
208                    Ok(s) => return Ok(s),
209                    Err(e) => last_error = Some(e),
210                }
211            }
212
213            Err(last_error.unwrap_or(PROXIError::NotFound))
214        }
215    }
216}
217
218fn transform_response(
219    backend: PROXIBackend,
220    response: Result<PROXIResponse, reqwest::Error>,
221) -> Result<(PROXIBackend, Vec<PROXISpectrum>), PROXIError> {
222    match response {
223        Ok(PROXIResponse::Spectra(s))
224            if s.iter()
225                .all(|s| s.status.map(|s| s == Status::Readable).unwrap_or(true)) =>
226        {
227            Ok((backend, s))
228        }
229        Ok(PROXIResponse::Spectra(s)) => Err(PROXIError::PeakUnavailable(backend, s)),
230        Ok(PROXIResponse::Error {
231            detail,
232            status,
233            title,
234            kind,
235        }) => Err(PROXIError::Error {
236            backend,
237            detail,
238            status,
239            title,
240            kind,
241        }),
242        Err(err) => Err(PROXIError::IO(backend, err)),
243    }
244}
245
246#[derive(Debug, Deserialize)]
247#[serde(untagged)]
248enum PROXIResponse {
249    Spectra(Vec<PROXISpectrum>),
250    Error {
251        #[serde(alias = "message", default)]
252        detail: String,
253        #[serde(alias = "code")]
254        status: usize,
255        title: PROXIErrorType,
256        #[serde(rename = "type", default)]
257        kind: String,
258    },
259}
260
261/// An error returned when accessing a PROXI server
262#[derive(Debug)]
263pub enum PROXIError {
264    /// An error during the network request or decoding of the JSON response
265    IO(PROXIBackend, reqwest::Error),
266    /// A returned error by the server
267    Error {
268        /// Which backend failed
269        backend: PROXIBackend,
270        /// The type of error
271        title: PROXIErrorType,
272        /// Detailed explanation on the error
273        detail: String,
274        /// HTTP status code
275        status: usize,
276        /// The error kind, often "about:blank"
277        kind: String,
278    },
279    /// When the server returns only spectra with [`Status::PeakUnavailable`].
280    PeakUnavailable(PROXIBackend, Vec<PROXISpectrum>),
281    /// An error when none of the aggregated backends returned a positive or negative result
282    NotFound,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub enum PROXIErrorType {
287    /// The dataset is not present on this PROXI backend, it could be present on a different backend or the identifier does not exist
288    #[serde(rename = "DatasetNotHere")]
289    DataSetNotHere,
290    /// The dataset is present, but the ms run is not, likely a mistake in the ms run name
291    #[serde(rename = "MsRunNotFound")]
292    MsRunNotFound,
293    /// The dataset and the ms run are available, but the scan number does not exist in this file
294    #[serde(rename = "ScanNotFound")]
295    ScanNotFound,
296    /// The dataset identifier is not of a recognisable format, commonly PXD identifiers are used eg 'PXD004939', but see the USI spec for more details
297    #[serde(rename = "UnrecognizedIdentifierFormat")]
298    UnrecognizedIdentifierFormat,
299    /// The interpretation part of the USI is unable to be parsed, note that some PROXI backends
300    /// require the addition of a charge to all peptides. Additionally, the PROXI servers do not
301    /// require the existence of the interpretation part of the USI, so removing this field before
302    /// sending the request might help prevent errors.
303    #[serde(rename = "MalformedInterpretation")]
304    MalformedInterpretation,
305    /// The index flag (scan/index/nativeid) is malformed
306    #[serde(rename = "UnrecognizedIndexFlag")]
307    UnrecognizedIndexFlag,
308    /// Mandatory 'mzspec:' preamble is missing from the USI
309    #[serde(rename = "MissingPreamble")]
310    MissingPreamble,
311    /// The USI is malformed and has too few fields
312    #[serde(rename = "TooFewFields")]
313    TooFewFields,
314    #[serde(untagged)]
315    Other(String),
316}
317
318impl std::fmt::Display for PROXIErrorType {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        write!(
321            f,
322            "{}",
323            match self {
324                Self::DataSetNotHere => "DataSetNotHere",
325                Self::MsRunNotFound => "MsRunNotFound",
326                Self::ScanNotFound => "ScanNotFound",
327                Self::UnrecognizedIdentifierFormat => "UnrecognizedIdentifierFormat",
328                Self::MalformedInterpretation => "MalformedInterpretation",
329                Self::UnrecognizedIndexFlag => "UnrecognizedIndexFlag",
330                Self::MissingPreamble => "MissingPreamble",
331                Self::TooFewFields => "TooFewFields",
332                Self::Other(o) => o,
333            }
334        )
335    }
336}
337
338#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
339pub enum Status {
340    #[serde(rename = "READABLE")]
341    Readable,
342    #[serde(rename = "PEAK UNAVAILABLE")]
343    PeakUnavailable,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct PROXIValue(Value);
348
349impl Default for PROXIValue {
350    fn default() -> Self {
351        Self(Value::Empty)
352    }
353}
354
355impl PROXIValue {
356    pub const fn is_empty(&self) -> bool {
357        matches!(self.0, Value::Empty)
358    }
359}
360
361fn proxi_value_serialize<S>(proxi_value: &PROXIValue, serializer: S) -> Result<S::Ok, S::Error>
362where
363    S: serde::Serializer,
364{
365    match &proxi_value.0 {
366        Value::String(v) => serializer.serialize_str(v),
367        Value::Float(v) => serializer.serialize_f64(*v),
368        Value::Int(v) => serializer.serialize_i64(*v),
369        Value::Buffer(v) => serializer.serialize_bytes(v),
370        Value::Boolean(v) => serializer.serialize_bool(*v),
371        Value::Empty => serializer.serialize_unit(),
372        Value::List(v) => serializer.collect_seq(v.iter()),
373    }
374}
375
376fn proxi_value_deserialize<'de, D>(deserializer: D) -> Result<PROXIValue, D::Error>
377where
378    D: serde::Deserializer<'de>,
379{
380    struct PROXIValueVisit {}
381    impl<'de> serde::de::Visitor<'de> for PROXIValueVisit {
382        type Value = PROXIValue;
383
384        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
385            formatter.write_str("PROXIValue string")
386        }
387
388        fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
389            match v.parse::<PROXIValue>() {
390                Ok(v) => Ok(v),
391                Err(e) => Err(E::custom(e)),
392            }
393        }
394
395        fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<Self::Value, E> {
396            Ok(Value::Boolean(v).into())
397        }
398
399        fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
400            Ok(Value::Int(v).into())
401        }
402
403        fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
404            Ok(Value::Int(v as i64).into())
405        }
406
407        fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
408            Ok(Value::Float(v).into())
409        }
410
411        fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
412            Ok(Value::Empty.into())
413        }
414
415        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
416        where
417            A: SeqAccess<'de>,
418        {
419            let mut entries = Vec::new();
420            while let Some(val) = SeqAccess::next_element::<Value>(&mut seq)? {
421                entries.push(val);
422            }
423            Ok(Value::List(entries.into_boxed_slice()).into())
424        }
425
426        fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
427            Ok(Value::Empty.into())
428        }
429    }
430
431    deserializer.deserialize_any(PROXIValueVisit {})
432}
433
434impl ParamValue for PROXIValue {
435    fn is_empty(&self) -> bool {
436        <Value as ParamValue>::is_empty(&self.0)
437    }
438
439    fn is_i64(&self) -> bool {
440        <Value as ParamValue>::is_i64(&self.0)
441    }
442
443    fn is_f64(&self) -> bool {
444        <Value as ParamValue>::is_f64(&self.0)
445    }
446
447    fn is_buffer(&self) -> bool {
448        <Value as ParamValue>::is_buffer(&self.0)
449    }
450
451    fn is_str(&self) -> bool {
452        <Value as ParamValue>::is_str(&self.0)
453    }
454
455    fn to_f64(&self) -> Result<f64, crate::params::ParamValueParseError> {
456        <Value as ParamValue>::to_f64(&self.0)
457    }
458
459    fn to_i64(&self) -> Result<i64, crate::params::ParamValueParseError> {
460        <Value as ParamValue>::to_i64(&self.0)
461    }
462
463    fn to_str(&self) -> std::borrow::Cow<'_, str> {
464        <Value as ParamValue>::to_str(&self.0)
465    }
466
467    fn to_buffer(&self) -> Result<std::borrow::Cow<'_, [u8]>, crate::params::ParamValueParseError> {
468        <Value as ParamValue>::to_buffer(&self.0)
469    }
470
471    fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
472        <Value as ParamValue>::parse(&self.0)
473    }
474
475    fn as_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
476        <Value as ParamValue>::as_bytes(&self.0)
477    }
478
479    fn as_ref(&self) -> crate::params::ValueRef<'_> {
480        <Value as ParamValue>::as_ref(&self.0)
481    }
482
483    fn data_len(&self) -> usize {
484        <Value as ParamValue>::data_len(&self.0)
485    }
486
487    fn is_boolean(&self) -> bool {
488        <Value as ParamValue>::is_boolean(&self.0)
489    }
490
491    fn to_bool(&self) -> Result<bool, crate::params::ParamValueParseError> {
492        <Value as ParamValue>::to_bool(&self.0)
493    }
494
495    fn is_list(&self) -> bool {
496        <Value as ParamValue>::is_list(&self.0)
497    }
498
499    fn as_slice(&self) -> std::borrow::Cow<'_, [Value]> {
500        <Value as ParamValue>::as_slice(&self.0)
501    }
502}
503
504impl Display for PROXIValue {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        match &self.0 {
507            Value::String(v) => write!(f, "{v}"),
508            Value::Float(v) => write!(f, "{v}"),
509            Value::Int(v) => write!(f, "{v}"),
510            Value::Buffer(v) => write!(f, "{v:?}"),
511            Value::Boolean(v) => write!(f, "{v}"),
512            Value::Empty => Ok(()),
513            Value::List(_) => write!(f, "{}", self.0),
514        }
515    }
516}
517
518impl FromStr for PROXIValue {
519    type Err = <Value as FromStr>::Err;
520
521    fn from_str(s: &str) -> Result<Self, Self::Err> {
522        let inner = s.parse()?;
523        Ok(Self(inner))
524    }
525}
526
527impl From<Value> for PROXIValue {
528    fn from(value: Value) -> Self {
529        Self(value)
530    }
531}
532
533impl From<PROXIValue> for Value {
534    fn from(value: PROXIValue) -> Self {
535        value.0
536    }
537}
538
539fn curie_serialize<S>(curie: &CURIE, serializer: S) -> Result<S::Ok, S::Error>
540where
541    S: serde::Serializer,
542{
543    serializer.serialize_str(&curie.to_string())
544}
545
546fn curie_deserialize<'de, D>(deserializer: D) -> Result<CURIE, D::Error>
547where
548    D: serde::Deserializer<'de>,
549{
550    struct CURIEVisit {}
551    impl<'de> serde::de::Visitor<'de> for CURIEVisit {
552        type Value = CURIE;
553
554        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
555            formatter.write_str("expected CURIE string")
556        }
557
558        fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
559            match v.parse::<CURIE>() {
560                Ok(v) => Ok(v),
561                Err(e) => Err(E::custom(e)),
562            }
563        }
564    }
565
566    deserializer.deserialize_str(CURIEVisit {})
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
570pub struct PROXIParam {
571    pub accession: PROXIAccession,
572    pub name: String,
573    #[serde(
574        serialize_with = "proxi_value_serialize",
575        deserialize_with = "proxi_value_deserialize",
576        skip_serializing_if = "PROXIValue::is_empty",
577        default
578    )]
579    pub value: PROXIValue,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
583pub enum PROXIAccession {
584    #[serde(
585        untagged,
586        serialize_with = "curie_serialize",
587        deserialize_with = "curie_deserialize"
588    )]
589    CURIE(CURIE),
590    #[serde(untagged)]
591    Other(String),
592}
593
594impl PROXIAccession {
595    fn controlled_vocabulary(&self) -> ControlledVocabulary {
596        match self {
597            PROXIAccession::CURIE(c) => c.controlled_vocabulary,
598            PROXIAccession::Other(_) => ControlledVocabulary::Unknown,
599        }
600    }
601}
602
603impl ParamValue for PROXIParam {
604    fn is_empty(&self) -> bool {
605        <PROXIValue as ParamValue>::is_empty(&self.value)
606    }
607
608    fn is_i64(&self) -> bool {
609        <PROXIValue as ParamValue>::is_i64(&self.value)
610    }
611
612    fn is_f64(&self) -> bool {
613        <PROXIValue as ParamValue>::is_f64(&self.value)
614    }
615
616    fn is_buffer(&self) -> bool {
617        <PROXIValue as ParamValue>::is_buffer(&self.value)
618    }
619
620    fn is_str(&self) -> bool {
621        <PROXIValue as ParamValue>::is_str(&self.value)
622    }
623
624    fn to_f64(&self) -> Result<f64, crate::params::ParamValueParseError> {
625        <PROXIValue as ParamValue>::to_f64(&self.value)
626    }
627
628    fn to_i64(&self) -> Result<i64, crate::params::ParamValueParseError> {
629        <PROXIValue as ParamValue>::to_i64(&self.value)
630    }
631
632    fn to_str(&self) -> std::borrow::Cow<'_, str> {
633        <PROXIValue as ParamValue>::to_str(&self.value)
634    }
635
636    fn to_buffer(&self) -> Result<std::borrow::Cow<'_, [u8]>, crate::params::ParamValueParseError> {
637        <PROXIValue as ParamValue>::to_buffer(&self.value)
638    }
639
640    fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
641        <PROXIValue as ParamValue>::parse(&self.value)
642    }
643
644    fn as_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
645        <PROXIValue as ParamValue>::as_bytes(&self.value)
646    }
647
648    fn as_ref(&self) -> crate::params::ValueRef<'_> {
649        <PROXIValue as ParamValue>::as_ref(&self.value)
650    }
651
652    fn data_len(&self) -> usize {
653        <PROXIValue as ParamValue>::data_len(&self.value)
654    }
655
656    fn is_boolean(&self) -> bool {
657        <PROXIValue as ParamValue>::is_boolean(&self.value)
658    }
659
660    fn to_bool(&self) -> Result<bool, crate::params::ParamValueParseError> {
661        <PROXIValue as ParamValue>::to_bool(&self.value)
662    }
663
664    fn is_list(&self) -> bool {
665        <PROXIValue as ParamValue>::is_list(&self.value)
666    }
667
668    fn as_slice(&self) -> std::borrow::Cow<'_, [Value]> {
669        <PROXIValue as ParamValue>::as_slice(&self.value)
670    }
671}
672
673impl PROXIParam {
674    pub fn new<S: ToString, V: Into<PROXIValue>>(accession: CURIE, name: S, value: V) -> Self {
675        Self {
676            accession: PROXIAccession::CURIE(accession),
677            name: name.to_string(),
678            value: value.into(),
679        }
680    }
681}
682
683impl From<Param> for PROXIParam {
684    fn from(value: Param) -> Self {
685        Self {
686            accession: PROXIAccession::CURIE(value.curie().unwrap()),
687            name: value.name,
688            value: value.value.into(),
689        }
690    }
691}
692
693impl<'a> From<ParamCow<'a>> for PROXIParam {
694    fn from(value: ParamCow<'a>) -> Self {
695        Self {
696            accession: PROXIAccession::CURIE(value.curie().unwrap()),
697            name: value.name.to_string(),
698            value: Value::from(value.value).into(),
699        }
700    }
701}
702
703const MS1_SPECTRUM: ParamCow = ControlledVocabulary::MS.const_param_ident("MS1 spectrum", 1000579);
704const MSN_SPECTRUM: ParamCow = ControlledVocabulary::MS.const_param_ident("MSn spectrum", 1000580);
705const NEGATIVE_SCAN: ParamCow =
706    ControlledVocabulary::MS.const_param_ident("negative scan", 1000129);
707const POSITIVE_SCAN: ParamCow =
708    ControlledVocabulary::MS.const_param_ident("positive scan", 1000130);
709const PROFILE_SPECTRUM: ParamCow =
710    ControlledVocabulary::MS.const_param_ident("profile spectrum", 1000128);
711const CENTROID_SPECTRUM: ParamCow =
712    ControlledVocabulary::MS.const_param_ident("centroid spectrum", 1000127);
713
714fn usi_serialize<S>(usi: &Option<USI>, serializer: S) -> Result<S::Ok, S::Error>
715where
716    S: serde::Serializer,
717{
718    match usi {
719        Some(usi) => serializer.serialize_str(&usi.to_string()),
720        None => serializer.serialize_none(),
721    }
722}
723
724fn usi_deserialize<'de, D>(deserializer: D) -> Result<Option<USI>, D::Error>
725where
726    D: serde::Deserializer<'de>,
727{
728    struct USIVisit {}
729    impl<'de> serde::de::Visitor<'de> for USIVisit {
730        type Value = Option<USI>;
731
732        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
733            formatter.write_str("USI string or null")
734        }
735
736        fn visit_none<E>(self) -> Result<Self::Value, E> {
737            Ok(None)
738        }
739
740        fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
741            Ok(None)
742        }
743
744        fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
745            if v == "null" {
746                Ok(None)
747            } else {
748                match v.parse::<USI>() {
749                    Ok(v) => Ok(Some(v)),
750                    Err(e) => Err(E::custom(e)),
751                }
752            }
753        }
754    }
755
756    deserializer.deserialize_any(USIVisit {})
757}
758
759use serde::de::{self, Visitor};
760
761/// MassIVE returns a list of strings instead of a list of numbers, this type can be deserialized if a number of string is given in the JSON
762#[derive(Debug, Default, Clone, Copy)]
763pub struct Wrapped<T>(T);
764
765impl<'de, T: 'static + Default + Copy + FromStr> serde::Deserialize<'de> for Wrapped<T>
766where
767    T::Err: Display,
768    f64: AsPrimitive<T>,
769    u64: AsPrimitive<T>,
770    i64: AsPrimitive<T>,
771{
772    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
773    where
774        D: de::Deserializer<'de>,
775    {
776        deserializer
777            .deserialize_any(PotentiallyWrappedNumberVisitor::<T>::default())
778            .map(Wrapped)
779    }
780}
781
782impl serde::Serialize for Wrapped<f64> {
783    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
784    where
785        S: serde::Serializer,
786    {
787        serializer.serialize_f64(self.0)
788    }
789}
790
791impl serde::Serialize for Wrapped<f32> {
792    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
793    where
794        S: serde::Serializer,
795    {
796        serializer.serialize_f32(self.0)
797    }
798}
799
800#[derive(Debug, Default)]
801struct PotentiallyWrappedNumberVisitor<T>(PhantomData<T>);
802
803impl<'de, T: 'static + Copy + FromStr> Visitor<'de> for PotentiallyWrappedNumberVisitor<T>
804where
805    T::Err: Display,
806    f64: AsPrimitive<T>,
807    u64: AsPrimitive<T>,
808    i64: AsPrimitive<T>,
809{
810    type Value = T;
811
812    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
813        formatter.write_str("a wrapped number")
814    }
815
816    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
817        Ok(value.as_())
818    }
819
820    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
821        Ok(value.as_())
822    }
823
824    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
825        Ok(value.as_())
826    }
827
828    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
829        value.parse().map_err(|e| de::Error::custom(e))
830    }
831}
832
833fn deserialize_wrapped_series<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
834where
835    T: Deserialize<'de> + Default + Copy + FromStr + 'static,
836    T::Err: Display,
837    f64: AsPrimitive<T>,
838    u64: AsPrimitive<T>,
839    i64: AsPrimitive<T>,
840    D: Deserializer<'de>,
841{
842    struct WrappingVisitor<T>(PhantomData<fn() -> T>);
843
844    impl<'de, T> Visitor<'de> for WrappingVisitor<T>
845    where
846        T: Deserialize<'de> + Default + Copy + FromStr + 'static,
847        T::Err: Display,
848        f64: AsPrimitive<T>,
849        u64: AsPrimitive<T>,
850        i64: AsPrimitive<T>,
851    {
852        type Value = Vec<T>;
853
854        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
855            formatter.write_str("a nonempty sequence of numbers")
856        }
857
858        fn visit_seq<S>(self, mut seq: S) -> Result<Vec<T>, S::Error>
859        where
860            S: SeqAccess<'de>,
861        {
862            let mut buf = Vec::new();
863            while let Some(value) = seq.next_element::<Wrapped<T>>()? {
864                buf.push(value.0);
865            }
866            Ok(buf)
867        }
868    }
869    let visitor = WrappingVisitor::<T>(PhantomData);
870    deserializer.deserialize_seq(visitor)
871}
872
873/// A spectrum returnd by a PROXI server
874#[derive(Debug, Default, Clone, Serialize, Deserialize)]
875pub struct PROXISpectrum {
876    /// The USI as returned by the PROXI server
877    #[serde(
878        default,
879        serialize_with = "usi_serialize",
880        deserialize_with = "usi_deserialize"
881    )]
882    pub usi: Option<USI>,
883    /// The status of this request
884    pub status: Option<Status>,
885    /// Metadata for this spectrum
886    #[serde(default)]
887    pub attributes: Vec<PROXIParam>,
888    #[serde(default, deserialize_with = "deserialize_wrapped_series")]
889    pub mzs: Vec<f64>,
890    #[serde(default, deserialize_with = "deserialize_wrapped_series")]
891    pub intensities: Vec<f32>,
892    #[serde(skip_serializing_if = "Option::is_none", default)]
893    pub charges: Option<Vec<i32>>,
894}
895
896impl PROXISpectrum {
897    pub fn add_attribute<P: Into<PROXIParam>>(&mut self, param: P) {
898        self.attributes.push(param.into());
899    }
900}
901
902impl From<&PROXISpectrum> for SpectrumDescription {
903    fn from(value: &PROXISpectrum) -> Self {
904        let mut this = Self::default();
905        if let Some(usi) = value.usi.as_ref() {
906            this.id = usi.to_string();
907            if let Some(ident) = usi.identifier.as_ref() {
908                match ident {
909                    super::usi::Identifier::Scan(i) => this.index = (i.saturating_sub(1)) as usize,
910                    super::usi::Identifier::Index(i) => this.index = *i as usize,
911                    super::usi::Identifier::NativeID(_values) => todo!(),
912                }
913            } else {
914                this.index = 0;
915            }
916        }
917
918        let mut has_precursor = false;
919        let mut precursor = Precursor::default();
920
921        for param in &value.attributes {
922            if matches!(
923                param.accession.controlled_vocabulary(),
924                ControlledVocabulary::UO
925            ) {
926                continue;
927            }
928            match param.name.as_str() {
929                "ms level" => {
930                    this.ms_level = param.value.to_i32().expect("Failed to parse ms level") as u8;
931                }
932                "scan polarity" => match param.value.as_str().as_ref() {
933                    "positive scan" => {
934                        this.polarity = ScanPolarity::Positive;
935                    }
936                    "negative scan" => {
937                        this.polarity = ScanPolarity::Negative;
938                    }
939                    _ => (),
940                },
941                "spectrum representation" => match param.value.as_str().as_ref() {
942                    "profile spectrum" => {
943                        this.signal_continuity = SignalContinuity::Profile;
944                    }
945                    "centroid spectrum" => {
946                        this.signal_continuity = SignalContinuity::Centroid;
947                    }
948                    _ => (),
949                },
950                "scan start time" => {
951                    if let Some(s) = this.acquisition.first_scan_mut() {
952                        s.start_time = param
953                            .value
954                            .to_f64()
955                            .expect("Failed to extract scan start time")
956                            / 60.0;
957                    }
958                }
959                "ion injection time" => {
960                    if let Some(s) = this.acquisition.first_scan_mut() {
961                        s.injection_time = param
962                            .to_f32()
963                            .expect("Failed to extract ion injection time");
964                    }
965                }
966                "filter string" => {
967                    if let Some(s) = this.acquisition.first_scan_mut() {
968                        let mut param =
969                            Param::new_key_value("filter string", param.value.to_string());
970                        param.controlled_vocabulary = Some(ControlledVocabulary::MS);
971                        param.accession = Some(1000512);
972                        s.add_param(param);
973                    }
974                }
975
976                "selected ion m/z" => {
977                    has_precursor = true;
978                    precursor.ion_mut().unwrap().mz =
979                        param.to_f64().expect("Failed to parse ion m/z");
980                }
981                "peak intensity" => {
982                    has_precursor = true;
983                    precursor.ion_mut().unwrap().intensity =
984                        param.to_f32().expect("Failed to parse peak intensity");
985                }
986                "charge state" => {
987                    has_precursor = true;
988                    precursor.ion_mut().unwrap().charge =
989                        Some(param.to_i32().expect("Failed to parse ion charge"));
990                }
991
992                "isolation window target m/z" => {
993                    has_precursor = true;
994                    let window = IsolationWindowBuilder(precursor.isolation_window_mut());
995                    window.target(param.to_f32().expect("Failed to parse isolation window target"));
996                }
997                "isolation window lower offset" => {
998                    has_precursor = true;
999                    let lower_bound = param
1000                        .to_f32()
1001                        .expect("Failed to parse isolation window limit");
1002                    let window = IsolationWindowBuilder(precursor.isolation_window_mut());
1003                    window.lower_offset(lower_bound);
1004                }
1005                "isolation window upper offset" => {
1006                    has_precursor = true;
1007                    let upper_bound = param
1008                        .to_f32()
1009                        .expect("Failed to parse isolation window limit");
1010                    let window = IsolationWindowBuilder(precursor.isolation_window_mut());
1011                    window.upper_offset(upper_bound);
1012                }
1013                "isolation window lower limit" => {
1014                    has_precursor = true;
1015                    let window = IsolationWindowBuilder(precursor.isolation_window_mut());
1016                    let lower_bound = param
1017                        .to_f32()
1018                        .expect("Failed to parse isolation window limit");
1019                    window.lower_limit(lower_bound);
1020                }
1021                "isolation window upper limit" => {
1022                    has_precursor = true;
1023                    let upper_bound = param
1024                        .to_f32()
1025                        .expect("Failed to parse isolation window limit");
1026                    let window = IsolationWindowBuilder(precursor.isolation_window_mut());
1027                    window.upper_limit(upper_bound);
1028                }
1029                _ => {
1030                    let mut p = Param::new_key_value(param.name.clone(), param.value.clone());
1031                    if let PROXIAccession::CURIE(c) = param.accession {
1032                        p.accession = Some(c.accession);
1033                    } else {
1034                        log::debug!("{:?} could not be translated to a compact CURIE instance, dropping accession", param.accession)
1035                    }
1036                    p.controlled_vocabulary = Some(param.accession.controlled_vocabulary());
1037                    this.add_param(p);
1038                }
1039            }
1040        }
1041
1042        if has_precursor {
1043            this.precursor = vec![precursor];
1044        }
1045        this
1046    }
1047}
1048
1049impl<
1050        C: CentroidLike + BuildFromArrayMap + BuildArrayMapFrom,
1051        D: DeconvolutedCentroidLike + BuildFromArrayMap + BuildArrayMapFrom,
1052    > From<PROXISpectrum> for MultiLayerSpectrum<C, D>
1053{
1054    fn from(value: PROXISpectrum) -> Self {
1055        let descr: SpectrumDescription = (&value).into();
1056        let mut arrays = BinaryArrayMap::default();
1057
1058        let mut mz_array =
1059            DataArray::from_name_and_type(&ArrayType::MZArray, BinaryDataArrayType::Float64);
1060        mz_array.extend(&value.mzs).unwrap();
1061        arrays.add(mz_array);
1062
1063        let mut intensity_array =
1064            DataArray::from_name_and_type(&ArrayType::IntensityArray, BinaryDataArrayType::Float32);
1065        intensity_array.extend(&value.intensities).unwrap();
1066        arrays.add(intensity_array);
1067
1068        if let Some(charges) = value.charges.as_ref() {
1069            let mut charge_arrays =
1070                DataArray::from_name_and_type(&ArrayType::ChargeArray, BinaryDataArrayType::Int32);
1071            charge_arrays.extend(charges).unwrap();
1072            arrays.add(charge_arrays);
1073        };
1074
1075        Self::from_arrays_and_description(arrays, descr)
1076    }
1077}
1078
1079impl<T> From<&T> for PROXISpectrum
1080where
1081    T: SpectrumLike,
1082{
1083    fn from(value: &T) -> Self {
1084        let mut this = Self {
1085            status: Some(Status::Readable),
1086            ..Default::default()
1087        };
1088
1089        match value.ms_level().cmp(&1) {
1090            Ordering::Equal => this.add_attribute(MS1_SPECTRUM.clone()),
1091            Ordering::Greater => this.add_attribute(MSN_SPECTRUM.clone()),
1092            Ordering::Less => (),
1093        }
1094
1095        for param in value.params().iter().filter(|p| p.is_controlled()) {
1096            if param.curie().unwrap() == curie!(MS:1003063) {
1097                this.usi = Some(param.value.as_str().parse().unwrap());
1098            } else {
1099                this.add_attribute(param.clone());
1100            }
1101        }
1102
1103        this.add_attribute(PROXIParam::new(
1104            curie!(MS:10003057),
1105            "scan number",
1106            Value::Int((value.index() + 1) as i64),
1107        ));
1108
1109        this.add_attribute(PROXIParam {
1110            name: "ms level".to_string(),
1111            value: PROXIValue(Value::Int(value.ms_level() as i64)),
1112            accession: PROXIAccession::CURIE(curie!(MS:1000511)),
1113        });
1114
1115        match value.polarity() {
1116            crate::spectrum::ScanPolarity::Unknown => {}
1117            crate::spectrum::ScanPolarity::Positive => {
1118                this.attributes.push(Param::from(POSITIVE_SCAN).into());
1119            }
1120            crate::spectrum::ScanPolarity::Negative => {
1121                this.attributes.push(Param::from(NEGATIVE_SCAN).into());
1122            }
1123        }
1124
1125        match value.signal_continuity() {
1126            crate::spectrum::SignalContinuity::Unknown => {}
1127            crate::spectrum::SignalContinuity::Centroid => {
1128                this.attributes.push(Param::from(CENTROID_SPECTRUM).into());
1129            }
1130            crate::spectrum::SignalContinuity::Profile => {
1131                this.attributes.push(Param::from(PROFILE_SPECTRUM).into());
1132            }
1133        }
1134
1135        if let Some(precursor) = value.precursor() {
1136            let iw = precursor.isolation_window();
1137            this.add_attribute(ControlledVocabulary::MS.param_val(
1138                "MS:1000827",
1139                "isolation window target m/z",
1140                iw.target,
1141            ));
1142            this.add_attribute(ControlledVocabulary::MS.param_val(
1143                "MS:1000828",
1144                "isolation window lower offset",
1145                iw.target - iw.lower_bound,
1146            ));
1147            this.add_attribute(ControlledVocabulary::MS.param_val(
1148                "MS:1000829",
1149                "isolation window upper offset",
1150                iw.upper_bound - iw.target,
1151            ));
1152
1153            let ion = precursor.ion().unwrap();
1154            this.add_attribute(PROXIParam::new(
1155                curie!(MS:1000744),
1156                "selected ion m/z",
1157                Value::Float(ion.mz),
1158            ));
1159
1160            if let Some(z) = ion.charge {
1161                this.add_attribute(PROXIParam::new(
1162                    curie!(MS:1000041),
1163                    "charge state",
1164                    Value::Int(z as i64),
1165                ));
1166            }
1167        }
1168
1169        for param in value.acquisition().params() {
1170            if param.is_controlled() {
1171                this.add_attribute(param.clone());
1172            }
1173        }
1174
1175        for event in value.acquisition().iter() {
1176            let p = PROXIParam {
1177                name: "scan start time".into(),
1178                accession: PROXIAccession::CURIE(curie!(MS:1000016)),
1179                value: Value::Float(event.start_time * 60.0).into(),
1180            };
1181            this.add_attribute(p);
1182            for param in event.params() {
1183                if param.is_controlled() {
1184                    this.add_attribute(param.clone());
1185                }
1186            }
1187        }
1188
1189        match value.peaks() {
1190            crate::spectrum::RefPeakDataLevel::Missing => {}
1191            crate::spectrum::RefPeakDataLevel::RawData(arrays) => {
1192                this.mzs = arrays.mzs().unwrap().iter().copied().collect();
1193                this.intensities = arrays.intensities().unwrap().iter().copied().collect();
1194                if let Ok(arr) = arrays.charges() {
1195                    this.charges = Some(arr.to_vec());
1196                }
1197            }
1198            crate::spectrum::RefPeakDataLevel::Centroid(peaks) => {
1199                (this.mzs, this.intensities) =
1200                    peaks.iter().map(|p| (p.mz(), p.intensity())).unzip();
1201            }
1202            crate::spectrum::RefPeakDataLevel::Deconvoluted(peaks) => {
1203                (this.mzs, this.intensities) =
1204                    peaks.iter().map(|p| (p.mz(), p.intensity())).unzip();
1205                this.charges = Some(
1206                    peaks
1207                        .iter()
1208                        .map(mzpeaks::KnownCharge::charge)
1209                        .collect::<Vec<_>>(),
1210                );
1211            }
1212        }
1213
1214        if this.mzs.is_empty() {
1215            this.status = Some(Status::PeakUnavailable);
1216        }
1217
1218        this
1219    }
1220}
1221
1222#[cfg(test)]
1223mod test {
1224    use std::io;
1225
1226    use super::*;
1227    use crate::MZReader;
1228    use serde_json;
1229
1230    #[test]
1231    fn test_convert() -> io::Result<()> {
1232        let mut reader = MZReader::open_path("./test/data/batching_test.mzML")?;
1233
1234        let scan = reader.get_spectrum_by_index(0).unwrap();
1235        let scan_message = PROXISpectrum::from(&scan);
1236
1237        let message = serde_json::to_string(&scan_message)?;
1238        let dup: PROXISpectrum = serde_json::from_str(&message)?;
1239        assert_eq!(dup.usi, scan_message.usi);
1240        assert_eq!(dup.attributes, scan_message.attributes);
1241        Ok(())
1242    }
1243
1244    #[test]
1245    fn get_peptide_atlas() {
1246        let usi: USI =
1247            "mzspec:PXD000561:Adult_Frontalcortex_bRP_Elite_85_f09:scan:17555:VLHPLEGAVVIIFK/2"
1248                .parse()
1249                .unwrap();
1250        let (_, response) = usi
1251            .download_spectrum_blocking(Some(PROXIBackend::PeptideAtlas), None)
1252            .unwrap();
1253        assert!(!response.is_empty());
1254    }
1255
1256    #[ignore]
1257    #[test]
1258    fn get_massive() {
1259        let usi: USI = "mzspec:MSV000078547:120228_nbut_3610_it_it_take2:scan:389"
1260            .parse()
1261            .unwrap();
1262        let (_, response) = usi
1263            .download_spectrum_blocking(Some(PROXIBackend::MassIVE), None)
1264            .unwrap();
1265        assert!(!response.is_empty());
1266    }
1267
1268    #[test]
1269    fn get_pride() {
1270        let usi: USI =
1271            "mzspec:PXD043489:20201103_F1_UM5_Peng0013_SA_139H2_InS_Elastase.raw:scan:11809:VSLFPPSSEQLTSNASVV/2"
1272                .parse()
1273                .unwrap();
1274        let (_, response) = usi
1275            .download_spectrum_blocking(Some(PROXIBackend::Pride), None)
1276            .unwrap();
1277        assert!(!response.is_empty());
1278    }
1279
1280    #[test]
1281    fn get_proteomexchange() {
1282        let usi: USI =
1283            "mzspec:PXD004939:Rice_phos_ABA_3h_20per_F1_R2:scan:2648:DAEKS[UNIMOD:21]PIN[UNIMOD:7]GR/2"
1284                .parse()
1285                .unwrap();
1286        let (_, response) = usi
1287            .download_spectrum_blocking(Some(PROXIBackend::ProteomeXchange), None)
1288            .unwrap();
1289        assert!(!response.is_empty());
1290    }
1291
1292    #[test]
1293    fn get_aggregate() {
1294        for usi in [
1295            "mzspec:PXD000561:Adult_Frontalcortex_bRP_Elite_85_f09:scan:17555:VLHPLEGAVVIIFK/2",
1296            "mzspec:MSV000078547:120228_nbut_3610_it_it_take2:scan:389",
1297            "mzspec:PXD043489:20201103_F1_UM5_Peng0013_SA_139H2_InS_Elastase.raw:scan:11809:VSLFPPSSEQLTSNASVV/2",
1298            "mzspec:PXD004939:Rice_phos_ABA_3h_20per_F1_R2:scan:2648:DAEKS[UNIMOD:21]PIN[UNIMOD:7]GR/2"] {
1299            println!("Trying: {usi}");
1300            let usi: USI = usi.parse().unwrap();
1301            let (_, response) = usi.download_spectrum_blocking(None, None).unwrap();
1302            assert!(!response.is_empty());
1303        }
1304    }
1305
1306    #[test]
1307    fn test_proxi_parse() {
1308        let spec: PROXISpectrum =
1309            serde_json::from_reader(std::fs::File::open("test/data/proxi_test.json").unwrap())
1310                .unwrap();
1311        assert!(!spec.mzs.is_empty());
1312        assert!(!spec.attributes.is_empty());
1313    }
1314}
1315
1316#[cfg(all(feature = "proxi-async", feature = "async_partial"))]
1317#[cfg(test)]
1318mod test_async {
1319    use super::*;
1320
1321    #[tokio::test]
1322    async fn get_aggregate_async() {
1323        for usi in [
1324            "mzspec:PXD000561:Adult_Frontalcortex_bRP_Elite_85_f09:scan:17555:VLHPLEGAVVIIFK/2",
1325            "mzspec:MSV000078547:120228_nbut_3610_it_it_take2:scan:389",
1326            "mzspec:PXD043489:20201103_F1_UM5_Peng0013_SA_139H2_InS_Elastase.raw:scan:11809:VSLFPPSSEQLTSNASVV/2",
1327            "mzspec:PXD004939:Rice_phos_ABA_3h_20per_F1_R2:scan:2648:DAEKS[UNIMOD:21]PIN[UNIMOD:7]GR/2"] {
1328            println!("Trying: {usi}");
1329            let usi: USI = usi.parse().unwrap();
1330            let (_, response) = usi.download_spectrum_async(None, None).await.unwrap();
1331            assert!(!response.is_empty());
1332        }
1333    }
1334}