├── example ├── .gitignore ├── input │ ├── to_be_removed │ ├── .bashrc │ ├── test.toml │ ├── test.yaml │ └── test.json ├── expected_output │ ├── created_dir │ │ └── .gitkeep │ ├── .gitconfig │ ├── .bashrc │ ├── filled_template.txt │ ├── test.yaml │ ├── lines_present.txt │ ├── test.toml │ ├── test.json │ └── rust-logo.svg ├── checkers │ ├── vars.toml │ ├── lines_present.txt │ ├── test.zip │ ├── test.tar.gz │ ├── template.txt │ ├── folder │ │ ├── relative_local_file.toml │ │ └── local_file.toml │ ├── http_check_config_relative.toml │ ├── http_check_config.toml │ ├── copy.toml │ ├── unpack.toml │ ├── check-config.toml │ └── python-logo.svg ├── lines_present.txt ├── pyproject.toml ├── test_via_pyproject.sh ├── test.sh └── check-config-for-usage-doc.toml ├── tests └── resources │ └── checkers │ ├── entry_absent │ ├── 1 │ │ ├── input.toml │ │ ├── check-config.toml │ │ ├── expected_output.toml │ │ ├── input.yaml │ │ ├── expected_output.yaml │ │ ├── expected_output.json │ │ └── input.json │ └── 2 │ │ ├── expected_output.toml │ │ ├── expected_output.yaml │ │ ├── input.toml │ │ ├── input.yaml │ │ ├── check-config.toml │ │ ├── expected_output.json │ │ └── input.json │ ├── key_absent │ └── 1 │ │ ├── check-config.toml │ │ ├── expected_output.yaml │ │ ├── expected_output.toml │ │ ├── input.yaml │ │ ├── input.toml │ │ ├── expected_output.json │ │ └── input.json │ ├── entry_present │ ├── 1 │ │ ├── input.toml │ │ ├── check-config.toml │ │ ├── input.yaml │ │ ├── expected_output.toml │ │ ├── expected_output.yaml │ │ ├── input.json │ │ └── expected_output.json │ └── 2 │ │ ├── input.yaml │ │ ├── input.toml │ │ ├── check-config.toml │ │ ├── expected_output.toml │ │ ├── expected_output.yaml │ │ ├── input.json │ │ └── expected_output.json │ ├── key_value_regex_match │ ├── 1 │ │ ├── expected_output.yaml │ │ ├── expected_output.toml │ │ ├── expected_output.json │ │ ├── check-config.toml │ │ ├── input.yaml │ │ ├── input.toml │ │ └── input.json │ └── 2 │ │ ├── expected_output.yaml │ │ ├── expected_output.toml │ │ ├── expected_output.json │ │ ├── check-config.toml │ │ ├── input.yaml │ │ ├── input.toml │ │ └── input.json │ ├── key_value_regex_matched │ ├── 1 │ │ ├── expected_output.yaml │ │ ├── expected_output.toml │ │ ├── expected_output.json │ │ ├── check-config.toml │ │ ├── input.yaml │ │ ├── input.toml │ │ └── input.json │ └── 2 │ │ ├── expected_output.yaml │ │ ├── expected_output.toml │ │ ├── expected_output.json │ │ ├── check-config.toml │ │ ├── input.yaml │ │ ├── input.toml │ │ └── input.json │ └── key_value_present │ ├── 1 │ ├── check-config.toml │ ├── input.yaml │ ├── input.toml │ ├── expected_output.toml │ ├── expected_output.yaml │ ├── input.json │ └── expected_output.json │ └── 2 │ ├── check-config.toml │ ├── input.yaml │ ├── expected_output.yaml │ ├── input.toml │ ├── expected_output.toml │ ├── input.json │ └── expected_output.json ├── example_checkers ├── bash.toml ├── black.toml ├── mypy.toml ├── python.toml ├── ruff.toml └── ruff_.toml ├── .gitignore ├── src ├── mapping │ ├── mod.rs │ ├── generic.rs │ ├── yaml.rs │ └── json.rs ├── lib.rs ├── bin │ └── check-config.rs ├── file_types │ ├── json.rs │ ├── toml.rs │ ├── yaml.rs │ ├── mod.rs │ └── ini.rs ├── integration_test.rs ├── checkers │ ├── package │ │ ├── custom.rs │ │ ├── rust.rs │ │ ├── python.rs │ │ ├── package_absent.rs │ │ ├── package_present.rs │ │ ├── command.rs │ │ └── mod.rs │ ├── file │ │ ├── file_absent.rs │ │ ├── key_value_present.rs │ │ ├── key_absent.rs │ │ ├── dir_absent.rs │ │ ├── entry_absent.rs │ │ ├── dir_present.rs │ │ ├── entry_present.rs │ │ ├── lines_absent.rs │ │ ├── dir_copied.rs │ │ ├── key_value_regex_match.rs │ │ └── file_present.rs │ ├── test_helpers.rs │ ├── base.rs │ └── utils.rs └── cli.rs ├── docs ├── requirements.txt ├── support.md ├── examples.md ├── installation.md ├── features.md ├── alternatives.md ├── glossary.md ├── index.md └── usage.md ├── scripts └── mkdocs.sh ├── .readthedocs.yaml ├── .pre-commit-hook.yaml ├── mkdocs.yml ├── LICENSE ├── pyproject.toml ├── Cargo.toml ├── README.md ├── CHANGES.md └── .github └── workflows └── ci.yml /example/.gitignore: -------------------------------------------------------------------------------- 1 | output/ -------------------------------------------------------------------------------- /example/input/to_be_removed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/expected_output/created_dir/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/checkers/vars.toml: -------------------------------------------------------------------------------- 1 | [variables] 2 | date = "2025-10-10" 3 | -------------------------------------------------------------------------------- /example/input/.bashrc: -------------------------------------------------------------------------------- 1 | export KEY="SAMPLE" 2 | alias to_be_removed='ls' 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/input.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [1, 2] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/check-config.toml: -------------------------------------------------------------------------------- 1 | key.list = [2, 3, 4] 2 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/input.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [1, 2] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | match: true -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/expected_output.yaml: -------------------------------------------------------------------------------- 1 | match: false -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | match: true -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/expected_output.yaml: -------------------------------------------------------------------------------- 1 | match: false -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [1] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/input.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - 1 4 | - 2 -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | match = true 2 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/expected_output.toml: -------------------------------------------------------------------------------- 1 | match = false 2 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | match = true 2 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - 1 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/check-config.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [2, 3, 4] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/input.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - 1 4 | - 2 5 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/check-config.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bar" 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/expected_output.toml: -------------------------------------------------------------------------------- 1 | match = false 2 | -------------------------------------------------------------------------------- /example_checkers/bash.toml: -------------------------------------------------------------------------------- 1 | [[file_present]] 2 | file = ".bashrc" 3 | regex = "export KEY=.*" 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/expected_output.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [{ key = 1 }] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/expected_output.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - key: 1 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/input.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [{ key = 1 }, { key = 2 }] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/input.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - key: 1 4 | - key: 2 -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [1, 2, 3, 4] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/input.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - key: 1 4 | - key: 2 -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true 3 | } -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/input.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [{ key = 1 }, { key = 2 }] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false 3 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": true 3 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": false 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .venv 3 | .vscode 4 | test 5 | lcov.info 6 | output 7 | .docs/html 8 | uv.lock 9 | -------------------------------------------------------------------------------- /example/checkers/lines_present.txt: -------------------------------------------------------------------------------- 1 | These lines will be added to lines_present.txt together with a marker 2 | -------------------------------------------------------------------------------- /example/checkers/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrijken/check-config/HEAD/example/checkers/test.zip -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | -------------------------------------------------------------------------------- /example/checkers/test.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrijken/check-config/HEAD/example/checkers/test.tar.gz -------------------------------------------------------------------------------- /example/expected_output/.gitconfig: -------------------------------------------------------------------------------- 1 | [user] 2 | name = 3 | email = 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/check-config.toml: -------------------------------------------------------------------------------- 1 | key.list = [{ key = 3 }, { key = 2 }, { key = 4 }] 2 | -------------------------------------------------------------------------------- /example/expected_output/.bashrc: -------------------------------------------------------------------------------- 1 | export KEY="SAMPLE" 2 | 3 | alias ll='ls -alF' 4 | 5 | export DATE="2025-10-10" 6 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bar" 3 | version = "1.0" 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: bar 3 | version: '1.0' 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | version = "[0-9][0-9]" 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | version = "[0-9][0-9]" 3 | -------------------------------------------------------------------------------- /src/mapping/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod generic; 2 | pub(crate) mod json; 3 | pub(crate) mod toml; 4 | pub(crate) mod yaml; 5 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - 1 4 | - 2 5 | - 3 6 | - 4 7 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/check-config.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [{ key = 2 }, { key = 3 }, { key = 4 }] 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | version = "[0-9]\\.[0-9]" 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | version = "[0-9]\\.[0-9]" 3 | -------------------------------------------------------------------------------- /example/checkers/template.txt: -------------------------------------------------------------------------------- 1 | This file will be copied. 2 | During copy a date will be substituted for \${date}: ${date} 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod checkers; 2 | pub mod cli; 3 | mod file_types; 4 | mod integration_test; 5 | mod mapping; 6 | pub mod uri; 7 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/check-config.toml: -------------------------------------------------------------------------------- 1 | [dependencies.bar] 2 | version = "2.0" 3 | features = ["bar"] 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocstrings 3 | mkdocs-gen-files 4 | mkdocs-literate-nav 5 | mkdocs-material 6 | markdown-include 7 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/expected_output.toml: -------------------------------------------------------------------------------- 1 | [key] 2 | list = [{ key = 1 }, { key = 2 }, { key = 3 }, { key = 4 }] 3 | -------------------------------------------------------------------------------- /example/checkers/folder/relative_local_file.toml: -------------------------------------------------------------------------------- 1 | [[key_value_regex_matched]] 2 | file = "output/test.json" 3 | key.regex = "m[a-z]tches" 4 | -------------------------------------------------------------------------------- /example/checkers/http_check_config_relative.toml: -------------------------------------------------------------------------------- 1 | [[key_value_regex_matched]] 2 | file = "output/test.json" 3 | key.regex = "m[a-z]tches" 4 | -------------------------------------------------------------------------------- /example/expected_output/filled_template.txt: -------------------------------------------------------------------------------- 1 | This file will be copied. 2 | During copy a date will be substituted for ${date}: 2025-10-10 3 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/expected_output.yaml: -------------------------------------------------------------------------------- 1 | key: 2 | list: 3 | - key: 1 4 | - key: 2 5 | - key: 3 6 | - key: 4 7 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/expected_output.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: foo 3 | version: '1.0' 4 | dependencies: 5 | foo: '1.0' 6 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | We welcome issues, pull request and feature requests via [Github](https://github.com/mrijken/check-config). 4 | -------------------------------------------------------------------------------- /src/bin/check-config.rs: -------------------------------------------------------------------------------- 1 | use check_config::cli; 2 | use std::process::ExitCode; 3 | pub(crate) fn main() -> ExitCode { 4 | cli::cli() 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | 1 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/expected_output.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | foo = "1.0" 7 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | } 6 | } -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | 1, 5 | 2 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | 1, 5 | 2 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: foo 3 | version: '1.0' 4 | dependencies: 5 | bar: 6 | version: '1.0' 7 | foo: '1.0' -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "bar", 4 | "version": "1.0" 5 | } 6 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | bar = { version = "1.0" } 7 | foo = "1.0" 8 | -------------------------------------------------------------------------------- /example/checkers/http_check_config.toml: -------------------------------------------------------------------------------- 1 | include = ["http_check_config_relative.toml"] 2 | 3 | [[key_value_regex_matched]] 4 | file = "output/test.json" 5 | key.regex = "m[a-z]tches" 6 | -------------------------------------------------------------------------------- /example/input/test.toml: -------------------------------------------------------------------------------- 1 | to_be_removed = "value" 2 | regex = "matches" 3 | list = ["to_be_removed", "to_be_kept"] 4 | list2 = ["already_present"] 5 | 6 | [to_be_kept] 7 | key = "value" 8 | -------------------------------------------------------------------------------- /example/checkers/folder/local_file.toml: -------------------------------------------------------------------------------- 1 | include = ["config:relative_local_file.toml"] 2 | 3 | [[entry_present]] 4 | file = "output/test.yaml" 5 | entry.list = ["to_be_added", "to_be_kept"] 6 | -------------------------------------------------------------------------------- /example/input/test.yaml: -------------------------------------------------------------------------------- 1 | to_be_removed: value 2 | to_be_kept: 3 | key: value 4 | list: 5 | - to_be_kept 6 | - to_be_removed 7 | list2: 8 | - already_present 9 | regex: matches 10 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | { 5 | "key": 1 6 | } 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" 4 | dependencies: 5 | bar: 6 | version: "1.0" 7 | features: 8 | - foo 9 | toml: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" 4 | dependencies: 5 | bar: 6 | version: "1.0" 7 | features: 8 | - foo 9 | toml: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" 4 | dependencies: 5 | bar: 6 | version: "1.0" 7 | features: 8 | - foo 9 | toml: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" 4 | dependencies: 5 | bar: 6 | version: "1.0" 7 | features: 8 | - foo 9 | toml: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/input.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "foo" 3 | version: "1.0" 4 | dependencies: 5 | bar: 6 | version: "1.0" 7 | features: 8 | - foo 9 | toml: "1.0" -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | 1, 5 | 2, 6 | 3, 7 | 4 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "foo": "1.0" 4 | }, 5 | "package": { 6 | "name": "foo", 7 | "version": "1.0" 8 | } 9 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/expected_output.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: foo 3 | version: '1.0' 4 | dependencies: 5 | bar: 6 | version: '2.0' 7 | features: 8 | - bar 9 | toml: '1.0' 10 | -------------------------------------------------------------------------------- /example/lines_present.txt: -------------------------------------------------------------------------------- 1 | # text from file config:lines_present.txt (check-config start) 2 | These lines will be added to lines_present.txt together with a marker 3 | # text from file config:lines_present.txt (check-config end) 4 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "1.0" 10 | features = ["foo"] 11 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "1.0" 10 | features = ["foo"] 11 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "1.0" 10 | features = ["foo"] 11 | -------------------------------------------------------------------------------- /example/expected_output/test.yaml: -------------------------------------------------------------------------------- 1 | regex: matches 2 | to_be_kept: 3 | key: value 4 | list: 5 | - to_be_kept 6 | - to_be_added 7 | list2: 8 | - already_present 9 | to_be_added: value 10 | placeholder_regex: matches 11 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "1.0" 10 | features = ["foo"] 11 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/input.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "1.0" 10 | features = ["foo"] 11 | -------------------------------------------------------------------------------- /example/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example" 3 | version = "0.1.0" 4 | 5 | [tool.check-config] 6 | include = ["config:checkers/check-config.toml"] 7 | 8 | [tool.ruff] 9 | target-version = "py312" 10 | line-length = 120 11 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/expected_output.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foo" 3 | version = "1.0" 4 | 5 | [dependencies] 6 | toml = "1.0" 7 | 8 | [dependencies.bar] 9 | version = "2.0" 10 | features = ["bar"] 11 | -------------------------------------------------------------------------------- /example/expected_output/lines_present.txt: -------------------------------------------------------------------------------- 1 | # text from file config:lines_present.txt (check-config start) 2 | These lines will be added to lines_present.txt together with a marker 3 | # text from file config:lines_present.txt (check-config end) 4 | -------------------------------------------------------------------------------- /scripts/mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | uv venv 3 | uv pip install --upgrade --no-cache-dir mkdocs 4 | uv pip install -r docs/requirements.txt 5 | uv run mkdocs build --clean --site-dir .docs/html --config-file mkdocs.yml 6 | rm uv.lock 7 | rm -r .venv 8 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_absent/2/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | { 5 | "key": 1 6 | }, 7 | { 8 | "key": 2 9 | } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | { 5 | "key": 1 6 | }, 7 | { 8 | "key": 2 9 | } 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /example/expected_output/test.toml: -------------------------------------------------------------------------------- 1 | regex = "matches" 2 | list = ["to_be_kept", "to_be_added"] 3 | list2 = ["already_present"] 4 | to_be_added = "value" 5 | placeholder_regex = "matches" 6 | 7 | [to_be_kept] 8 | key = "value" 9 | 10 | [added] 11 | key = "value" 12 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_absent/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0" 9 | }, 10 | "foo": "1.0" 11 | } 12 | } -------------------------------------------------------------------------------- /example/input/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [ 3 | "to_be_removed", 4 | "to_be_kept" 5 | ], 6 | "list2": ["already_present"], 7 | "to_be_removed": "foo", 8 | "to_be_kept": { 9 | "key": "value" 10 | }, 11 | "regex": "matches" 12 | } 13 | -------------------------------------------------------------------------------- /example/test_via_pyproject.sh: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=$(dirname "$BASH_SOURCE") 2 | cd $SCRIPT_DIR 3 | rm -r output 4 | cp -r input output 5 | cargo run -- --fix -vv 6 | diff -w -B expected_output output 7 | if [ $? -eq 0 ]; then 8 | echo "Expected output matches actual output" 9 | else 10 | echo "Expected output does not match actual output" 11 | fi 12 | -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0", 9 | "features": [ 10 | "foo" 11 | ] 12 | }, 13 | "toml": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0", 9 | "features": [ 10 | "foo" 11 | ] 12 | }, 13 | "toml": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_match/2/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0", 9 | "features": [ 10 | "foo" 11 | ] 12 | }, 13 | "toml": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/1/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0", 9 | "features": [ 10 | "foo" 11 | ] 12 | }, 13 | "toml": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_regex_matched/2/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "name": "foo", 4 | "version": "1.0" 5 | }, 6 | "dependencies": { 7 | "bar": { 8 | "version": "1.0", 9 | "features": [ 10 | "foo" 11 | ] 12 | }, 13 | "toml": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/resources/checkers/key_value_present/2/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bar": { 4 | "features": [ 5 | "bar" 6 | ], 7 | "version": "2.0" 8 | }, 9 | "toml": "1.0" 10 | }, 11 | "package": { 12 | "name": "foo", 13 | "version": "1.0" 14 | } 15 | } -------------------------------------------------------------------------------- /example/test.sh: -------------------------------------------------------------------------------- 1 | SCRIPT_DIR=$(dirname "$0") 2 | cd "$SCRIPT_DIR" || exit 3 | rm -r output 4 | cp -r input output 5 | cargo run -- --fix -vvv --skip-tags not_selected -p pyproject.toml 6 | diff -w -B expected_output output 7 | if diff -w -B expected_output output; then 8 | echo "Expected output matches actual output" 9 | else 10 | echo "Expected output does not match actual output" 11 | fi 12 | -------------------------------------------------------------------------------- /tests/resources/checkers/entry_present/2/expected_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": { 3 | "list": [ 4 | { 5 | "key": 1 6 | }, 7 | { 8 | "key": 2 9 | }, 10 | { 11 | "key": 3 12 | }, 13 | { 14 | "key": 4 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /example/expected_output/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "added": { 3 | "key": "1" 4 | }, 5 | "list": [ 6 | "to_be_kept", 7 | "to_be_added" 8 | ], 9 | "list2": [ 10 | "already_present" 11 | ], 12 | "placeholder_regex": "matches", 13 | "regex": "matches", 14 | "to_be_added": "value", 15 | "to_be_kept": { 16 | "key": "value" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Python 4 | 5 | ```toml 6 | {! ../example_checkers/python.toml!} 7 | ``` 8 | 9 | ## Ruff 10 | 11 | ```toml 12 | {! ../example_checkers/ruff.toml!} 13 | ``` 14 | 15 | ## Black 16 | 17 | ```toml 18 | {! ../example_checkers/black.toml!} 19 | ``` 20 | 21 | ## Mypy 22 | 23 | ```toml 24 | {! ../example_checkers/mypy.toml!} 25 | ``` 26 | 27 | ## Bashrc 28 | 29 | ```toml 30 | {! ../example_checkers/bash.toml!} 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /example/check-config-for-usage-doc.toml: -------------------------------------------------------------------------------- 1 | [[lines_present]] 2 | file = "~/.bashrc" 3 | lines = "export SHELL=/bin/bash" 4 | 5 | [[lines_present]] 6 | file = "~/.bashrc" 7 | lines = "export EDITOR=hx" 8 | 9 | 10 | [[key_value_present]] 11 | file = "test.json" 12 | key1.key2.key3="value" 13 | 14 | 15 | [[key_value_present]] 16 | file = "test.toml" 17 | inline_table.key="value" 18 | 19 | 20 | [[key_value_present.table]] 21 | file = "test.toml" 22 | key="value" 23 | -------------------------------------------------------------------------------- /example_checkers/black.toml: -------------------------------------------------------------------------------- 1 | [[key_value_present]] 2 | file = "pyproject.toml" 3 | 4 | [key_value_present.key.tool.black] 5 | line-length = 120 6 | exclude = "(/(notebooks|\\.git|\\.venv)/)" 7 | 8 | [[entry_present]] 9 | file = ".pre-commit-config.yaml" 10 | key.repos = [ 11 | { repo = "local", hooks = [ 12 | { id = "black", name = "black", language = "system", entry = "poetry run black .", pass_filenames = false, always_run = true }, 13 | ] }, 14 | ] 15 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The preferred installation is via [uv](https://docs.astral.sh/uv/guides/tools/). 4 | 5 | ```shell 6 | uv tool install check-config 7 | ``` 8 | 9 | As it is packages as a Python application, you can also install it via pip(x): 10 | 11 | ```shell 12 | pip install check-config 13 | pipx install check-config 14 | ``` 15 | 16 | Alternatively you can use, which does not need Python: 17 | 18 | ```shell 19 | cargo install check-config 20 | ``` 21 | -------------------------------------------------------------------------------- /src/file_types/json.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::base::CheckError; 2 | 3 | use super::FileType; 4 | 5 | pub(crate) struct Json {} 6 | 7 | impl Json { 8 | pub(crate) fn new() -> Json { 9 | Json {} 10 | } 11 | } 12 | 13 | impl FileType for Json { 14 | fn to_mapping( 15 | &self, 16 | contents: &str, 17 | ) -> Result, CheckError> { 18 | crate::mapping::json::from_string(contents) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/file_types/toml.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::base::CheckError; 2 | 3 | use super::FileType; 4 | 5 | pub(crate) struct Toml {} 6 | 7 | impl Toml { 8 | pub(crate) fn new() -> Toml { 9 | Toml {} 10 | } 11 | } 12 | 13 | impl FileType for Toml { 14 | fn to_mapping( 15 | &self, 16 | contents: &str, 17 | ) -> Result, CheckError> { 18 | crate::mapping::toml::from_string(contents) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/file_types/yaml.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::base::CheckError; 2 | 3 | use super::FileType; 4 | 5 | pub(crate) struct Yaml {} 6 | 7 | impl Yaml { 8 | pub(crate) fn new() -> Yaml { 9 | Yaml {} 10 | } 11 | } 12 | 13 | impl FileType for Yaml { 14 | fn to_mapping( 15 | &self, 16 | contents: &str, 17 | ) -> Result, CheckError> { 18 | crate::mapping::yaml::from_string(contents) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/file_types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod json; 2 | 3 | use crate::{checkers::base::CheckError, mapping::generic::Mapping}; 4 | pub mod toml; 5 | pub mod yaml; 6 | 7 | #[derive(PartialEq, Clone, Debug)] 8 | pub enum RegexValidateResult { 9 | Valid, 10 | Invalid { 11 | key: String, 12 | regex: String, 13 | found: String, 14 | }, 15 | } 16 | 17 | pub(crate) trait FileType { 18 | fn to_mapping(&self, contents: &str) -> Result, CheckError>; 19 | } 20 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | 16 | # Optionally declare the Python requirements required to build your docs 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /example/checkers/copy.toml: -------------------------------------------------------------------------------- 1 | [[file_copied]] 2 | # copy from url 3 | destination = "output/rust-logo.svg" 4 | source = "https://rust-lang.org/static/images/rust-logo-blk.svg" 5 | 6 | [[file_copied]] 7 | # copy from path relative to the current file 8 | destination = "output/python-logo.svg" 9 | source = "config:python-logo.svg" 10 | 11 | [[file_copied]] 12 | destination = "output/filled_template.txt" 13 | source = "config:template.txt" 14 | is_template = true 15 | 16 | [[file_present]] 17 | file = "output/rust-logo.svg" 18 | check_only = true 19 | 20 | [[file_present]] 21 | file = "output/python-logo.svg" 22 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Current and Future Features 2 | 3 | ## Schemes 4 | 5 | Fetch checkers from more schemes, like: 6 | 7 | - [x] file 8 | - [x] asset in Python package 9 | - [x] http(s) 10 | - [ ] ... 11 | 12 | ## File types 13 | 14 | File types which can be handled: 15 | 16 | - [x] plain text (.bashrc, .env, ...) 17 | - [x] json 18 | - [x] yaml 19 | - [x] toml 20 | - [ ] ... 21 | 22 | ## Check types 23 | 24 | The file can be checked for: 25 | 26 | - [x] check key/value present 27 | - [x] check key/value absent 28 | - [x] check key/value matches regex 29 | - [x] check lines present 30 | - [x] check lines absent 31 | - [x] use templates with variables 32 | - [ ] schema compliance 33 | - [ ] ... 34 | -------------------------------------------------------------------------------- /example/checkers/unpack.toml: -------------------------------------------------------------------------------- 1 | [[file_copied]] 2 | source = "config:test.zip" 3 | destination = "temp/test.zip" 4 | 5 | [[file_copied]] 6 | source = "config:test.tar.gz" 7 | destination = "temp/test.tar.gz" 8 | 9 | [[file_unpacked]] 10 | source = "temp/test.zip" 11 | destination_dir = "output/unpack/zip" 12 | 13 | [[file_unpacked]] 14 | source = "temp/test.tar.gz" 15 | destination_dir = "output/unpack/tar.gz" 16 | 17 | [[file_present]] 18 | file = "output/unpack/zip/test/sample" 19 | check_only = true 20 | 21 | [[file_present]] 22 | file = "output/unpack/tar.gz/test/sample" 23 | check_only = true 24 | 25 | [[file_absent]] 26 | file = "temp/test.zip" 27 | 28 | [[file_absent]] 29 | file = "temp/test.tar.gz" 30 | -------------------------------------------------------------------------------- /docs/alternatives.md: -------------------------------------------------------------------------------- 1 | # Alternatives 2 | 3 | ## dotfiles 4 | 5 | Keeping your dotfiles in a git repo is a great way to keep track of your configuration. 6 | 7 | `check-config`: 8 | 9 | - can install packages 10 | - works not just on complete files, but also on snippets 11 | - works both on global files and on per-project/repository files 12 | - supports templating with variables (in toml file or from environment variables) 13 | 14 | ## [dotbins](https://github.com/basnijholt/dotbins) 15 | 16 | dotbins is an greate extension to dotbins to install packages. 17 | 18 | `check-config`: 19 | 20 | - does support custom install / uninstall commands 21 | - works not just on complete files, but also on snippets, which can be included 22 | from several locations 23 | -------------------------------------------------------------------------------- /example_checkers/mypy.toml: -------------------------------------------------------------------------------- 1 | [[key_value_present]] 2 | file = "pyproject.toml" 3 | 4 | [key_value_present.key.tool.mypy] 5 | explicit_package_bases = true 6 | namespace_packages = true 7 | ignore_missing_imports = true 8 | 9 | [[entry_present]] 10 | file = ".pre-commit-config.yaml" 11 | entry.repos = [ 12 | { repo = "local", hooks = [ 13 | { id = "mypy", name = "mypy", language = "system", entry = "poetry run mypy dvb", pass_filenames = false, always_run = true }, 14 | ] }, 15 | { repo = "local", hooks = [ 16 | { id = "mypy_on_tests", name = "mypy", language = "system", entry = "poetry run mypy tests", pass_filenames = false, always_run = true }, 17 | ] }, 18 | ] 19 | 20 | [[lines_present]] 21 | file = ".gitignore" 22 | lines = ".mypy_cache" 23 | -------------------------------------------------------------------------------- /example_checkers/python.toml: -------------------------------------------------------------------------------- 1 | include = ["config:black.toml", "config:mypy.toml", "config:ruff.toml"] 2 | 3 | # Do not use setup.cfg 4 | [[file_absent]] 5 | file = "setup.cfg" 6 | 7 | # Do not use setup.py 8 | [[file_absent]] 9 | file = "setup.py" 10 | 11 | # Do not use requirements.txt 12 | [[file_absent]] 13 | file = "requirements.txt" 14 | 15 | # use poetry as build tool 16 | [[key_value_present]] 17 | file = "pyproject.toml" 18 | key.build-system.requires = ["poetry-core>=1.0.0"] 19 | key.build-system.build-backend = "poetry.core.masonry.api" 20 | 21 | # prevent from adding .venv and cache to git 22 | [[lines_present]] 23 | file = ".gitignore" 24 | lines = "__pycache__" 25 | 26 | [[lines_present]] 27 | file = ".gitignore" 28 | lines = ".cache" 29 | 30 | [[lines_present]] 31 | file = ".gitignore" 32 | lines = ".venv" 33 | -------------------------------------------------------------------------------- /.pre-commit-hook.yaml: -------------------------------------------------------------------------------- 1 | - id: check_config_fix_install_via_rust 2 | name: check configuration files based on check-config.toml and try to fix them 3 | language: rust 4 | entry: check-config --fix 5 | pass_filenames: false 6 | always_run: true 7 | - id: check_config_fix_install_via_python 8 | name: check configuration files based on check-config.toml and try to fix them 9 | language: python 10 | entry: check-config --fix 11 | pass_filenames: false 12 | always_run: true 13 | - id: check_config_check_install_via_rust 14 | name: check configuration files based on check-config.toml 15 | language: rust 16 | entry: check-config 17 | pass_filenames: false 18 | always_run: true 19 | - id: check_config_check_install_via_python 20 | name: check configuration files based on check-config.toml 21 | language: python 22 | entry: check-config 23 | pass_filenames: false 24 | always_run: true 25 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: check-config 2 | repo_url: https://github.com/mrijken/check-config 3 | 4 | nav: 5 | - "index.md" 6 | - "installation.md" 7 | - "usage.md" 8 | - "checkers.md" 9 | - "examples.md" 10 | - "features.md" 11 | - "support.md" 12 | 13 | theme: 14 | name: "material" 15 | features: 16 | - navigation.instant 17 | - navigation.instant.prefetch 18 | - navigation.tracking 19 | - content.code.annotate 20 | - toc.integrate 21 | - toc.follow 22 | - navigation.path 23 | - navigation.top 24 | - content.code.copy 25 | 26 | markdown_extensions: 27 | - markdown_include.include: 28 | base_path: docs 29 | - admonition 30 | - toc: 31 | permalink: true 32 | - pymdownx.highlight: 33 | anchor_linenums: true 34 | line_spans: __span 35 | pygments_lang_class: true 36 | - pymdownx.inlinehilite 37 | - pymdownx.snippets 38 | - pymdownx.superfences 39 | - pymdownx.details 40 | - pymdownx.tasklist: 41 | custom_checkbox: true 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marc Rijken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | | item | definition | 4 | | -------------- | ------------------------------------------------------------------------------------------------- | 5 | | checker type | A type of the check to be executed. Like `lines_present` | 6 | | checker object | The object being checked, like a file | 7 | | checker | A concrete instance of a check to be executed, like an `lines_present` for the object `~/.bashrc` | 8 | | check | A concrete execution of a checker | 9 | 10 | ## Naming conventions 11 | 12 | The name of a checker type consist of `_`. 13 | 14 | So ie `file_present` can be read as `file is present` and grammatical analysed as: 15 | 16 | - `file` – noun (subject of the sentence). 17 | - `is` – verb (linking verb, present tense, 3rd person singular of to be). 18 | - `present` – adjective (subject complement, describing the state of the file). 19 | -------------------------------------------------------------------------------- /src/file_types/ini.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::{Check, Entry, FilePresent}; 4 | 5 | impl Entry for IniEntryPresent { 6 | fn new(path: PathBuf, table: toml_edit::Table) -> Self { 7 | Self { path, table } 8 | } 9 | 10 | fn path(&self) -> &Path { 11 | &self.path 12 | } 13 | 14 | fn table(&self) -> &toml_edit::Table { 15 | &self.table 16 | } 17 | } 18 | 19 | impl Check for IniEntryPresent { 20 | fn check(&self) -> Result<(), String> { 21 | FilePresent { 22 | path: self.path.clone(), 23 | } 24 | .check()?; 25 | Ok(()) 26 | } 27 | 28 | fn fix(&self) -> Result<(), String> { 29 | FilePresent { 30 | path: self.path.clone(), 31 | } 32 | .fix()?; 33 | Ok(()) 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub(crate) struct IniEntryAbsent { 39 | path: PathBuf, 40 | table: toml_edit::Table, 41 | } 42 | impl Check for IniEntryAbsent { 43 | fn check(&self) -> Result<(), String> { 44 | Ok(()) 45 | } 46 | 47 | fn fix(&self) -> Result<(), String> { 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example_checkers/ruff.toml: -------------------------------------------------------------------------------- 1 | ["pyproject.toml".key_value_present.tool.ruff] 2 | line-length = 120 3 | select = ["ALL"] 4 | ignore = [ 5 | "D", # pydocstyle 6 | "ANN102", # Missing type annotation for {name} in classmethod 7 | "ANN101", # Missing type annotation for `self` in method 8 | "EM102", # Exception must not use an f-string literal, assign to variable first 9 | "TCH", # Use Type Checking Block 10 | "TRY003", # Avoid specifying long messages outside the exception class 11 | "EM", # Error messages must not use string literal, must not use f-string 12 | "FBT", # Do not use positional / default boolean arguments 13 | ] 14 | 15 | ["pyproject.toml".key_value_regex_match.tool.ruff] 16 | target-version = "(py310)|(py311)" 17 | 18 | ["pyproject.toml".key_value_present.tool.ruff.per-file-ignores] 19 | "tests/**" = [ 20 | "S101", # No usage of assert 21 | "INP", # No implicit namespace packages 22 | "SLF", # No private member accessed 23 | "ARG", # unused arguments 24 | "PLR2004", # No usage of magic contants 25 | "ANN201", # Missing return type annotation 26 | ] 27 | "notebooks/**" = ["ALL"] 28 | 29 | ["pyproject.toml".key_value_present.tool.ruff.isort] 30 | force-single-line = true 31 | -------------------------------------------------------------------------------- /src/integration_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use std::collections::HashMap; 4 | 5 | use crate::checkers; 6 | use crate::cli; 7 | use crate::uri; 8 | use dircpy::*; 9 | 10 | #[test] 11 | #[ignore = "needs internet connection"] 12 | fn test_example() { 13 | let _ = std::fs::remove_dir_all("output"); 14 | 15 | CopyBuilder::new("example/input", "output").run().unwrap(); 16 | 17 | let file_with_checks = 18 | uri::ReadablePath::from_string("example/pyproject.toml", None).unwrap(); 19 | let mut variables = HashMap::new(); 20 | let checks = checkers::read_checks_from_path( 21 | &file_with_checks, 22 | &mut variables, 23 | ) 24 | .into_iter() 25 | .filter(|c| { 26 | cli::filter_checks( 27 | &c.generic_checker().tags, 28 | &[], 29 | &[], 30 | &["not_selected".to_string()], 31 | ) 32 | }) 33 | .collect(); 34 | 35 | assert_eq!(cli::run_checks(&checks, true), cli::ExitStatus::Success); 36 | 37 | assert!(!dir_diff::is_different("output", "example/expected_output").unwrap()); 38 | 39 | std::fs::remove_dir_all("output").unwrap(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example_checkers/ruff_.toml: -------------------------------------------------------------------------------- 1 | [[key_value_present]] 2 | file = "pyproject.toml" 3 | key.tool.ruff.line-length = 120 4 | key.tool.ruff.select = ["ALL"] 5 | key.tool.ruff.ignore = [ 6 | "D", # pydocstyle 7 | "ANN102", # Missing type annotation for {name} in classmethod 8 | "ANN101", # Missing type annotation for `self` in method 9 | "EM102", # Exception must not use an f-string literal, assign to variable first 10 | "TCH", # Use Type Checking Block 11 | "TRY003", # Avoid specifying long messages outside the exception class 12 | "EM", # Error messages must not use string literal, must not use f-string 13 | "FBT", # Do not use positional / default boolean arguments 14 | ] 15 | 16 | [[key_value_present]] 17 | file = "pyproject.toml" 18 | key.tool.ruff.per-file-ignores."tests/**" = [ 19 | "S101", # No usage of assert 20 | "INP", # No implicit namespace packages 21 | "SLF", # No private member accessed 22 | "ARG", # unused arguments 23 | "PLR2004", # No usage of magic contants 24 | "ANN201", # Missing return type annotation 25 | ] 26 | key.tool.ruff.per-file-ignores."notebooks/**" = ["ALL"] 27 | 28 | [[key_value_present]] 29 | file = "pyproject.toml" 30 | key.tool.ruff.isort.force-single-line = true 31 | 32 | [[key_value_regex_matched]] 33 | file = "pyproject.toml" 34 | key.tool.ruff.target-version = "(py310)|(py311)" 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "check-config" 3 | version = "0.9.11" 4 | description = "Check configuration files." 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = { file = "LICENSE" } 8 | authors = [{ name = "Marc Rijken", email = "marc@rijken.org" }] 9 | maintainers = [{ name = "Marc Rijken", email = "marc@rijken.org" }] 10 | keywords = ["automation", "configuration"] 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Environment :: Console", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3 :: Only", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Programming Language :: Rust", 24 | "Topic :: Software Development :: Quality Assurance", 25 | ] 26 | 27 | [project.urls] 28 | homepage = "https://check-config.readthedocs.io" 29 | source = "https://github.com/mrijken/check-config" 30 | 31 | [build-system] 32 | requires = ["maturin>=1.0,<2.0"] 33 | build-backend = "maturin" 34 | 35 | [tool.maturin] 36 | bindings = "bin" 37 | manifest-path = "Cargo.toml" 38 | module-name = "check-config" 39 | profile = "release" 40 | strip = true 41 | -------------------------------------------------------------------------------- /src/checkers/package/custom.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | base::CheckError, 3 | package::{ 4 | CustomInstaller, 5 | command::{run_command_stream, run_command_stream_capture_stdout}, 6 | }, 7 | }; 8 | 9 | fn make_command_and_args(shell_command: &str) -> (&str, Vec<&str>) { 10 | if cfg!(target_os = "windows") { 11 | ("cmd", vec!["/C", shell_command]) 12 | } else { 13 | ("sh", vec!["-c", shell_command]) 14 | } 15 | } 16 | 17 | pub fn install(package: &CustomInstaller) -> Result<(), CheckError> { 18 | let shell_command = package.install_command.as_ref().ok_or(CheckError::String( 19 | "install_command is not specified".to_string(), 20 | ))?; 21 | let (command, args) = make_command_and_args(shell_command); 22 | run_command_stream(command, &args) 23 | } 24 | 25 | pub fn uninstall(package: &CustomInstaller) -> Result<(), CheckError> { 26 | let shell_command = package 27 | .uninstall_command 28 | .as_ref() 29 | .ok_or(CheckError::String( 30 | "uninstall_command is not specified".to_string(), 31 | ))?; 32 | let (command, args) = make_command_and_args(shell_command); 33 | 34 | run_command_stream(command, &args) 35 | } 36 | 37 | pub fn is_installed(package: &CustomInstaller) -> Result { 38 | let shell_command = &package.version_command; 39 | let (command, args) = make_command_and_args(shell_command); 40 | 41 | let stdout = run_command_stream_capture_stdout(command, &args)?; 42 | 43 | Ok(stdout.contains(package.version.as_str())) 44 | } 45 | 46 | pub fn is_upgradable(_package: &CustomInstaller) -> Result { 47 | Ok(true) 48 | } 49 | -------------------------------------------------------------------------------- /src/checkers/package/rust.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | base::CheckError, 3 | package::{ 4 | Installer, Package, 5 | command::{run_command_stream, run_command_stream_capture_stdout}, 6 | }, 7 | }; 8 | 9 | pub(crate) struct Cargo; 10 | 11 | impl Installer for Cargo { 12 | fn install(package: &Package) -> Result<(), CheckError> { 13 | let package_specifier = if let Some(version) = &package.version { 14 | format!("{package}@{version}", package = &package.name) 15 | } else { 16 | package.name.to_owned() 17 | }; 18 | 19 | run_command_stream( 20 | "cargo", 21 | vec!["install", package_specifier.as_str()].as_ref(), 22 | ) 23 | } 24 | 25 | fn uninstall(package: &Package) -> Result<(), CheckError> { 26 | run_command_stream("cargo", vec!["uninstall", package.name.as_str()].as_ref()) 27 | } 28 | 29 | fn is_upgradable(package: &Package) -> Result { 30 | Ok(package.version.is_none()) 31 | } 32 | 33 | fn is_installed(package: &Package) -> Result { 34 | let stdout = 35 | run_command_stream_capture_stdout("cargo", vec!["install", "--list"].as_ref())?; 36 | 37 | let packages: Vec<&str> = stdout 38 | .lines() 39 | .filter(|line| line.starts_with(format!("{package} ", package = package.name).as_str())) 40 | .collect(); 41 | 42 | Ok(if packages.len() != 1 { 43 | false 44 | } else if let Some(version) = package.version.as_ref() { 45 | packages 46 | .first() 47 | .expect("1 item present") 48 | .split_once(" ") 49 | .expect("space is present") 50 | .1 51 | .contains(version) 52 | } else { 53 | true 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "check-config" 3 | version = "0.9.11" 4 | edition = "2024" 5 | description = "Check configuration files." 6 | documentation = "https://check-config.readthedocs.io" 7 | readme = "README.md" 8 | homepage = "https://pypi.org/project/check-config/" 9 | repository = "https://github.com/mrijken/check-config" 10 | license = "MIT" 11 | keywords = ["automation", "configuration"] 12 | exclude = [".github/*", "videos/*"] 13 | 14 | [package.metadata.cargo-machete] 15 | ignored = [ 16 | "openssl", # needed for https connections 17 | ] 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | [[bin]] 21 | name = "check-config" 22 | 23 | [dependencies] 24 | clap = { version = "4.5.45", features = ["env"] } 25 | clap-verbosity-flag = "3.0.3" 26 | derive_more = { version = "2.0.1", default-features = false, features = [ 27 | "as_ref", 28 | "display", 29 | "from", 30 | ] } 31 | dir-diff = "0.3.3" 32 | dircpy = { version = "0.3.19", default-features = false } 33 | dirs = "6.0.0" 34 | env_logger = { version = "0.11.8", default-features = false } 35 | flate2 = "1.1.2" 36 | git2 = "0.20.2" 37 | log = "0.4.27" 38 | openssl = { version = "0.10.73", features = ["vendored"] } 39 | regex = "1.11.1" 40 | reqwest = { version = "0.12.23", default-features = false, features = [ 41 | "default-tls", 42 | "blocking", 43 | ] } 44 | serde = { version = "1.0", default-features = false } 45 | serde_json = "1.0.142" 46 | serde_yaml_ng = "0.10.0" 47 | similar = "2.7.0" 48 | tar = "0.4.44" 49 | tempfile = { version = "3.20.0", default-features = false } 50 | test-log = { version = "0.2.18", default-features = false } 51 | thiserror = { version = "2.0.15", default-features = false } 52 | toml = { version = "0.9.5", default-features = false } 53 | toml_edit = "0.23.3" 54 | url = { version = "2.5.4", default-features = false } 55 | zip = "6.0.0" 56 | 57 | [profile.release] 58 | lto = "fat" 59 | panic = "abort" 60 | codegen-units = 1 61 | -------------------------------------------------------------------------------- /src/checkers/package/python.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | base::CheckError, 3 | package::{ 4 | Installer, Package, 5 | command::{run_command_stream, run_command_stream_capture_stdout}, 6 | }, 7 | }; 8 | 9 | pub(crate) struct UV; 10 | 11 | impl Installer for UV { 12 | fn install(package: &Package) -> Result<(), CheckError> { 13 | let mut options = vec!["tool", "install"]; 14 | 15 | let package_specifier = if let Some(version) = &package.version { 16 | format!("{package}=={version}", package = &package.name) 17 | } else { 18 | package.name.to_owned() 19 | }; 20 | 21 | options.push(&package_specifier); 22 | 23 | if package.version.is_some() { 24 | options.push("-U"); 25 | } 26 | 27 | run_command_stream("uv", &options) 28 | } 29 | 30 | fn uninstall(package: &Package) -> Result<(), CheckError> { 31 | run_command_stream( 32 | "uv", 33 | vec!["tool", "uninstall", package.name.as_str()].as_ref(), 34 | ) 35 | } 36 | 37 | fn is_upgradable(package: &Package) -> Result { 38 | Ok(package.version.is_none()) 39 | } 40 | 41 | fn is_installed(package: &Package) -> Result { 42 | let stdout = run_command_stream_capture_stdout("uv", vec!["tool", "list"].as_ref())?; 43 | 44 | let packages: Vec<&str> = stdout 45 | .lines() 46 | .filter(|line| line.starts_with(format!("{package} ", package = package.name).as_str())) 47 | .collect(); 48 | 49 | Ok(if packages.len() != 1 { 50 | false 51 | } else if let Some(version) = package.version.as_ref() { 52 | packages 53 | .first() 54 | .expect("1 item present") 55 | .split_once(" ") 56 | .expect("space is present") 57 | .1 58 | == version 59 | } else { 60 | true 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/checkers/package/package_absent.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | base::{CheckResult, Checker}, 3 | package::{PackageType, read_package_from_check_table}, 4 | }; 5 | 6 | use super::super::{ 7 | GenericChecker, 8 | base::{CheckConstructor, CheckDefinitionError, CheckError}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct PackageAbsent { 13 | generic_check: GenericChecker, 14 | package: PackageType, 15 | } 16 | 17 | impl CheckConstructor for PackageAbsent { 18 | type Output = Self; 19 | 20 | fn from_check_table( 21 | generic_check: GenericChecker, 22 | value: toml_edit::Table, 23 | ) -> Result { 24 | let package_type = read_package_from_check_table(&value)?; 25 | Ok(Self { 26 | generic_check, 27 | package: package_type, 28 | }) 29 | } 30 | } 31 | 32 | impl Checker for PackageAbsent { 33 | fn checker_type(&self) -> String { 34 | "package_absent".to_string() 35 | } 36 | 37 | fn generic_checker(&self) -> &GenericChecker { 38 | &self.generic_check 39 | } 40 | 41 | fn checker_object(&self) -> String { 42 | format!("{}", self.package) 43 | } 44 | 45 | fn check_(&self, fix: bool) -> Result { 46 | let to_uninstall = self.package.is_installed()?; 47 | 48 | let action_message = if to_uninstall { 49 | format!("uninstall package {}", self.package) 50 | } else { 51 | "".to_string() 52 | }; 53 | 54 | let check_result = match (to_uninstall, fix) { 55 | (false, _) => CheckResult::NoFixNeeded, 56 | (true, false) => CheckResult::FixNeeded(action_message), 57 | (true, true) => { 58 | self.package.uninstall()?; 59 | CheckResult::FixExecuted(action_message) 60 | } 61 | }; 62 | 63 | Ok(check_result) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use std::fs::File; 70 | 71 | use super::*; 72 | 73 | use tempfile::tempdir; 74 | } 75 | -------------------------------------------------------------------------------- /src/checkers/package/package_present.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | GenericChecker, 3 | base::{CheckConstructor, CheckDefinitionError, CheckError, CheckResult, Checker}, 4 | package::{PackageType, read_package_from_check_table}, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub(crate) struct PackagePresent { 9 | generic_checker: GenericChecker, 10 | package: PackageType, 11 | } 12 | 13 | impl CheckConstructor for PackagePresent { 14 | type Output = Self; 15 | 16 | fn from_check_table( 17 | generic_check: GenericChecker, 18 | check_table: toml_edit::Table, 19 | ) -> Result { 20 | let package = read_package_from_check_table(&check_table)?; 21 | Ok(Self { 22 | generic_checker: generic_check, 23 | package, 24 | }) 25 | } 26 | } 27 | 28 | impl Checker for PackagePresent { 29 | fn checker_type(&self) -> String { 30 | "package_present".to_string() 31 | } 32 | 33 | fn generic_checker(&self) -> &GenericChecker { 34 | &self.generic_checker 35 | } 36 | 37 | fn checker_object(&self) -> String { 38 | format!("{}", self.package) 39 | } 40 | 41 | fn check_(&self, fix: bool) -> Result { 42 | let to_install = !self.package.is_installed()?; 43 | let try_to_upgrade = self.package.is_upgradable()?; 44 | 45 | let action_message = if to_install { 46 | format!("install package {}", self.package) 47 | } else if try_to_upgrade { 48 | format!("try to upgrade package {} to latest", self.package,) 49 | } else { 50 | "".to_string() 51 | }; 52 | 53 | let check_result = match (to_install, fix) { 54 | (false, _) => CheckResult::NoFixNeeded, 55 | (true, false) => CheckResult::FixNeeded(action_message), 56 | (true, true) => { 57 | self.package.install()?; 58 | CheckResult::FixExecuted(action_message) 59 | } 60 | }; 61 | 62 | Ok(check_result) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use std::fs::File; 69 | 70 | use super::*; 71 | 72 | use tempfile::tempdir; 73 | } 74 | -------------------------------------------------------------------------------- /example/expected_output/rust-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/checkers/file/file_absent.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::base::CheckResult; 2 | use crate::checkers::file::FileCheck; 3 | 4 | use super::super::GenericChecker; 5 | use super::super::base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub(crate) struct FileAbsent { 9 | file_check: FileCheck, 10 | } 11 | 12 | // [[file_absent]] 13 | // file = "file" 14 | impl CheckConstructor for FileAbsent { 15 | type Output = Self; 16 | 17 | fn from_check_table( 18 | generic_check: GenericChecker, 19 | check_table: toml_edit::Table, 20 | ) -> Result { 21 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 22 | Ok(Self { file_check }) 23 | } 24 | } 25 | impl Checker for FileAbsent { 26 | fn checker_type(&self) -> String { 27 | "file_absent".to_string() 28 | } 29 | 30 | fn checker_object(&self) -> String { 31 | self.file_check.check_object() 32 | } 33 | 34 | fn generic_checker(&self) -> &GenericChecker { 35 | &self.file_check.generic_check 36 | } 37 | 38 | fn check_(&self, fix: bool) -> Result { 39 | self.file_check.conclude_check_with_remove(fix) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | 46 | use std::fs::File; 47 | 48 | use crate::checkers::test_helpers; 49 | 50 | use super::*; 51 | 52 | use tempfile::{TempDir, tempdir}; 53 | 54 | fn get_file_absent_check() -> (FileAbsent, TempDir) { 55 | let generic_check = test_helpers::get_generic_check(); 56 | 57 | let mut check_table = toml_edit::Table::new(); 58 | let dir = tempdir().unwrap(); 59 | let file_to_check = dir.path().join("file_to_check"); 60 | check_table.insert("file", file_to_check.to_string_lossy().to_string().into()); 61 | 62 | ( 63 | FileAbsent::from_check_table(generic_check, check_table).unwrap(), 64 | dir, 65 | ) 66 | } 67 | 68 | #[test] 69 | fn test_file_absent() { 70 | let (file_absent_check, _tmpdir) = get_file_absent_check(); 71 | 72 | assert_eq!( 73 | file_absent_check.check_(false).unwrap(), 74 | CheckResult::NoFixNeeded 75 | ); 76 | 77 | File::create(file_absent_check.file_check.file_to_check.as_ref()).expect("file is created"); 78 | 79 | assert_eq!( 80 | file_absent_check.check_(true).unwrap(), 81 | CheckResult::FixExecuted("remove file".into()) 82 | ); 83 | 84 | assert_eq!( 85 | file_absent_check.check_(false).unwrap(), 86 | CheckResult::NoFixNeeded 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/checkers/test_helpers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::{fs, path::PathBuf, str::FromStr}; 3 | 4 | use crate::mapping::{generic::Mapping, json}; 5 | 6 | type TestFiles = Vec<(String, Box, String, toml_edit::Table)>; 7 | 8 | use crate::checkers::GenericChecker; 9 | use crate::uri::ReadablePath; 10 | 11 | #[allow(dead_code)] 12 | pub(crate) fn get_generic_check() -> GenericChecker { 13 | let file = tempfile::NamedTempFile::new().expect("temp file is created"); 14 | GenericChecker { 15 | file_with_checks: ReadablePath::from_string( 16 | format!("file://{}", file.path().to_string_lossy()).as_str(), 17 | None, 18 | ) 19 | .expect("valid path"), 20 | tags: Vec::new(), 21 | check_only: true, 22 | variables: HashMap::new(), 23 | } 24 | } 25 | 26 | #[allow(dead_code)] 27 | pub(crate) fn read_test_files(check_type: &str) -> TestFiles { 28 | let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 29 | test_dir.push("tests/resources/checkers/".to_string() + check_type); 30 | 31 | let mut tests = vec![]; 32 | 33 | for test in test_dir.read_dir().expect("read_dir call failed") { 34 | let test = test.unwrap().path(); 35 | let file_checker_content = fs::read_to_string(test.join("check-config.toml")).unwrap(); 36 | let file_checker = toml_edit::DocumentMut::from_str(file_checker_content.as_str()) 37 | .unwrap() 38 | .as_table() 39 | .clone(); 40 | 41 | let json_input = json::from_path(test.join("input.json")).unwrap(); 42 | let json_expected_output = 43 | fs::read_to_string(test.join("expected_output.json")).unwrap() + "\n"; 44 | 45 | tests.push(( 46 | test.join("input.json").to_string_lossy().to_string(), 47 | json_input, 48 | json_expected_output, 49 | file_checker.clone(), 50 | )); 51 | 52 | let toml_input = crate::mapping::toml::from_path(test.join("input.toml")).unwrap(); 53 | let toml_expected_output = fs::read_to_string(test.join("expected_output.toml")).unwrap(); 54 | 55 | tests.push(( 56 | test.join("input.toml").to_string_lossy().to_string(), 57 | toml_input, 58 | toml_expected_output, 59 | file_checker.clone(), 60 | )); 61 | 62 | let yaml_input = crate::mapping::yaml::from_path(test.join("input.yaml")).unwrap(); 63 | let yaml_expected_output = fs::read_to_string(test.join("expected_output.yaml")).unwrap(); 64 | 65 | tests.push(( 66 | test.join("input.yaml").to_string_lossy().to_string(), 67 | yaml_input, 68 | yaml_expected_output, 69 | file_checker.clone(), 70 | )); 71 | } 72 | tests 73 | } 74 | -------------------------------------------------------------------------------- /src/checkers/package/command.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader, Read}; 2 | use std::process::{Command, Stdio}; 3 | 4 | use crate::checkers::base::CheckError; 5 | 6 | pub fn run_command_stream(command: &str, args: &[&str]) -> Result<(), CheckError> { 7 | let mut child = Command::new(command) 8 | .args(args) 9 | .stdout(Stdio::piped()) 10 | .stderr(Stdio::piped()) 11 | .spawn()?; // Starts the command, doesn't wait yet 12 | 13 | let stdout = child.stdout.take().unwrap(); 14 | let stderr = child.stderr.take().unwrap(); 15 | 16 | let cmd = command.to_string(); 17 | 18 | // Spawn threads to read stdout and stderr concurrently 19 | let stdout_handle = std::thread::spawn(move || { 20 | let reader = BufReader::new(stdout); 21 | for line in reader.lines() { 22 | if let Ok(line) = line { 23 | log::warn!("stdout {}: {}", cmd, line); 24 | } 25 | } 26 | }); 27 | 28 | let cmd = command.to_string(); 29 | let stderr_handle = std::thread::spawn(move || { 30 | let reader = BufReader::new(stderr); 31 | for line in reader.lines() { 32 | if let Ok(line) = line { 33 | log::error!("stderr {}: {}", cmd, line); 34 | } 35 | } 36 | }); 37 | 38 | // Wait for the command to finish 39 | let status = child.wait()?; 40 | 41 | // Ensure threads are finished 42 | stdout_handle.join().unwrap(); 43 | stderr_handle.join().unwrap(); 44 | 45 | match status.success() { 46 | true => Ok(()), 47 | false => Err(CheckError::CommandFailed(format!( 48 | "{}, {:?}", 49 | command, args 50 | ))), 51 | } 52 | } 53 | 54 | pub fn run_command_stream_capture_stdout( 55 | command: &str, 56 | args: &[&str], 57 | ) -> Result { 58 | // Spawn the process 59 | let mut child = Command::new(command) 60 | .args(args) 61 | .stdout(Stdio::piped()) 62 | .stderr(Stdio::piped()) 63 | .spawn()?; 64 | 65 | let stdout = child.stdout.take().unwrap(); 66 | let stderr = child.stderr.take().unwrap(); 67 | 68 | let cmd = command.to_string(); 69 | // --- Stream STDERR in real-time --- 70 | let stderr_handle = std::thread::spawn(move || { 71 | let reader = BufReader::new(stderr); 72 | for line in reader.lines() { 73 | if let Ok(line) = line { 74 | log::error!("stderr {}: {}", cmd, line); 75 | } 76 | } 77 | }); 78 | 79 | // --- Capture STDOUT fully --- 80 | let mut stdout_reader = BufReader::new(stdout); 81 | let mut stdout_str = String::new(); 82 | stdout_reader.read_to_string(&mut stdout_str)?; 83 | 84 | // Wait for command to finish 85 | let status = child.wait()?; 86 | stderr_handle.join().unwrap(); 87 | 88 | match status.success() { 89 | true => Ok(stdout_str), 90 | false => Err(CheckError::CommandFailed(format!( 91 | "{}, {:?}", 92 | command, args 93 | ))), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Effortless Configuration Management with check-config 4 | 5 | 6 | **Keep your development environment consistent, shareable, and version-controlled** 7 | 8 | check-config is a fast, lightweight, declarative configuration management tool that ensures your configuration files 9 | contain exactly what they should. Instead of managing entire config files, 10 | you declare specific parts that must be present - making configurations shareable, maintainable, and verifiable. 11 | 12 | ## How It Works 13 | 14 | Define your configuration requirements in simple TOML files, then let check-config ensure they're applied: 15 | 16 | ```toml 17 | # Set your preferred editor 18 | [[lines_present]] 19 | file = "~/.bashrc" 20 | lines = "export EDITOR=hx" 21 | ``` 22 | 23 | ```toml 24 | # Ensure git signing is configured 25 | [[lines_present]] 26 | file = "~/.gitconfig" 27 | lines = """ 28 | [gpg] 29 | format = ssh 30 | [commit] 31 | gpgsign = true 32 | """ 33 | ``` 34 | 35 | Run `check-config --fix` to apply changes, or `check-config` to verify everything is in sync. 36 | 37 | ## Why check-config? 38 | 39 | ### 🔧 **Shareable Configuration Snippets** 40 | 41 | Traditional dotfile repositories force users to adopt entire configuration files. check-config lets you share just the essential parts: 42 | - Share your preferred Python formatting rules without forcing your entire `pyproject.toml` 43 | - Distribute security settings without overwriting personal aliases 44 | - Collaborate on team standards while preserving individual preferences 45 | 46 | ### ✅ **Enforce Team Standards** 47 | 48 | Ensure consistent development environments across your team: 49 | 50 | ```shell 51 | # In CI: Verify configurations are up-to-date 52 | check-config 53 | 54 | # For developers: Apply required configurations 55 | check-config --fix 56 | ``` 57 | 58 | Perfect for ensuring tools like Ruff, Black, and ESLint use consistent settings across all developers and CI pipelines. 59 | 60 | ### 📦 **Composable Configuration** 61 | 62 | Combine multiple configuration files to build your complete setup: 63 | - Base configurations for your team 64 | - Personal tweaks and preferences 65 | - Project-specific requirements 66 | - Environment-specific overrides 67 | 68 | ## Beyond Simple Lines 69 | 70 | check-config supports multiple checker types for different configuration needs: 71 | - **Lines present/absent**: Shell configs, text files 72 | - **Key-value pairs**: TOML, JSON, YAML files 73 | - **File existence**: Ensure critical files exist 74 | - **And more**: See [docs/checkers.md](docs/checkers.md) for all features 75 | 76 | ## Get Started 77 | 78 | Make a `check-config.toml` according your needs: 79 | 80 | ```toml 81 | # Set your preferred editor 82 | [[lines_present]] 83 | file = "~/.bashrc" 84 | lines = "export EDITOR=hx" 85 | ``` 86 | 87 | And use it: 88 | 89 | ```shell 90 | # Check if configurations match requirements 91 | check-config 92 | 93 | # Apply missing configurations 94 | check-config --fix 95 | ``` 96 | 97 | ## Documentation 98 | 99 | 📖 **[Full Documentation](https://check-config.readthedocs.io)** - Complete guides, examples, and API reference 100 | 101 | --- 102 | 103 | *Declare what you need. Share what matters. Keep everything in sync.* 104 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Effortless Configuration Management with check-config 2 | 3 | **Keep your development environment consistent, shareable, and version-controlled** 4 | 5 | check-config is a fast, lightweight, declarative configuration management tool that ensures your configuration files 6 | contain exactly what they should. Instead of managing entire config files, 7 | you declare specific parts that must be present - making configurations shareable, maintainable, and verifiable. 8 | 9 | ## How It Works 10 | 11 | Define your configuration requirements in simple TOML files, then let check-config ensure they're applied: 12 | 13 | ```toml 14 | # Set your preferred editor 15 | [[lines_present]] 16 | file = "~/.bashrc" 17 | lines = "export EDITOR=hx" 18 | ``` 19 | 20 | ```toml 21 | # Ensure git signing is configured 22 | [[lines_present]] 23 | file = "~/.gitconfig" 24 | lines = """ 25 | [gpg] 26 | format = ssh 27 | [commit] 28 | gpgsign = true 29 | """ 30 | ``` 31 | 32 | Run `check-config --fix` to apply changes, or `check-config` to verify everything is in sync. 33 | 34 | ## Why check-config? 35 | 36 | ### 🔧 **Shareable Configuration Snippets** 37 | 38 | Traditional dotfile repositories force users to adopt entire configuration files. check-config lets you share just the essential parts: 39 | 40 | - Share your preferred Python formatting rules without forcing your entire `pyproject.toml` 41 | - Distribute security settings without overwriting personal aliases 42 | - Collaborate on team standards while preserving individual preferences 43 | 44 | ### ✅ **Enforce Team Standards** 45 | 46 | Ensure consistent development environments across your team: 47 | 48 | ```shell 49 | # In CI: Verify configurations are up-to-date 50 | check-config 51 | 52 | # For developers: Apply required configurations 53 | check-config --fix 54 | ``` 55 | 56 | Perfect for ensuring tools like Ruff, Black, and ESLint use consistent settings across all developers and CI pipelines. 57 | 58 | ### 📦 **Composable Configuration** 59 | 60 | Combine multiple configuration files to build your complete setup: 61 | 62 | - Base configurations for your team 63 | - Personal tweaks and preferences 64 | - Project-specific requirements 65 | - Environment-specific overrides 66 | 67 | ## Beyond Simple Lines 68 | 69 | check-config supports multiple checker types for different configuration needs: 70 | 71 | - **Lines present/absent**: Shell configs, text files 72 | - **Key-value pairs**: TOML, JSON, YAML files 73 | - **File existence**: Ensure critical files exist 74 | - **And more**: See [docs/checkers.md](docs/checkers.md) for all features 75 | 76 | ## Get Started 77 | 78 | Make a `check-config.toml` according your needs: 79 | 80 | ```toml 81 | # Set your preferred editor 82 | [[lines_present]] 83 | file = "~/.bashrc" 84 | lines = "export EDITOR=hx" 85 | ``` 86 | 87 | And use it: 88 | 89 | ```shell 90 | # Check if configurations match requirements 91 | check-config 92 | 93 | # Apply missing configurations 94 | check-config --fix 95 | ``` 96 | 97 | ## Read on 98 | 99 | A large number of [file types](features.md/#file-types) and [checks](checkers.md#checkers) are supported or will 100 | be supported in the near [future](features.md#current-and-future-features). 101 | 102 | ## Alternatives 103 | 104 | When `check-config` is not suitable for your, may be some [alternatives.md](alternatives.md) are. 105 | -------------------------------------------------------------------------------- /src/checkers/file/key_value_present.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | checkers::{base::CheckDefinitionError, file::FileCheck}, 3 | mapping::generic::Mapping, 4 | }; 5 | 6 | use super::super::{ 7 | GenericChecker, 8 | base::{CheckConstructor, CheckError, Checker}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct KeyValuePresent { 13 | file_check: FileCheck, 14 | key_value: toml_edit::Table, 15 | } 16 | 17 | // [[key_value_present]] 18 | // file = "file" 19 | // key.key = "value" 20 | impl CheckConstructor for KeyValuePresent { 21 | type Output = Self; 22 | 23 | fn from_check_table( 24 | generic_check: GenericChecker, 25 | check_table: toml_edit::Table, 26 | ) -> Result { 27 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 28 | 29 | let key_value_present = match check_table.get("key") { 30 | None => { 31 | return Err(CheckDefinitionError::InvalidDefinition( 32 | "`key` key is not present".into(), 33 | )); 34 | } 35 | Some(key_value) => match key_value.as_table() { 36 | None => { 37 | return Err(CheckDefinitionError::InvalidDefinition( 38 | "`key` is not a table".into(), 39 | )); 40 | } 41 | Some(key_value) => key_value.clone(), 42 | }, 43 | }; 44 | 45 | Ok(Self { 46 | file_check, 47 | key_value: key_value_present, 48 | }) 49 | } 50 | } 51 | 52 | impl Checker for KeyValuePresent { 53 | fn checker_type(&self) -> String { 54 | "key_value_present".to_string() 55 | } 56 | 57 | fn checker_object(&self) -> String { 58 | self.file_check.check_object() 59 | } 60 | 61 | fn generic_checker(&self) -> &GenericChecker { 62 | &self.file_check.generic_check 63 | } 64 | 65 | fn check_(&self, fix: bool) -> Result { 66 | let mut doc = self.file_check.get_mapping()?; 67 | 68 | set_key_value(doc.as_mut(), &self.key_value); 69 | 70 | self.file_check.conclude_check_with_new_doc(doc, fix) 71 | } 72 | } 73 | 74 | pub fn set_key_value(doc: &mut dyn Mapping, table_to_set: &dyn toml_edit::TableLike) { 75 | for (k, v) in table_to_set.iter() { 76 | if v.is_table_like() { 77 | set_key_value( 78 | doc.get_mapping(k, true).expect("key exists"), 79 | v.as_table_like().expect("value is a table"), 80 | ); 81 | continue; 82 | } 83 | doc.insert( 84 | table_to_set.key(k).expect("key exists"), 85 | &toml_edit::Item::Value(v.as_value().unwrap().to_owned()), 86 | ); 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use crate::checkers::test_helpers::read_test_files; 93 | 94 | use super::*; 95 | 96 | #[test] 97 | fn test_test_files() { 98 | for (test_path, test_input, test_expected_output, checker) in 99 | read_test_files("key_value_present") 100 | { 101 | let mut test_input = test_input; 102 | set_key_value(test_input.as_mut(), &checker); 103 | 104 | assert_eq!( 105 | *test_expected_output, 106 | test_input.to_string(4).unwrap(), 107 | "test_path {test_path} failed" 108 | ); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGES 2 | 3 | ## 0.9.11 4 | 5 | - Fix: fix typo in default filename 6 | 7 | ## 0.9.10 8 | 9 | - Fix: fix checking the version for rust packages in package_present 10 | 11 | ## 0.9.9 12 | 13 | - Feat: add source option to lines_present, so the lines can be loaded from a file 14 | 15 | ## 0.9.8 16 | 17 | - Fix: replace variables in diff of file_copied 18 | 19 | ## 0.9.7 20 | 21 | - Feat: show difference when source and destination are different for file_copied 22 | 23 | ## 0.9.6 24 | 25 | - Feat: add check_config.toml to path when path is a directory 26 | - Feat: use Cargo.toml (just like pyproject.toml) as possible source for the path 27 | 28 | ## 0.9.5 29 | 30 | - Fix: fix using the version when installing a package via Cargo 31 | - Fix: fix upgrade to latest version when version is not given 32 | 33 | ## 0.9.4 34 | 35 | - Feat: add dir_absent 36 | - Fix: make paths handling consistent 37 | 38 | ## 0.9.3 39 | 40 | - Feat: add package_present and package_absent 41 | 42 | ## 0.9.2 43 | 44 | - Feat: add variables and templating 45 | 46 | ## 0.9.1 47 | 48 | - Feat: add dir_present 49 | 50 | ## 0.9.0 51 | 52 | - BREAKING: refactor the check-config toml files. See documentation for new format. 53 | 54 | ## 0.8.6 55 | 56 | - The path specified on the cli with, can also be a URL. 57 | 58 | ## 0.8.5 59 | 60 | - Add tags to select checkers 61 | - BREAKING: Remove `__config__` tag. Use `__include__` as top level key 62 | 63 | ## 0.8.4 64 | 65 | - Improve readme 66 | 67 | ## 0.8.3 68 | 69 | - Pass some cli options via env variables 70 | - Add option to enable creation of intermediate directories 71 | 72 | ## 0.8.2 73 | 74 | - Add marker to lines_present checktype 75 | - Add marker to lines_absent checktype 76 | 77 | ## 0.8.1 78 | 79 | - fix using `check-config` as command name 80 | - fix usage of relative paths 81 | 82 | ## 0.8.0 83 | 84 | - Add \_\_replacements_regex in lines_present 85 | - BREAKING: use [__config__] in stead of [check-config] as config table 86 | - BREAKING: use `check-config.toml` as default name 87 | - preserve formatting of toml files, including comments 88 | - fix several small bugs and command output 89 | - fix usage of relative urls and home dir (ie " ~/.bashrc") 90 | - add list-checkers option 91 | 92 | ## 0.7.1 93 | 94 | - fix reading python paths 95 | 96 | ## 0.7.0 97 | 98 | - fix rename additional checks to include [#7](https://github.com/mrijken/check-config/issues/7) 99 | - fix relative includes [#8](https://github.com/mrijken/check-config/issues/8) 100 | - add fallback to pyproject.toml [[#6](https://github.com/mrijken/check-config/issues/6)] 101 | 102 | ## 0.6.1 103 | 104 | - BREAKING: Use check_config.toml (in stead of checkers.toml) as default style file 105 | - Add pre-commit hook 106 | 107 | ## 0.6.0 108 | 109 | - Add optional placeholder when creating a file with file_present 110 | - Add optional placeholder when creating a file with file_regex 111 | 112 | ## 0.5.1 113 | 114 | - Update documentation 115 | 116 | ## 0.5.0 117 | 118 | - Support http(s) location for checkers. 119 | 120 | ## 0.4.1 121 | 122 | - Update dependencies 123 | 124 | ## 0.4.0 125 | 126 | - Refactor checkers 127 | - Add file regex 128 | 129 | ## 0.3.3 130 | 131 | - Add documentation 132 | 133 | ## 0.3.2 134 | 135 | - Fix: improve error handling 136 | 137 | ## 0.3.1 138 | 139 | - Fix: empty json and yaml files where not processed correctly 140 | 141 | ## 0.3.0 142 | 143 | - Support files from Python packages. 144 | 145 | ## 0.2.0 146 | 147 | - Add entry_absent and entry_present 148 | 149 | ## 0.1.1 150 | 151 | - Fix stable classifier 152 | 153 | ## 0.1.0 154 | 155 | - initial release 156 | -------------------------------------------------------------------------------- /src/checkers/file/key_absent.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | checkers::{base::CheckDefinitionError, file::FileCheck}, 3 | mapping::generic::Mapping, 4 | }; 5 | 6 | use super::super::{ 7 | GenericChecker, 8 | base::{CheckConstructor, CheckError, Checker}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct KeyAbsent { 13 | file_check: FileCheck, 14 | value: toml_edit::Table, 15 | } 16 | 17 | // [[key_absent]] 18 | // file = "file" 19 | // key.key_to_remove = {} 20 | impl CheckConstructor for KeyAbsent { 21 | type Output = Self; 22 | 23 | fn from_check_table( 24 | generic_check: GenericChecker, 25 | check_table: toml_edit::Table, 26 | ) -> Result { 27 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 28 | 29 | let key_absent = match check_table.get("key") { 30 | None => { 31 | return Err(CheckDefinitionError::InvalidDefinition( 32 | "`key` key is not present".into(), 33 | )); 34 | } 35 | Some(absent) => match absent.as_table() { 36 | None => { 37 | return Err(CheckDefinitionError::InvalidDefinition( 38 | "`key` is not a table".into(), 39 | )); 40 | } 41 | Some(absent) => { 42 | // TODO: check if there is an array in absent 43 | absent.clone() 44 | } 45 | }, 46 | }; 47 | 48 | Ok(Self { 49 | file_check, 50 | value: key_absent, 51 | }) 52 | } 53 | } 54 | 55 | impl Checker for KeyAbsent { 56 | fn checker_type(&self) -> String { 57 | "key_absent".to_string() 58 | } 59 | 60 | fn generic_checker(&self) -> &GenericChecker { 61 | &self.file_check.generic_check 62 | } 63 | 64 | fn checker_object(&self) -> String { 65 | self.file_check.check_object() 66 | } 67 | 68 | fn check_(&self, fix: bool) -> Result { 69 | let mut doc = self.file_check.get_mapping()?; 70 | 71 | unset_key(doc.as_mut(), &self.value); 72 | 73 | self.file_check.conclude_check_with_new_doc(doc, fix) 74 | } 75 | } 76 | 77 | fn unset_key(doc: &mut dyn Mapping, table_to_unset: &dyn toml_edit::TableLike) { 78 | for (key_to_unset, value_to_unset) in table_to_unset.iter() { 79 | if let Some(child_table_to_unset) = value_to_unset.as_table_like() { 80 | if child_table_to_unset.is_empty() { 81 | doc.remove(key_to_unset); 82 | } else if let Ok(child_doc) = doc.get_mapping(key_to_unset, false) { 83 | unset_key(child_doc, child_table_to_unset); 84 | } else { 85 | log::info!( 86 | "Key {key_to_unset} is not found in toml, so we can not remove that key", 87 | ); 88 | } 89 | } 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use crate::checkers::test_helpers::read_test_files; 96 | 97 | use super::*; 98 | 99 | #[test] 100 | fn test_test_files() { 101 | for (test_path, test_input, test_expected_output, checker) in read_test_files("key_absent") 102 | { 103 | let mut test_input = test_input; 104 | unset_key(test_input.as_mut(), &checker); 105 | 106 | assert_eq!( 107 | *test_expected_output, 108 | test_input.to_string(4).unwrap(), 109 | "test_path {test_path} failed" 110 | ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/checkers/base.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug as DebugTrait; 2 | use std::io; 3 | use thiserror::Error; 4 | 5 | use crate::{checkers::RelativeUrl, uri::PathError}; 6 | 7 | use super::GenericChecker; 8 | 9 | #[derive(Clone, Debug, PartialEq)] 10 | pub enum CheckResult { 11 | NoFixNeeded, 12 | FixNeeded(String), 13 | FixExecuted(String), 14 | Error(String), 15 | } 16 | 17 | #[derive(Error, Debug, PartialEq)] 18 | pub(crate) enum CheckDefinitionError { 19 | #[error("invalid check definition ({0})")] 20 | InvalidDefinition(String), 21 | #[error("Unknown checktype ({0})")] 22 | UnknownCheckType(String), 23 | } 24 | 25 | #[derive(Error, Debug)] 26 | pub(crate) enum CheckError { 27 | #[error("file can not be read")] 28 | FileCanNotBeRead(#[from] io::Error), 29 | #[error("unknown file type ({0}); do not know how to handle")] 30 | UnknownFileType(String), 31 | #[error("file can not be removed")] 32 | FileCanNotBeRemoved, 33 | #[error("file can not be written")] 34 | FileCanNotBeWritten, 35 | #[error("invalid file format ({0})")] 36 | InvalidFileFormat(String), 37 | #[error("invalid regex format ({0})")] 38 | InvalidRegex(String), 39 | #[error("permission not available on this system")] 40 | PermissionsNotAccessable, 41 | #[error("git error ({0})")] 42 | GitError(String), 43 | #[error("file can not be fetched")] 44 | FetchError(#[from] PathError), 45 | #[error("{0}")] 46 | String(String), 47 | #[error("executing command failed")] 48 | CommandFailed(String), 49 | } 50 | 51 | pub(crate) trait CheckConstructor { 52 | type Output; 53 | fn from_check_table( 54 | generic_check: GenericChecker, 55 | value: toml_edit::Table, 56 | ) -> Result; 57 | } 58 | pub(crate) trait Checker: DebugTrait { 59 | fn checker_type(&self) -> String; 60 | fn generic_checker(&self) -> &GenericChecker; 61 | fn checker_object(&self) -> String; 62 | 63 | fn list_checker(&self, enabled: bool) { 64 | log::error!( 65 | "{} {} - {} - {} - {:?}", 66 | if enabled { "⬜" } else { " ✖️" }, 67 | self.generic_checker().file_with_checks.short_url_str(), 68 | self.checker_type(), 69 | self.checker_object(), 70 | self.generic_checker().tags 71 | ) 72 | } 73 | fn print(&self, check_result: &CheckResult) { 74 | let (check_result_str, action_message) = match check_result { 75 | CheckResult::NoFixNeeded => ("✅", "".to_string()), 76 | CheckResult::FixNeeded(action) => ("❌", format!(" - {action}")), 77 | CheckResult::FixExecuted(action) => ("🔧", format!(" - {action}")), 78 | CheckResult::Error(e) => ("🚨", format!(" - {e}")), 79 | }; 80 | let msg = format!( 81 | "{} {} - {} - {}{}", 82 | check_result_str, 83 | self.generic_checker().file_with_checks().short_url_str(), 84 | self.checker_type(), 85 | self.checker_object(), 86 | action_message 87 | ); 88 | match check_result { 89 | CheckResult::NoFixNeeded => log::info!("{msg}"), 90 | CheckResult::FixExecuted(_) => log::info!("{msg}"), 91 | CheckResult::FixNeeded(_) => log::warn!("{msg}"), 92 | CheckResult::Error(_) => log::error!("{msg}"), 93 | } 94 | } 95 | 96 | fn check_(&self, fix: bool) -> Result; 97 | 98 | fn check(&self, fix: bool) -> CheckResult { 99 | let check_result = match self.check_(fix) { 100 | Ok(check_result) => check_result, 101 | Err(e) => CheckResult::Error(e.to_string()), 102 | }; 103 | 104 | self.print(&check_result); 105 | 106 | check_result 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/checkers/file/dir_absent.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use crate::{ 4 | checkers::{base::CheckResult, file::get_string_value_from_checktable}, 5 | uri::WritablePath, 6 | }; 7 | 8 | use super::super::{ 9 | GenericChecker, 10 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub(crate) struct DirAbsent { 15 | generic_check: GenericChecker, 16 | dir: WritablePath, 17 | } 18 | 19 | //[[dir_absent]] 20 | // dir = "dir" 21 | 22 | impl CheckConstructor for DirAbsent { 23 | type Output = Self; 24 | 25 | fn from_check_table( 26 | generic_check: GenericChecker, 27 | check_table: toml_edit::Table, 28 | ) -> Result { 29 | let dir = WritablePath::from_string( 30 | get_string_value_from_checktable(&check_table, "dir")?.as_str(), 31 | ) 32 | .map_err(|_| CheckDefinitionError::InvalidDefinition("invalid destination path".into()))?; 33 | 34 | Ok(Self { generic_check, dir }) 35 | } 36 | } 37 | impl Checker for DirAbsent { 38 | fn checker_type(&self) -> String { 39 | "dir_absent".to_string() 40 | } 41 | 42 | fn generic_checker(&self) -> &GenericChecker { 43 | &self.generic_check 44 | } 45 | fn checker_object(&self) -> String { 46 | self.dir.to_string() 47 | } 48 | fn check_(&self, fix: bool) -> Result { 49 | let mut action_messages: Vec = vec![]; 50 | 51 | let remove_dir = self.dir.exists(); 52 | 53 | if remove_dir { 54 | action_messages.push("remove dir".into()); 55 | } 56 | 57 | let action_message = action_messages.join("\n"); 58 | 59 | let check_result = match (remove_dir, fix) { 60 | (false, _) => CheckResult::NoFixNeeded, 61 | (true, false) => CheckResult::FixNeeded(action_message), 62 | (true, true) => { 63 | fs::remove_dir_all(self.dir.as_ref())?; 64 | CheckResult::FixExecuted(action_message) 65 | } 66 | }; 67 | 68 | Ok(check_result) 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | 75 | use crate::checkers::{base::CheckResult, test_helpers}; 76 | 77 | use super::*; 78 | 79 | use tempfile::tempdir; 80 | 81 | fn get_dir_absent_check_present_check_with_result() 82 | -> (Result, tempfile::TempDir) { 83 | let generic_check = test_helpers::get_generic_check(); 84 | 85 | let mut check_table = toml_edit::Table::new(); 86 | let dir = tempdir().unwrap(); 87 | let dir_to_check = dir.path().join("dir_to_check"); 88 | check_table.insert("dir", dir_to_check.to_string_lossy().to_string().into()); 89 | 90 | (DirAbsent::from_check_table(generic_check, check_table), dir) 91 | } 92 | 93 | fn get_dir_absent_check() -> (DirAbsent, tempfile::TempDir) { 94 | let (dir_absent_with_result, tempdir) = get_dir_absent_check_present_check_with_result(); 95 | 96 | (dir_absent_with_result.unwrap(), tempdir) 97 | } 98 | 99 | #[test] 100 | fn test_dir_absent() { 101 | let (dir_absent_check, _tempdir) = get_dir_absent_check(); 102 | 103 | assert_eq!( 104 | dir_absent_check.check_(false).unwrap(), 105 | CheckResult::NoFixNeeded 106 | ); 107 | 108 | fs::create_dir(dir_absent_check.dir.as_ref()).unwrap(); 109 | 110 | assert_eq!( 111 | dir_absent_check.check_(false).unwrap(), 112 | CheckResult::FixNeeded("remove dir".into()) 113 | ); 114 | assert_eq!( 115 | dir_absent_check.check_(true).unwrap(), 116 | CheckResult::FixExecuted("remove dir".into()) 117 | ); 118 | 119 | assert_eq!( 120 | dir_absent_check.check_(false).unwrap(), 121 | CheckResult::NoFixNeeded 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/checkers/check-config.toml: -------------------------------------------------------------------------------- 1 | include = [ 2 | # "https://raw.githubusercontent.com/mrijken/check-config/refs/heads/main/example/checkers/http_check_config.toml", 3 | "config:vars.toml", 4 | "config:http_check_config.toml", 5 | "config:folder/local_file.toml", 6 | "config:copy.toml", 7 | "config:unpack.toml", 8 | ] 9 | 10 | [[dir_present]] 11 | dir = "output/created_dir" 12 | 13 | [[file_present]] 14 | file = "output/created_dir/.gitkeep" 15 | 16 | [[file_present]] 17 | file = "output/.bashrc" 18 | 19 | [[file_present]] 20 | file = "output/.gitconfig" 21 | placeholder = """ 22 | [user] 23 | name = 24 | email = 25 | """ 26 | 27 | [[file_present]] 28 | file = "output/.bashrc" 29 | regex = "export KEY=.*" 30 | 31 | [[file_absent]] 32 | file = "output/to_be_removed" 33 | 34 | [[lines_present]] 35 | file = "output/.bashrc" 36 | lines = "alias ll='ls -alF'" 37 | 38 | [[lines_present]] 39 | file = "output/.bashrc" 40 | lines = "export DATE=\"${date}\"" 41 | is_template = true 42 | 43 | [[lines_absent]] 44 | file = "output/.bashrc" 45 | lines = "alias to_be_removed='ls'" 46 | 47 | [[key_absent]] 48 | file = "output/test.json" 49 | key.to_be_removed = {} 50 | 51 | [[key_absent]] 52 | file = "output/test.toml" 53 | key.to_be_removed = {} 54 | 55 | [[key_absent]] 56 | file = "output/test.yaml" 57 | key.to_be_removed = {} 58 | 59 | [[key_value_present]] 60 | file = "output/test.json" 61 | key.to_be_added = "value" 62 | 63 | [[key_value_present]] 64 | file = "output/test.toml" 65 | key.to_be_added = "value" 66 | 67 | [[key_value_present]] 68 | file = "output/test.yaml" 69 | key.to_be_added = "value" 70 | 71 | [[key_value_present]] 72 | file = "output/test.json" 73 | key.to_be_kept.key = "value" 74 | key.added.key = "1" 75 | 76 | [[key_value_present]] 77 | file = "output/test.toml" 78 | key.to_be_kept.key = "value" 79 | key.added.key = "value" 80 | 81 | [[key_value_present]] 82 | file = "output/test.yaml" 83 | key.to_be_kept.key = "value" 84 | 85 | [[key_value_regex_matched]] 86 | file = "output/test.json" 87 | key.placeholder_regex = "m[a-z]tches" 88 | placeholder = "matches" 89 | 90 | [[key_value_regex_matched]] 91 | file = "output/test.toml" 92 | key.placeholder_regex = "m[a-z]tches" 93 | placeholder = "matches" 94 | 95 | [[key_value_regex_matched]] 96 | file = "output/test.yaml" 97 | key.placeholder_regex = "m[a-z]tches" 98 | placeholder = "matches" 99 | 100 | [[entry_absent]] 101 | file = "output/absent.json" 102 | entry.list2 = ["already_absent"] 103 | 104 | [[entry_absent]] 105 | file = "output/absent.json" 106 | entry.list = ["already_absent"] 107 | 108 | [[entry_absent]] 109 | file = "output/absent.json" 110 | entry.list = ["to_be_removed", "already_absent"] 111 | 112 | [[entry_absent]] 113 | file = "output/absent.toml" 114 | entry.list2 = ["already_absent"] 115 | 116 | [[entry_absent]] 117 | file = "output/absent.toml" 118 | entry.list = ["already_absent"] 119 | 120 | [[entry_absent]] 121 | file = "output/test.toml" 122 | entry.list = ["to_be_removed"] 123 | 124 | [[entry_absent]] 125 | file = "output/test.yaml" 126 | entry.list2 = ["already_absent"] 127 | 128 | [[entry_absent]] 129 | file = "output/test.json" 130 | entry.list = ["to_be_removed"] 131 | 132 | [[entry_absent]] 133 | file = "output/absent.yaml" 134 | entry.list = ["already_absent"] 135 | 136 | [[entry_absent]] 137 | file = "output/test.yaml" 138 | entry.list = ["to_be_removed"] 139 | 140 | [[entry_present]] 141 | file = "output/test.json" 142 | entry.list2 = ["already_present"] 143 | 144 | [[entry_present]] 145 | file = "output/test.json" 146 | entry.list = ["to_be_added", "to_be_kept"] 147 | 148 | [[entry_present]] 149 | file = "output/test.toml" 150 | entry.list = ["to_be_added", "to_be_kept"] 151 | 152 | [[entry_present]] 153 | file = "output/test.toml" 154 | entry.list2 = ["already_present"] 155 | 156 | [[entry_present]] 157 | file = "output/test.yaml" 158 | entry.list = ["to_be_added", "to_be_kept"] 159 | 160 | [[entry_present]] 161 | file = "output/test.yaml" 162 | tags = ["selected"] 163 | entry.list2 = ["already_present"] 164 | 165 | [[entry_present]] 166 | file = "output/test.yaml" 167 | tags = ["not_selected"] 168 | entry.list3 = ["not added"] 169 | 170 | [[git_fetched]] 171 | dir = "output/check-config" 172 | repo = "https://github.com/mrijken/check-config.git" 173 | branch = "main" 174 | 175 | [[dir_absent]] 176 | dir = "output/check-config" 177 | 178 | [[package_present]] 179 | type = "python" 180 | package = "ruff" 181 | version = "0.9.0" 182 | 183 | [[package_absent]] 184 | type = "python" 185 | package = "ty" 186 | 187 | [[lines_present]] 188 | file = "output/lines_present.txt" 189 | marker = "# text from file config:lines_present.txt" 190 | source = "config:lines_present.txt" 191 | -------------------------------------------------------------------------------- /src/checkers/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use regex::Regex; 4 | 5 | use crate::checkers::{base::CheckDefinitionError, file::get_option_string_value_from_checktable}; 6 | 7 | pub(crate) fn get_marker_from_check_table( 8 | value: &toml_edit::Table, 9 | ) -> Result, CheckDefinitionError> { 10 | let marker_lines = match value.get("marker") { 11 | None => None, 12 | Some(marker) => match marker.as_str() { 13 | None => { 14 | return Err(CheckDefinitionError::InvalidDefinition( 15 | "`marker` is not a string".to_string(), 16 | )); 17 | } 18 | Some(marker) => { 19 | let marker = marker.trim_end(); 20 | Some(( 21 | format!("{marker} (check-config start)\n"), 22 | format!("{marker} (check-config end)\n"), 23 | )) 24 | } 25 | }, 26 | }; 27 | 28 | Ok(marker_lines) 29 | } 30 | 31 | /// Get the lines from value 32 | /// When absent, return an error or return the default_value when Some 33 | pub(crate) fn get_lines_from_check_table( 34 | check_table: &toml_edit::Table, 35 | default_value: Option, 36 | ) -> Result { 37 | match get_option_string_value_from_checktable(check_table, "lines") { 38 | Ok(None) => Ok(default_value.unwrap_or("".to_string()).to_string()), 39 | Ok(Some(lines)) => { 40 | let lines_with_trailing_new_line_when_not_empty = append_str(&lines, ""); 41 | Ok(lines_with_trailing_new_line_when_not_empty) 42 | } 43 | Err(err) => Err(err), 44 | } 45 | } 46 | 47 | /// Replace the text between markers with replacement 48 | /// The markers re not removed 49 | /// When the markers are not present, the markers and replacement 50 | /// are appended to the contents 51 | pub(crate) fn replace_between_markers( 52 | contents: &str, 53 | start_marker: &str, 54 | end_marker: &str, 55 | replacement: &str, 56 | ) -> String { 57 | if let (Some(start_pos), Some(end_pos)) = 58 | (contents.find(start_marker), contents.find(end_marker)) 59 | && start_pos < end_pos 60 | { 61 | let before = &contents[..start_pos + start_marker.len()]; 62 | let after = &contents[end_pos..]; 63 | return format!("{}{}{}", before, replacement, after); 64 | } 65 | 66 | append_str( 67 | contents, 68 | format!("{}{}{}", start_marker, replacement, end_marker).as_str(), 69 | ) // if markers not found or in wrong order, append the string 70 | } 71 | 72 | /// Remove the markers and every between it from contents 73 | /// When the markes are not present, the orginal contents are returned 74 | pub(crate) fn remove_between_markers( 75 | contents: &str, 76 | start_marker: &str, 77 | end_marker: &str, 78 | ) -> String { 79 | if let (Some(start_pos), Some(end_pos)) = 80 | (contents.find(start_marker), contents.find(end_marker)) 81 | && start_pos < end_pos 82 | { 83 | let before = &contents[..start_pos]; 84 | let after = &contents[end_pos + end_marker.len()..]; 85 | return format!("{}{}", before, after); 86 | } 87 | 88 | contents.to_string() 89 | } 90 | 91 | pub(crate) fn append_str(contents: &str, lines: &str) -> String { 92 | if lines.trim().is_empty() { 93 | return contents.to_string(); 94 | } 95 | if contents.trim().is_empty() { 96 | return lines.to_string(); 97 | } 98 | let contents = contents.trim_end(); 99 | format!("{}\n\n{}", contents, lines) 100 | } 101 | 102 | /// replace ${} with the value of var in `vars` 103 | /// backslash is used to escape the substitution and will replace \${} with ${} 104 | pub(crate) fn replace_vars(template: &str, vars: &HashMap) -> String { 105 | // This regex matches escaped or normal placeholders 106 | let re = Regex::new(r"\\(\$\{[^}]+\})|\$\{([^}]+)\}|\{([^}]+)\}").unwrap(); 107 | 108 | re.replace_all(template, |caps: ®ex::Captures| { 109 | if let Some(escaped) = caps.get(1) { 110 | // Return the escaped placeholder without the backslash 111 | escaped.as_str().to_string() 112 | } else { 113 | // Handle ${var} or {var} 114 | let key = caps.get(2).or(caps.get(3)).unwrap().as_str(); 115 | vars.get(key) 116 | .cloned() 117 | .unwrap_or_else(|| caps[0].to_string()) 118 | } 119 | }) 120 | .into_owned() 121 | } 122 | 123 | mod tests { 124 | #[test] 125 | fn test_replace_vars() { 126 | let template = r#"Hello ${name} \${name}!"#; 127 | let vars = std::collections::HashMap::from([("name".to_string(), "world".to_string())]); 128 | let result = super::replace_vars(template, &vars); 129 | assert_eq!(result, "Hello world ${name}!"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/checkers/file/entry_absent.rs: -------------------------------------------------------------------------------- 1 | use crate::{checkers::file::FileCheck, mapping::generic::Mapping}; 2 | 3 | use super::super::{ 4 | GenericChecker, 5 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub(crate) struct EntryAbsent { 10 | file_check: FileCheck, 11 | absent: toml_edit::Table, 12 | } 13 | 14 | // [[entry_absent]] 15 | // file = "file" 16 | // entry.key = ["item1"] 17 | impl CheckConstructor for EntryAbsent { 18 | type Output = Self; 19 | fn from_check_table( 20 | generic_check: GenericChecker, 21 | check_table: toml_edit::Table, 22 | ) -> Result { 23 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 24 | let absent_entries = match check_table.get("entry") { 25 | None => { 26 | return Err(CheckDefinitionError::InvalidDefinition( 27 | "`entry` key is not present".into(), 28 | )); 29 | } 30 | Some(absent) => match absent.as_table() { 31 | None => { 32 | return Err(CheckDefinitionError::InvalidDefinition( 33 | "`entry` is not a table".into(), 34 | )); 35 | } 36 | Some(absent) => { 37 | // TODO: check if there is an array in absent 38 | absent.clone() 39 | } 40 | }, 41 | }; 42 | 43 | Ok(Self { 44 | file_check, 45 | absent: absent_entries, 46 | }) 47 | } 48 | } 49 | 50 | // [[entry_absent]] 51 | // file = "test.json" 52 | // entry.key = ["item"] 53 | impl Checker for EntryAbsent { 54 | fn checker_type(&self) -> String { 55 | "entry_absent".to_string() 56 | } 57 | 58 | fn checker_object(&self) -> String { 59 | self.file_check.check_object() 60 | } 61 | 62 | fn generic_checker(&self) -> &GenericChecker { 63 | &self.file_check.generic_check 64 | } 65 | 66 | fn check_(&self, fix: bool) -> Result { 67 | let mut doc = self.file_check.get_mapping()?; 68 | 69 | remove_entries(doc.as_mut(), &self.absent); 70 | 71 | self.file_check.conclude_check_with_new_doc(doc, fix) 72 | } 73 | } 74 | 75 | fn remove_entries(doc: &mut dyn Mapping, entries_to_remove: &toml_edit::Table) { 76 | for (key_to_remove, value_to_remove) in entries_to_remove { 77 | if !doc.contains_key(key_to_remove) { 78 | // key_to_remove does not exists, so no need to remove value_to_remove 79 | continue; 80 | } 81 | if let Some(value_to_remove) = value_to_remove.as_array() { 82 | let doc_array = match doc.get_array(key_to_remove, false) { 83 | Ok(a) => a, 84 | Err(_) => { 85 | log::error!("expecting key to exist"); 86 | std::process::exit(1); 87 | } 88 | }; 89 | 90 | for item in value_to_remove.iter() { 91 | doc_array.remove(&toml_edit::Item::Value(item.to_owned())) 92 | } 93 | continue; 94 | } 95 | let child_doc = doc 96 | .get_mapping(key_to_remove, false) 97 | .expect("key exists from which value is removed"); 98 | if let Some(value_to_remove) = value_to_remove.as_table() { 99 | remove_entries(child_doc, value_to_remove); 100 | } 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use std::str::FromStr; 107 | 108 | use crate::checkers::test_helpers::read_test_files; 109 | use crate::file_types::{self, FileType}; 110 | 111 | use super::*; 112 | 113 | #[test] 114 | fn test_test_files() { 115 | for (test_path, test_input, test_expected_output, checker) in 116 | read_test_files("entry_absent") 117 | { 118 | let mut test_input = test_input; 119 | remove_entries(test_input.as_mut(), &checker); 120 | 121 | assert_eq!( 122 | *test_expected_output, 123 | test_input.to_string(4).unwrap(), 124 | "test_path {test_path} failed" 125 | ); 126 | } 127 | } 128 | 129 | #[test] 130 | fn test_remove_entries_with_tables() { 131 | let entries_to_remove = r#" 132 | [key] 133 | list = [{key = "3"}, {key = "2"}, {key = "4"}] 134 | "#; 135 | let entries_to_remove = toml_edit::DocumentMut::from_str(entries_to_remove).unwrap(); 136 | let entries_to_remove = entries_to_remove.as_table(); 137 | 138 | let toml_contents = r#"[key] 139 | list = [{key = "1"}, {key = "2"}] 140 | "#; 141 | let toml_new_contents = "[key]\nlist = [{key = \"1\"}]\n"; 142 | 143 | let mut toml_doc = file_types::toml::Toml::new() 144 | .to_mapping(toml_contents) 145 | .unwrap(); 146 | remove_entries(toml_doc.as_mut(), entries_to_remove); 147 | 148 | assert_eq!(toml_new_contents, toml_doc.to_string(4).unwrap()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.2.0 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - "*" 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | coverage: 23 | runs-on: ubuntu-latest 24 | env: 25 | CARGO_TERM_COLOR: always 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: dtolnay/rust-toolchain@stable 29 | with: 30 | components: llvm-tools-preview 31 | - uses: taiki-e/install-action@cargo-llvm-cov 32 | - uses: taiki-e/install-action@nextest 33 | - name: Collect coverage data (including doctests) 34 | run: | 35 | cargo llvm-cov --no-report nextest 36 | cargo +nightly llvm-cov --no-report --doc 37 | cargo llvm-cov report --doctests --lcov --output-path lcov.info 38 | - name: Upload coverage to Codecov 39 | uses: codecov/codecov-action@v3 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 42 | files: lcov.info 43 | fail_ci_if_error: false 44 | linux: 45 | # Use Ubuntu 20.04 here, because latest (22.04) uses OpenSSL v3. 46 | # Currently, OpenSSL v3 causes compile issues with openssl-sys 47 | runs-on: ubuntu-latest 48 | strategy: 49 | matrix: 50 | target: [x86_64, x86, aarch64, armv7] 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Build wheels 54 | uses: PyO3/maturin-action@v1 55 | with: 56 | target: ${{ matrix.target }} 57 | args: --release --out dist 58 | sccache: "true" 59 | manylinux: auto 60 | before-script-linux: | 61 | # If we're running on rhel centos, install needed packages. 62 | if command -v yum &> /dev/null; then 63 | yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic 64 | 65 | # If we're running on i686 we need to symlink libatomic 66 | # in order to build openssl with -latomic flag. 67 | if [[ ! -d "/usr/lib64" ]]; then 68 | ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so 69 | fi 70 | else 71 | # If we're running on debian-based system. 72 | apt update -y && apt-get install -y libssl-dev openssl pkg-config 73 | fi 74 | - name: Upload wheels 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: wheels-linux-${{ matrix.target }} 78 | path: dist 79 | 80 | windows: 81 | runs-on: windows-latest 82 | strategy: 83 | matrix: 84 | target: [x64, x86] 85 | steps: 86 | - uses: actions/checkout@v4 87 | - name: Build wheels 88 | uses: PyO3/maturin-action@v1 89 | with: 90 | target: ${{ matrix.target }} 91 | args: --release --out dist 92 | sccache: "true" 93 | - name: Upload wheels 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: wheels-windows-${{ matrix.target }} 97 | path: dist 98 | 99 | macos: 100 | runs-on: macos-latest 101 | strategy: 102 | matrix: 103 | target: [x86_64, aarch64] 104 | steps: 105 | - uses: actions/checkout@v4 106 | - name: Build wheels 107 | uses: PyO3/maturin-action@v1 108 | with: 109 | target: ${{ matrix.target }} 110 | args: --release --out dist 111 | sccache: "true" 112 | - name: Upload wheels 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: wheels-macos-${{ matrix.target }} 116 | path: dist 117 | 118 | sdist: 119 | runs-on: ubuntu-latest 120 | steps: 121 | - uses: actions/checkout@v4 122 | - name: Build sdist 123 | uses: PyO3/maturin-action@v1 124 | with: 125 | command: sdist 126 | args: --out dist 127 | - name: Upload sdist 128 | uses: actions/upload-artifact@v4 129 | with: 130 | name: wheels-sdist 131 | path: dist 132 | 133 | pypi_release: 134 | name: PyPI Release 135 | runs-on: ubuntu-latest 136 | if: "startsWith(github.ref, 'refs/tags/')" 137 | needs: [linux, windows, macos, sdist] 138 | steps: 139 | - uses: actions/download-artifact@v4 140 | with: 141 | pattern: wheels-* 142 | merge-multiple: true 143 | - name: Publish to PyPI 144 | uses: PyO3/maturin-action@v1 145 | env: 146 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 147 | with: 148 | command: upload 149 | args: --skip-existing * 150 | 151 | crates_release: 152 | name: Crates Release 153 | runs-on: ubuntu-latest 154 | if: "startsWith(github.ref, 'refs/tags/')" 155 | needs: [linux, windows, macos, sdist] 156 | steps: 157 | - name: Checkout sources 158 | uses: actions/checkout@v4 159 | 160 | - name: Install stable toolchain 161 | uses: actions-rs/toolchain@v1 162 | with: 163 | profile: minimal 164 | toolchain: stable 165 | override: true 166 | 167 | - run: cargo publish --token ${CRATES_TOKEN} 168 | env: 169 | CRATES_TOKEN: ${{ secrets.CRATES_API_TOKEN }} 170 | -------------------------------------------------------------------------------- /src/mapping/generic.rs: -------------------------------------------------------------------------------- 1 | use derive_more::derive::Display; 2 | 3 | use crate::checkers::base::CheckError; 4 | 5 | #[derive(Debug, Display)] 6 | pub(crate) enum MappingError { 7 | MissingKey(String), 8 | WrongType(String), 9 | } 10 | 11 | pub(crate) trait Mapping: Send + Sync { 12 | fn to_string(&self, indent: usize) -> Result; 13 | 14 | fn get_mapping( 15 | &mut self, 16 | key: &str, 17 | create_missing: bool, 18 | ) -> Result<&mut dyn Mapping, MappingError>; 19 | fn contains_key(&self, key: &str) -> bool; 20 | fn get_array( 21 | &mut self, 22 | key: &str, 23 | create_missing: bool, 24 | ) -> Result<&mut dyn Array, MappingError>; 25 | fn get_string(&self, key: &str) -> Result; 26 | fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item); 27 | fn remove(&mut self, key: &str); 28 | } 29 | 30 | pub(crate) trait Array { 31 | fn insert_when_not_present(&mut self, value: &toml_edit::Item); 32 | 33 | fn remove(&mut self, value: &toml_edit::Item); 34 | 35 | fn contains_item(&self, value: &toml_edit::Item) -> bool; 36 | } 37 | pub(crate) trait Value { 38 | fn from_toml_value(value: &toml_edit::Item) -> Self 39 | where 40 | Self: Sized; 41 | } 42 | 43 | #[cfg(test)] 44 | pub(crate) mod tests { 45 | 46 | use super::Mapping; 47 | 48 | pub(crate) fn get_test_table() -> toml_edit::Item { 49 | let toml = r#"str = "string" 50 | # comment for int 51 | int = 1 52 | float = 1.1 53 | bool = true 54 | array = [ 55 | # comment for item 1 56 | 1, 57 | # comment for item 2 58 | 2 59 | ] 60 | dict = { str = "string", int = 1, float = 1.1, bool = true, array = [1, 2] } 61 | "#; 62 | toml.parse::() 63 | .expect("invalid doc") 64 | .as_item() 65 | .to_owned() 66 | } 67 | 68 | pub(crate) fn test_mapping(mut mapping_to_check: Box) { 69 | assert!( 70 | mapping_to_check 71 | .get_array("array", false) 72 | .expect("") 73 | .contains_item(&toml_edit::Item::from(toml_edit::Value::from(1))) 74 | ); 75 | 76 | assert_eq!( 77 | mapping_to_check.get_string("str").expect(""), 78 | "string".to_string() 79 | ); 80 | assert!(mapping_to_check.get_string("int").is_err(),); 81 | assert!(mapping_to_check.get_string("absent").is_err(),); 82 | assert!(mapping_to_check.get_array("absent", false).is_err(),); 83 | assert_eq!( 84 | mapping_to_check 85 | .get_mapping("dict", false) 86 | .expect("") 87 | .get_string("str") 88 | .unwrap(), 89 | "string".to_string() 90 | ); 91 | 92 | assert!( 93 | mapping_to_check 94 | .get_mapping("dict", false) 95 | .expect("") 96 | .get_array("array", false) 97 | .unwrap() 98 | .contains_item(&toml_edit::Item::from(toml_edit::Value::from(1))) 99 | ); 100 | 101 | mapping_to_check 102 | .get_mapping("new_dict", true) 103 | .unwrap() 104 | .insert( 105 | &toml_edit::Key::new("key"), 106 | &toml_edit::Item::Value(toml_edit::Value::from("new_dict_value")), 107 | ); 108 | 109 | assert_eq!( 110 | mapping_to_check 111 | .get_mapping("new_dict", false) 112 | .expect("") 113 | .get_string("key") 114 | .unwrap(), 115 | "new_dict_value".to_string() 116 | ); 117 | 118 | mapping_to_check 119 | .get_mapping("dict", false) 120 | .unwrap() 121 | .get_mapping("new_nested_dict", true) 122 | .unwrap() 123 | .insert( 124 | &toml_edit::Key::new("key"), 125 | &toml_edit::Item::Value(toml_edit::Value::from("new_nested_dict_value")), 126 | ); 127 | 128 | assert_eq!( 129 | mapping_to_check 130 | .get_mapping("dict", false) 131 | .unwrap() 132 | .get_mapping("new_nested_dict", false) 133 | .unwrap() 134 | .get_string("key") 135 | .unwrap(), 136 | "new_nested_dict_value".to_string() 137 | ); 138 | 139 | mapping_to_check 140 | .get_array("new_array", true) 141 | .unwrap() 142 | .insert_when_not_present(&toml_edit::Item::from(toml_edit::Value::from( 143 | "new_array_value", 144 | ))); 145 | 146 | assert!( 147 | mapping_to_check 148 | .get_array("new_array", false) 149 | .unwrap() 150 | .contains_item(&toml_edit::Item::from(toml_edit::Value::from( 151 | "new_array_value" 152 | ))) 153 | ); 154 | 155 | mapping_to_check 156 | .get_mapping("dict", false) 157 | .unwrap() 158 | .get_array("new_nested_array", true) 159 | .unwrap() 160 | .insert_when_not_present(&toml_edit::Item::from(toml_edit::Value::from( 161 | "new_nested_array_value", 162 | ))); 163 | 164 | assert!( 165 | mapping_to_check 166 | .get_mapping("dict", false) 167 | .unwrap() 168 | .get_array("new_nested_array", false) 169 | .unwrap() 170 | .contains_item(&toml_edit::Item::from(toml_edit::Value::from( 171 | "new_nested_array_value" 172 | ))) 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | After installation, with the next command you can check your configuration files 4 | 5 | ```shell 6 | check-config 7 | ``` 8 | 9 | Note: with [uvx](https://docs.astral.sh/uv/guides/tools/) it is also possible to run it without installation: 10 | 11 | ```shell 12 | uvx check-config 13 | ``` 14 | 15 | It will output nothing when the check succeeds: 16 | 17 | ```console 18 | 🥇 No violations found. 19 | ``` 20 | 21 | If you use verbose mode with -v, more will be outputted: 22 | 23 | ```console 24 | 2 checks successful. 25 | 🥇 No violations found. 26 | ``` 27 | 28 | And with -vv as option, even more output will be given: 29 | 30 | ```console 31 | Starting check-config 32 | Using checkers from file:///home/ubuntu/repos/check-config/example/check-config-for-usage-doc.toml 33 | Fix: false 34 | ✅ example/check-config-for-usage-doc.toml - /home/ubuntu/.bashrc - lines_present 35 | ✅ example/check-config-for-usage-doc.toml - /home/ubuntu/.bashrc - lines_present 36 | 2 checks successful. 37 | 🥇 No violations found 38 | ``` 39 | 40 | When there fixes possible, you will get the next output. 41 | 42 | No verbose: 43 | 44 | ```console 45 | 🪛 There is 1 violation to fix. 46 | ``` 47 | 48 | Single verhose (-v): 49 | 50 | ```console 51 | ❌ example/check-config-for-usage-doc.toml - /home/ubuntu/.bashrc - lines_present - Set file contents to: 52 | @@ -128,3 +128,4 @@ 53 | 54 | export EDITOR=hx 55 | +export SHELL=/bin/bash 56 | 57 | 1 checks successful. 58 | 🪛 There is 1 violation to fix. 59 | ``` 60 | 61 | Double verbose (-vv): 62 | 63 | ```console 64 | Starting check-config 65 | Using checkers from example/check-config-for-usage-doc.toml 66 | Fix: false 67 | ❌ example/check-config-for-usage-doc.toml - /home/ubuntu/.bashrc - lines_present - Set file contents to: 68 | @@ -128,3 +128,4 @@ 69 | 70 | export EDITOR=hx 71 | +export SHELL=/bin/bash 72 | 73 | ✅ example/check-config-for-usage-doc.toml - /home/ubuntu/.bashrc - lines_present 74 | 1 checks successful. 75 | 🪛 There is 1 violation to fix. 76 | ``` 77 | 78 | Check Config will use the checkers as defined in `check-config.toml`. 79 | 80 | Optionally you can specify another path to a toml file with checkers: 81 | 82 | ```shell 83 | check-config -p # or --path 84 | ``` 85 | 86 | When the path is a file, that file is used. When the path is a directory, it will use the 87 | `check-config.toml` file in that directory. 88 | 89 | Note: it is also possible to re-use configuration files of other platforms 90 | to reduce the number of config files: 91 | 92 | | file | top level | 93 | | ---------------- | -------------------------------- | 94 | | `pyproject.toml` | `[tool.check-config]` | 95 | | `Cargo.toml` | `[package.metadata.check-confg]` | 96 | 97 | You can submit the path also via an environment variable: 98 | 99 | ```shell 100 | export CHECK_CONFIG_PATH= 101 | ``` 102 | 103 | Optionally you can not just check your files, but also try to fix them: 104 | 105 | ```shell 106 | check-config --fix 107 | ``` 108 | 109 | Or just view the checkers without executing them 110 | 111 | ```shell 112 | check-config --list-checkers # or -l 113 | ``` 114 | 115 | When fixing files, files will be created, modified or deleted. No intermediate directories 116 | will be created, unless you ask to do so: 117 | 118 | ```shell 119 | check-config -c # or --create-missing-directories 120 | ``` 121 | 122 | This can also be enabled via an environment variable: 123 | 124 | ```shell 125 | export CHECK_CONFIG_CREATE_DIRS=true 126 | ``` 127 | 128 | ## Tags 129 | 130 | When tags are specified in the checkers, it is possible restrict the executing to 131 | tags. 132 | 133 | ```shell 134 | check-config --any-tags tag1,tag2 --all-tags tag3,tag4 --skip-tags tag5,tag6 135 | ``` 136 | 137 | This invocation call checkers which has one of [tag1, tag2], all of 138 | [tag3, tag4] and not one of [tag5, tag6] specified in their `tags` key. 139 | 140 | ## Environment variables 141 | 142 | You can use your environment variables in templates of the checkers via the 143 | `--env` option: 144 | 145 | ```shell 146 | check-config --env 147 | ``` 148 | 149 | ## Pre-commit 150 | 151 | [pre-commit](https://pre-commit.com/) helps checking your code before 152 | committing git, so you can catch errors before the build pipeline does. 153 | 154 | Add the next repo to the `.pre-commit-config.yaml` in your repository with the id of the hook 155 | you want to use: 156 | 157 | ```yaml 158 | repos: 159 | - repo: https://github.com/mrijken/check-config 160 | rev: v0.9.11 161 | hooks: 162 | # Install via Cargo and execute `check-config --fix` 163 | - id: check_config_fix_install_via_rust 164 | # Install via pip and execute `check-config --fix` 165 | - id: check_config_fix_install_via_python 166 | # Install via Cargo and execute `check-config` 167 | - id: check_config_check_install_via_rust 168 | # Install via pip and execute `check-config` 169 | - id: check_config_check_install_via_python 170 | ``` 171 | 172 | If you want to call check-config with other arguments, like a different toml, you can create your own hook 173 | in your `.pre-commit-config.toml`: 174 | 175 | ```yaml 176 | - repo: local 177 | hooks: 178 | - id: check_config_fix_install_via_rust 179 | name: check configuration files based on check_config.toml and try to fix them 180 | language: rust 181 | entry: check-config --fix -p check.toml -vv 182 | pass_filenames: false 183 | always_run: true 184 | ``` 185 | 186 | ## Exit Codes 187 | 188 | We use the following exit codes, which you can make use of in your build pipelines. 189 | 190 | | code | meaning | 191 | | ---- | ----------------------------------------------------------------------------------------------------- | 192 | | 0 | OK | 193 | | 1 | Parsing error: the checkers file is not valid TOML, has a wrong check type or any other parsing error | 194 | | 2 | Violation error: one or more of you checker have failed | 195 | -------------------------------------------------------------------------------- /src/checkers/file/dir_present.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | #[cfg(not(target_os = "windows"))] 3 | use std::os::unix::fs::PermissionsExt; 4 | 5 | use crate::{ 6 | checkers::{ 7 | base::CheckResult, 8 | file::{file_present::get_permissions_from_checktable, get_string_value_from_checktable}, 9 | }, 10 | uri::WritablePath, 11 | }; 12 | 13 | use super::super::{ 14 | GenericChecker, 15 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 16 | }; 17 | 18 | #[derive(Debug)] 19 | pub(crate) struct DirPresent { 20 | generic_check: GenericChecker, 21 | dir: WritablePath, 22 | permissions: Option, 23 | } 24 | 25 | //[[dir_present]] 26 | // dir = "dir" 27 | // permissions = "755" # optional 28 | 29 | impl CheckConstructor for DirPresent { 30 | type Output = Self; 31 | 32 | fn from_check_table( 33 | generic_check: GenericChecker, 34 | check_table: toml_edit::Table, 35 | ) -> Result { 36 | let dir = WritablePath::from_string( 37 | get_string_value_from_checktable(&check_table, "dir")?.as_str(), 38 | ) 39 | .map_err(|_| CheckDefinitionError::InvalidDefinition("invalid destination path".into()))?; 40 | 41 | let permissions = get_permissions_from_checktable(&check_table)?; 42 | 43 | Ok(Self { 44 | generic_check, 45 | dir, 46 | permissions, 47 | }) 48 | } 49 | } 50 | impl Checker for DirPresent { 51 | fn checker_type(&self) -> String { 52 | "dir_present".to_string() 53 | } 54 | 55 | fn generic_checker(&self) -> &GenericChecker { 56 | &self.generic_check 57 | } 58 | fn checker_object(&self) -> String { 59 | self.dir.to_string() 60 | } 61 | fn check_(&self, fix: bool) -> Result { 62 | let mut action_messages: Vec = vec![]; 63 | 64 | let create_dir = !self.dir.exists(); 65 | 66 | if create_dir { 67 | action_messages.push("create dir".into()); 68 | } 69 | 70 | let fix_permissions = if let Some(permissions) = self.permissions.clone() { 71 | #[cfg(target_os = "windows")] 72 | { 73 | false 74 | } 75 | 76 | #[cfg(not(target_os = "windows"))] 77 | { 78 | if create_dir { 79 | true 80 | } else { 81 | let current_permissions = match self.dir.as_ref().metadata() { 82 | Err(_) => { 83 | return Err(CheckError::PermissionsNotAccessable); 84 | } 85 | Ok(metadata) => metadata.permissions(), 86 | }; 87 | 88 | // we only check for the last 3 octal digits 89 | 90 | (current_permissions.mode() & 0o777) != (permissions.mode() & 0o777) 91 | } 92 | } 93 | } else { 94 | false 95 | }; 96 | 97 | #[cfg(not(target_os = "windows"))] 98 | if fix_permissions { 99 | action_messages.push(format!( 100 | "fix permissions to {:o}", 101 | self.permissions.clone().unwrap().to_owned().mode() 102 | )); 103 | } 104 | 105 | let action_message = action_messages.join("\n"); 106 | 107 | let check_result = match (create_dir || fix_permissions, fix) { 108 | (false, _) => CheckResult::NoFixNeeded, 109 | (true, false) => CheckResult::FixNeeded(action_message), 110 | (true, true) => { 111 | fs::create_dir_all(self.dir.as_ref())?; 112 | if fix_permissions { 113 | fs::set_permissions(self.dir.as_ref(), self.permissions.clone().unwrap())?; 114 | } 115 | CheckResult::FixExecuted(action_message) 116 | } 117 | }; 118 | 119 | Ok(check_result) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | 126 | use crate::checkers::{base::CheckResult, test_helpers}; 127 | 128 | use super::*; 129 | 130 | use tempfile::tempdir; 131 | 132 | fn get_dir_present_check_present_check_with_result( 133 | permissions: Option, 134 | ) -> (Result, tempfile::TempDir) { 135 | let generic_check = test_helpers::get_generic_check(); 136 | 137 | let mut check_table = toml_edit::Table::new(); 138 | let dir = tempdir().unwrap(); 139 | let dir_to_check = dir.path().join("dir_to_check"); 140 | check_table.insert("dir", dir_to_check.to_string_lossy().to_string().into()); 141 | 142 | if let Some(permissions) = permissions { 143 | check_table.insert("permissions", permissions.into()); 144 | } 145 | 146 | ( 147 | DirPresent::from_check_table(generic_check, check_table), 148 | dir, 149 | ) 150 | } 151 | 152 | fn get_dir_present_check(permissions: Option) -> (DirPresent, tempfile::TempDir) { 153 | let (dir_present_with_result, tempdir) = 154 | get_dir_present_check_present_check_with_result(permissions); 155 | 156 | (dir_present_with_result.unwrap(), tempdir) 157 | } 158 | #[test] 159 | fn test_dir_present() { 160 | let (dir_present_check, _tempdir) = get_dir_present_check(None); 161 | 162 | assert_eq!( 163 | dir_present_check.check_(false).unwrap(), 164 | CheckResult::FixNeeded("create dir".into()) 165 | ); 166 | 167 | assert_eq!( 168 | dir_present_check.check_(true).unwrap(), 169 | CheckResult::FixExecuted("create dir".into()) 170 | ); 171 | assert_eq!( 172 | dir_present_check.check_(false).unwrap(), 173 | CheckResult::NoFixNeeded 174 | ); 175 | } 176 | 177 | #[test] 178 | fn test_dir_present_with_permissions() { 179 | let (dir_present_check, _tempdir) = get_dir_present_check(Some("777".into())); 180 | 181 | assert_eq!( 182 | dir_present_check.check_(false).unwrap(), 183 | CheckResult::FixNeeded("create dir\nfix permissions to 777".into()) 184 | ); 185 | 186 | assert_eq!( 187 | dir_present_check.check_(true).unwrap(), 188 | CheckResult::FixExecuted("create dir\nfix permissions to 777".into()) 189 | ); 190 | 191 | assert_eq!( 192 | dir_present_check.check_(false).unwrap(), 193 | CheckResult::NoFixNeeded 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/checkers/file/entry_present.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::file::FileCheck; 2 | pub(crate) use crate::mapping::generic::Mapping; 3 | 4 | use super::super::{ 5 | GenericChecker, 6 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct EntryPresent { 11 | file_check: FileCheck, 12 | present: toml_edit::Table, 13 | } 14 | 15 | // [[entry_present]] 16 | // file = "file" 17 | // entry.key = ["item1"] 18 | impl CheckConstructor for EntryPresent { 19 | type Output = Self; 20 | fn from_check_table( 21 | generic_check: GenericChecker, 22 | check_table: toml_edit::Table, 23 | ) -> Result { 24 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 25 | let present_entries = match check_table.get("entry") { 26 | None => { 27 | return Err(CheckDefinitionError::InvalidDefinition( 28 | "`entry` key is not present".into(), 29 | )); 30 | } 31 | Some(present) => match present.as_table() { 32 | None => { 33 | return Err(CheckDefinitionError::InvalidDefinition( 34 | "`entry` is not a table".into(), 35 | )); 36 | } 37 | Some(present) => { 38 | // TODO: check if there is an array in present 39 | present.clone() 40 | } 41 | }, 42 | }; 43 | 44 | Ok(Self { 45 | file_check, 46 | present: present_entries, 47 | }) 48 | } 49 | } 50 | 51 | // [[entry_present]] 52 | // file = "test.json" 53 | // present.key = ["item"] 54 | impl Checker for EntryPresent { 55 | fn checker_type(&self) -> String { 56 | "entry_present".to_string() 57 | } 58 | 59 | fn checker_object(&self) -> String { 60 | self.file_check.check_object() 61 | } 62 | 63 | fn generic_checker(&self) -> &GenericChecker { 64 | &self.file_check.generic_check 65 | } 66 | 67 | fn check_(&self, fix: bool) -> Result { 68 | let mut doc = self.file_check.get_mapping()?; 69 | 70 | add_entries(doc.as_mut(), &self.present); 71 | 72 | self.file_check.conclude_check_with_new_doc(doc, fix) 73 | } 74 | } 75 | 76 | fn add_entries(doc: &mut dyn Mapping, entries_to_add: &toml_edit::Table) { 77 | for (key_to_add, value_to_add) in entries_to_add { 78 | if let Some(array_to_add) = value_to_add.as_array() { 79 | let doc_array = doc 80 | .get_array(key_to_add, true) 81 | .expect("expecting key to exist"); 82 | 83 | for item in array_to_add { 84 | doc_array.insert_when_not_present(&toml_edit::Item::Value(item.to_owned())) 85 | } 86 | continue; 87 | } 88 | 89 | let child_doc = doc 90 | .get_mapping(key_to_add, true) 91 | .expect("expecting key to exist as mapping"); 92 | if let Some(table_to_add) = value_to_add.as_table() { 93 | add_entries(child_doc, table_to_add); 94 | } 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | use std::{fs::read_to_string, io::Write, str::FromStr}; 101 | 102 | use crate::checkers::test_helpers::read_test_files; 103 | 104 | use super::*; 105 | 106 | #[test] 107 | fn test_test_files() { 108 | for (test_path, test_input, test_expected_output, checker) in 109 | read_test_files("entry_present") 110 | { 111 | let mut test_input = test_input; 112 | add_entries(test_input.as_mut(), &checker); 113 | 114 | assert_eq!( 115 | *test_expected_output, 116 | test_input.to_string(4).unwrap(), 117 | "test_path {test_path} failed" 118 | ); 119 | } 120 | } 121 | fn get_file_check_with_result( 122 | entry: i64, 123 | indent: Option, 124 | ) -> ( 125 | Result, 126 | tempfile::TempDir, 127 | ) { 128 | let generic_check = crate::checkers::test_helpers::get_generic_check(); 129 | let tmp_dir = tempfile::tempdir().unwrap(); 130 | let path_to_check = tmp_dir.path().join("file_to_check.json"); 131 | let mut file_to_check = std::fs::File::create(&path_to_check).unwrap(); 132 | 133 | let indent = if let Some(indent) = indent { 134 | format!("indent = {}", indent) 135 | } else { 136 | "".to_string() 137 | }; 138 | 139 | let check_doc = toml_edit::DocumentMut::from_str( 140 | format!( 141 | r#" 142 | file="{}" 143 | entry.list = [ {} ] 144 | {}"#, 145 | path_to_check.to_string_lossy(), 146 | entry, 147 | indent 148 | ) 149 | .as_str(), 150 | ) 151 | .unwrap(); 152 | let check_table = check_doc.as_table().clone(); 153 | 154 | writeln!(file_to_check, "{{\"list\": [1,2] }}").unwrap(); 155 | 156 | ( 157 | EntryPresent::from_check_table(generic_check, check_table), 158 | tmp_dir, 159 | ) 160 | } 161 | 162 | fn get_file_unpacked_check( 163 | entry: i64, 164 | indent: Option, 165 | ) -> (EntryPresent, tempfile::TempDir) { 166 | let (file_check_with_result, tempdir) = get_file_check_with_result(entry, indent); 167 | 168 | ( 169 | file_check_with_result.expect("check without issues"), 170 | tempdir, 171 | ) 172 | } 173 | 174 | #[test] 175 | fn test_indent() { 176 | let (result, _dir) = get_file_check_with_result(12, Some(-2)); 177 | assert_eq!( 178 | result.err().unwrap(), 179 | CheckDefinitionError::InvalidDefinition("indent must be >= 0".into()) 180 | ); 181 | 182 | let (result, dir) = get_file_check_with_result(12, Some(2)); 183 | result.unwrap().check(true); 184 | let file_to_check = dir.path().join("file_to_check.json"); 185 | let contents = read_to_string(file_to_check).unwrap(); 186 | assert_eq!( 187 | contents, 188 | "{\n \"list\": [\n 1,\n 2,\n 12\n ]\n}\n" 189 | ); 190 | 191 | let (result, dir) = get_file_check_with_result(12, Some(4)); 192 | result.unwrap().check(true); 193 | let file_to_check = dir.path().join("file_to_check.json"); 194 | let contents = read_to_string(file_to_check).unwrap(); 195 | assert_eq!( 196 | contents, 197 | "{\n \"list\": [\n 1,\n 2,\n 12\n ]\n}\n" 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/checkers/package/mod.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Display; 2 | 3 | use crate::checkers::{ 4 | base::{CheckDefinitionError, CheckError}, 5 | file::{get_option_string_value_from_checktable, get_string_value_from_checktable}, 6 | }; 7 | 8 | mod command; 9 | pub(crate) mod custom; 10 | pub(crate) mod package_absent; 11 | pub(crate) mod package_present; 12 | pub(crate) mod python; 13 | pub(crate) mod rust; 14 | 15 | // inspiration: https://github.com/mason-org/mason.nvim/tree/ad7146aa61dcaeb54fa900144d768f040090bff0/lua/mason-core/installer/managers 16 | 17 | #[derive(Clone, Debug, PartialEq, Display)] 18 | #[display("custom: {name}")] 19 | pub(crate) struct CustomInstaller { 20 | name: String, 21 | install_command: Option, 22 | version_command: String, 23 | uninstall_command: Option, 24 | version: String, 25 | } 26 | 27 | #[derive(Clone, Debug, PartialEq, Display)] 28 | pub(crate) enum PackageType { 29 | #[display("python: {name}", name=_0.name)] 30 | Python(Package), 31 | #[display("crate: {name}", name=_0.name)] 32 | Rust(Package), 33 | #[display("github: {name}", name=_0.name)] 34 | GithubRelease(Package), 35 | #[display("gitlab: {name}", name=_0.name)] 36 | GitlabRelease(Package), 37 | #[display("command: {name}", name=_0.name)] 38 | Custom(CustomInstaller), 39 | } 40 | 41 | impl PackageType { 42 | // TODO: add installer to Package to differentiate between installer (ie uv or pipx for 43 | // PythonPackage) 44 | 45 | // Install a package 46 | pub(crate) fn install(&self) -> Result<(), CheckError> { 47 | match self { 48 | PackageType::Python(package) => python::UV::install(package), 49 | PackageType::Rust(package) => rust::Cargo::install(package), 50 | PackageType::Custom(package) => custom::install(package), 51 | 52 | _ => todo!(), 53 | } 54 | } 55 | 56 | pub(crate) fn uninstall(&self) -> Result<(), CheckError> { 57 | match self { 58 | PackageType::Python(package) => python::UV::uninstall(package), 59 | PackageType::Rust(package) => rust::Cargo::uninstall(package), 60 | PackageType::Custom(package) => custom::uninstall(package), 61 | _ => todo!(), 62 | } 63 | } 64 | 65 | // is the package installed with the given version. 66 | // if no version is given, return true if the package is installed 67 | pub(crate) fn is_installed(&self) -> Result { 68 | match self { 69 | PackageType::Python(package) => python::UV::is_installed(package), 70 | PackageType::Rust(package) => rust::Cargo::is_installed(package), 71 | PackageType::Custom(package) => custom::is_installed(package), 72 | _ => todo!(), 73 | } 74 | } 75 | // if no version is specified, we will upgrade the package to the latest version. 76 | // return True when there is possible or certain a newer version available 77 | // Note: possible is the case when the package manager can not report a newer version without 78 | // installing it 79 | pub(crate) fn is_upgradable(&self) -> Result { 80 | match self { 81 | PackageType::Python(package) => python::UV::is_upgradable(package), 82 | PackageType::Rust(package) => rust::Cargo::is_upgradable(package), 83 | PackageType::Custom(package) => custom::is_upgradable(package), 84 | _ => todo!(), 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone, PartialEq, Display)] 90 | #[display("{name}")] // TODO: add version when not none 91 | pub(crate) struct Package { 92 | pub(crate) name: String, 93 | pub(crate) version: Option, 94 | pub(crate) bins: Vec, 95 | // platform: Platform 96 | // arch: Arch 97 | } 98 | 99 | pub(crate) trait Installer { 100 | fn install(package: &Package) -> Result<(), CheckError>; 101 | fn uninstall(package: &Package) -> Result<(), CheckError>; 102 | fn is_installed(package: &Package) -> Result; 103 | fn is_upgradable(package: &Package) -> Result; 104 | } 105 | 106 | pub(crate) fn read_package_from_check_table( 107 | value: &toml_edit::Table, 108 | ) -> Result { 109 | let package_type = match value.get("type") { 110 | None => Err(CheckDefinitionError::InvalidDefinition( 111 | "No type present".into(), 112 | )), 113 | Some(package_type) => match package_type.as_str() { 114 | None => Err(CheckDefinitionError::InvalidDefinition( 115 | "type is not a string".into(), 116 | )), 117 | Some(package_type) => Ok(package_type.to_lowercase()), 118 | }, 119 | }?; 120 | 121 | if package_type == "custom" { 122 | return Ok(PackageType::Custom(CustomInstaller { 123 | name: get_string_value_from_checktable(value, "package")?, 124 | install_command: get_option_string_value_from_checktable(value, "install_command")?, 125 | uninstall_command: get_option_string_value_from_checktable(value, "uninstall_command")?, 126 | version_command: get_string_value_from_checktable(value, "version_command")?, 127 | version: get_string_value_from_checktable(value, "version")?, 128 | })); 129 | } 130 | 131 | let package_name = read_package_name_from_check_table(value)?; 132 | 133 | let package_version = read_optional_version_from_check_table(value)?; 134 | 135 | let package = Package { 136 | name: package_name, 137 | version: package_version, 138 | bins: vec![], 139 | }; 140 | 141 | match package_type.to_lowercase().as_str() { 142 | "python" => Ok(PackageType::Python(package)), 143 | "rust" => Ok(PackageType::Rust(package)), 144 | _ => Err(CheckDefinitionError::InvalidDefinition(format!( 145 | "unknown package_type {package_type}" 146 | ))), 147 | } 148 | } 149 | 150 | pub(crate) fn read_package_name_from_check_table( 151 | value: &toml_edit::Table, 152 | ) -> Result { 153 | match value.get("package") { 154 | None => Err(CheckDefinitionError::InvalidDefinition( 155 | "No package present".into(), 156 | )), 157 | Some(package) => match package.as_str() { 158 | None => Err(CheckDefinitionError::InvalidDefinition( 159 | "package is not a string".into(), 160 | )), 161 | Some(package) => Ok(package.to_owned()), 162 | }, 163 | } 164 | } 165 | 166 | pub(crate) fn read_optional_version_from_check_table( 167 | value: &toml_edit::Table, 168 | ) -> Result, CheckDefinitionError> { 169 | match value.get("version") { 170 | None => Ok(None), 171 | Some(package) => match package.as_str() { 172 | None => Err(CheckDefinitionError::InvalidDefinition( 173 | "version is not a string".into(), 174 | )), 175 | Some(package) => Ok(Some(package.to_owned())), 176 | }, 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/checkers/file/lines_absent.rs: -------------------------------------------------------------------------------- 1 | use crate::checkers::{ 2 | file::FileCheck, 3 | get_option_boolean_from_check_table, 4 | utils::{ 5 | get_lines_from_check_table, get_marker_from_check_table, remove_between_markers, 6 | replace_vars, 7 | }, 8 | }; 9 | 10 | use super::super::base::CheckConstructor; 11 | pub(super) use super::super::{ 12 | GenericChecker, 13 | base::{CheckDefinitionError, CheckError, Checker}, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub(crate) struct LinesAbsent { 18 | file_check: FileCheck, 19 | lines: String, 20 | marker_lines: Option<(String, String)>, 21 | } 22 | 23 | // [[lines_absent]] 24 | // file = "file" 25 | // lines = "lines" # lines or marker must be given 26 | // marker = "marker" 27 | // is_template = false # optional, default to to false. true for replace ${var} 28 | impl CheckConstructor for LinesAbsent { 29 | type Output = LinesAbsent; 30 | fn from_check_table( 31 | generic_check: GenericChecker, 32 | check_table: toml_edit::Table, 33 | ) -> Result { 34 | let marker_lines = get_marker_from_check_table(&check_table)?; 35 | 36 | let lines = get_lines_from_check_table(&check_table, None)?; 37 | let is_template = 38 | get_option_boolean_from_check_table(&check_table, "is_template")?.unwrap_or(false); 39 | let lines = if is_template { 40 | replace_vars(lines.as_str(), &generic_check.variables) 41 | } else { 42 | lines 43 | }; 44 | 45 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 46 | 47 | Ok(Self { 48 | file_check, 49 | lines, 50 | marker_lines, 51 | }) 52 | } 53 | } 54 | 55 | impl Checker for LinesAbsent { 56 | fn checker_type(&self) -> String { 57 | "lines_absent".to_string() 58 | } 59 | 60 | fn checker_object(&self) -> String { 61 | self.file_check.check_object() 62 | } 63 | 64 | fn generic_checker(&self) -> &GenericChecker { 65 | &self.file_check.generic_check 66 | } 67 | 68 | fn check_(&self, fix: bool) -> Result { 69 | let contents = self.file_check.get_file_contents()?; 70 | 71 | let new_contents = if let Some((start_marker, end_marker)) = self.marker_lines.as_ref() { 72 | remove_between_markers(&contents, start_marker, end_marker) 73 | } else { 74 | // remove with leading new line when a block is in front of it 75 | let mut contents = contents.clone(); 76 | contents = contents.replace(format!("\n{}", self.lines).as_str(), ""); 77 | contents = contents.replace(self.lines.as_str(), ""); 78 | contents 79 | }; 80 | 81 | self.file_check 82 | .conclude_check_new_contents(new_contents, fix) 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use std::fs::File; 89 | use std::io::Write; 90 | 91 | use crate::checkers::base::CheckResult; 92 | use crate::checkers::test_helpers; 93 | 94 | use super::*; 95 | 96 | use tempfile::tempdir; 97 | 98 | fn get_lines_absent_check( 99 | lines: String, 100 | marker: Option, 101 | ) -> (LinesAbsent, tempfile::TempDir) { 102 | let generic_check = test_helpers::get_generic_check(); 103 | 104 | let mut check_table = toml_edit::Table::new(); 105 | let dir = tempdir().unwrap(); 106 | let file_to_check = dir.path().join("file_to_check"); 107 | check_table.insert("file", file_to_check.to_string_lossy().to_string().into()); 108 | check_table.insert("lines", lines.into()); 109 | 110 | if let Some(marker) = marker { 111 | check_table.insert("marker", marker.into()); 112 | } 113 | 114 | ( 115 | LinesAbsent::from_check_table(generic_check, check_table).unwrap(), 116 | dir, 117 | ) 118 | } 119 | 120 | // #[test] 121 | // fn test_add_line_ending_when_needed() { 122 | // let lines_absent_check = get_lines_absent_check(lines, marker); 123 | 124 | // let mut check_table = toml_edit::Table::new(); 125 | // check_table.insert("__lines__", "".into()); 126 | 127 | // let lines_absent_checker = 128 | // LinesAbsent::from_check_table(generic_check.clone(), check_table).unwrap(); 129 | // assert_eq!(lines_absent_checker.lines, "".to_string()); 130 | 131 | // let mut check_table = toml_edit::Table::new(); 132 | // check_table.insert("__lines__", "1".into()); 133 | 134 | // let lines_absent_checker = 135 | // LinesAbsent::from_check_table(generic_check.clone(), check_table).unwrap(); 136 | // assert_eq!(lines_absent_checker.lines, "1\n".to_string()); 137 | 138 | // let mut check_table = toml_edit::Table::new(); 139 | // check_table.insert("__lines__", "2\n".into()); 140 | 141 | // let lines_absent_checker = 142 | // LinesAbsent::from_check_table(generic_check.clone(), check_table).unwrap(); 143 | // assert_eq!(lines_absent_checker.lines, "2\n".to_string()); 144 | // } 145 | 146 | #[test] 147 | fn test_lines_absent() { 148 | let (lines_absent_check, _tmpdir) = get_lines_absent_check("1\n2\n".into(), None); 149 | 150 | // not existing file 151 | assert_eq!( 152 | lines_absent_check.check_(false).unwrap(), 153 | CheckResult::NoFixNeeded 154 | ); 155 | 156 | // empty file 157 | File::create(lines_absent_check.file_check.file_to_check.as_ref().clone()).unwrap(); 158 | assert_eq!( 159 | lines_absent_check.check_(false).unwrap(), 160 | CheckResult::NoFixNeeded 161 | ); 162 | 163 | // file with other contents 164 | let mut file: File = 165 | File::create(lines_absent_check.file_check.file_to_check.as_ref().clone()).unwrap(); 166 | writeln!(file, "a").unwrap(); 167 | assert_eq!( 168 | lines_absent_check.check_(false).unwrap(), 169 | CheckResult::NoFixNeeded 170 | ); 171 | 172 | // file with incorrect contents 173 | let mut file: File = 174 | File::create(lines_absent_check.file_check.file_to_check.as_ref().clone()).unwrap(); 175 | write!(file, "1\n2\nb\n").unwrap(); 176 | assert_eq!( 177 | lines_absent_check.check_(false).unwrap(), 178 | CheckResult::FixNeeded( 179 | "Set file contents to: \n@@ -1,3 +1 @@\n-1\n-2\n b\n".to_string() 180 | ) 181 | ); 182 | 183 | assert_eq!( 184 | lines_absent_check.check_(true).unwrap(), 185 | CheckResult::FixExecuted( 186 | "Set file contents to: \n@@ -1,3 +1 @@\n-1\n-2\n b\n".to_string() 187 | ) 188 | ); 189 | 190 | assert_eq!( 191 | lines_absent_check.check_(false).unwrap(), 192 | CheckResult::NoFixNeeded, 193 | ); 194 | } 195 | 196 | // TODO: add test with marker 197 | } 198 | -------------------------------------------------------------------------------- /src/checkers/file/dir_copied.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | checkers::{base::CheckResult, file::get_string_value_from_checktable}, 3 | uri::WritablePath, 4 | }; 5 | 6 | use super::super::{ 7 | GenericChecker, 8 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub(crate) struct DirCopied { 13 | source: WritablePath, 14 | destination: WritablePath, 15 | generic_check: GenericChecker, 16 | } 17 | 18 | //[[dir_copied]] 19 | // source = "path directory to copy" 20 | // destination = "path in which the directory contents will be copied" 21 | // desintation_dir = "path in which the directory will be copied" 22 | // 23 | // check if file is copied 24 | // if source is a relative path, it's relative to the check file, so the dir 25 | // which contain the file which defines this check. 26 | impl CheckConstructor for DirCopied { 27 | type Output = Self; 28 | 29 | fn from_check_table( 30 | generic_check: GenericChecker, 31 | check_table: toml_edit::Table, 32 | ) -> Result { 33 | let source = WritablePath::from_string( 34 | get_string_value_from_checktable(&check_table, "source")?.as_str(), 35 | ) 36 | .map_err(|_| CheckDefinitionError::InvalidDefinition("invalid source url".into()))?; 37 | 38 | let destination = if check_table.contains_key("destination") { 39 | WritablePath::from_string( 40 | get_string_value_from_checktable(&check_table, "destination")?.as_str(), 41 | ) 42 | .map_err(|_| { 43 | CheckDefinitionError::InvalidDefinition("invalid destination path".into()) 44 | })? 45 | } else { 46 | let destination_dir = WritablePath::from_string( 47 | get_string_value_from_checktable(&check_table, "destination_dir")?.as_str(), 48 | ) 49 | .map_err(|_| { 50 | CheckDefinitionError::InvalidDefinition("invalid destination_dir path".into()) 51 | })?; 52 | 53 | let file_name = match source.as_ref().file_name() { 54 | Some(filename) => filename, 55 | None => { 56 | return Err(CheckDefinitionError::InvalidDefinition( 57 | "Source has to filename".into(), 58 | )); 59 | } 60 | }; 61 | 62 | WritablePath::new(destination_dir.as_ref().join(file_name)) 63 | }; 64 | 65 | Ok(Self { 66 | destination, 67 | source, 68 | generic_check, 69 | }) 70 | } 71 | } 72 | 73 | /// Recursively copy all contents of `src` into `dst`. 74 | fn copy_dir_contents(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { 75 | // Create destination directory if it doesn’t exist 76 | if !dst.exists() { 77 | std::fs::create_dir_all(dst)?; 78 | } 79 | 80 | for entry in std::fs::read_dir(src)? { 81 | let entry = entry?; 82 | let path = entry.path(); 83 | let dest_path = dst.join(entry.file_name()); 84 | 85 | if path.is_dir() { 86 | // Recurse into subdirectory 87 | copy_dir_contents(&path, &dest_path)?; 88 | } else { 89 | // Copy file 90 | std::fs::copy(&path, &dest_path)?; 91 | } 92 | } 93 | 94 | Ok(()) 95 | } 96 | 97 | impl Checker for DirCopied { 98 | fn checker_type(&self) -> String { 99 | "dir_copied".to_string() 100 | } 101 | 102 | fn generic_checker(&self) -> &GenericChecker { 103 | &self.generic_check 104 | } 105 | fn checker_object(&self) -> String { 106 | self.source.as_ref().to_string_lossy().to_string() 107 | } 108 | fn check_(&self, fix: bool) -> Result { 109 | // TODO: check whether the file is changed 110 | let mut action_messages: Vec = vec![]; 111 | 112 | if !self.source.exists() { 113 | return Err(CheckError::String("source dir does not exists".into())); 114 | } 115 | 116 | // TODO: check also all subdirs and files 117 | let copy_dir_needed = !self.destination.exists(); 118 | 119 | if copy_dir_needed { 120 | action_messages.push("copy dir".into()); 121 | } 122 | let action_message = action_messages.join("\n"); 123 | 124 | let check_result = match (copy_dir_needed, fix) { 125 | (false, _) => CheckResult::NoFixNeeded, 126 | (true, false) => CheckResult::FixNeeded(action_message), 127 | (true, true) => { 128 | match copy_dir_contents(self.source.as_ref(), self.destination.as_ref()) { 129 | Ok(_) => CheckResult::FixExecuted(action_message), 130 | Err(e) => return Err(CheckError::String(e.to_string())), 131 | } 132 | } 133 | }; 134 | 135 | Ok(check_result) 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | 142 | use crate::checkers::{base::CheckResult, test_helpers}; 143 | 144 | use super::*; 145 | 146 | use tempfile::tempdir; 147 | 148 | fn get_dir_copied_check_with_result() 149 | -> (Result, tempfile::TempDir) { 150 | let generic_check = test_helpers::get_generic_check(); 151 | 152 | let mut check_table = toml_edit::Table::new(); 153 | let dir = tempdir().unwrap(); 154 | let source = dir.path().join("source"); 155 | let subdir = source.join("subdir"); 156 | std::fs::create_dir_all(&subdir).unwrap(); 157 | let _ = std::fs::create_dir(&subdir); 158 | let file = subdir.join("file"); 159 | std::fs::File::create(file).unwrap(); 160 | let destination = dir.path().join("destination"); 161 | check_table.insert( 162 | "destination", 163 | destination.to_string_lossy().to_string().into(), 164 | ); 165 | check_table.insert("source", source.to_string_lossy().to_string().into()); 166 | (DirCopied::from_check_table(generic_check, check_table), dir) 167 | } 168 | 169 | #[test] 170 | fn test_dir_copied_from_fs() { 171 | let (dir_copied_check, _tempdir) = get_dir_copied_check_with_result(); 172 | let dir_copied_check = dir_copied_check.expect("no errors"); 173 | 174 | assert_eq!( 175 | dir_copied_check.check_(false).unwrap(), 176 | CheckResult::FixNeeded("copy dir".into()) 177 | ); 178 | 179 | assert_eq!( 180 | dir_copied_check.check_(true).unwrap(), 181 | CheckResult::FixExecuted("copy dir".into()) 182 | ); 183 | assert_eq!( 184 | dir_copied_check.check_(false).unwrap(), 185 | CheckResult::NoFixNeeded 186 | ); 187 | 188 | assert!(dir_copied_check.destination.as_ref().exists()); 189 | assert!( 190 | dir_copied_check 191 | .destination 192 | .as_ref() 193 | .join("subdir/file") 194 | .exists() 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::process::ExitCode; 2 | use std::{collections::HashMap, io::Write}; 3 | 4 | use clap::Parser; 5 | 6 | use crate::checkers::base::Checker; 7 | use crate::uri::ReadablePath; 8 | 9 | use super::checkers::read_checks_from_path; 10 | 11 | #[derive(Copy, Clone, Debug, PartialEq)] 12 | pub enum ExitStatus { 13 | /// Reading checks was successful and there are no checks to fix 14 | Success, 15 | /// Reading checks was successful and there are checks to fix 16 | Failure, 17 | /// Reading checks was failed or executing fixes was failed 18 | Error, 19 | } 20 | 21 | impl From for ExitCode { 22 | fn from(status: ExitStatus) -> Self { 23 | match status { 24 | ExitStatus::Success => ExitCode::from(0), 25 | ExitStatus::Failure => ExitCode::from(1), 26 | ExitStatus::Error => ExitCode::from(2), 27 | } 28 | } 29 | } 30 | 31 | /// Config Checker will check and optional fix your config files based on checkers defined in a toml file. 32 | /// It can check toml, yaml, json and plain text files. 33 | #[derive(Parser)] 34 | #[command(author, version, about, long_about = None)] 35 | struct Cli { 36 | /// Path or URL to the root checkers file in toml format 37 | /// Defaults (in order of precedence): 38 | /// - check-config.toml 39 | /// - pyproject.toml with a tool.check-config key 40 | #[arg(short, long, env = "CHECK_CONFIG_PATH", verbatim_doc_comment)] 41 | path: Option, 42 | 43 | /// Try to fix the config 44 | #[arg(long, default_value = "false")] 45 | fix: bool, 46 | 47 | /// List all checks. Checks are not executed. 48 | #[arg(short, long, default_value = "false")] 49 | list_checkers: bool, 50 | 51 | /// Use the env variables as variables for template 52 | #[arg(long = "env", default_value = "false")] 53 | use_env_variables: bool, 54 | 55 | /// Execute the checkers with one of the specified tags 56 | #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ANY_TAGS")] 57 | any_tags: Vec, 58 | 59 | /// Execute the checkers with all of the specified tags 60 | #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ALL_TAGS")] 61 | all_tags: Vec, 62 | 63 | /// Do not execute the checkers with any of the specified tags. 64 | #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_SKIP_TAGS")] 65 | skip_tags: Vec, 66 | 67 | /// Create missing directories 68 | #[arg(short, long, default_value = "false", env = "CHECK_CONFIG_CREATE_DIRS")] 69 | create_missing_directories: bool, 70 | 71 | // -v s 72 | // -vv show all 73 | #[clap(flatten)] 74 | verbose: clap_verbosity_flag::Verbosity, 75 | } 76 | 77 | pub(crate) fn filter_checks( 78 | checker_tags: &[String], 79 | any_tags: &[String], 80 | all_tags: &[String], 81 | skip_tags: &[String], 82 | ) -> bool { 83 | // At least one must match 84 | if !any_tags.is_empty() && !any_tags.iter().any(|t| checker_tags.contains(t)) { 85 | return false; 86 | } 87 | 88 | // All must match 89 | if !all_tags.is_empty() && !all_tags.iter().all(|t| checker_tags.contains(t)) { 90 | return false; 91 | } 92 | 93 | // None must match 94 | if !skip_tags.is_empty() && skip_tags.iter().any(|t| checker_tags.contains(t)) { 95 | return false; 96 | } 97 | 98 | true 99 | } 100 | 101 | pub fn cli() -> ExitCode { 102 | let cli = Cli::parse(); 103 | env_logger::Builder::new() 104 | .filter_level(cli.verbose.log_level_filter()) 105 | .format(|buf, record| writeln!(buf, "{}", record.args())) 106 | .init(); 107 | 108 | log::info!("Starting check-config"); 109 | 110 | let mut variables: HashMap = if cli.use_env_variables { 111 | std::env::vars().collect() 112 | } else { 113 | HashMap::new() 114 | }; 115 | 116 | let path_str = if let Some(path) = cli.path { 117 | path 118 | } else { 119 | "check-config.toml".to_string() 120 | }; 121 | let path = match ReadablePath::from_string(path_str.as_str(), None) { 122 | Ok(path) => path, 123 | Err(_) => { 124 | log::error!( 125 | "Unable to load checkers. Path ({path_str}) specified is not a valid path.", 126 | ); 127 | return ExitCode::from(ExitStatus::Error); 128 | } 129 | }; 130 | 131 | let mut checks = read_checks_from_path(&path, &mut variables); 132 | 133 | log::info!("Fix: {}", &cli.fix); 134 | 135 | if cli.list_checkers { 136 | log::error!("List of checks (type, location of definition, file to check, tags)"); 137 | checks.iter().for_each(|check| { 138 | let enabled = filter_checks( 139 | &check.generic_checker().tags, 140 | &cli.any_tags, 141 | &cli.all_tags, 142 | &cli.skip_tags, 143 | ); 144 | 145 | check.list_checker(enabled); 146 | }); 147 | return ExitCode::from(ExitStatus::Success); 148 | } 149 | 150 | // log::info!( 151 | // "☰ Restricting checkers which have tags which all are part of: {:?}", 152 | // restricted_tags, 153 | // ); 154 | 155 | checks.retain(|check| { 156 | filter_checks( 157 | &check.generic_checker().tags, 158 | &cli.any_tags, 159 | &cli.all_tags, 160 | &cli.skip_tags, 161 | ) 162 | }); 163 | 164 | ExitCode::from(run_checks(&checks, cli.fix)) 165 | } 166 | 167 | pub(crate) fn run_checks(checks: &Vec>, fix: bool) -> ExitStatus { 168 | let mut fix_needed_count = 0; 169 | let mut fix_executed_count = 0; 170 | let mut no_fix_needed_count = 0; 171 | let mut error_count = 0; 172 | 173 | for check in checks { 174 | let fix = !check.generic_checker().check_only && fix; 175 | let result = check.check(fix); 176 | match result { 177 | crate::checkers::base::CheckResult::NoFixNeeded => no_fix_needed_count += 1, 178 | crate::checkers::base::CheckResult::FixExecuted(_) => fix_executed_count += 1, 179 | crate::checkers::base::CheckResult::FixNeeded(_) => fix_needed_count += 1, 180 | crate::checkers::base::CheckResult::Error(_) => error_count += 1, 181 | }; 182 | } 183 | 184 | log::warn!("⬜ {checks} checks found", checks = checks.len()); 185 | if fix { 186 | log::warn!("✅ {fix_executed_count} checks fixed"); 187 | log::warn!("✅ {no_fix_needed_count} checks did not need a fix"); 188 | } 189 | 190 | match fix_needed_count { 191 | 0 => log::error!("🥇 No violations found."), 192 | 1 => log::error!("🪛 There is 1 violation to fix.",), 193 | _ => log::error!("🪛 There are {fix_needed_count} violations to fix.",), 194 | } 195 | 196 | match error_count { 197 | 0 => (), 198 | 199 | 1 => log::error!("🚨 There was 1 error executing a fix.",), 200 | _ => log::error!("🚨 There are {error_count} errors executing a fix.",), 201 | } 202 | if error_count > 0 { 203 | ExitStatus::Error 204 | } else if fix_needed_count > 0 { 205 | ExitStatus::Failure 206 | } else { 207 | ExitStatus::Success 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/checkers/file/key_value_regex_match.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | use crate::{ 4 | checkers::{ 5 | base::{CheckDefinitionError, CheckResult}, 6 | file::{FileCheck, get_option_string_value_from_checktable}, 7 | }, 8 | file_types::RegexValidateResult, 9 | mapping::generic::Mapping, 10 | }; 11 | 12 | use super::super::{ 13 | GenericChecker, 14 | base::{CheckConstructor, CheckError, Checker}, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub(crate) struct EntryRegexMatched { 19 | file_check: FileCheck, 20 | key_regex: toml_edit::Table, 21 | placeholder: Option, 22 | } 23 | 24 | // [key_value_regex_matched] 25 | // file = "file" 26 | // key.key = "regex" 27 | // placeholder = "optional value to be set when key is absent" 28 | impl CheckConstructor for EntryRegexMatched { 29 | type Output = Self; 30 | 31 | fn from_check_table( 32 | generic_check: GenericChecker, 33 | check_table: toml_edit::Table, 34 | ) -> Result { 35 | let file_check = FileCheck::from_check_table(generic_check, &check_table)?; 36 | 37 | let key_value_regex = match check_table.get("key") { 38 | None => { 39 | return Err(CheckDefinitionError::InvalidDefinition( 40 | "`key` key is not present".into(), 41 | )); 42 | } 43 | Some(key_value_regex) => match key_value_regex.as_table() { 44 | None => { 45 | return Err(CheckDefinitionError::InvalidDefinition( 46 | "`key` is not a table".into(), 47 | )); 48 | } 49 | Some(key_value_regex) => key_value_regex.clone(), 50 | }, 51 | }; 52 | 53 | let placeholder = get_option_string_value_from_checktable(&check_table, "placeholder")?; 54 | 55 | Ok(Self { 56 | file_check, 57 | key_regex: key_value_regex, 58 | placeholder, 59 | }) 60 | } 61 | } 62 | impl Checker for EntryRegexMatched { 63 | fn checker_type(&self) -> String { 64 | "key_value_regex_matched".to_string() 65 | } 66 | 67 | fn checker_object(&self) -> String { 68 | self.file_check.check_object() 69 | } 70 | 71 | fn generic_checker(&self) -> &GenericChecker { 72 | &self.file_check.generic_check 73 | } 74 | 75 | fn check_(&self, fix: bool) -> Result { 76 | let mut doc = self.file_check.get_mapping()?; 77 | 78 | let fix_needed = match validate_key_value_regex( 79 | doc.as_mut(), 80 | &self.key_regex, 81 | "".to_string(), 82 | self.placeholder.clone(), 83 | ) { 84 | Ok(RegexValidateResult::Valid) => false, 85 | Ok(RegexValidateResult::Invalid { 86 | key: _, 87 | regex: _, 88 | found: _, 89 | }) => true, 90 | Err(e) => return Err(CheckError::InvalidRegex(e.to_string())), 91 | }; 92 | 93 | let action_message = match fix_needed { 94 | false => "".to_string(), 95 | true => { 96 | if self.placeholder.is_none() { 97 | "content of key does not match regex".to_string() 98 | } else { 99 | format!( 100 | "content of key does not match regex (setting placeholder to {})", 101 | self.placeholder.clone().unwrap() 102 | ) 103 | } 104 | } 105 | }; 106 | 107 | match (fix_needed, fix) { 108 | (false, _) => Ok(crate::checkers::base::CheckResult::NoFixNeeded), 109 | (true, false) => Ok(CheckResult::FixNeeded(action_message)), 110 | (true, true) => { 111 | if self.placeholder.is_some() { 112 | self.file_check.conclude_check_with_new_doc(doc, fix)?; 113 | Ok(CheckResult::FixExecuted(action_message)) 114 | } else { 115 | Ok(CheckResult::FixNeeded(action_message)) 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | fn make_key_path(parent: &str, key: &str) -> String { 123 | if parent.is_empty() { 124 | key.to_string() 125 | } else { 126 | parent.to_string() + "." + key 127 | } 128 | } 129 | 130 | fn validate_key_value_regex( 131 | doc: &mut dyn Mapping, 132 | table_with_regex: &toml_edit::Table, 133 | key_path: String, 134 | placeholder: Option, 135 | ) -> Result { 136 | for (key, value) in table_with_regex { 137 | match value { 138 | toml_edit::Item::Value(toml_edit::Value::String(raw_regex)) => { 139 | match doc.get_string(key) { 140 | Ok(string_to_match) => { 141 | let regex = match Regex::new(raw_regex.value()) { 142 | Ok(regex) => regex, 143 | Err(s) => return Err(CheckError::InvalidRegex(s.to_string())), 144 | }; 145 | if regex.is_match(string_to_match.as_str()) { 146 | return Ok(RegexValidateResult::Valid); 147 | } else { 148 | return Ok(RegexValidateResult::Invalid { 149 | key: make_key_path(&key_path, key), 150 | regex: raw_regex.value().to_owned(), 151 | found: string_to_match.clone(), 152 | }); 153 | } 154 | } 155 | _ => { 156 | if let Some(placeholder) = placeholder { 157 | doc.insert( 158 | &key.to_string().into(), 159 | &toml_edit::Item::Value(placeholder.into()), 160 | ); 161 | } 162 | return Ok(RegexValidateResult::Invalid { 163 | key: make_key_path(&key_path, key), 164 | regex: raw_regex.value().to_owned(), 165 | found: "".to_string(), 166 | }); 167 | } 168 | } 169 | } 170 | toml_edit::Item::Table(t) => match doc.get_mapping(key, false) { 171 | Ok(child_doc) => { 172 | return validate_key_value_regex( 173 | child_doc, 174 | t, 175 | make_key_path(&key_path, key), 176 | placeholder, 177 | ); 178 | } 179 | _ => { 180 | return Ok(RegexValidateResult::Invalid { 181 | key: make_key_path(&key_path, key), 182 | regex: "".to_string(), 183 | found: "".to_string(), 184 | }); 185 | } 186 | }, 187 | 188 | _ => {} 189 | } 190 | } 191 | Ok(RegexValidateResult::Valid) 192 | } 193 | 194 | #[cfg(test)] 195 | mod tests { 196 | use crate::checkers::test_helpers::read_test_files; 197 | 198 | use super::*; 199 | 200 | #[test] 201 | fn test_test_files() { 202 | for (test_path, test_input, test_expected_output, checker) in 203 | read_test_files("key_value_regex_matched") 204 | { 205 | let mut test_input = test_input; 206 | let result = 207 | validate_key_value_regex(test_input.as_mut(), &checker, "".to_string(), None) 208 | .unwrap(); 209 | 210 | if test_expected_output.contains("true") { 211 | assert_eq!( 212 | result, 213 | RegexValidateResult::Valid, 214 | "test_path {test_path} failed" 215 | ); 216 | } else { 217 | assert_ne!( 218 | result, 219 | RegexValidateResult::Valid, 220 | "test_path {test_path} failed" 221 | ); 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/mapping/yaml.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use crate::checkers::base::CheckError; 4 | 5 | use super::generic::{Array, Mapping, MappingError, Value}; 6 | 7 | pub(crate) fn from_path(path: PathBuf) -> Result, CheckError> { 8 | let file_contents = fs::read_to_string(path)?; 9 | from_string(&file_contents) 10 | } 11 | 12 | pub(crate) fn from_string( 13 | doc: &str, 14 | ) -> Result, crate::checkers::base::CheckError> { 15 | if doc.trim().is_empty() { 16 | return Ok(Box::new(serde_yaml_ng::Mapping::new())); 17 | } 18 | let doc: serde_yaml_ng::Value = 19 | serde_yaml_ng::from_str(doc).map_err(|e| CheckError::InvalidFileFormat(e.to_string()))?; 20 | Ok(Box::new( 21 | doc.as_mapping() 22 | .ok_or(CheckError::InvalidFileFormat("No object".to_string()))? 23 | .clone(), 24 | )) 25 | } 26 | 27 | impl Mapping for serde_yaml_ng::Mapping { 28 | fn to_string(&self, _indent: usize) -> Result { 29 | if self.is_empty() { 30 | return Ok("".to_string()); 31 | } 32 | Ok(serde_yaml_ng::to_string(&self).unwrap()) 33 | } 34 | fn contains_key(&self, key: &str) -> bool { 35 | self.contains_key(key) 36 | } 37 | fn get_mapping( 38 | &mut self, 39 | key: &str, 40 | create_missing: bool, 41 | ) -> Result<&mut dyn Mapping, MappingError> { 42 | if !self.contains_key(key) { 43 | if !create_missing { 44 | return Err(MappingError::MissingKey(key.to_string())); 45 | } 46 | self.insert( 47 | serde_yaml_ng::value::Value::String(key.to_string()), 48 | serde_yaml_ng::value::Value::Mapping(serde_yaml_ng::Mapping::new()), 49 | ); 50 | } 51 | let value = self.get_mut(key).unwrap(); 52 | if !value.is_mapping() { 53 | Err(MappingError::WrongType(format!("{key} is not a mapping"))) 54 | } else { 55 | Ok(value.as_mapping_mut().unwrap()) 56 | } 57 | } 58 | fn get_array( 59 | &mut self, 60 | key: &str, 61 | create_missing: bool, 62 | ) -> Result<&mut dyn Array, MappingError> { 63 | if !self.contains_key(key) { 64 | if !create_missing { 65 | return Err(MappingError::MissingKey(key.to_string())); 66 | } 67 | self.insert( 68 | serde_yaml_ng::value::Value::String(key.to_string()), 69 | serde_yaml_ng::value::Value::Sequence(serde_yaml_ng::Sequence::new()), 70 | ); 71 | } 72 | let value = self.get_mut(key).unwrap(); 73 | if !value.is_sequence() { 74 | Err(MappingError::WrongType(format!("{key} is not an array"))) 75 | } else { 76 | Ok(value.as_sequence_mut().unwrap()) 77 | } 78 | } 79 | fn get_string(&self, key: &str) -> Result { 80 | if !self.contains_key(key) { 81 | return Err(MappingError::MissingKey(key.to_string())); 82 | } 83 | let value = self.get(key).unwrap(); 84 | if !value.is_string() { 85 | Err(MappingError::WrongType(format!("{key} is not a string"))) 86 | } else { 87 | Ok(value.as_str().unwrap().to_string()) 88 | } 89 | } 90 | fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item) { 91 | self.insert( 92 | serde_yaml_ng::value::Value::String(key.to_string()), 93 | serde_yaml_ng::Value::from_toml_value(value), 94 | ); 95 | } 96 | fn remove(&mut self, key: &str) { 97 | if self.contains_key(key) { 98 | self.remove(key); 99 | } 100 | } 101 | } 102 | 103 | impl Array for serde_yaml_ng::value::Sequence { 104 | fn insert_when_not_present(&mut self, value: &toml_edit::Item) { 105 | let value = serde_yaml_ng::value::Value::from_toml_value(value); 106 | if !self.contains(&value) { 107 | self.push(value); 108 | } 109 | } 110 | 111 | fn remove(&mut self, value: &toml_edit::Item) { 112 | let value = serde_yaml_ng::value::Value::from_toml_value(value); 113 | let array = self; 114 | for (idx, array_item) in array.iter().enumerate() { 115 | if *array_item == value { 116 | array.remove(idx); 117 | return; 118 | } 119 | } 120 | } 121 | 122 | fn contains_item(&self, value: &toml_edit::Item) -> bool { 123 | let value = serde_yaml_ng::value::Value::from_toml_value(value); 124 | self.contains(&value) 125 | } 126 | } 127 | 128 | impl Value for serde_yaml_ng::value::Value { 129 | fn from_toml_value(value: &toml_edit::Item) -> serde_yaml_ng::value::Value { 130 | match value { 131 | toml_edit::Item::Value(toml_edit::Value::String(v)) => { 132 | serde_yaml_ng::value::Value::String(v.value().to_owned()) 133 | } 134 | toml_edit::Item::Value(toml_edit::Value::Integer(v)) => { 135 | serde_yaml_ng::value::Value::Number(serde_yaml_ng::Number::from( 136 | v.value().to_owned(), 137 | )) 138 | } 139 | toml_edit::Item::Value(toml_edit::Value::Float(v)) => { 140 | serde_yaml_ng::value::Value::Number(serde_yaml_ng::Number::from( 141 | v.value().to_owned(), 142 | )) 143 | } 144 | toml_edit::Item::Value(toml_edit::Value::Boolean(v)) => { 145 | serde_yaml_ng::value::Value::Bool(v.value().to_owned()) 146 | } 147 | toml_edit::Item::Value(toml_edit::Value::Datetime(v)) => { 148 | serde_yaml_ng::value::Value::String(v.to_string()) 149 | } 150 | toml_edit::Item::Value(toml_edit::Value::Array(v)) => { 151 | let mut a = vec![]; 152 | for v_item in v { 153 | a.push(serde_yaml_ng::value::Value::from_toml_value( 154 | &toml_edit::Item::Value(v_item.to_owned()), 155 | )) 156 | } 157 | serde_yaml_ng::value::Value::Sequence(a) 158 | } 159 | toml_edit::Item::Table(v) => { 160 | let mut a: serde_yaml_ng::value::Mapping = serde_yaml_ng::value::Mapping::new(); 161 | for (k, v_item) in v { 162 | a.insert( 163 | serde_yaml_ng::value::Value::String(k.to_owned()), 164 | serde_yaml_ng::value::Value::from_toml_value(v_item), 165 | ); 166 | } 167 | 168 | serde_yaml_ng::value::Value::Mapping(a) 169 | } 170 | toml_edit::Item::Value(toml_edit::Value::InlineTable(v)) => { 171 | let mut a: serde_yaml_ng::value::Mapping = serde_yaml_ng::value::Mapping::new(); 172 | for (k, v_item) in v { 173 | a.insert( 174 | serde_yaml_ng::value::Value::String(k.to_owned()), 175 | serde_yaml_ng::value::Value::from_toml_value(&toml_edit::Item::Value( 176 | v_item.to_owned(), 177 | )), 178 | ); 179 | } 180 | 181 | serde_yaml_ng::value::Value::Mapping(a) 182 | } 183 | toml_edit::Item::ArrayOfTables(v) => { 184 | let mut a = vec![]; 185 | for v_item in v { 186 | a.push(serde_yaml_ng::value::Value::from_toml_value( 187 | &toml_edit::Item::Table(v_item.to_owned()), 188 | )) 189 | } 190 | serde_yaml_ng::value::Value::Sequence(a) 191 | } 192 | _ => serde_yaml_ng::value::Value::Null, 193 | } 194 | } 195 | } 196 | 197 | #[cfg(test)] 198 | mod tests { 199 | 200 | use super::*; 201 | 202 | use super::super::generic::tests::get_test_table; 203 | use super::super::generic::tests::test_mapping; 204 | 205 | #[test] 206 | fn test_access_map() { 207 | let table = get_test_table(); 208 | let binding = serde_yaml_ng::Value::from_toml_value(&table); 209 | let mut mapping_to_check = binding.as_mapping().unwrap().to_owned(); 210 | 211 | test_mapping(Box::new(mapping_to_check.clone())); 212 | 213 | assert_eq!( 214 | mapping_to_check 215 | .get_mapping("dict", false) 216 | .expect("") 217 | .to_string(4) 218 | .unwrap(), 219 | "str: string\nint: 1\nfloat: 1.1\nbool: true\narray:\n- 1\n- 2\n".to_string() 220 | ); 221 | } 222 | 223 | #[test] 224 | fn test_from_toml_value() { 225 | let table = get_test_table(); 226 | 227 | let yaml_table = serde_yaml_ng::Value::from_toml_value(&table); 228 | 229 | assert_eq!( 230 | serde_yaml_ng::to_string(&yaml_table).unwrap(), 231 | "str: string\nint: 1\nfloat: 1.1\nbool: true\narray:\n- 1\n- 2\ndict:\n str: string\n int: 1\n float: 1.1\n bool: true\n array:\n - 1\n - 2\n" 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/mapping/json.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::checkers::base::CheckError; 6 | 7 | use super::generic::{Array, Mapping, MappingError, Value}; 8 | 9 | pub(crate) fn from_path(path: PathBuf) -> Result, CheckError> { 10 | let file_contents = fs::read_to_string(path)?; 11 | from_string(&file_contents) 12 | } 13 | 14 | pub(crate) fn from_string(doc: &str) -> Result, CheckError> { 15 | if doc.trim().is_empty() { 16 | return Ok(Box::new(serde_json::Map::new())); 17 | } 18 | let doc: serde_json::Value = 19 | serde_json::from_str(doc).map_err(|e| CheckError::InvalidFileFormat(e.to_string()))?; 20 | let doc = doc 21 | .as_object() 22 | .ok_or(CheckError::InvalidFileFormat("No object".to_string()))?; 23 | Ok(Box::new(doc.clone())) 24 | } 25 | 26 | impl Mapping for serde_json::Map { 27 | fn to_string(&self, indent: usize) -> Result { 28 | if self.is_empty() { 29 | return Ok("".to_string()); 30 | } 31 | let buf = Vec::new(); 32 | 33 | let indent = " ".repeat(indent); 34 | 35 | let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()); 36 | let mut ser = serde_json::Serializer::with_formatter(buf, formatter); 37 | self.serialize(&mut ser).unwrap(); 38 | Ok(String::from_utf8(ser.into_inner()).unwrap() + "\n") 39 | } 40 | 41 | fn contains_key(&self, key: &str) -> bool { 42 | self.contains_key(key) 43 | } 44 | fn get_mapping( 45 | &mut self, 46 | key: &str, 47 | create_missing: bool, 48 | ) -> Result<&mut dyn Mapping, MappingError> { 49 | if !self.contains_key(key) { 50 | if !create_missing { 51 | return Err(MappingError::MissingKey(key.to_string())); 52 | } 53 | self.insert( 54 | key.to_string(), 55 | serde_json::Value::Object(serde_json::Map::new()), 56 | ); 57 | } 58 | let value = self.get_mut(key).unwrap(); 59 | if !value.is_object() { 60 | Err(MappingError::WrongType(format!("{key} is not a mapping"))) 61 | } else { 62 | Ok(value.as_object_mut().unwrap()) 63 | } 64 | } 65 | fn get_array( 66 | &mut self, 67 | key: &str, 68 | create_missing: bool, 69 | ) -> Result<&mut dyn Array, MappingError> { 70 | if !self.contains_key(key) { 71 | if !create_missing { 72 | return Err(MappingError::MissingKey(key.to_string())); 73 | } 74 | self.insert(key.to_string(), serde_json::Value::Array(vec![])); 75 | } 76 | let value = self.get_mut(key).unwrap(); 77 | if !value.is_array() { 78 | Err(MappingError::WrongType(format!("{key} is not an array"))) 79 | } else { 80 | Ok(value) 81 | } 82 | } 83 | fn get_string(&self, key: &str) -> Result { 84 | if !self.contains_key(key) { 85 | return Err(MappingError::MissingKey(key.to_string())); 86 | } 87 | let value = self.get(key).unwrap(); 88 | if !value.is_string() { 89 | Err(MappingError::WrongType(format!("{key} is not a string"))) 90 | } else { 91 | Ok(value.as_str().unwrap().to_string()) 92 | } 93 | } 94 | fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item) { 95 | self.insert(key.to_string(), serde_json::Value::from_toml_value(value)); 96 | } 97 | fn remove(&mut self, key: &str) { 98 | if self.contains_key(key) { 99 | self.remove(key); 100 | } 101 | } 102 | } 103 | 104 | impl Array for serde_json::Value { 105 | fn insert_when_not_present(&mut self, value: &toml_edit::Item) { 106 | let value = serde_json::Value::from_toml_value(value); 107 | if !self.as_array().unwrap().contains(&value) { 108 | self.as_array_mut().unwrap().push(value); 109 | } 110 | } 111 | 112 | fn remove(&mut self, value: &toml_edit::Item) { 113 | let value = serde_json::Value::from_toml_value(value); 114 | let array = self.as_array_mut().unwrap(); 115 | for (idx, array_item) in array.iter().enumerate() { 116 | if *array_item == value { 117 | array.remove(idx); 118 | return; 119 | } 120 | } 121 | } 122 | 123 | fn contains_item(&self, value: &toml_edit::Item) -> bool { 124 | let value = serde_json::Value::from_toml_value(value); 125 | self.as_array().unwrap().contains(&value) 126 | } 127 | } 128 | 129 | impl Value for serde_json::Value { 130 | fn from_toml_value(value: &toml_edit::Item) -> serde_json::Value { 131 | match value { 132 | toml_edit::Item::Value(toml_edit::Value::String(v)) => { 133 | serde_json::Value::String(v.value().to_owned()) 134 | } 135 | toml_edit::Item::Value(toml_edit::Value::Integer(v)) => { 136 | serde_json::Value::Number(serde_json::Number::from(*v.value())) 137 | } 138 | toml_edit::Item::Value(toml_edit::Value::Float(v)) => { 139 | serde_json::Value::Number(serde_json::Number::from_f64(*v.value()).unwrap()) 140 | } 141 | toml_edit::Item::Value(toml_edit::Value::Boolean(v)) => { 142 | serde_json::Value::Bool(*v.value()) 143 | } 144 | toml_edit::Item::Value(toml_edit::Value::Datetime(v)) => { 145 | serde_json::Value::String(v.value().to_string()) 146 | } 147 | toml_edit::Item::Value(toml_edit::Value::Array(v)) => { 148 | let mut a = vec![]; 149 | for v_item in v { 150 | a.push(serde_json::Value::from_toml_value(&toml_edit::Item::Value( 151 | v_item.to_owned(), 152 | ))) 153 | } 154 | serde_json::Value::Array(a) 155 | } 156 | toml_edit::Item::Table(v) => { 157 | let mut a: serde_json::Map = serde_json::Map::new(); 158 | for (k, v_item) in v { 159 | a.insert(k.to_string(), serde_json::Value::from_toml_value(v_item)); 160 | } 161 | 162 | serde_json::Value::Object(a) 163 | } 164 | toml_edit::Item::Value(toml_edit::Value::InlineTable(v)) => { 165 | let mut a: serde_json::Map = serde_json::Map::new(); 166 | for (k, v_item) in v { 167 | a.insert( 168 | k.to_string(), 169 | serde_json::Value::from_toml_value(&toml_edit::Item::Value( 170 | v_item.to_owned(), 171 | )), 172 | ); 173 | } 174 | 175 | serde_json::Value::Object(a) 176 | } 177 | toml_edit::Item::ArrayOfTables(v) => { 178 | let mut a = vec![]; 179 | for v_item in v { 180 | a.push(serde_json::Value::from_toml_value(&toml_edit::Item::Table( 181 | v_item.to_owned(), 182 | ))) 183 | } 184 | serde_json::Value::Array(a) 185 | } 186 | _ => serde_json::Value::Null, 187 | } 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | 194 | use serde_json::json; 195 | 196 | use super::super::generic::tests::get_test_table; 197 | use super::super::generic::tests::test_mapping; 198 | use super::*; 199 | 200 | #[test] 201 | fn test_access_map() { 202 | let table = get_test_table(); 203 | let binding = serde_json::Value::from_toml_value(&table); 204 | let mut mapping_to_check = binding.as_object().unwrap().to_owned(); 205 | 206 | test_mapping(Box::new(mapping_to_check.clone())); 207 | 208 | assert_eq!( 209 | mapping_to_check 210 | .get_mapping("dict", false) 211 | .expect("") 212 | .to_string(4) 213 | .unwrap(), 214 | "{\n \"array\": [\n 1,\n 2\n ],\n \"bool\": true,\n \"float\": 1.1,\n \"int\": 1,\n \"str\": \"string\"\n}\n".to_string() 215 | ); 216 | 217 | assert_eq!( 218 | mapping_to_check 219 | .get_mapping("dict", false) 220 | .expect("") 221 | .to_string(2) 222 | .unwrap(), 223 | "{\n \"array\": [\n 1,\n 2\n ],\n \"bool\": true,\n \"float\": 1.1,\n \"int\": 1,\n \"str\": \"string\"\n}\n".to_string() 224 | ); 225 | } 226 | 227 | #[test] 228 | fn test_from_toml_value() { 229 | let table = get_test_table(); 230 | 231 | let json_table = serde_json::Value::from_toml_value(&table); 232 | 233 | assert_eq!( 234 | json_table, 235 | json!({ "str": "string", "int": 1, "float": 1.1, "bool": true, "array": [1, 2], "dict": {"str": "string", "int": 1, "float": 1.1, "bool": true, "array": [1, 2], 236 | 237 | 238 | }}) 239 | ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/checkers/file/file_present.rs: -------------------------------------------------------------------------------- 1 | use std::fs::Permissions; 2 | #[cfg(not(target_os = "windows"))] 3 | use std::os::unix::fs::PermissionsExt; 4 | 5 | use regex::Regex; 6 | 7 | use crate::checkers::file::{FileCheck, get_option_string_value_from_checktable}; 8 | 9 | use super::super::{ 10 | GenericChecker, 11 | base::{CheckConstructor, CheckDefinitionError, CheckError, Checker}, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub(crate) struct FilePresent { 16 | file_check: FileCheck, 17 | permissions: Option, 18 | placeholder: Option, 19 | regex: Option, 20 | } 21 | 22 | pub(crate) fn get_permissions_from_checktable( 23 | check_table: &toml_edit::Table, 24 | ) -> Result, CheckDefinitionError> { 25 | if let Some(permissions) = check_table.get("permissions") { 26 | #[cfg(target_os = "windows")] 27 | { 28 | Ok(None) 29 | } 30 | 31 | #[cfg(not(target_os = "windows"))] 32 | match permissions.as_str() { 33 | None => Err(CheckDefinitionError::InvalidDefinition( 34 | "permissions is not a string".into(), 35 | )), 36 | Some(permissions) => match u32::from_str_radix(permissions, 8) { 37 | Err(_) => Err(CheckDefinitionError::InvalidDefinition( 38 | "permission can not be converted to an octal mode".into(), 39 | )), 40 | Ok(mode) => { 41 | if mode > 0o777 { 42 | return Err(CheckDefinitionError::InvalidDefinition( 43 | "permission is not a valid mode".into(), 44 | )); 45 | } 46 | Ok(Some(std::fs::Permissions::from_mode(mode))) 47 | } 48 | }, 49 | } 50 | } else { 51 | Ok(None) 52 | } 53 | } 54 | 55 | //[[file_present]] 56 | // file = "file" 57 | // placeholder = "placeholder" # optional 58 | // regex = "[0-9]*" # optional 59 | // permissions = "644" # optional 60 | pub(crate) fn get_regex_from_checktable( 61 | check_table: &toml_edit::Table, 62 | ) -> Result, CheckDefinitionError> { 63 | match get_option_string_value_from_checktable(check_table, "regex") { 64 | Err(err) => Err(err), 65 | Ok(None) => Ok(None), 66 | Ok(Some(regex)) => match Regex::new(regex.as_str()) { 67 | Ok(r) => Ok(Some(r)), 68 | Err(_) => Err(CheckDefinitionError::InvalidDefinition(format!( 69 | "regex ({regex}) is not a valid regex" 70 | ))), 71 | }, 72 | } 73 | } 74 | 75 | impl CheckConstructor for FilePresent { 76 | type Output = Self; 77 | 78 | fn from_check_table( 79 | generic_check: GenericChecker, 80 | value: toml_edit::Table, 81 | ) -> Result { 82 | let file_check = FileCheck::from_check_table(generic_check, &value)?; 83 | 84 | let permissions = get_permissions_from_checktable(&value)?; 85 | 86 | let placeholder = get_option_string_value_from_checktable(&value, "placeholder")?; 87 | 88 | let regex = get_regex_from_checktable(&value)?; 89 | Ok(Self { 90 | file_check, 91 | permissions, 92 | placeholder, 93 | regex, 94 | }) 95 | } 96 | } 97 | impl Checker for FilePresent { 98 | fn checker_type(&self) -> String { 99 | "file_present".to_string() 100 | } 101 | 102 | fn generic_checker(&self) -> &GenericChecker { 103 | &self.file_check.generic_check 104 | } 105 | fn checker_object(&self) -> String { 106 | self.file_check.check_object() 107 | } 108 | fn check_(&self, fix: bool) -> Result { 109 | self.file_check.conclude_check_file_exists( 110 | self.placeholder.clone(), 111 | self.permissions.clone(), 112 | self.regex.clone(), 113 | fix, 114 | ) 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | 121 | use std::fs::write; 122 | 123 | use crate::checkers::{base::CheckResult, test_helpers}; 124 | 125 | use super::*; 126 | 127 | use tempfile::tempdir; 128 | 129 | fn get_file_present_check_with_result( 130 | placeholder: Option, 131 | permissions: Option, 132 | regex: Option, 133 | ) -> (Result, tempfile::TempDir) { 134 | let generic_check = test_helpers::get_generic_check(); 135 | 136 | let mut check_table = toml_edit::Table::new(); 137 | let dir = tempdir().unwrap(); 138 | let file_to_check = dir.path().join("file_to_check"); 139 | check_table.insert("file", file_to_check.to_string_lossy().to_string().into()); 140 | 141 | if let Some(placeholder) = placeholder { 142 | check_table.insert("placeholder", placeholder.into()); 143 | } 144 | 145 | if let Some(permissions) = permissions { 146 | check_table.insert("permissions", permissions.into()); 147 | } 148 | 149 | if let Some(regex) = regex { 150 | check_table.insert("regex", regex.into()); 151 | } 152 | 153 | ( 154 | FilePresent::from_check_table(generic_check, check_table), 155 | dir, 156 | ) 157 | } 158 | 159 | fn get_file_present_check( 160 | placeholder: Option, 161 | permissions: Option, 162 | regex: Option, 163 | ) -> (FilePresent, tempfile::TempDir) { 164 | let (file_present_with_result, tempdir) = 165 | get_file_present_check_with_result(placeholder, permissions, regex); 166 | 167 | (file_present_with_result.unwrap(), tempdir) 168 | } 169 | #[test] 170 | fn test_file_present() { 171 | let (file_present_check, _tempdir) = get_file_present_check(None, None, None); 172 | 173 | assert_eq!( 174 | file_present_check.check_(false).unwrap(), 175 | CheckResult::FixNeeded("create file".into()) 176 | ); 177 | 178 | assert_eq!( 179 | file_present_check.check_(true).unwrap(), 180 | CheckResult::FixExecuted("create file".into()) 181 | ); 182 | assert_eq!( 183 | file_present_check.check_(false).unwrap(), 184 | CheckResult::NoFixNeeded 185 | ); 186 | } 187 | 188 | #[test] 189 | fn test_file_present_with_placeholder() { 190 | let (file_present_check, _tempdir) = 191 | get_file_present_check(Some("placeholder".into()), None, None); 192 | 193 | assert_eq!( 194 | file_present_check.check_(false).unwrap(), 195 | CheckResult::FixNeeded("create file\nset contents to placeholder".into()) 196 | ); 197 | 198 | assert_eq!( 199 | file_present_check.check_(true).unwrap(), 200 | CheckResult::FixExecuted("create file\nset contents to placeholder".into()) 201 | ); 202 | assert_eq!( 203 | file_present_check.check_(false).unwrap(), 204 | CheckResult::NoFixNeeded 205 | ); 206 | } 207 | 208 | #[test] 209 | fn test_file_present_with_permissions() { 210 | let (file_present_check, _tempdir) = get_file_present_check(None, Some("666".into()), None); 211 | 212 | assert_eq!( 213 | file_present_check.check_(false).unwrap(), 214 | CheckResult::FixNeeded("create file\nfix permissions to 666".into()) 215 | ); 216 | 217 | assert_eq!( 218 | file_present_check.check_(true).unwrap(), 219 | CheckResult::FixExecuted("create file\nfix permissions to 666".into()) 220 | ); 221 | 222 | assert_eq!( 223 | file_present_check.check_(false).unwrap(), 224 | CheckResult::NoFixNeeded 225 | ); 226 | } 227 | 228 | #[test] 229 | fn test_file_present_with_regex() { 230 | let file_present_error = 231 | get_file_present_check_with_result(None, None, Some("^[0-9]{1,3$".into())) 232 | .0 233 | .expect_err("must give error"); 234 | 235 | assert_eq!( 236 | file_present_error, 237 | CheckDefinitionError::InvalidDefinition( 238 | "regex (^[0-9]{1,3$) is not a valid regex".into() 239 | ) 240 | ); 241 | let (file_present_check, _tempdir) = 242 | get_file_present_check(None, None, Some("[0-9]{1,3}".into())); 243 | 244 | assert_eq!( 245 | file_present_check.check_(false).unwrap(), 246 | CheckResult::FixNeeded("create file\nfix content to match regex \"[0-9]{1,3}\"".into()) 247 | ); 248 | 249 | let _ = write( 250 | file_present_check.file_check.file_to_check.as_ref().clone(), 251 | "bla", 252 | ); 253 | 254 | assert_eq!( 255 | file_present_check.check_(false).unwrap(), 256 | CheckResult::FixNeeded("fix content to match regex \"[0-9]{1,3}\"".into()) 257 | ); 258 | 259 | let _ = write( 260 | file_present_check.file_check.file_to_check.as_ref().clone(), 261 | "129", 262 | ); 263 | 264 | assert_eq!( 265 | file_present_check.check_(false).unwrap(), 266 | CheckResult::NoFixNeeded 267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /example/checkers/python-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 61 | 63 | 65 | 69 | 73 | 74 | 76 | 80 | 84 | 85 | 87 | 91 | 95 | 96 | 98 | 102 | 106 | 107 | 109 | 113 | 117 | 118 | 120 | 124 | 128 | 129 | 138 | 147 | 157 | 167 | 177 | 187 | 197 | 207 | 218 | 228 | 238 | 249 | 250 | 254 | 258 | 265 | 266 | --------------------------------------------------------------------------------