pochoir-lang 0.12.2

Custom parser and interpreter for the pochoir template engine
Documentation
//! Sort an array following some rules.
//!
//! If you want to sort [`Value::Number`]s or [`Value::String`]s, you should use the `sort`
//! function:
//!
//! - [`Value::Number`]s are sorted by their numerical value
//! - [`Value::String`]s are sorted in alphabetical order
//!
//! If you want to sort [`Value::Array`]s or [`Value::Object`]s, you should use the `sort_by`
//! function. It takes a list of indexes separated by `.`. The indexes can be strings (to index
//! objects) or positive numbers (to index arrays).
//!
//! ### Example
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid red;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">input</span><code><span class="fn">sort</span>([<span class="string">"foo"</span>, <span class="string">"bar"</span>, <span class="string">"baz"</span>])</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid red;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">output</span><code>[<span class="string">"bar"</span>, <span class="string">"baz"</span>, <span class="string">"foo"</span>]</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="position: relative; margin-top: 2rem; border-left: 2px solid blue;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">input</span><code><span class="fn">sort_by</span>([{ meta: [<span class="string">"foo"</span>, <span class="number">3</span>] }, { meta: [<span class="string">"bar"</span>, <span class="number">12</span>] }, { meta: [<span class="string">"qux"</span>, <span class="number">2</span>] }], <span class="string">"meta.1"</span>)</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid blue;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">output</span><code>[{ meta: [<span class="string">"qux"</span>, <span class="number">2</span>] }, { meta: [<span class="string">"foo"</span>, <span class="number">3</span>] }, { meta: [<span class="string">"bar"</span>, <span class="number">12</span>] }]</code></pre></div>
use std::cmp::Ordering;

use crate::{FunctionResult, Value};

fn index_value<'a>(val: &'a Value, index: &str) -> FunctionResult<&'a Value> {
    match val {
        Value::Object(obj) => {
            if let Some(res) = obj.get(index) {
                Ok(res)
            } else {
                Err(
                    format!("object `{val}` does not have the field {index}, failed to sort",)
                        .into(),
                )
            }
        }
        Value::Array(arr) => {
            if let Ok(index) = index.parse::<usize>() {
                if let Some(res) = arr.get(index) {
                    Ok(res)
                } else {
                    Err(
                        format!("array `{val}` does not have the index {index}, failed to sort",)
                            .into(),
                    )
                }
            } else {
                Err("array index must be a positive number".into())
            }
        }
        _ => Err(format!("{:?} cannot be indexed, failed to sort", val.type_name()).into()),
    }
}

fn compare_values(val1: &Value, val2: &Value) -> FunctionResult<Ordering> {
    match (val1, val2) {
        (Value::String(val1), Value::String(val2)) => Ok(val1.partial_cmp(val2).unwrap()),
        (Value::Number(val1), Value::Number(val2)) => Ok(val1.partial_cmp(val2).unwrap()),
        _ => Err(format!(
            "cannot compare {} with {}, failed to sort",
            val1.type_name(),
            val2.type_name(),
        )
        .into()),
    }
}

pub(crate) fn sort(mut val: Vec<Value>) -> FunctionResult<Vec<Value>> {
    let mut err = None;

    val.sort_by(|val1, val2| {
        if err.is_some() {
            // Quickly skip all remaining elements if there was an error
            return Ordering::Equal;
        }

        match compare_values(val1, val2) {
            Ok(ord) => ord,
            Err(e) => {
                // Only return the first error
                if err.is_none() {
                    err = Some(e);
                }

                Ordering::Equal
            }
        }
    });

    if let Some(err) = err {
        Err(err)
    } else {
        Ok(val)
    }
}

pub(crate) fn sort_by(mut val: Vec<Value>, by: String) -> FunctionResult<Vec<Value>> {
    let mut err = None;

    val.sort_by(|mut val1, mut val2| {
        if err.is_some() {
            // Quickly skip all remaining elements if there was an error
            return Ordering::Equal;
        }

        for index in by.split('.') {
            val1 = match index_value(val1, index) {
                Ok(val) => val,
                Err(e) => {
                    if err.is_none() {
                        err = Some(e);
                    }

                    return Ordering::Equal;
                }
            };
            val2 = match index_value(val2, index) {
                Ok(val) => val,
                Err(e) => {
                    if err.is_none() {
                        err = Some(e);
                    }

                    return Ordering::Equal;
                }
            };
        }

        match compare_values(val1, val2) {
            Ok(ord) => ord,
            Err(e) => {
                // Only return the first error
                if err.is_none() {
                    err = Some(e);
                }

                Ordering::Equal
            }
        }
    });

    if let Some(err) = err {
        Err(err)
    } else {
        Ok(val)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{object, IntoValue};

    #[test]
    #[allow(clippy::too_many_lines)]
    fn sort_test() {
        assert_eq!(
            sort(vec![
                "foo".into_value(),
                "bar".into_value(),
                "baz".into_value()
            ])
            .unwrap(),
            vec!["bar".into_value(), "baz".into_value(), "foo".into_value()],
        );

        assert_eq!(
            sort_by(
                vec![
                    object! {
                        "a" => object! {
                            "b" => "a",
                        },
                    }
                    .into_value(),
                    object! {
                        "a" => object! {
                            "b" => "d",
                        },
                    }
                    .into_value(),
                    object! {
                        "a" => object! {
                            "b" => "b",
                        },
                    }
                    .into_value(),
                    object! {
                        "a" => object! {
                            "b" => "c",
                        },
                    }
                    .into_value(),
                ],
                "a.b".to_string()
            )
            .unwrap(),
            vec![
                object! {
                    "a" => object! {
                        "b" => "a",
                    },
                }
                .into_value(),
                object! {
                    "a" => object! {
                        "b" => "b",
                    },
                }
                .into_value(),
                object! {
                    "a" => object! {
                        "b" => "c",
                    },
                }
                .into_value(),
                object! {
                    "a" => object! {
                        "b" => "d",
                    },
                }
                .into_value(),
            ],
        );

        assert_eq!(
            sort_by(
                vec![
                    vec!["About".into_value(), 12.into_value()].into_value(),
                    vec!["Login".into_value(), 10e10.into_value()].into_value(),
                    vec!["Home".into_value(), (-13).into_value()].into_value(),
                    vec!["Contact".into_value(), 24.12.into_value()].into_value(),
                    vec!["Signup".into_value(), 25.into_value()].into_value(),
                ],
                "1".to_string()
            )
            .unwrap(),
            vec![
                vec!["Home".into_value(), (-13).into_value()].into_value(),
                vec!["About".into_value(), 12.into_value()].into_value(),
                vec!["Contact".into_value(), 24.12.into_value()].into_value(),
                vec!["Signup".into_value(), 25.into_value()].into_value(),
                vec!["Login".into_value(), 10e10.into_value()].into_value(),
            ],
        );

        assert_eq!(
            sort_by(
                vec![
                    object! {
                        "meta" => vec!["foo".into_value(), 3.into_value()],
                    }
                    .into_value(),
                    object! {
                        "meta" => vec!["bar".into_value(), 12.into_value()],
                    }
                    .into_value(),
                    object! {
                        "meta" => vec!["qux".into_value(), 2.into_value()],
                    }
                    .into_value(),
                ],
                "meta.1".to_string()
            )
            .unwrap(),
            vec![
                object! {
                    "meta" => vec!["qux".into_value(), 2.into_value()],
                }
                .into_value(),
                object! {
                    "meta" => vec!["foo".into_value(), 3.into_value()],
                }
                .into_value(),
                object! {
                    "meta" => vec!["bar".into_value(), 12.into_value()],
                }
                .into_value(),
            ],
        );
    }
}