cargo-funnel 0.5.1

cargo subcommand to reorganize and sort your Cargo.toml
use toml_edit::{table, Item, Table};

struct OrderedField {
    name: &'static str,
    required_always: bool,
    required_publish: bool,
}

const PACKAGE_FIELDS_ORDER: [OrderedField; 27] = [
    OrderedField {
        name: "name",
        required_always: true,
        required_publish: true,
    },
    OrderedField {
        name: "version",
        required_always: true,
        required_publish: true,
    },
    OrderedField {
        name: "authors",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "edition",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "rust-version",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "description",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "documentation",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "readme",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "homepage",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "repository",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "license",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "license-file",
        required_always: false,
        required_publish: true,
    },
    OrderedField {
        name: "keywords",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "categories",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "workspace",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "build",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "links",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "exclude",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "include",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "publish",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "metadata",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "default-run",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "autobins",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "autoexamples",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "autotests",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "autobenches",
        required_always: false,
        required_publish: false,
    },
    OrderedField {
        name: "resolver",
        required_always: false,
        required_publish: false,
    },
];

pub fn sort_package_fields(package: &mut Table) -> Item {
    let is_publish = if let Some(publish) = package.get("publish") {
        publish.as_bool().unwrap()
    } else {
        true
    };

    let has_license_or_license_file =
        package.get("license").is_some() || package.get("license-file").is_some();
    let sorted_package =
        PACKAGE_FIELDS_ORDER
            .iter()
            .fold(table(), |mut acc, field| match package.remove(field.name) {
                Some(x)=>{
                    acc[field.name] = x;
                    acc
                }
                None => {
                    if is_publish {
                        if field.name == "license" || field.name == "license-file"  {
                            assert!(has_license_or_license_file, "one of [`license`, `license-file`] is required when publishing to crates.io");
                        } else {
                            assert!(!(field.required_publish || field.required_always), "missing required field: {}", field.name);
                        }
                    } else {
                        assert!(!field.required_always, "missing required field: {}", field.name);
                    }

                    acc
                }
            });
    let sorted_package = package.iter().fold(sorted_package, |mut acc, x| {
        acc[x.0] = x.1.clone();
        acc
    });
    sorted_package
}

#[cfg(test)]
mod util_test {
    use super::*;
    use toml_edit::Document;

    mod sort_package_fields_test {
        use super::*;
        use test_case::test_case;

        #[test]
        fn orders_all_fields_correctly() {
            let fields = r#"
            [package]
            workspace = "i don't like you"
            publish = false
            rust-version = 1.40
            metadata = []
            default-run = "oh my god.exe"
            autobins = false
            autoexamples = false
            autotests = false
            autobenches = false
            resolver = 2
            include = "out of control"
            exclude = "i don't like youuuuuuu"
            links = ["i don't the things you say"]
            build = "say it right to my face"
            authors = ["out of control", "don't wanna see me lose my mind"]
            license-file = "things u do.txt"
            license = "MIT"
            categories = ["can't", "get", "in", "my", "head"]
            keywords = []
            readme = "README.md"
            repository = "dont like u .git"
            homepage = "overwhelmed.com"
            documentation = "docs.rs/but im not"
            description = "words come over me, feels like im somebody else"
            edition = "2018"
            version = "1.0.0"
            name = "overwhelmed"
            "#;

            let mut toml = fields.parse::<Document>().expect("heck");

            let package_fields = toml["package"].as_table_mut().unwrap();
            toml["package"] = sort_package_fields(package_fields);
            insta::assert_snapshot!(format!("{}", toml.to_string()));
        }

        #[test]
        #[should_panic]
        fn panics_if_name_missing() {
            std::panic::set_hook(Box::new(|_| {}));
            let fields = r#"
            [package]
            version = "1.0.0"
            "#;

            let mut toml = fields.parse::<Document>().expect("heck");

            let package_fields = toml["package"].as_table_mut().unwrap();
            toml["package"] = sort_package_fields(package_fields);
        }

        #[test]
        #[should_panic]
        fn panics_if_version_missing() {
            std::panic::set_hook(Box::new(|_| {}));
            let fields = r#"
            [package]
            name = "back down"
            "#;

            let mut toml = fields.parse::<Document>().expect("heck");

            let package_fields = toml["package"].as_table_mut().unwrap();
            toml["package"] = sort_package_fields(package_fields);
        }

        #[test]
        fn only_requires_name_and_version_if_publish_false() {
            let fields = r#"
            [package]
            version = "1.0.0"
            name = "overwhelmed"
            publish = false
            "#;

            let mut toml = fields.parse::<Document>().expect("heck");

            let package_fields = toml["package"].as_table_mut().unwrap();
            toml["package"] = sort_package_fields(package_fields);
            insta::assert_snapshot!(format!("{}", toml.to_string()));
        }

        #[test_case("" => panics; "publish missing")]
        #[test_case("publish = true" => panics; "publish true")]
        #[test_case(r#"description= "check in""# => panics; "documentation missing")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
        "# => panics; "homepage missing")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
            homepage = "lie cheat steal"
        "# => panics; "repository missing")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
            homepage = "lie cheat steal"
            repository = "kill win"
        "# => panics; "readme missing")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
            homepage = "lie cheat steal"
            repository = "kill win"
            readme = "banging on my adversaries"
        "# => panics; "license")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
            homepage = "lie cheat steal"
            repository = "kill win"
            readme = "banging on my adversaries"
            license = "MIT"
        "# ; "all present")]
        #[test_case(r#"
            description= "shoulda been a dentist"
            documentation = "mom said it"
            homepage = "lie cheat steal"
            repository = "kill win"
            readme = "banging on my adversaries"
            license-file = "license.txt"
        "# ; "all present - license file")]
        fn requires_more_fields_if_publish_true_or_missing(extra_fields: &str) {
            let fields = r#"
            [package]
            version = "1.0.0"
            name = "overwhelmed"
            "#;
            let fields = format!("{}{}", fields, extra_fields);

            let mut toml = fields.parse::<Document>().expect("heck");

            let package_fields = toml["package"].as_table_mut().unwrap();
            toml["package"] = sort_package_fields(package_fields);
        }
    }
}