<commit_before>//! Handling of `preserveAspectRatio` values
//!
//! This module handles `preserveAspectRatio` values [per the SVG specification][spec].
//! We have an [`AspectRatio`] struct which encapsulates such a value.
//!
//! ```
//! assert_eq!(
//!     AspectRatio::parse("xMidYMid", ()),
//!     Ok(AspectRatio {
//!         defer: false,
//!         align: Some(Align {
//!             align: AlignMode::XmidYmid,
//!             fit: FitMode::Meet,
//!         }),
//!     })
//! );
//! ```
//!
//! [`AspectRatio`]: struct.AspectRatio.html
//! [spec]: https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute

use error::*;
use parsers::Parse;
use parsers::ParseError;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct AspectRatio {
    defer: bool,
    align: Option<Align>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum FitMode {
    Meet,
    Slice,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum AlignMode {
    XminYmin,
    XmidYmin,
    XmaxYmin,
    XminYmid,
    XmidYmid,
    XmaxYmid,
    XminYmax,
    XmidYmax,
    XmaxYmax,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct Align {
    align: AlignMode,
    fit: FitMode,
}

#[derive(Debug, Copy, Clone)]
enum Align1D {
    Min,
    Mid,
    Max,
}

fn align_1d(a: Align1D, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 {
    match a {
        Align1D::Min => dest_pos,
        Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0,
        Align1D::Max => dest_pos + dest_size - obj_size,
    }
}

impl AspectRatio {
    pub fn is_slice(&self) -> bool {
        match self.align {
            Some(Align {
                fit: FitMode::Slice,
                ..
            }) => true,

            _ => false,
        }
    }

    pub fn compute(
        &self,
        object_width: f64,
        object_height: f64,
        dest_x: f64,
        dest_y: f64,
        dest_width: f64,
        dest_height: f64,
    ) -> (f64, f64, f64, f64) {
        match self.align {
            None => (dest_x, dest_y, dest_width, dest_height),

            Some(Align { align, fit }) => {
                let w_factor = dest_width / object_width;
                let h_factor = dest_height / object_height;
                let factor: f64;

                match fit {
                    FitMode::Meet => {
                        factor = w_factor.min(h_factor);
                    }
                    FitMode::Slice => {
                        factor = w_factor.max(h_factor);
                    }
                }

                let w = object_width * factor;
                let h = object_height * factor;

                let xalign: Align1D;
                let yalign: Align1D;

                match align {
                    AlignMode::XminYmin => {
                        xalign = Align1D::Min;
                        yalign = Align1D::Min;
                    }
                    AlignMode::XminYmid => {
                        xalign = Align1D::Min;
                        yalign = Align1D::Mid;
                    }
                    AlignMode::XminYmax => {
                        xalign = Align1D::Min;
                        yalign = Align1D::Max;
                    }
                    AlignMode::XmidYmin => {
                        xalign = Align1D::Mid;
                        yalign = Align1D::Min;
                    }
                    AlignMode::XmidYmid => {
                        xalign = Align1D::Mid;
                        yalign = Align1D::Mid;
                    }
                    AlignMode::XmidYmax => {
                        xalign = Align1D::Mid;
                        yalign = Align1D::Max;
                    }
                    AlignMode::XmaxYmin => {
                        xalign = Align1D::Max;
                        yalign = Align1D::Min;
                    }
                    AlignMode::XmaxYmid => {
                        xalign = Align1D::Max;
                        yalign = Align1D::Mid;
                    }
                    AlignMode::XmaxYmax => {
                        xalign = Align1D::Max;
                        yalign = Align1D::Max;
                    }
                }

                let xpos = align_1d(xalign, dest_x, dest_width, w);
                let ypos = align_1d(yalign, dest_y, dest_height, h);

                (xpos, ypos, w, h)
            }
        }
    }
}

impl Default for AspectRatio {
    fn default() -> AspectRatio {
        AspectRatio {
            defer: false,
            align: Some(Align {
                align: AlignMode::XmidYmid,
                fit: FitMode::Meet,
            }),
        }
    }
}

fn parse_align(s: &str) -> Result<Option<AlignMode>, AttributeError> {
    match s {
        "none" => Ok(None),

        "xMinYMin" => Ok(Some(AlignMode::XminYmin)),
        "xMidYMin" => Ok(Some(AlignMode::XmidYmin)),
        "xMaxYMin" => Ok(Some(AlignMode::XmaxYmin)),

        "xMinYMid" => Ok(Some(AlignMode::XminYmid)),
        "xMidYMid" => Ok(Some(AlignMode::XmidYmid)),
        "xMaxYMid" => Ok(Some(AlignMode::XmaxYmid)),

        "xMinYMax" => Ok(Some(AlignMode::XminYmax)),
        "xMidYMax" => Ok(Some(AlignMode::XmidYmax)),
        "xMaxYMax" => Ok(Some(AlignMode::XmaxYmax)),

        _ => Err(make_err()),
    }
}

fn parse_fit_mode(s: &str) -> Result<FitMode, AttributeError> {
    match s {
        "meet" => Ok(FitMode::Meet),
        "slice" => Ok(FitMode::Slice),
        _ => Err(make_err()),
    }
}

enum ParseState {
    Defer,
    Align,
    Fit,
    Finished,
}

fn make_err() -> AttributeError {
    AttributeError::Parse(ParseError::new(
        "expected \"[defer] <align> [meet | slice]\"",
    ))
}

impl Parse for AspectRatio {
    type Data = ();
    type Err = AttributeError;

    fn parse(s: &str, _: ()) -> Result<AspectRatio, AttributeError> {
        let mut defer = false;
        let mut align = Some(AlignMode::XmidYmid);
        let mut fit_mode = FitMode::Meet;

        let mut state = ParseState::Defer;

        for v in s.split_whitespace() {
            match state {
                ParseState::Defer => {
                    if v == "defer" {
                        defer = true;
                        state = ParseState::Align;
                    } else {
                        align = parse_align(v)?;
                        state = ParseState::Fit;
                    }
                }

                ParseState::Align => {
                    align = parse_align(v)?;
                    state = ParseState::Fit;
                }

                ParseState::Fit => {
                    fit_mode = parse_fit_mode(v)?;
                    state = ParseState::Finished;
                }

                _ => {
                    return Err(make_err());
                }
            }
        }

        // The string must match "[defer] <align> [meet | slice]".
        // Since the meet|slice is optional, we can end up in either
        // of the following states:
        match state {
            ParseState::Fit | ParseState::Finished => {}
            _ => {
                return Err(make_err());
            }
        }

        Ok(AspectRatio {
            defer,
            align: match align {
                None => None,
                Some(align_mode) => Some(Align {
                    align: align_mode,
                    fit: fit_mode,
                }),
            },
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parsing_invalid_strings_yields_error() {
        assert!(AspectRatio::parse("", ()).is_err());

        assert!(AspectRatio::parse("defer", ()).is_err());

        assert!(AspectRatio::parse("defer foo", ()).is_err());

        assert!(AspectRatio::parse("defer xmidymid", ()).is_err());

        assert!(AspectRatio::parse("defer xMidYMid foo", ()).is_err());

        assert!(AspectRatio::parse("xmidymid", ()).is_err());

        assert!(AspectRatio::parse("xMidYMid foo", ()).is_err());

        assert!(AspectRatio::parse("defer xMidYMid meet foo", ()).is_err());
    }

    #[test]
    fn parses_valid_strings() {
        assert_eq!(
            AspectRatio::parse("defer none", ()),
            Ok(AspectRatio {
                defer: true,
                align: None,
            })
        );

        assert_eq!(
            AspectRatio::parse("xMidYMid", ()),
            Ok(AspectRatio {
                defer: false,
                align: Some(Align {
                    align: AlignMode::XmidYmid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMidYMid", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    align: AlignMode::XmidYmid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMinYMax", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    align: AlignMode::XminYmax,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMaxYMid meet", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    align: AlignMode::XmaxYmid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMinYMax slice", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    align: AlignMode::XminYmax,
                    fit: FitMode::Slice,
                }),
            })
        );
    }

    #[test]
    fn aligns() {
        assert_eq!(
            AspectRatio::parse("xMinYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMinYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMinYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -99.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -99.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0
            ),
            (0.0, -99.0, 10.0, 100.0)
        );
    }
}
<commit_msg>aspect_ratio.rs: split AlignMode into (x,y)<commit_after>//! Handling of `preserveAspectRatio` values
//!
//! This module handles `preserveAspectRatio` values [per the SVG specification][spec].
//! We have an [`AspectRatio`] struct which encapsulates such a value.
//!
//! ```ignore
//! assert_eq!(
//!     AspectRatio::parse("xMidYMid", ()),
//!     Ok(AspectRatio {
//!         defer: false,
//!         align: Some(Align {
//!             x: Align1D::Mid,
//!             y: Align1D::Mid,
//!             fit: FitMode::Meet,
//!         }),
//!     })
//! );
//! ```
//!
//! [`AspectRatio`]: struct.AspectRatio.html
//! [spec]: https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute

use error::*;
use parsers::Parse;
use parsers::ParseError;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct AspectRatio {
    defer: bool,
    align: Option<Align>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum FitMode {
    Meet,
    Slice,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct Align {
    x: Align1D,
    y: Align1D,
    fit: FitMode,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Align1D {
    Min,
    Mid,
    Max,
}

impl Align1D {
    fn compute(self, dest_pos: f64, dest_size: f64, obj_size: f64) -> f64 {
        match self {
            Align1D::Min => dest_pos,
            Align1D::Mid => dest_pos + (dest_size - obj_size) / 2.0,
            Align1D::Max => dest_pos + dest_size - obj_size,
        }
    }
}

impl AspectRatio {
    pub fn is_slice(&self) -> bool {
        match self.align {
            Some(Align { fit: FitMode::Slice, .. }) => true,

            _ => false,
        }
    }

    pub fn compute(
        &self,
        object_width: f64,
        object_height: f64,
        dest_x: f64,
        dest_y: f64,
        dest_width: f64,
        dest_height: f64,
    ) -> (f64, f64, f64, f64) {
        match self.align {
            None => (dest_x, dest_y, dest_width, dest_height),

            Some(Align { x, y, fit }) => {
                let w_factor = dest_width / object_width;
                let h_factor = dest_height / object_height;
                let factor = match fit {
                    FitMode::Meet => w_factor.min(h_factor),
                    FitMode::Slice => w_factor.max(h_factor),
                };

                let w = object_width * factor;
                let h = object_height * factor;

                let xpos = x.compute(dest_x, dest_width, w);
                let ypos = y.compute(dest_y, dest_height, h);

                (xpos, ypos, w, h)
            }
        }
    }
}

impl Default for AspectRatio {
    fn default() -> AspectRatio {
        AspectRatio {
            defer: false,
            align: Some(Align::default()),
        }
    }
}

impl Default for FitMode {
    fn default() -> FitMode {
        FitMode::Meet
    }
}

impl Default for Align {
    fn default() -> Align {
        Align {
            x: Align1D::Mid,
            y: Align1D::Mid,
            fit: FitMode::default(),
        }
    }
}

impl Align {
    fn parse(s: &str) -> Result<Option<Align>, AttributeError> {
        match s {
            "none" => Ok(None),

            "xMinYMin" => Ok(Some((Align1D::Min, Align1D::Min))),
            "xMidYMin" => Ok(Some((Align1D::Mid, Align1D::Min))),
            "xMaxYMin" => Ok(Some((Align1D::Max, Align1D::Min))),

            "xMinYMid" => Ok(Some((Align1D::Min, Align1D::Mid))),
            "xMidYMid" => Ok(Some((Align1D::Mid, Align1D::Mid))),
            "xMaxYMid" => Ok(Some((Align1D::Max, Align1D::Mid))),

            "xMinYMax" => Ok(Some((Align1D::Min, Align1D::Max))),
            "xMidYMax" => Ok(Some((Align1D::Mid, Align1D::Max))),
            "xMaxYMax" => Ok(Some((Align1D::Max, Align1D::Max))),

            _ => Err(make_err()),
        }.map(|xy| {
            xy.map(|(x, y)| {
                Align {
                    x,
                    y,
                    fit: FitMode::default(),
                }
            })
        })
    }
}

impl FitMode {
    fn parse(s: &str) -> Result<FitMode, AttributeError> {
        match s {
            "meet" => Ok(FitMode::Meet),
            "slice" => Ok(FitMode::Slice),
            _ => Err(make_err()),
        }
    }
}

enum ParseState {
    Defer,
    Align,
    Fit,
    Finished,
}

fn make_err() -> AttributeError {
    AttributeError::Parse(ParseError::new(
        "expected \"[defer] <align> [meet | slice]\"",
    ))
}

impl Parse for AspectRatio {
    type Data = ();
    type Err = AttributeError;

    fn parse(s: &str, _: ()) -> Result<AspectRatio, AttributeError> {
        let mut defer = false;
        let mut align = Some(Align::default());
        let mut fit_mode = FitMode::Meet;

        let mut state = ParseState::Defer;

        for v in s.split_whitespace() {
            match state {
                ParseState::Defer => {
                    if v == "defer" {
                        defer = true;
                        state = ParseState::Align;
                    } else {
                        align = Align::parse(v)?;
                        state = ParseState::Fit;
                    }
                }

                ParseState::Align => {
                    align = Align::parse(v)?;
                    state = ParseState::Fit;
                }

                ParseState::Fit => {
                    fit_mode = FitMode::parse(v)?;
                    state = ParseState::Finished;
                }

                _ => {
                    return Err(make_err());
                }
            }
        }

        // The string must match "[defer] <align> [meet | slice]".
        // Since the meet|slice is optional, we can end up in either
        // of the following states:
        match state {
            ParseState::Fit | ParseState::Finished => {}
            _ => {
                return Err(make_err());
            }
        }

        Ok(AspectRatio {
            defer,
            align: match align {
                None => None,
                Some(Align { x, y, .. }) => Some(Align {
                    x,
                    y,
                    fit: fit_mode,
                }),
            },
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parsing_invalid_strings_yields_error() {
        assert!(AspectRatio::parse("", ()).is_err());

        assert!(AspectRatio::parse("defer", ()).is_err());

        assert!(AspectRatio::parse("defer foo", ()).is_err());

        assert!(AspectRatio::parse("defer xmidymid", ()).is_err());

        assert!(AspectRatio::parse("defer xMidYMid foo", ()).is_err());

        assert!(AspectRatio::parse("xmidymid", ()).is_err());

        assert!(AspectRatio::parse("xMidYMid foo", ()).is_err());

        assert!(AspectRatio::parse("defer xMidYMid meet foo", ()).is_err());
    }

    #[test]
    fn parses_valid_strings() {
        assert_eq!(
            AspectRatio::parse("defer none", ()),
            Ok(AspectRatio {
                defer: true,
                align: None,
            })
        );

        assert_eq!(
            AspectRatio::parse("xMidYMid", ()),
            Ok(AspectRatio {
                defer: false,
                align: Some(Align {
                    x: Align1D::Mid,
                    y: Align1D::Mid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMidYMid", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    x: Align1D::Mid,
                    y: Align1D::Mid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMinYMax", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    x: Align1D::Min,
                    y: Align1D::Max,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMaxYMid meet", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    x: Align1D::Max,
                    y: Align1D::Mid,
                    fit: FitMode::Meet,
                }),
            })
        );

        assert_eq!(
            AspectRatio::parse("defer xMinYMax slice", ()),
            Ok(AspectRatio {
                defer: true,
                align: Some(Align {
                    x: Align1D::Min,
                    y: Align1D::Max,
                    fit: FitMode::Slice,
                }),
            })
        );
    }

    #[test]
    fn aligns() {
        assert_eq!(
            AspectRatio::parse("xMinYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMinYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMinYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMinYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -99.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMidYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (4.95, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMidYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -99.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMin meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMin slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, 0.0, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMid meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMid slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -49.5, 10.0, 100.0)
        );

        assert_eq!(
            AspectRatio::parse("xMaxYMax meet", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (9.9, 0.0, 0.1, 1.0)
        );
        assert_eq!(
            AspectRatio::parse("xMaxYMax slice", ()).unwrap().compute(
                1.0,
                10.0,
                0.0,
                0.0,
                10.0,
                1.0,
            ),
            (0.0, -99.0, 10.0, 100.0)
        );
    }
}
<|endoftext|>


<commit_before>use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::process;

use anyhow::Context;
use protobuf_parse::Parser;

use crate::customize::CustomizeCallback;
use crate::customize::CustomizeCallbackHolder;
use crate::gen_and_write::gen_and_write;
use crate::Customize;

#[derive(Debug)]
enum WhichParser {
    Pure,
    Protoc,
}

impl Default for WhichParser {
    fn default() -> WhichParser {
        WhichParser::Pure
    }
}

/// Entry point for `.proto` to `.rs` code generation.
///
/// This is similar to `protoc --rust_out...`.
#[derive(Debug, Default)]
pub struct Codegen {
    /// What parser to use to parse `.proto` files.
    which_parser: Option<WhichParser>,
    /// Create out directory.
    create_out_dir: bool,
    /// --lang_out= param
    out_dir: PathBuf,
    /// -I args
    includes: Vec<PathBuf>,
    /// List of .proto files to compile
    inputs: Vec<PathBuf>,
    /// Customize code generation
    customize: Customize,
    /// Customize code generation
    customize_callback: CustomizeCallbackHolder,
    /// Protoc command path
    protoc: Option<PathBuf>,
    /// Extra `protoc` args
    protoc_extra_args: Vec<OsString>,
}

impl Codegen {
    /// Create new codegen object.
    ///
    /// Uses `protoc` from `$PATH` by default.
    ///
    /// Can be switched to pure rust parser using [`pure`](Self::pure) function.
    pub fn new() -> Self {
        Self::default()
    }

    /// Switch to pure Rust parser of `.proto` files.
    pub fn pure(&mut self) -> &mut Self {
        self.which_parser = Some(WhichParser::Pure);
        self
    }

    /// Switch to `protoc` parser of `.proto` files.
    pub fn protoc(&mut self) -> &mut Self {
        self.which_parser = Some(WhichParser::Protoc);
        self
    }

    /// Output directory for generated code.
    ///
    /// When invoking from `build.rs`, consider using
    /// [`cargo_out_dir`](Self::cargo_out_dir) instead.
    pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
        self.out_dir = out_dir.as_ref().to_owned();
        self
    }

    /// Set output directory relative to Cargo output dir.
    ///
    /// With this option, output directory is erased and recreated during invocation.
    pub fn cargo_out_dir(&mut self, rel: &str) -> &mut Self {
        let rel = Path::new(rel);
        let mut not_empty = false;
        for comp in rel.components() {
            match comp {
                Component::ParentDir => {
                    panic!("parent path in components of rel path: `{}`", rel.display());
                }
                Component::CurDir => {
                    continue;
                }
                Component::Normal(..) => {}
                Component::RootDir | Component::Prefix(..) => {
                    panic!("root dir in components of rel path: `{}`", rel.display());
                }
            }
            not_empty = true;
        }

        if !not_empty {
            panic!("empty rel path: `{}`", rel.display());
        }

        let cargo_out_dir = env::var("OUT_DIR").expect("OUT_DIR env var not set");
        let mut path = PathBuf::from(cargo_out_dir);
        path.push(rel);
        self.create_out_dir = true;
        self.out_dir(path)
    }

    /// Add an include directory.
    pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
        self.includes.push(include.as_ref().to_owned());
        self
    }

    /// Add include directories.
    pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
        for include in includes {
            self.include(include);
        }
        self
    }

    /// Append a `.proto` file path to compile
    pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
        self.inputs.push(input.as_ref().to_owned());
        self
    }

    /// Append multiple `.proto` file paths to compile
    pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
        for input in inputs {
            self.input(input);
        }
        self
    }

    /// Specify `protoc` command path to be used when invoking code generation.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # mod protoc_bin_vendored {
    /// #   pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> {
    /// #       unimplemented!()
    /// #   }
    /// # }
    ///
    /// use protobuf_codegen::Codegen;
    ///
    /// Codegen::new()
    ///     .protoc()
    ///     .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap())
    ///     // ...
    ///     .run()
    ///     .unwrap();
    /// ```
    ///
    /// This option is ignored when pure Rust parser is used.
    pub fn protoc_path(&mut self, protoc: &Path) -> &mut Self {
        self.protoc = Some(protoc.to_owned());
        self
    }

    /// Extra command line flags for `protoc` invocation.
    ///
    /// For example, `--experimental_allow_proto3_optional` option.
    ///
    /// This option is ignored when pure Rust parser is used.
    pub fn protoc_extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
        self.protoc_extra_args.push(arg.into());
        self
    }

    /// Set options to customize code generation
    pub fn customize(&mut self, customize: Customize) -> &mut Self {
        self.customize.update_with(&customize);
        self
    }

    /// Callback for dynamic per-element customization.
    pub fn customize_callback(&mut self, callback: impl CustomizeCallback) -> &mut Self {
        self.customize_callback = CustomizeCallbackHolder::new(callback);
        self
    }

    /// Invoke the code generation.
    ///
    /// This is roughly equivalent to `protoc --rust_out=...` but
    /// without requiring `protoc-gen-rust` command in `$PATH`.
    ///
    /// This function uses pure Rust parser or `protoc` parser depending on
    /// how this object was configured.
    pub fn run(&self) -> anyhow::Result<()> {
        if self.create_out_dir {
            if self.out_dir.exists() {
                fs::remove_dir_all(&self.out_dir)?;
            }
            fs::create_dir(&self.out_dir)?;
        }

        let mut parser = Parser::new();
        parser.protoc();
        if let Some(protoc) = &self.protoc {
            parser.protoc_path(protoc);
        }
        match &self.which_parser {
            Some(WhichParser::Protoc) => {
                parser.protoc();
            }
            Some(WhichParser::Pure) => {
                parser.pure();
            }
            None => {}
        }

        parser.inputs(&self.inputs);
        parser.includes(&self.includes);
        let parsed_and_typechecked = parser
            .parse_and_typecheck()
            .context("parse and typecheck")?;

        gen_and_write(
            &parsed_and_typechecked.file_descriptors,
            &parsed_and_typechecked.parser,
            &parsed_and_typechecked.relative_paths,
            &self.out_dir,
            &self.customize,
            &*self.customize_callback,
        )
    }

    /// Similar to `run`, but prints the message to stderr and exits the process on error.
    pub fn run_from_script(&self) {
        if let Err(e) = self.run() {
            eprintln!("codegen failed: {:#}", e);
            process::exit(1);
        }
    }
}
<commit_msg>Print stack trace on error<commit_after>use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::process;

use anyhow::Context;
use protobuf_parse::Parser;

use crate::customize::CustomizeCallback;
use crate::customize::CustomizeCallbackHolder;
use crate::gen_and_write::gen_and_write;
use crate::Customize;

#[derive(Debug)]
enum WhichParser {
    Pure,
    Protoc,
}

impl Default for WhichParser {
    fn default() -> WhichParser {
        WhichParser::Pure
    }
}

/// Entry point for `.proto` to `.rs` code generation.
///
/// This is similar to `protoc --rust_out...`.
#[derive(Debug, Default)]
pub struct Codegen {
    /// What parser to use to parse `.proto` files.
    which_parser: Option<WhichParser>,
    /// Create out directory.
    create_out_dir: bool,
    /// --lang_out= param
    out_dir: PathBuf,
    /// -I args
    includes: Vec<PathBuf>,
    /// List of .proto files to compile
    inputs: Vec<PathBuf>,
    /// Customize code generation
    customize: Customize,
    /// Customize code generation
    customize_callback: CustomizeCallbackHolder,
    /// Protoc command path
    protoc: Option<PathBuf>,
    /// Extra `protoc` args
    protoc_extra_args: Vec<OsString>,
}

impl Codegen {
    /// Create new codegen object.
    ///
    /// Uses `protoc` from `$PATH` by default.
    ///
    /// Can be switched to pure rust parser using [`pure`](Self::pure) function.
    pub fn new() -> Self {
        Self::default()
    }

    /// Switch to pure Rust parser of `.proto` files.
    pub fn pure(&mut self) -> &mut Self {
        self.which_parser = Some(WhichParser::Pure);
        self
    }

    /// Switch to `protoc` parser of `.proto` files.
    pub fn protoc(&mut self) -> &mut Self {
        self.which_parser = Some(WhichParser::Protoc);
        self
    }

    /// Output directory for generated code.
    ///
    /// When invoking from `build.rs`, consider using
    /// [`cargo_out_dir`](Self::cargo_out_dir) instead.
    pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
        self.out_dir = out_dir.as_ref().to_owned();
        self
    }

    /// Set output directory relative to Cargo output dir.
    ///
    /// With this option, output directory is erased and recreated during invocation.
    pub fn cargo_out_dir(&mut self, rel: &str) -> &mut Self {
        let rel = Path::new(rel);
        let mut not_empty = false;
        for comp in rel.components() {
            match comp {
                Component::ParentDir => {
                    panic!("parent path in components of rel path: `{}`", rel.display());
                }
                Component::CurDir => {
                    continue;
                }
                Component::Normal(..) => {}
                Component::RootDir | Component::Prefix(..) => {
                    panic!("root dir in components of rel path: `{}`", rel.display());
                }
            }
            not_empty = true;
        }

        if !not_empty {
            panic!("empty rel path: `{}`", rel.display());
        }

        let cargo_out_dir = env::var("OUT_DIR").expect("OUT_DIR env var not set");
        let mut path = PathBuf::from(cargo_out_dir);
        path.push(rel);
        self.create_out_dir = true;
        self.out_dir(path)
    }

    /// Add an include directory.
    pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
        self.includes.push(include.as_ref().to_owned());
        self
    }

    /// Add include directories.
    pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
        for include in includes {
            self.include(include);
        }
        self
    }

    /// Append a `.proto` file path to compile
    pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
        self.inputs.push(input.as_ref().to_owned());
        self
    }

    /// Append multiple `.proto` file paths to compile
    pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
        for input in inputs {
            self.input(input);
        }
        self
    }

    /// Specify `protoc` command path to be used when invoking code generation.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # mod protoc_bin_vendored {
    /// #   pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> {
    /// #       unimplemented!()
    /// #   }
    /// # }
    ///
    /// use protobuf_codegen::Codegen;
    ///
    /// Codegen::new()
    ///     .protoc()
    ///     .protoc_path(&protoc_bin_vendored::protoc_bin_path().unwrap())
    ///     // ...
    ///     .run()
    ///     .unwrap();
    /// ```
    ///
    /// This option is ignored when pure Rust parser is used.
    pub fn protoc_path(&mut self, protoc: &Path) -> &mut Self {
        self.protoc = Some(protoc.to_owned());
        self
    }

    /// Extra command line flags for `protoc` invocation.
    ///
    /// For example, `--experimental_allow_proto3_optional` option.
    ///
    /// This option is ignored when pure Rust parser is used.
    pub fn protoc_extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
        self.protoc_extra_args.push(arg.into());
        self
    }

    /// Set options to customize code generation
    pub fn customize(&mut self, customize: Customize) -> &mut Self {
        self.customize.update_with(&customize);
        self
    }

    /// Callback for dynamic per-element customization.
    pub fn customize_callback(&mut self, callback: impl CustomizeCallback) -> &mut Self {
        self.customize_callback = CustomizeCallbackHolder::new(callback);
        self
    }

    /// Invoke the code generation.
    ///
    /// This is roughly equivalent to `protoc --rust_out=...` but
    /// without requiring `protoc-gen-rust` command in `$PATH`.
    ///
    /// This function uses pure Rust parser or `protoc` parser depending on
    /// how this object was configured.
    pub fn run(&self) -> anyhow::Result<()> {
        if self.create_out_dir {
            if self.out_dir.exists() {
                fs::remove_dir_all(&self.out_dir)?;
            }
            fs::create_dir(&self.out_dir)?;
        }

        let mut parser = Parser::new();
        parser.protoc();
        if let Some(protoc) = &self.protoc {
            parser.protoc_path(protoc);
        }
        match &self.which_parser {
            Some(WhichParser::Protoc) => {
                parser.protoc();
            }
            Some(WhichParser::Pure) => {
                parser.pure();
            }
            None => {}
        }

        parser.inputs(&self.inputs);
        parser.includes(&self.includes);
        let parsed_and_typechecked = parser
            .parse_and_typecheck()
            .context("parse and typecheck")?;

        gen_and_write(
            &parsed_and_typechecked.file_descriptors,
            &parsed_and_typechecked.parser,
            &parsed_and_typechecked.relative_paths,
            &self.out_dir,
            &self.customize,
            &*self.customize_callback,
        )
    }

    /// Similar to `run`, but prints the message to stderr and exits the process on error.
    pub fn run_from_script(&self) {
        if let Err(e) = self.run() {
            eprintln!("codegen failed: {:?}", e);
            process::exit(1);
        }
    }
}
<|endoftext|>


<commit_before>//  Copyright (C) 2020 Éloïs SANCHEZ.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::net::{IpAddr, SocketAddr};

use crate::anti_spam::AntiSpam;
use crate::*;

pub struct BadRequest(pub anyhow::Error);

impl std::fmt::Debug for BadRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl warp::reject::Reject for BadRequest {}

struct GraphQlResponse(async_graphql::Response);
impl warp::reply::Reply for GraphQlResponse {
    fn into_response(self) -> warp::reply::Response {
        let mut resp = warp::reply::with_header(
            warp::reply::json(&self.0),
            "content-type",
            "application/json",
        )
        .into_response();
        add_cache_control(&mut resp, &self.0);
        resp
    }
}

fn add_cache_control(http_resp: &mut warp::reply::Response, resp: &async_graphql::Response) {
    if resp.is_ok() {
        if let Some(cache_control) = resp.cache_control.value() {
            if let Ok(value) = cache_control.parse() {
                http_resp.headers_mut().insert("cache-control", value);
            }
        }
    }
}

pub(crate) fn graphql(
    conf: &GvaConf,
    schema: GvaSchema,
    opts: async_graphql::http::MultipartOptions,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
    let anti_spam = AntiSpam::from(conf);
    let opts = Arc::new(opts);
    warp::path::path(conf.get_path())
        .and(warp::method())
        .and(warp::query::raw().or(warp::any().map(String::new)).unify())
        .and(warp::addr::remote())
        .and(warp::header::optional::<IpAddr>("X-Real-IP"))
        .and(warp::header::optional::<String>("content-type"))
        .and(warp::body::stream())
        .and(warp::any().map(move || opts.clone()))
        .and(warp::any().map(move || schema.clone()))
        .and(warp::any().map(move || anti_spam.clone()))
        .and_then(
            |method,
             query: String,
             remote_addr: Option<SocketAddr>,
             x_real_ip: Option<IpAddr>,
             content_type,
             body,
             opts: Arc<async_graphql::http::MultipartOptions>,
             schema,
             anti_spam: AntiSpam| async move {
                if anti_spam
                    .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip())))
                    .await
                {
                    if method == http::Method::GET {
                        let request: async_graphql::Request = serde_urlencoded::from_str(&query)
                            .map_err(|err| warp::reject::custom(BadRequest(err.into())))?;
                        Ok::<_, Rejection>((schema, request))
                    } else {
                        let request = async_graphql::http::receive_body(
                            content_type,
                            futures::TryStreamExt::map_err(body, |err| {
                                std::io::Error::new(std::io::ErrorKind::Other, err)
                            })
                            .map_ok(|mut buf| warp::Buf::to_bytes(&mut buf))
                            .into_async_read(),
                            async_graphql::http::MultipartOptions::clone(&opts),
                        )
                        .await
                        .map_err(|err| warp::reject::custom(BadRequest(err.into())))?;
                        Ok::<_, Rejection>((schema, request))
                    }
                } else {
                    Err(warp::reject::custom(BadRequest(anyhow::Error::msg(
                        r#"{ "error": "too many requests" }"#,
                    ))))
                }
            },
        )
        .and_then(
            |(schema, request): (GvaSchema, async_graphql::Request)| async move {
                Ok::<_, Infallible>(GraphQlResponse(schema.execute(request).await))
            },
        )
}

pub(crate) fn graphql_ws(
    conf: &GvaConf,
    schema: GvaSchema,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
    let anti_spam = AntiSpam::from(conf);
    warp::path::path(conf.get_subscriptions_path())
        .and(warp::addr::remote())
        .and(warp::header::optional::<IpAddr>("X-Real-IP"))
        .and(warp::ws())
        .and(warp::any().map(move || schema.clone()))
        .and(warp::any().map(move || anti_spam.clone()))
        .and_then(
            |remote_addr: Option<SocketAddr>,
             x_real_ip: Option<IpAddr>,
             ws: warp::ws::Ws,
             schema: GvaSchema,
             anti_spam: AntiSpam| async move {
                if anti_spam
                    .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip())))
                    .await
                {
                    Ok((ws, schema))
                } else {
                    Err(warp::reject::custom(BadRequest(anyhow::Error::msg(
                        r#"{ "error": "too many requests" }"#,
                    ))))
                }
            },
        )
        .and_then(|(ws, schema): (warp::ws::Ws, GvaSchema)| {
            let reply = ws.on_upgrade(move |websocket| {
                let (ws_sender, ws_receiver) = websocket.split();

                async move {
                    let _ = async_graphql::http::WebSocket::new(
                        schema,
                        ws_receiver
                            .take_while(|msg| futures::future::ready(msg.is_ok()))
                            .map(Result::unwrap)
                            .map(warp::ws::Message::into_bytes),
                        async_graphql::http::WebSocketProtocols::GraphQLWS,
                    )
                    .map(warp::ws::Message::text)
                    .map(Ok)
                    .forward(ws_sender)
                    .await;
                }
            });

            futures::future::ready(Ok::<_, Rejection>(warp::reply::with_header(
                reply,
                "Sec-WebSocket-Protocol",
                "graphql-ws",
            )))
        })
}
<commit_msg>[fix]gva:subs:use subscriptions-transport-ws proto instead of graphql-ws<commit_after>//  Copyright (C) 2020 Éloïs SANCHEZ.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::net::{IpAddr, SocketAddr};

use crate::anti_spam::AntiSpam;
use crate::*;

pub struct BadRequest(pub anyhow::Error);

impl std::fmt::Debug for BadRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl warp::reject::Reject for BadRequest {}

struct GraphQlResponse(async_graphql::Response);
impl warp::reply::Reply for GraphQlResponse {
    fn into_response(self) -> warp::reply::Response {
        let mut resp = warp::reply::with_header(
            warp::reply::json(&self.0),
            "content-type",
            "application/json",
        )
        .into_response();
        add_cache_control(&mut resp, &self.0);
        resp
    }
}

fn add_cache_control(http_resp: &mut warp::reply::Response, resp: &async_graphql::Response) {
    if resp.is_ok() {
        if let Some(cache_control) = resp.cache_control.value() {
            if let Ok(value) = cache_control.parse() {
                http_resp.headers_mut().insert("cache-control", value);
            }
        }
    }
}

pub(crate) fn graphql(
    conf: &GvaConf,
    schema: GvaSchema,
    opts: async_graphql::http::MultipartOptions,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
    let anti_spam = AntiSpam::from(conf);
    let opts = Arc::new(opts);
    warp::path::path(conf.get_path())
        .and(warp::method())
        .and(warp::query::raw().or(warp::any().map(String::new)).unify())
        .and(warp::addr::remote())
        .and(warp::header::optional::<IpAddr>("X-Real-IP"))
        .and(warp::header::optional::<String>("content-type"))
        .and(warp::body::stream())
        .and(warp::any().map(move || opts.clone()))
        .and(warp::any().map(move || schema.clone()))
        .and(warp::any().map(move || anti_spam.clone()))
        .and_then(
            |method,
             query: String,
             remote_addr: Option<SocketAddr>,
             x_real_ip: Option<IpAddr>,
             content_type,
             body,
             opts: Arc<async_graphql::http::MultipartOptions>,
             schema,
             anti_spam: AntiSpam| async move {
                if anti_spam
                    .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip())))
                    .await
                {
                    if method == http::Method::GET {
                        let request: async_graphql::Request = serde_urlencoded::from_str(&query)
                            .map_err(|err| warp::reject::custom(BadRequest(err.into())))?;
                        Ok::<_, Rejection>((schema, request))
                    } else {
                        let request = async_graphql::http::receive_body(
                            content_type,
                            futures::TryStreamExt::map_err(body, |err| {
                                std::io::Error::new(std::io::ErrorKind::Other, err)
                            })
                            .map_ok(|mut buf| warp::Buf::to_bytes(&mut buf))
                            .into_async_read(),
                            async_graphql::http::MultipartOptions::clone(&opts),
                        )
                        .await
                        .map_err(|err| warp::reject::custom(BadRequest(err.into())))?;
                        Ok::<_, Rejection>((schema, request))
                    }
                } else {
                    Err(warp::reject::custom(BadRequest(anyhow::Error::msg(
                        r#"{ "error": "too many requests" }"#,
                    ))))
                }
            },
        )
        .and_then(
            |(schema, request): (GvaSchema, async_graphql::Request)| async move {
                Ok::<_, Infallible>(GraphQlResponse(schema.execute(request).await))
            },
        )
}

pub(crate) fn graphql_ws(
    conf: &GvaConf,
    schema: GvaSchema,
) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
    let anti_spam = AntiSpam::from(conf);
    warp::path::path(conf.get_subscriptions_path())
        .and(warp::addr::remote())
        .and(warp::header::optional::<IpAddr>("X-Real-IP"))
        .and(warp::ws())
        .and(warp::any().map(move || schema.clone()))
        .and(warp::any().map(move || anti_spam.clone()))
        .and_then(
            |remote_addr: Option<SocketAddr>,
             x_real_ip: Option<IpAddr>,
             ws: warp::ws::Ws,
             schema: GvaSchema,
             anti_spam: AntiSpam| async move {
                if anti_spam
                    .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip())))
                    .await
                {
                    Ok((ws, schema))
                } else {
                    Err(warp::reject::custom(BadRequest(anyhow::Error::msg(
                        r#"{ "error": "too many requests" }"#,
                    ))))
                }
            },
        )
        .and_then(|(ws, schema): (warp::ws::Ws, GvaSchema)| {
            let reply = ws.on_upgrade(move |websocket| {
                let (ws_sender, ws_receiver) = websocket.split();

                async move {
                    let _ = async_graphql::http::WebSocket::new(
                        schema,
                        ws_receiver
                            .take_while(|msg| futures::future::ready(msg.is_ok()))
                            .map(Result::unwrap)
                            .map(warp::ws::Message::into_bytes),
                        async_graphql::http::WebSocketProtocols::SubscriptionsTransportWS,
                    )
                    .map(warp::ws::Message::text)
                    .map(Ok)
                    .forward(ws_sender)
                    .await;
                }
            });

            futures::future::ready(Ok::<_, Rejection>(warp::reply::with_header(
                reply,
                "Sec-WebSocket-Protocol",
                "graphql-ws",
            )))
        })
}
<|endoftext|>


<commit_before>// pest. The Elegant Parser
// Copyright (c) 2018 Dragoș Tiselice
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.

use std::fmt;
use std::hash::{Hash, Hasher};
use std::ptr;
use std::rc::Rc;
use std::str;

use super::flat_pairs::{self, FlatPairs};
use super::pair::{self, Pair};
use super::queueable_token::QueueableToken;
use super::tokens::{self, Tokens};
use RuleType;

/// An iterator over [`Pair`]s. It is created by [`pest::state`] and [`Pair::into_inner`].
///
/// [`Pair`]: struct.Pair.html
/// [`pest::state`]: ../fn.state.html
/// [`Pair::into_inner`]: struct.Pair.html#method.into_inner
#[derive(Clone)]
pub struct Pairs<'i, R> {
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &'i str,
    start: usize,
    end: usize
}

pub fn new<R: RuleType>(
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &str,
    start: usize,
    end: usize
) -> Pairs<R> {
    Pairs {
        queue,
        input,
        start,
        end
    }
}

impl<'i, R: RuleType> Pairs<'i, R> {
    /// Captures a slice from the `&str` defined by the starting position of the first token `Pair`
    /// and the ending position of the last token `Pair` of the `Pairs`. This also captures
    /// the input between those two token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.as_str(), "a b");
    /// ```
    #[inline]
    pub fn as_str(&self) -> &'i str {
        let start = self.pos(self.start);
        let end = self.pos(self.end - 1);

        // Generated positions always come from Positions and are UTF-8 borders.
        &self.input[start..end]
    }

    /// Captures inner token `Pair`s and concatenates resulting `&str`s. This does not capture
    /// the input between token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.concat(), "ab");
    /// ```
    #[inline]
    pub fn concat(&self) -> String {
        self.clone().fold(String::new(), |string, pair| string + pair.as_str())
    }

    /// Flattens the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating nested Token pair with Rule::b inside Rule::a
    /// #     state.rule(Rule::a, |state| {
    /// #         state.rule(Rule::b, |s| Ok(s))
    /// #     })
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.flatten().tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 4);
    /// ```
    #[inline]
    pub fn flatten(self) -> FlatPairs<'i, R> {
        flat_pairs::new(self.queue, self.input, self.start, self.end)
    }

    /// Returns the `Tokens` for the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pair with Rule::a ...
    /// #     state.rule(Rule::a, |s| Ok(s))
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 2);
    /// ```
    #[inline]
    pub fn tokens(self) -> Tokens<'i, R> {
        tokens::new(self.queue, self.input, self.start, self.end)
    }

    fn pair(&self) -> usize {
        match self.queue[self.start] {
            QueueableToken::Start {
                end_token_index, ..
            } => end_token_index,
            _ => unreachable!()
        }
    }

    fn pair_from_end(&self) -> usize {
        match self.queue[self.end - 1] {
            QueueableToken::End {
                start_token_index, ..
            } => start_token_index,
            _ => unreachable!()
        }
    }

    fn pos(&self, index: usize) -> usize {
        match self.queue[index] {
            QueueableToken::Start { input_pos, .. } | QueueableToken::End { input_pos, .. } => {
                input_pos
            }
        }
    }
}

impl<'i, R: RuleType> Iterator for Pairs<'i, R> {
    type Item = Pair<'i, R>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.start >= self.end {
            return None;
        }

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.start);

        self.start = self.pair() + 1;

        Some(pair)
    }
}

impl<'i, R: RuleType> DoubleEndedIterator for Pairs<'i, R> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.end <= self.start {
            return None;
        }

        self.end = self.pair_from_end();

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.end);

        Some(pair)
    }
}

impl<'i, R: RuleType> fmt::Debug for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list().entries(self.clone()).finish()
    }
}

impl<'i, R: RuleType> fmt::Display for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "[{}]",
            self.clone()
                .map(|pair| format!("{}", pair))
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}

impl<'i, R: PartialEq> PartialEq for Pairs<'i, R> {
    fn eq(&self, other: &Pairs<'i, R>) -> bool {
        Rc::ptr_eq(&self.queue, &other.queue) && ptr::eq(self.input, other.input)
            && self.start == other.start && self.end == other.end
    }
}

impl<'i, R: Eq> Eq for Pairs<'i, R> {}

impl<'i, R: Hash> Hash for Pairs<'i, R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (&*self.queue as *const Vec<QueueableToken<R>>).hash(state);
        (self.input.as_bytes() as *const [u8]).hash(state);
        self.start.hash(state);
        self.end.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::super::super::Parser;
    use super::super::super::macros::tests::*;

    #[test]
    fn as_str() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.as_str(), "abcde");
    }

    #[test]
    fn concat() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.concat(), "abce");
    }

    #[test]
    fn pairs_debug() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{:?}", pairs),
            "[\
                Pair { rule: a, span: Span { str: \"abc\", start: 0, end: 3 }, inner: [\
                    Pair { rule: b, span: Span { str: \"b\", start: 1, end: 2 }, inner: [] }\
                ] }, \
                Pair { rule: c, span: Span { str: \"e\", start: 4, end: 5 }, inner: [] }\
            ]"
        .to_owned()
        );
    }

    #[test]
    fn pairs_display() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{}", pairs),
            "[a(0, 3, [b(1, 2)]), c(4, 5)]".to_owned()
        );
    }

    #[test]
    fn iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::a, Rule::c]
        );
    }

    #[test]
    fn double_ended_iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.rev().map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::c, Rule::a]
        );
    }
}
<commit_msg>Update pest/src/iterators/pairs.rs<commit_after>// pest. The Elegant Parser
// Copyright (c) 2018 Dragoș Tiselice
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.

use std::fmt;
use std::hash::{Hash, Hasher};
use std::ptr;
use std::rc::Rc;
use std::str;

use super::flat_pairs::{self, FlatPairs};
use super::pair::{self, Pair};
use super::queueable_token::QueueableToken;
use super::tokens::{self, Tokens};
use RuleType;

/// An iterator over [`Pair`]s. It is created by [`pest::state`] and [`Pair::into_inner`].
///
/// [`Pair`]: struct.Pair.html
/// [`pest::state`]: ../fn.state.html
/// [`Pair::into_inner`]: struct.Pair.html#method.into_inner
#[derive(Clone)]
pub struct Pairs<'i, R> {
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &'i str,
    start: usize,
    end: usize
}

pub fn new<R: RuleType>(
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &str,
    start: usize,
    end: usize
) -> Pairs<R> {
    Pairs {
        queue,
        input,
        start,
        end
    }
}

impl<'i, R: RuleType> Pairs<'i, R> {
    /// Captures a slice from the `&str` defined by the starting position of the first token `Pair`
    /// and the ending position of the last token `Pair` of the `Pairs`. This also captures
    /// the input between those two token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.as_str(), "a b");
    /// ```
    #[inline]
    pub fn as_str(&self) -> &'i str {
        let start = self.pos(self.start);
        let end = self.pos(self.end - 1);

        // Generated positions always come from Positions and are UTF-8 borders.
        &self.input[start..end]
    }

    /// Captures inner token `Pair`s and concatenates resulting `&str`s. This does not capture
    /// the input between token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.concat(), "ab");
    /// ```
    #[inline]
    pub fn concat(&self) -> String {
        self.clone().fold(String::new(), |string, pair| string + pair.as_str())
    }

    /// Flattens the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating nested Token pair with Rule::b inside Rule::a
    /// #     state.rule(Rule::a, |state| {
    /// #         state.rule(Rule::b, |s| Ok(s))
    /// #     })
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.flatten().tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 4);
    /// ```
    #[inline]
    pub fn flatten(self) -> FlatPairs<'i, R> {
        flat_pairs::new(self.queue, self.input, self.start, self.end)
    }

    /// Returns the `Tokens` for the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pair with Rule::a ...
    /// #     state.rule(Rule::a, |s| Ok(s))
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 2);
    /// ```
    #[inline]
    pub fn tokens(self) -> Tokens<'i, R> {
        tokens::new(self.queue, self.input, self.start, self.end)
    }

    fn pair(&self) -> usize {
        match self.queue[self.start] {
            QueueableToken::Start {
                end_token_index, ..
            } => end_token_index,
            _ => unreachable!()
        }
    }

    fn pair_from_end(&self) -> usize {
        match self.queue[self.end - 1] {
            QueueableToken::End {
                start_token_index, ..
            } => start_token_index,
            _ => unreachable!()
        }
    }

    fn pos(&self, index: usize) -> usize {
        match self.queue[index] {
            QueueableToken::Start { input_pos, .. } | QueueableToken::End { input_pos, .. } => {
                input_pos
            }
        }
    }
}

impl<'i, R: RuleType> Iterator for Pairs<'i, R> {
    type Item = Pair<'i, R>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.start >= self.end {
            return None;
        }

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.start);

        self.start = self.pair() + 1;

        Some(pair)
    }
}

impl<'i, R: RuleType> DoubleEndedIterator for Pairs<'i, R> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.end <= self.start {
            return None;
        }

        self.end = self.pair_from_end();

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.end);

        Some(pair)
    }
}

impl<'i, R: RuleType> fmt::Debug for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list().entries(self.clone()).finish()
    }
}

impl<'i, R: RuleType> fmt::Display for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "[{}]",
            self.clone()
                .map(|pair| format!("{}", pair))
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}

impl<'i, R: PartialEq> PartialEq for Pairs<'i, R> {
    fn eq(&self, other: &Pairs<'i, R>) -> bool {
        Rc::ptr_eq(&self.queue, &other.queue) && ptr::eq(self.input, other.input)
            && self.start == other.start && self.end == other.end
    }
}

impl<'i, R: Eq> Eq for Pairs<'i, R> {}

impl<'i, R: Hash> Hash for Pairs<'i, R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (&*self.queue as *const Vec<QueueableToken<R>>).hash(state);
        (self.input as *const str).hash(state);
        self.start.hash(state);
        self.end.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::super::super::Parser;
    use super::super::super::macros::tests::*;

    #[test]
    fn as_str() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.as_str(), "abcde");
    }

    #[test]
    fn concat() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.concat(), "abce");
    }

    #[test]
    fn pairs_debug() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{:?}", pairs),
            "[\
                Pair { rule: a, span: Span { str: \"abc\", start: 0, end: 3 }, inner: [\
                    Pair { rule: b, span: Span { str: \"b\", start: 1, end: 2 }, inner: [] }\
                ] }, \
                Pair { rule: c, span: Span { str: \"e\", start: 4, end: 5 }, inner: [] }\
            ]"
        .to_owned()
        );
    }

    #[test]
    fn pairs_display() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{}", pairs),
            "[a(0, 3, [b(1, 2)]), c(4, 5)]".to_owned()
        );
    }

    #[test]
    fn iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::a, Rule::c]
        );
    }

    #[test]
    fn double_ended_iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.rev().map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::c, Rule::a]
        );
    }
}
<|endoftext|>


<commit_before>//! List owned projects
//!
//! https://docs.gitlab.com/ce/api/projects.html#list-owned-projects
//!
//! # List owned projects
//!
//! Get a list of projects which are owned by the authenticated user.
//!
//! ```text
//! GET /projects/owned
//! ```
//!
//! Parameters:
//!
//! | Attribute | Type | Required | Description |
//! | --------- | ---- | -------- | ----------- |
//! | `archived` | boolean | no | Limit by archived status |
//! | `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
//! | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
//! | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
//! | `search` | string | no | Return list of authorized projects matching the search criteria |
<commit_msg>Add project owned<commit_after>//! List owned projects
//!
//! https://docs.gitlab.com/ce/api/projects.html#list-owned-projects
//!
//! # List owned projects
//!
//! Get a list of projects which are owned by the authenticated user.
//!
//! ```text
//! GET /projects/owned
//! ```
//!
//! Parameters:
//!
//! | Attribute | Type | Required | Description |
//! | --------- | ---- | -------- | ----------- |
//! | `archived` | boolean | no | Limit by archived status |
//! | `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
//! | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
//! | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
//! | `search` | string | no | Return list of authorized projects matching the search criteria |

use BuildQuery;

#[derive(Debug, Copy, Clone)]
pub enum ListingVisibility {
    Public,
    Internal,
    Private,
}

#[derive(Debug, Copy, Clone)]
pub enum ListingOrderBy {
    Id,
    Name,
    Path,
    CreatedAt,
    UpdatedAt,
    LastActivityAt,
}

#[derive(Debug, Copy, Clone)]
pub enum ListingSort {
    Asc,
    Desc,
}

#[derive(Default, Debug, Clone)]
pub struct Listing {
    /// Limit by archived status
    archived:  Option<bool>,
    /// Limit by visibility.
    visibility: Option<ListingVisibility>,
    /// Return requests ordered by. Default is `ListingOrderBy::CreatedAt`.
    order_by:  Option<ListingOrderBy>,
    /// Return requests sorted. Default is `ListingSort::Desc`.
    sort: Option<ListingSort>,
    /// Return list of authorized projects matching the search criteria.
    search:  String,
}

impl Listing {
    pub fn new() -> Listing {
        Default::default()
    }
    pub fn archived(&mut self, archived: bool) -> &mut Listing {
        self.archived = Some(archived);
        self
    }
    pub fn visibility(&mut self, visibility: ListingVisibility) -> &mut Listing {
        self.visibility = Some(visibility);
        self
    }
    pub fn order_by(&mut self, order_by: ListingOrderBy) -> &mut Listing {
        self.order_by = Some(order_by);
        self
    }
    fn sort(&mut self, sort: ListingSort) -> &mut Listing {
        self.sort = Some(sort);
        self
    }
    pub fn search(&mut self, search: String) -> &mut Listing {
        self.search = search;
        self
    }
}

impl BuildQuery for Listing {
    fn build_query(&self) -> String {

        let mut query = String::from("projects/owned");

        let amp_char = "&";
        let none_char = "";
        let mut split_char = &none_char;

        // Append a "?" only if at least one of the `Option` is `Some(_)` or if
        // strings contain something.
        query.push_str(match (&self.archived,
                              &self.visibility,
                              &self.order_by,
                              &self.sort,
                              self.search.is_empty()) {
            (&None, &None, &None, &None, true) => "",
            _ => "?",
        });

        self.archived.map(|archived| {
            query.push_str(split_char);
            split_char = &amp_char;

            if archived {
                query.push_str("archived=true")
            } else {
                query.push_str("archived=false")
            }
        });

        self.visibility.map(|visibility| {
            query.push_str(split_char);
            split_char = &amp_char;

            query.push_str("visibility=");
            query.push_str(match visibility {
                ListingVisibility::Public => "public",
                ListingVisibility::Internal => "internal",
                ListingVisibility::Private => "private",
            });
        });

        self.order_by.map(|order_by| {
            query.push_str(split_char);
            split_char = &amp_char;

            query.push_str("order_by=");
            query.push_str(match order_by {
                ListingOrderBy::Id => "id",
                ListingOrderBy::Name => "name",
                ListingOrderBy::Path => "path",
                ListingOrderBy::CreatedAt => "created_at",
                ListingOrderBy::UpdatedAt => "updated_at",
                ListingOrderBy::LastActivityAt => "last_activity_at",
            });
        });

        self.sort.map(|sort| {
            query.push_str(split_char);
            split_char = &amp_char;

            query.push_str("sort=");
            query.push_str(match sort {
                ListingSort::Asc => "asc",
                ListingSort::Desc => "desc",
            });
        });

        if !self.search.is_empty() {
            query.push_str(split_char);
            split_char = &amp_char;

            query.push_str("search=");
            query.push_str(&self.search);
        }

        query
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use BuildQuery;

    #[test]
    fn build_query_default() {
        let expected_string = "projects/owned";
        let listing: Listing = Default::default();
        let query = listing.build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned";
        let listing = Listing::new();
        let query = listing.build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn build_query_archived() {
        let expected_string = "projects/owned?archived=true";
        let query = Listing::new().archived(true).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?archived=false";
        let query = Listing::new().archived(false).build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn build_query_visibility() {
        let expected_string = "projects/owned?visibility=public";
        let query = Listing::new().visibility(ListingVisibility::Public).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?visibility=internal";
        let query = Listing::new().visibility(ListingVisibility::Internal).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?visibility=private";
        let query = Listing::new().visibility(ListingVisibility::Private).build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn build_query_order_by() {
        let expected_string = "projects/owned?order_by=id";
        let query = Listing::new().order_by(ListingOrderBy::Id).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?order_by=name";
        let query = Listing::new().order_by(ListingOrderBy::Name).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?order_by=path";
        let query = Listing::new().order_by(ListingOrderBy::Path).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?order_by=created_at";
        let query = Listing::new().order_by(ListingOrderBy::CreatedAt).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?order_by=updated_at";
        let query = Listing::new().order_by(ListingOrderBy::UpdatedAt).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?order_by=last_activity_at";
        let query = Listing::new().order_by(ListingOrderBy::LastActivityAt).build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn build_query_sort() {
        let expected_string = "projects/owned?sort=asc";
        let query = Listing::new().sort(ListingSort::Asc).build_query();
        assert_eq!(query, expected_string);

        let expected_string = "projects/owned?sort=desc";
        let query = Listing::new().sort(ListingSort::Desc).build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn build_query_search() {
        let expected_string = "projects/owned?search=SearchPattern";
        let query = Listing::new().search(String::from("SearchPattern")).build_query();
        assert_eq!(query, expected_string);
    }

    #[test]
    fn groups_build_query_multiple() {
        let expected_string = "projects/owned?archived=false&sort=asc";
        let query = Listing::new().archived(false).sort(ListingSort::Asc).build_query();
        assert_eq!(query, expected_string);
    }
}
<|endoftext|>