gflights 0.3.0

Unofficial async Rust client for the Google Flights web API — search flights, price graphs, and booking offers.
Documentation
use anyhow::{anyhow, Result};
use percent_encoding::{AsciiSet, CONTROLS};
use serde::{Deserialize, Deserializer};

pub mod airline;
pub mod duration;
pub mod fixed_flights;
pub mod location;
pub mod travelers;
pub mod types;

pub use airline::{AirlineCode, AirlineFilter, Alliance};
pub use duration::{FlightTimes, StopoverDuration, TotalDuration};
pub use fixed_flights::FixedFlights;
pub use location::Location;
pub use travelers::Travelers;
pub use types::{PlaceType, SortOrder, StopOptions, TravelClass};

/// The set of characters that are percent-encoded in google flights requests.
pub(crate) const CHARACTERS_TO_ENCODE: &AsciiSet = &CONTROLS
    .add(b'[')
    .add(b']')
    .add(b'"')
    .add(b',')
    .add(b':')
    .add(b'\\');

/// Url is the url to make the request to
/// Body is the POST request body.
#[derive(Debug, Deserialize, serde::Serialize)]
pub struct RequestBody {
    pub url: String,
    pub body: String,
}

/// Trait, serialize the request to a request body (URL + body).
pub trait ToRequestBody {
    fn to_request_body(&self) -> Result<RequestBody>;
}

/// Trait to get the error messages from the response outer messages.
pub trait GetOuterErrorMessages {
    fn get_error_messages(&self) -> Option<Vec<String>>;
}

/// Trait to serialize a value into the Google Flights web request format.
pub trait SerializeToWeb {
    fn serialize_to_web(&self) -> Result<String>;
}

/// A vector is serialized as a JSON array.
impl<T> SerializeToWeb for Vec<T>
where
    T: SerializeToWeb,
{
    fn serialize_to_web(&self) -> Result<String> {
        let mut result = String::new();
        result.push('[');

        for (i, item) in self.iter().enumerate() {
            if i > 0 {
                result.push(',');
            }
            result.push_str(&item.serialize_to_web()?);
        }

        result.push(']');

        Ok(result)
    }
}

/// Extract and deserialize a single element from a JSON array by index.
/// Returns None if the index is out of bounds or the value fails to deserialize.
/// Use this instead of positional serde struct fields so trailing elements
/// added by Google never cause "trailing characters" parse errors.
pub(crate) fn get_idx<T: serde::de::DeserializeOwned>(
    arr: &[serde_json::Value],
    i: usize,
) -> Option<T> {
    arr.get(i)
        .and_then(|v| serde_json::from_value(v.clone()).ok())
}

/// Decode the outer object. Responses are in the format
/// `)]}'`
///
/// followed by the actual data (one batch-response line per result).
///
/// # Errors
/// Returns an error if the data is malformed.
/// Extract every `wrb.fr` data frame from a batchexecute streamed body.
///
/// A streamed response interleaves bare numeric length-prefix lines with JSON
/// data lines, may start with a `)]}'` guard, and can contain blank lines; the
/// frame count and length/data spacing vary across the multi-frame stream
/// (search results arrive over several seconds in multiple frames). Every data
/// frame is a single line beginning with `[["wrb.fr"`, and length lines are
/// numeric — so filtering all lines for that marker captures every frame
/// regardless of its position, which a fixed stride (`skip(3).step_by(2)`)
/// could silently miss.
fn wrb_frames(body: &str) -> Vec<&str> {
    body.lines()
        .map(str::trim)
        .filter(|line| line.starts_with(r#"[["wrb.fr""#))
        .collect()
}

pub(crate) fn decode_outer_object<T>(body: &str) -> Result<Vec<T>>
where
    T: for<'a> Deserialize<'a> + GetOuterErrorMessages,
{
    let lines = wrb_frames(body);

    let results = lines
        .iter()
        .map(|f| {
            let jd: &mut serde_json::Deserializer<serde_json::de::StrRead<'_>> =
                &mut serde_json::Deserializer::from_str(f);
            let result: Result<T, _> = serde_path_to_error::deserialize(jd);
            match result {
                Ok(x) => {
                    let test = x.get_error_messages();
                    match test {
                        Some(err) => {
                            let err_messages_joined = err.join("\n");
                            tracing::error!(errors = ?err, "Error in processing outer object: errors returned from the backend");
                            Err(anyhow!(err_messages_joined))
                        }
                        None => Ok(x),
                    }
                }
                Err(err) => {
                    let path = err.path().to_string();
                    tracing::error!(path = %path, error = ?err, "Error deserializing outer object");
                    Err(anyhow!(err))
                }
            }
        })
        .filter(|f| f.is_ok())
        .collect();
    results
}

/// Decode the inner object.
///
/// The outer object is two values and a 3rd which is data + a JSON value as a string.
/// Following there may be other values, but we are interested only in the 3rd one which contains all the data.
/// That is parsed and given out as an output.
///
/// # Errors
/// Returns an error if the data is malformed.
pub(crate) fn decode_inner_object<T: for<'a> Deserialize<'a>>(body: &str) -> Result<T> {
    let jd: &mut serde_json::Deserializer<serde_json::de::StrRead<'_>> =
        &mut serde_json::Deserializer::from_str(body);
    let result: Result<T, _> = serde_path_to_error::deserialize(jd);
    match result {
        Ok(x) => Ok(x),
        Err(err) => {
            let path = err.path().to_string();
            tracing::error!(path = %path, error = ?err, "Error deserializing inner object");
            Err(anyhow!(err))
        }
    }
}

/// Allows treating empty values as None.
/// This is needed because for some values, sometimes the API returns
/// `null` and some other times `[]`.
pub(crate) fn object_empty_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
    D: Deserializer<'de>,
    T: std::fmt::Debug,
    for<'a> T: Deserialize<'a>,
{
    use serde::de::{self, Visitor};
    use std::fmt;
    struct RawValueVisitor;

    impl<'de> Visitor<'de> for RawValueVisitor {
        type Value = serde_json::Value;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("any valid JSON value")
        }

        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            serde_json::from_str(v).map_err(de::Error::custom)
        }

        fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            serde_json::from_str(v).map_err(de::Error::custom)
        }

        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            serde_json::from_str(&v).map_err(de::Error::custom)
        }

        fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            serde_json::from_slice(v).map_err(de::Error::custom)
        }

        fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            serde_json::from_slice(v).map_err(de::Error::custom)
        }

        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
        where
            A: de::SeqAccess<'de>,
        {
            let mut elements = Vec::new();
            while let Some(value) = seq.next_element()? {
                elements.push(value);
            }
            Ok(serde_json::Value::Array(elements))
        }

        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
        where
            A: de::MapAccess<'de>,
        {
            let mut values = serde_json::Map::new();
            while let Some((key, value)) = map.next_entry()? {
                values.insert(key, value);
            }
            Ok(serde_json::Value::Object(values))
        }
    }

    #[derive(Deserialize, Debug)]
    #[serde(deny_unknown_fields)]
    struct Empty {}

    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    enum Aux<T> {
        T(T),
        Empty(Empty),
        Null,
        #[allow(dead_code)]
        Array(Vec<serde_json::Value>),
        #[allow(dead_code)]
        Number(serde_json::Number),
    }

    let raw_value: serde_json::Value = deserializer.deserialize_any(RawValueVisitor)?;

    let aux: Aux<T> = serde_json::from_value(raw_value).map_err(de::Error::custom)?;

    match aux {
        Aux::T(t) => Ok(Some(t)),
        Aux::Empty(_) | Aux::Null | Aux::Array(_) | Aux::Number(_) => Ok(None),
    }
}

#[cfg(test)]
mod frame_tests {
    use super::wrb_frames;

    #[test]
    fn collects_all_frames_at_irregular_positions() {
        // A length line, a data frame, another length line + frame, then a
        // second data frame with NO length line before it, then a non-wrb
        // end marker. A fixed `skip(3).step_by(2)` stride would drop a frame.
        let body = concat!(
            ")]}'\n",
            "\n",
            "26\n",
            "[[\"wrb.fr\",\"a\",\"[]\"]]\n",
            "31\n",
            "[[\"wrb.fr\",\"b\",\"[]\"]]\n",
            "[[\"wrb.fr\",\"c\",\"[]\"]]\n",
            "10\n",
            "[\"e\",4,null,5]\n",
        );
        let frames = wrb_frames(body);
        assert_eq!(frames.len(), 3, "every wrb.fr frame must be captured");
        assert!(frames[0].contains("\"a\""));
        assert!(frames[1].contains("\"b\""));
        assert!(frames[2].contains("\"c\""));
    }

    #[test]
    fn ignores_length_lines_and_end_markers() {
        let body = ")]}'\n5\n[[\"wrb.fr\",\"x\",\"[]\"]]\n[\"di\",12]\n[\"e\",4,null,5]\n";
        assert_eq!(wrb_frames(body).len(), 1);
    }
}