diff options
author | Jeongik Cha <jeongik@google.com> | 2023-09-27 09:17:56 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-09-27 09:17:56 +0000 |
commit | a74c53cb2bff4e7491602c78fa740ae5cb7149cd (patch) | |
tree | 3fd0f5f31a4a09f2e40d5a1e38db14fdc37e661f | |
parent | 26b5fe5d180a32c3f9390d236ae96b527c7b7478 (diff) | |
parent | 019f4830c7d44bfbb917980e893cb5c5bb00524f (diff) | |
download | configparser-a74c53cb2bff4e7491602c78fa740ae5cb7149cd.tar.gz |
Import configparser am: 7f2bc65135 am: 019f4830c7
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/configparser/+/2754250
Change-Id: I2359472928a0c74f5a5cf57c6f1a5faeb9497d9b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | .cargo_vcs_info.json | 6 | ||||
-rw-r--r-- | .github/workflows/rust.yaml | 52 | ||||
-rwxr-xr-x | .gitignore | 9 | ||||
-rw-r--r-- | Android.bp | 13 | ||||
-rwxr-xr-x | CHANGELOG.md | 86 | ||||
-rw-r--r-- | CODE_OF_CONDUCT.md | 127 | ||||
-rw-r--r-- | Cargo.toml | 46 | ||||
-rwxr-xr-x | Cargo.toml.orig | 22 | ||||
l--------- | LICENSE | 1 | ||||
-rw-r--r-- | LICENSE-MIT | 27 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rwxr-xr-x | README.md | 232 | ||||
-rwxr-xr-x | src/ini.rs | 972 | ||||
-rwxr-xr-x | src/lib.rs | 154 | ||||
-rwxr-xr-x | tests/test.ini | 19 | ||||
-rwxr-xr-x | tests/test.rs | 348 | ||||
-rw-r--r-- | tests/test_multiline.ini | 7 |
19 files changed, 2141 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..be07c69 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "aca329bc1872624d98cc3486b2cbe643e6641278" + }, + "path_in_vcs": "" +}
\ No newline at end of file diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml new file mode 100644 index 0000000..a4a28ae --- /dev/null +++ b/.github/workflows/rust.yaml @@ -0,0 +1,52 @@ +name: Rust CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.rust == 'nightly' }} + strategy: + fail-fast: false + matrix: + rust: + - stable + - beta + - nightly + + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt + + - uses: actions-rs/cargo@v1 + with: + command: build + args: --release --all-features -v + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features -v + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Security audit + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ee52156 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target +.DS_Store +Cargo.lock +output.ini +output2.ini +test2.ini +.vscode +output_async.ini +output_sync.ini diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..667c165 --- /dev/null +++ b/Android.bp @@ -0,0 +1,13 @@ +// This file is generated by cargo2android.py --run. +// Do not modify this file as changes will be overridden on upgrade. + + + +rust_library_host { + name: "libconfigparser", + crate_name: "configparser", + cargo_env_compat: true, + cargo_pkg_version: "3.0.2", + srcs: ["src/lib.rs"], + edition: "2021", +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..e251ec4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,86 @@ +## Changelog + +- 0.1.0 (yanked) + - First experimental version with only a public-facing load() function. +- 0.1.1 + - `configparser` module renamed to `ini`. +- 0.2.1 + - `Ini` struct is added along with file-loading, parsing and hashmap functions. Documentation is added. +- 0.2.2 + - Fixed docs. +- 0.3.0 + - Added `get()` for getting values from the map directly. Docs expanded as well. + - Mark `ini::load()` for deprecation. +- 0.3.1 + - Updated docs. + - All parameters now trimmed before insertion. + - Converted `ini::load()` into a wrapper around `Ini`. +- 0.4.0 + - Changed `Ini::load()` to return an `Ok(map)` with a clone of the stored `HashMap`. +- 0.4.1 + - Fixed and added docs. +- 0.5.0 (**BETA**) (yanked) + - Changelog added. + - Support for value-less keys. + - `HashMap` values are now `Option<String>` instead of `String` to denote empty values vs. no values. + - Documentation greatly improved. + - Syntax docs provided. + - `new()` and `get()` methods are simplified. +- 0.5.1 + - Fixed erroneous docs +- 0.6.0 (**BETA 2**) + - Tests added + - `get_map_ref()` and `get_mut_map()` are now added to allow direct `HashMap` index access making things greatly easier. +- 0.6.1 (yanked) + - Fixed tests +- 0.6.2 + - Fixed accidental binary delivery increasing crate size by ~20x +- 0.7.0 (**BETA 3**) + - Handy getter functions introduced such as `getint()`, `getuint()`, `getfloat()`, `getbool()` + - Fixed docs + - Fixed tests +- 0.7.1 + - Enable `Eq` and `PartialEq` traits + - Improve docs +- 0.8.0 (**BETA 4**) + - Added feature to set default headers. + - Added feature to parse from a `String`. +- 0.8.1 + - Added support for comments + - Improved docs + - 0.9.0 (**BETA 5**) + - Comment customization is here! (**note:** defaults are now changed to `#` and `;`) + - Fixed some docs + - Make more docs pass tests +- 0.9.1 + - Hotfix to change getters to return `Ok(None)` instead of failing parsing for `None` values +- 0.9.2 + - Added `getboolcoerce()` function to parse more `bool`-like values. + - Convert some snippets to doctests. +- 0.10.0 (**BETA 6**) + - Added `set()` and `setstr()` methods to add section, key, values to the configuration. + - Added more test, minor doc fixes. +- 0.11.0 (**BETA 7**) + - Writing to file is here! (`write()`). + - More doctests and docs fixed, next release is planned to be stable. +- 0.11.1 + - Hotfix to remove hardcoded default section and use set default section. + - Enabled auto-trait implementation of `Default` for empty inits. + - Added the `sections()` method to get a vector of sections. +- 0.12.0 (**BETA 8**) + - New function added, `writes()` to support writing configuration to a string. + - More doctests passed. +- 0.13.0 (**BETA 9**) + - New functions added, `clear()` and `remove_section()` to make handling similar to hashmaps. + - Docs fixed. On track to stable. +- 0.13.1 (yanked) + - New function added, `remove_key()` to remove a key from a section + - All doctests passing! +- 0.13.2 (**FINAL BETA**) + - Erroneous docs fixed. + - Final release before stable. +- 1.0.0 + - Dropped support for `ini::load()` + - Updated tests + +Older changelogs are preserved here, current changelog is present in [README.md](README.md). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f45978c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement, +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d9376a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,46 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "configparser" +version = "3.0.2" +authors = ["QEDK <qedk.en@gmail.com>"] +description = "A simple configuration parsing utility with no dependencies that allows you to parse INI and ini-style syntax. You can use this to write Rust programs which can be customized by end users easily." +homepage = "https://github.com/QEDK/configparser-rs" +documentation = "https://docs.rs/configparser" +readme = "README.md" +keywords = [ + "config", + "ini", + "settings", + "configuration", + "parser", +] +categories = [ + "config", + "encoding", + "parser-implementations", +] +license = "MIT OR LGPL-3.0-or-later" +repository = "https://github.com/QEDK/configparser-rs" +resolver = "2" + +[dependencies.async-std] +version = "1.12.0" +optional = true + +[dependencies.indexmap] +version = "1.9.1" +optional = true + +[badges.maintenance] +status = "actively-developed" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100755 index 0000000..f65b357 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,22 @@ +[package] +name = "configparser" +version = "3.0.2" +authors = ["QEDK <qedk.en@gmail.com>"] +edition = "2021" +description = "A simple configuration parsing utility with no dependencies that allows you to parse INI and ini-style syntax. You can use this to write Rust programs which can be customized by end users easily." +homepage = "https://github.com/QEDK/configparser-rs" +repository = "https://github.com/QEDK/configparser-rs" +documentation = "https://docs.rs/configparser" +readme = "README.md" +license = "MIT OR LGPL-3.0-or-later" +keywords = ["config", "ini", "settings", "configuration", "parser"] +categories = ["config", "encoding", "parser-implementations"] + +[badges] +maintenance = { status = "actively-developed" } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-std = { version = "1.12.0", optional = true } +indexmap = { version = "1.9.1", optional = true } @@ -0,0 +1 @@ +LICENSE-MIT
\ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..fbe1dcb --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) 2020 QEDK + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..6d01048 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "configparser" +description: "A simple configuration parsing utility with no dependencies that allows you to parse INI and ini-style syntax. You can use this to write Rust programs which can be customized by end users easily." +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/configparser" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/configparser/configparser-3.0.2.crate" + } + version: "3.0.2" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 8 + day: 23 + } +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT @@ -0,0 +1 @@ +include platform/prebuilts/rust:master:/OWNERS diff --git a/README.md b/README.md new file mode 100755 index 0000000..0481162 --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ +# configparser +[![Build Status](https://github.com/QEDK/configparser-rs/actions/workflows/rust.yaml/badge.svg)](https://github.com/QEDK/configparser-rs/actions/workflows/rust.yaml) [![Crates.io](https://img.shields.io/crates/l/configparser?color=black)](LICENSE-MIT) [![Crates.io](https://img.shields.io/crates/v/configparser?color=black)](https://crates.io/crates/configparser) [![Released API docs](https://docs.rs/configparser/badge.svg)](https://docs.rs/configparser) [![Maintenance](https://img.shields.io/maintenance/yes/2022)](https://github.com/QEDK/configparser-rs) + +This crate provides the `Ini` struct which implements a basic configuration language which provides a structure similar to whatβs found in Windows' `ini` files. You can use this to write Rust programs which can be customized by end users easily. + +This is a simple configuration parsing utility with no dependencies built on Rust. It is inspired by Python's `configparser`. + +The current release is stable and changes will take place at a slower pace. We'll be keeping semver in mind for future releases as well. + +## π Quick Start + +A basic `ini`-syntax file (we say ini-syntax files because the files don't need to be necessarily `*.ini`) looks like this: +```INI +[DEFAULT] +key1 = value1 +pizzatime = yes +cost = 9 + +[topsecrets] +nuclear launch codes = topsecret + +[github.com] +User = QEDK +``` +Essentially, the syntax consists of sections, each of which can which contains keys with values. The `Ini` struct can read and write such values to +strings as well as files. + +### π§° Installation +You can install this easily via `cargo` by including it in your `Cargo.toml` file like: +```TOML +[dependencies] +configparser = "3.0.2" +``` + +## β Supported datatypes +`configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common +that it's a safe bet that some values need to be parsed in other types. For this, the `Ini` struct provides easy functions like `getint()`, `getuint()`, +`getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so +`true` is the same as `True` just like `TRUE`). The crate also provides a stronger `getboolcoerce()` function that parses more values (such as `T`, `yes` and `0`, all case-insensitively), the function's documentation will give you the exact details. +```rust +use configparser::ini::Ini; + +let mut config = Ini::new(); +config.read(String::from( + "[somesection] + someintvalue = 5")); +let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); +assert_eq!(my_value, 5); // value accessible! + +//You can ofcourse just choose to parse the values yourself: +let my_string = String::from("1984"); +let my_int = my_string.parse::<i32>().unwrap(); +``` + +## π Supported `ini` file structure +A configuration file can consist of sections, each led by a `[section-name]` header, followed by key-value entries separated by a delimiter (`=` and `:`). By default, section names and key names are case-insensitive. Case-sensitivity can be enabled using the `Ini::new_cs()` constructor. All leading and trailing whitespace is removed from stored keys, values and section names. +Key values can be omitted, in which case the key-value delimiter +may also be left out (but this is different from putting a delimiter, we'll +explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the +API. Keep in mind that key-value pairs or section headers cannot span multiple lines. +Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). + +Let's take for example: +```INI +[section headers are case-insensitive by default] +[ section headers are case-insensitive by default ] +are the section headers above same? = yes +sectionheaders_and_keysarestored_in_lowercase? = yes +keys_are_also_case_insensitive = Values are case sensitive +Case-sensitive_keys_and_sections = using a special constructor +you can also use colons : instead of the equal symbol +;anything after a comment symbol is ignored +#this is also a comment +spaces in keys=allowed ;and everything before this is still valid! +spaces in values=allowed as well +spaces around the delimiter = also OK + + +[All values are strings] +values like this= 0000 +or this= 0.999 +are they treated as numbers? = no +integers, floats and booleans are held as= strings + +[value-less?] +a_valueless_key_has_None +this key has an empty string value has Some("") = + + [indented sections] + can_values_be_as_well = True + purpose = formatting for readability + is_this_same = yes + is_this_same=yes + +``` +An important thing to note is that values with the same keys will get updated, this means that the last inserted key (whether that's a section header +or property key) is the one that remains in the `HashMap`. +The only bit of magic the API does is the section-less properties are put in a section called "default". You can configure this variable via the API. +Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. + +## π Usage +Let's take another simple `ini` file and talk about working with it: +```INI +[topsecret] +KFC = the secret herb is orega- + +[values] +Uint = 31415 +``` +If you read the above sections carefully, you'll know that 1) all the keys are stored in lowercase, 2) `get()` can make access in a case-insensitive +manner and 3) we can use `getuint()` to parse the `Uint` value into an `u64`. Let's see that in action. + +```rust +use configparser::ini::Ini; +use std::error::Error; + +fn main() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new(); + + // You can easily load a file to get a clone of the map: + let map = config.load("tests/test.ini")?; + println!("{:?}", map); + // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() + + // If you want to access the value, then you can simply do: + let val = config.get("TOPSECRET", "KFC").unwrap(); + // Notice how get() can access indexes case-insensitively. + + assert_eq!(val, "the secret herb is orega-"); // value accessible! + + // What if you want remove KFC's secret recipe? Just use set(): + config.set("topsecret", "kfc", None); + + assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! + + // What if you want to get an unsigned integer? + let my_number = config.getuint("values", "Uint")?.unwrap(); + assert_eq!(my_number, 31415); // and we got it! + // The Ini struct provides more getters for primitive datatypes. + + // You can also access it like a normal hashmap: + let innermap = map["topsecret"].clone(); + // Remember that all indexes are stored in lowercase! + + // You can easily write the currently stored configuration to a file like: + config.write("output.ini"); + + // If you want to simply mutate the stored hashmap, you can use get_mut_map() + let map = config.get_mut_map(); + // You can then use normal HashMap functions on this map at your convenience. + // Remember that functions which rely on standard formatting might stop working + // if it's mutated differently. + + // If you want a case-sensitive map, just do: + let mut config = Ini::new_cs(); + // This automatically changes the behaviour of every function and parses the file as case-sensitive. + + Ok(()) +} +``` +The `Ini` struct offers great support for type conversion and type setting safely, as well as map accesses. See the API for more verbose documentation. + +## πFeatures + + - *indexmap*: Activating the `indexmap` feature allows using [indexmap](https://crates.io/crates/indexmap) in place + of `HashMap` to store the sections and keys. This ensures that insertion order is preserved when iterating on or + serializing the Ini object. + Due to the nature of indexmap, it offers mostly similar performance to stdlib HashMaps but with + [slower lookup times](https://github.com/bluss/indexmap#performance). + +You can activate it by adding it as a feature like this: +```TOML +[dependencies] +configparser = { version = "3.0.2", features = ["indexmap"] } +``` + + - *async-std*: Activating the `async-std` feature adds asynchronous functions for reading from (`load_async()`) and + writing to (`write_async()`) files using [async-std](https://crates.io/crates/async-std). + +You can activate it by adding it as a feature like this: +```TOML +[dependencies] +configparser = { version = "3.0.2", features = ["async-std"] } +``` + +## π License + +Licensed under either of + + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + * Lesser General Public license v3.0 or later ([LICENSE-LGPL](LICENSE-LGPL) or https://www.gnu.org/licenses/lgpl-3.0.html) + +at your option. + +### β Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the LGPL-3.0 license, shall be dual licensed as above, without any +additional terms or conditions. + +## π Changelog + +Old changelogs are in [CHANGELOG.md](CHANGELOG.md). +- 2.0.0 + - **BREAKING** Added Python-esque support for `:` as a delimiter. + - :new: Add support for case-sensitive maps with automatic handling under the hood. + - :hammer: Fixed buggy setters which went uncaught, to preserve case-insensitive nature. +- 2.0.1 + - Add first-class support for setting, loading and reading defaults + - New available struct `IniDefault` for fast templating +- 2.1.0 + - π― **BREAKING** Parse keys with higher priority, both brackets `[` and `]` can be part of values now. + - βΉ Only affects current behaviour **iff** your section headers had comments in front of them like, `comment[HEADER]`, you can fix it by adding the comment after the header like `[HEADER]#comment` or otherwise. + - π `load()` and `write()` work with `Path`-like arguments now. + - π Add docs for new struct +- 3.0.0 + - π
**BREAKING** `IniDefault` is now a non-exhaustive struct, this will make future upgrades easier and non-breaking in nature. This change might also have a few implications in updating your existing codebase, please read the [official docs](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute) for more guidance. + - `IniDefault` is now internally used for generating defaults, reducing crate size. + - π There is now a new optional `indexmap` feature that preserves insertion order of your loaded configurations. +- 3.0.1 + - Uses `CRLF` line endings for Windows files. + - Bumps crate to 2021 edition. + - Adds features to CI pipeline. +- 3.0.2 (**STABLE**) + - Adds support for multi-line key-value pairs. + - Adds `async-std` feature for asynchronous file operations. + - Some performance optimizations. + +### π Future plans + +- Support for appending sections, coercing them as well. +- Benchmarking against similar packages. diff --git a/src/ini.rs b/src/ini.rs new file mode 100755 index 0000000..d966e78 --- /dev/null +++ b/src/ini.rs @@ -0,0 +1,972 @@ +//!The ini module provides all the things necessary to load and parse ini-syntax files. The most important of which is the `Ini` struct. +//!See the [implementation](https://docs.rs/configparser/*/configparser/ini/struct.Ini.html) documentation for more details. +#[cfg(feature = "indexmap")] +use indexmap::IndexMap as Map; +#[cfg(not(feature = "indexmap"))] +use std::collections::HashMap as Map; + +#[cfg(feature = "async-std")] +use async_std::{fs as async_fs, path::Path as AsyncPath}; + +use std::collections::HashMap; +use std::convert::AsRef; +use std::fs; +use std::path::Path; +use std::fmt::Write; + +///The `Ini` struct simply contains a nested hashmap of the loaded configuration, the default section header and comment symbols. +///## Example +///```rust +///use configparser::ini::Ini; +/// +///let mut config = Ini::new(); +///``` +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[non_exhaustive] +pub struct Ini { + map: Map<String, Map<String, Option<String>>>, + default_section: std::string::String, + comment_symbols: Vec<char>, + delimiters: Vec<char>, + boolean_values: HashMap<bool, Vec<String>>, + case_sensitive: bool, + multiline: bool, +} + +///The `IniDefault` struct serves as a template to create other `Ini` objects from. It can be used to store and load +///default properties from different `Ini` objects. +///## Example +///```rust +///use configparser::ini::Ini; +/// +///let mut config = Ini::new(); +///let default = config.defaults(); +///let mut config2 = Ini::new_from_defaults(default); // default gets consumed +///``` +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct IniDefault { + ///Denotes the default section header name. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.default_section, "default"); + ///``` + pub default_section: std::string::String, + ///Denotes the set comment symbols for the object. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.comment_symbols, vec![';', '#']); + ///``` + pub comment_symbols: Vec<char>, + ///Denotes the set delimiters for the key-value pairs. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.delimiters, vec!['=', ':']); + ///``` + pub delimiters: Vec<char>, + pub boolean_values: HashMap<bool, Vec<String>>, + ///Denotes if the `Ini` object is case-sensitive. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.case_sensitive, false); + ///``` + pub case_sensitive: bool, + ///Denotes if the `Ini` object parses multiline strings. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///assert_eq!(default.multiline, false); + ///``` + pub multiline: bool, +} + +impl Default for IniDefault { + fn default() -> Self { + Self { + default_section: "default".to_owned(), + comment_symbols: vec![';', '#'], + delimiters: vec!['=', ':'], + multiline: false, + boolean_values: [ + ( + true, + vec!["true", "yes", "t", "y", "on", "1"] + .iter() + .map(|&s| s.to_owned()) + .collect(), + ), + ( + false, + vec!["false", "no", "f", "n", "off", "0"] + .iter() + .map(|&s| s.to_owned()) + .collect(), + ), + ] + .iter() + .cloned() + .collect(), + case_sensitive: false, + } + } +} + +#[cfg(windows)] +const LINE_ENDING: &str = "\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &str = "\n"; + +impl Ini { + ///Creates a new `Map` of `Map<String, Map<String, Option<String>>>` type for the struct. + ///All values in the Map are stored in `String` type. + /// + ///By default, [`std::collections::HashMap`] is used for the Map object. + ///The `indexmap` feature can be used to use an [`indexmap::map::IndexMap`] instead, which + ///allows keeping the insertion order for sections and keys. + /// + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new() -> Ini { + Ini::new_from_defaults(IniDefault::default()) + } + + ///Creates a new **case-sensitive** `Map` of `Map<String, Map<String, Option<String>>>` type for the struct. + ///All values in the Map are stored in `String` type. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new_cs(); + ///``` + ///Returns the struct and stores it in the calling variable. + pub fn new_cs() -> Ini { + Ini::new_from_defaults(IniDefault { case_sensitive: true, ..Default::default() }) + } + + ///Creates a new `Ini` with the given defaults from an existing `IniDefault` object. + ///## Example + ///```rust + ///use configparser::ini::Ini; + ///use configparser::ini::IniDefault; + /// + ///let mut default = IniDefault::default(); + ///default.comment_symbols = vec![';']; + ///default.delimiters = vec!['=']; + ///let mut config = Ini::new_from_defaults(default.clone()); + ///// Now, load as usual with new defaults: + ///let map = config.load("tests/test.ini").unwrap(); + ///assert_eq!(config.defaults(), default); + /// + ///``` + pub fn new_from_defaults(defaults: IniDefault) -> Ini { + Ini { + map: Map::new(), + default_section: defaults.default_section, + comment_symbols: defaults.comment_symbols, + delimiters: defaults.delimiters, + boolean_values: defaults.boolean_values, + case_sensitive: defaults.case_sensitive, + multiline: defaults.multiline, + } + } + + ///Fetches the defaults from the current `Ini` object and stores it as a `IniDefault` struct for usage elsewhere. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let default = config.defaults(); + ///``` + ///Returns an `IniDefault` object. Keep in mind that it will get borrowed since it has non-`Copy` types. + pub fn defaults(&self) -> IniDefault { + IniDefault { + default_section: self.default_section.to_owned(), + comment_symbols: self.comment_symbols.to_owned(), + delimiters: self.delimiters.to_owned(), + boolean_values: self.boolean_values.to_owned(), + case_sensitive: self.case_sensitive, + multiline: self.multiline, + } + } + + ///Takes an `IniDefault` object and stores its properties in the calling `Ini` object. This happens in-place and + ///does not work retroactively, only future operations are affected. + ///## Example + ///```rust + ///use configparser::ini::Ini; + ///use configparser::ini::IniDefault; + /// + ///let mut config = Ini::new(); + ///let mut default = IniDefault::default(); + ///default.case_sensitive = true; + ///// This is equivalent to ini_cs() defaults + ///config.load_defaults(default.clone()); + ///assert_eq!(config.defaults(), default); + ///``` + ///Returns nothing. + pub fn load_defaults(&mut self, defaults: IniDefault) { + self.default_section = defaults.default_section; + self.comment_symbols = defaults.comment_symbols; + self.delimiters = defaults.delimiters; + self.boolean_values = defaults.boolean_values; + self.case_sensitive = defaults.case_sensitive; + } + + ///Sets the default section header to the defined string (the default is `default`). + ///It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + /// + ///config.set_default_section("topsecret"); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_default_section(&mut self, section: &str) { + self.default_section = section.to_owned(); + } + + ///Sets the default comment symbols to the defined character slice (the defaults are `;` and `#`). + ///Keep in mind that this will remove the default symbols. It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_comment_symbols(&['!', '#']); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_comment_symbols(&mut self, symlist: &[char]) { + self.comment_symbols = symlist.to_vec(); + } + + ///Sets multiline string support. + ///It must be set before `load()` or `read()` is called in order to take effect. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.set_multiline(true); + ///let map = config.load("tests/test.ini").unwrap(); + ///``` + ///Returns nothing. + pub fn set_multiline(&mut self, multiline: bool) { + self.multiline = multiline; + } + + ///Gets all the sections of the currently-stored `Map` in a vector. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let sections = config.sections(); + ///``` + ///Returns `Vec<String>`. + pub fn sections(&self) -> Vec<String> { + self.map.keys().cloned().collect() + } + + ///Loads a file from a defined path, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let map = config.load("tests/test.ini").unwrap(); // we can get a clone like this, or just store it + /////Then, we can use standard hashmap functions like: + ///let values = map.get("values").unwrap(); + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn load<T: AsRef<Path>>( + &mut self, + path: T, + ) -> Result<Map<String, Map<String, Option<String>>>, String> { + self.map = match self.parse(match fs::read_to_string(&path) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(s) => s, + }) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(map) => map, + }; + Ok(self.map.clone()) + } + + ///Reads an input string, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let map = match config.read(String::from( + /// "[2000s] + /// 2020 = bad")) { + /// Err(why) => panic!("{}", why), + /// Ok(inner) => inner + ///}; + ///let this_year = map["2000s"]["2020"].clone().unwrap(); + ///assert_eq!(this_year, "bad"); // value accessible! + ///``` + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub fn read( + &mut self, + input: String, + ) -> Result<Map<String, Map<String, Option<String>>>, String> { + self.map = match self.parse(input) { + Err(why) => return Err(why), + Ok(map) => map, + }; + Ok(self.map.clone()) + } + + ///Writes the current configuation to the specified path. If a file is not present, it is automatically created for you, if a file already + ///exists, it is truncated and the configuration is written to it. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///fn main() -> std::io::Result<()> { + /// let mut config = Ini::new(); + /// config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + /// config.write("output.ini") + ///} + ///``` + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub fn write<T: AsRef<Path>>(&self, path: T) -> std::io::Result<()> { + fs::write(path.as_ref(), self.unparse()) + } + + ///Returns a string with the current configuration formatted with valid ini-syntax. This is always safe since the configuration is validated during + ///parsing. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[2000s] + /// 2020 = bad")); + ///let outstring = config.writes(); + ///``` + ///Returns a `String` type contatining the ini-syntax file. + pub fn writes(&self) -> String { + self.unparse() + } + + ///Private function that converts the currently stored configuration into a valid ini-syntax string. + fn unparse(&self) -> String { + // push key/value pairs in outmap to out string. + fn unparse_key_values( + out: &mut String, + outmap: &Map<String, Option<String>>, + multiline: bool, + ) { + for (key, val) in outmap.iter() { + out.push_str(key); + + if let Some(value) = val { + out.push('='); + + if multiline { + let mut lines = value.lines(); + + out.push_str(lines.next().unwrap()); + + for line in lines { + out.push_str(LINE_ENDING); + out.push_str(" "); + out.push_str(line); + } + } else { + out.push_str(value); + } + } + + out.push_str(LINE_ENDING); + } + } + + let mut out = String::new(); + + if let Some(defaultmap) = self.map.get(&self.default_section) { + unparse_key_values(&mut out, defaultmap, self.multiline); + } + + for (section, secmap) in self.map.iter() { + if section != &self.default_section { + write!(out, "[{}]", section).unwrap(); + out.push_str(LINE_ENDING); + unparse_key_values(&mut out, secmap, self.multiline); + } + } + out + } + + ///Private function that parses ini-style syntax into a Map. + fn parse(&self, input: String) -> Result<Map<String, Map<String, Option<String>>>, String> { + let mut map: Map<String, Map<String, Option<String>>> = Map::new(); + let mut section = self.default_section.clone(); + let mut current_key: Option<String> = None; + + let caser = |val: &str| { + if self.case_sensitive { + val.to_owned() + } else { + val.to_lowercase() + } + }; + + for (num, raw_line) in input.lines().enumerate() { + let line = match raw_line.find(|c: char| self.comment_symbols.contains(&c)) { + Some(idx) => &raw_line[..idx], + None => raw_line, + }; + + let trimmed = line.trim(); + + if trimmed.is_empty() { + continue; + } + + match (trimmed.find('['), trimmed.rfind(']')) { + (Some(0), Some(end)) => { + section = caser(trimmed[1..end].trim()); + + continue; + } + (Some(0), None) => { + return Err(format!( + "line {}: Found opening bracket for section name but no closing bracket", + num + )); + } + _ => {} + } + + if line.starts_with(char::is_whitespace) && self.multiline { + let key = match current_key.as_ref() { + Some(x) => x, + None => { + return Err(format!( + "line {}: Started with indentation but there is no current entry", + num, + )) + } + }; + + let valmap = map.entry(section.clone()).or_insert_with(Map::new); + + let val = valmap + .entry(key.clone()) + .or_insert_with(|| Some(String::new())); + + match val { + Some(x) => { + x.push_str(LINE_ENDING); + x.push_str(trimmed); + } + None => { + *val = Some(format!("{}{}", LINE_ENDING, trimmed)); + } + } + + continue; + } + + let valmap = map.entry(section.clone()).or_insert_with(Map::new); + + match trimmed.find(&self.delimiters[..]) { + Some(delimiter) => { + let key = caser(trimmed[..delimiter].trim()); + + if key.is_empty() { + return Err(format!("line {}:{}: Key cannot be empty", num, delimiter)); + } else { + current_key = Some(key.clone()); + + let value = trimmed[delimiter + 1..].trim().to_owned(); + + valmap.insert(key, Some(value)); + } + } + None => { + let key = caser(trimmed); + current_key = Some(key.clone()); + + valmap.insert(key, None); + } + } + } + + Ok(map) + } + + ///Private function that cases things automatically depending on the set variable. + fn autocase(&self, section: &str, key: &str) -> (String, String) { + if self.case_sensitive { + (section.to_owned(), key.to_owned()) + } else { + (section.to_lowercase(), key.to_lowercase()) + } + } + + ///Returns a clone of the stored value from the key stored in the defined section. + ///Unlike accessing the map directly, `get()` can process your input to make case-insensitive access *if* the + ///default constructor is used. + ///All `get` functions will do this automatically under the hood. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.get("default", "defaultvalues").unwrap(); + ///assert_eq!(value, String::from("defaultvalues")); + ///``` + ///Returns `Some(value)` of type `String` if value is found or else returns `None`. + pub fn get(&self, section: &str, key: &str) -> Option<String> { + let (section, key) = self.autocase(section, key); + self.map.get(§ion)?.get(&key)?.clone() + } + + ///Parses the stored value from the key stored in the defined section to a `bool`. + ///For ease of use, the function converts the type case-insensitively (`true` == `True`). + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getbool("values", "bool").unwrap().unwrap(); + ///assert!(value); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. + ///If the parsing fails, it returns an `Err(string)`. + pub fn getbool(&self, section: &str, key: &str) -> Result<Option<bool>, String> { + let (section, key) = self.autocase(section, key); + match self.map.get(§ion) { + Some(secmap) => match secmap.get(&key) { + Some(val) => match val { + Some(inner) => match inner.to_lowercase().parse::<bool>() { + Err(why) => Err(why.to_string()), + Ok(boolean) => Ok(Some(boolean)), + }, + None => Ok(None), + }, + None => Ok(None), + }, + None => Ok(None), + } + } + + ///Parses the stored value from the key stored in the defined section to a `bool`. For ease of use, the function converts the type coerces a match. + ///It attempts to case-insenstively find `true`, `yes`, `t`, `y`, `1` and `on` to parse it as `True`. + ///Similarly it attempts to case-insensitvely find `false`, `no`, `f`, `n`, `0` and `off` to parse it as `False`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getboolcoerce("values", "boolcoerce").unwrap().unwrap(); + ///assert!(!value); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. + ///If the parsing fails, it returns an `Err(string)`. + pub fn getboolcoerce(&self, section: &str, key: &str) -> Result<Option<bool>, String> { + let (section, key) = self.autocase(section, key); + match self.map.get(§ion) { + Some(secmap) => match secmap.get(&key) { + Some(val) => match val { + Some(inner) => { + let boolval = &inner.to_lowercase()[..]; + if self + .boolean_values + .get(&true) + .unwrap() + .iter() + .any(|elem| elem == boolval) + { + Ok(Some(true)) + } else if self + .boolean_values + .get(&false) + .unwrap() + .iter() + .any(|elem| elem == boolval) + { + Ok(Some(false)) + } else { + Err(format!( + "Unable to parse value into bool at {}:{}", + section, key + )) + } + } + None => Ok(None), + }, + None => Ok(None), + }, + None => Ok(None), + } + } + + ///Parses the stored value from the key stored in the defined section to an `i64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getint("values", "int").unwrap().unwrap(); + ///assert_eq!(value, -31415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `i64` if value is found or else returns `Ok(None)`. + ///If the parsing fails, it returns an `Err(string)`. + pub fn getint(&self, section: &str, key: &str) -> Result<Option<i64>, String> { + let (section, key) = self.autocase(section, key); + match self.map.get(§ion) { + Some(secmap) => match secmap.get(&key) { + Some(val) => match val { + Some(inner) => match inner.parse::<i64>() { + Err(why) => Err(why.to_string()), + Ok(int) => Ok(Some(int)), + }, + None => Ok(None), + }, + None => Ok(None), + }, + None => Ok(None), + } + } + + ///Parses the stored value from the key stored in the defined section to a `u64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getint("values", "Uint").unwrap().unwrap(); + ///assert_eq!(value, 31415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `u64` if value is found or else returns `Ok(None)`. + ///If the parsing fails, it returns an `Err(string)`. + pub fn getuint(&self, section: &str, key: &str) -> Result<Option<u64>, String> { + let (section, key) = self.autocase(section, key); + match self.map.get(§ion) { + Some(secmap) => match secmap.get(&key) { + Some(val) => match val { + Some(inner) => match inner.parse::<u64>() { + Err(why) => Err(why.to_string()), + Ok(uint) => Ok(Some(uint)), + }, + None => Ok(None), + }, + None => Ok(None), + }, + None => Ok(None), + } + } + + ///Parses the stored value from the key stored in the defined section to a `f64`. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.load("tests/test.ini"); + ///let value = config.getfloat("values", "float").unwrap().unwrap(); + ///assert_eq!(value, 3.1415); // value accessible! + ///``` + ///Returns `Ok(Some(value))` of type `f64` if value is found or else returns `Ok(None)`. + ///If the parsing fails, it returns an `Err(string)`. + pub fn getfloat(&self, section: &str, key: &str) -> Result<Option<f64>, String> { + let (section, key) = self.autocase(section, key); + match self.map.get(§ion) { + Some(secmap) => match secmap.get(&key) { + Some(val) => match val { + Some(inner) => match inner.parse::<f64>() { + Err(why) => Err(why.to_string()), + Ok(float) => Ok(Some(float)), + }, + None => Ok(None), + }, + None => Ok(None), + }, + None => Ok(None), + } + } + + ///Returns a clone of the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=values")); + ///let map = config.get_map().unwrap(); + ///assert_eq!(map, *config.get_map_ref()); // the cloned map is basically a snapshot that you own + ///``` + ///Returns `Some(map)` if map is non-empty or else returns `None`. + ///Similar to `load()` but returns an `Option` type with the currently stored `Map`. + pub fn get_map(&self) -> Option<Map<String, Map<String, Option<String>>>> { + if self.map.is_empty() { + None + } else { + Some(self.map.clone()) + } + } + + ///Returns an immutable reference to the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///let mapclone = config.read(String::from + /// ("[topsecrets] + /// Valueless key")).unwrap(); + /////Think of the clone as being a snapshot at a point of time while the reference always points to the current configuration. + ///assert_eq!(*config.get_map_ref(), mapclone); // same as expected. + ///``` + ///If you just need to definitely mutate the map, use `get_mut_map()` instead. Alternatively, you can generate a snapshot by getting a clone + ///with `get_map()` and work with that. + pub fn get_map_ref(&self) -> &Map<String, Map<String, Option<String>>> { + &self.map + } + + ///Returns a mutable reference to the `Map` stored in our struct. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from + /// ("[topsecrets] + /// Valueless key")); + /////We can then get the mutable map and insert a value like: + ///config.get_mut_map().get_mut("topsecrets").unwrap().insert(String::from("nuclear launch codes"), None); + ///assert_eq!(config.get("topsecrets", "nuclear launch codes"), None); // inserted successfully! + ///``` + ///If you just need to access the map without mutating, use `get_map_ref()` or make a clone with `get_map()` instead. + pub fn get_mut_map(&mut self) -> &mut Map<String, Map<String, Option<String>>> { + &mut self.map + } + + ///Sets an `Option<String>` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. + ///An existing value in the map will be overwritten. You can also set `None` safely. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=value")); + ///let key_value = String::from("value"); + ///config.set("section", "key", Some(key_value)); + ///config.set("section", "key", None); // also valid! + ///assert_eq!(config.get("section", "key"), None); // correct! + ///``` + ///Returns `None` if there is no existing value, else returns `Some(Option<String>)`, with the existing value being the wrapped `Option<String>`. + ///If you want to insert using a string literal, use `setstr()` instead. + pub fn set( + &mut self, + section: &str, + key: &str, + value: Option<String>, + ) -> Option<Option<String>> { + let (section, key) = self.autocase(section, key); + match self.map.get_mut(§ion) { + Some(secmap) => secmap.insert(key, value), + None => { + let mut valmap: Map<String, Option<String>> = Map::new(); + valmap.insert(key, value); + self.map.insert(section, valmap); + None + } + } + } + + ///Sets an `Option<&str>` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. + ///An existing value in the map will be overwritten. You can also set `None` safely. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=notvalue")); + ///config.setstr("section", "key", Some("value")); + ///config.setstr("section", "key", None); // also valid! + ///assert_eq!(config.get("section", "key"), None); // correct! + ///``` + ///Returns `None` if there is no existing value, else returns `Some(Option<String>)`, with the existing value being the wrapped `Option<String>`. + ///If you want to insert using a `String`, use `set()` instead. + pub fn setstr( + &mut self, + section: &str, + key: &str, + value: Option<&str>, + ) -> Option<Option<String>> { + let (section, key) = self.autocase(section, key); + self.set(§ion, &key, value.map(String::from)) + } + + ///Clears the map, removing all sections and properties from the hashmap. It keeps the allocated memory for reuse. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// key=somevalue")); + ///config.clear(); + ///assert!(config.get_map_ref().is_empty()); // our map is empty! + ///``` + ///Returns nothing. + pub fn clear(&mut self) { + self.map.clear(); + } + + ///Removes a section from the hashmap, returning the properties stored in the section if the section was previously in the map. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// updog=whatsupdog")); + ///config.remove_section("section"); // this will return a cloned hashmap of the stored property + ///assert!(config.get_map_ref().is_empty()); // with the last section removed, our map is now empty! + ///``` + ///Returns `Some(section_map)` if the section exists or else, `None`. + pub fn remove_section(&mut self, section: &str) -> Option<Map<String, Option<String>>> { + let section = if self.case_sensitive { + section.to_owned() + } else { + section.to_lowercase() + }; + self.map.remove(§ion) + } + + ///Removes a key from a section in the hashmap, returning the value attached to the key if it was previously in the map. + ///## Example + ///```rust + ///use configparser::ini::Ini; + /// + ///let mut config = Ini::new(); + ///config.read(String::from( + /// "[section] + /// updog=whatsupdog + /// [anothersection] + /// updog=differentdog")); + ///let val = config.remove_key("anothersection", "updog").unwrap().unwrap(); + ///assert_eq!(val, String::from("differentdog")); // with the last section removed, our map is now empty! + ///``` + ///Returns `Some(Option<String>)` if the value exists or else, `None`. + pub fn remove_key(&mut self, section: &str, key: &str) -> Option<Option<String>> { + let (section, key) = self.autocase(section, key); + self.map.get_mut(§ion)?.remove(&key) + } +} + +#[cfg(feature = "async-std")] +impl Ini { + ///Loads a file asynchronously from a defined path, parses it and puts the hashmap into our struct. + ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. + /// + ///Usage is similar to `load`, but `.await` must be called after along with the usual async rules. + /// + ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. + ///Use `get_mut_map()` if you want a mutable reference. + pub async fn load_async<T: AsRef<AsyncPath>>( + &mut self, + path: T, + ) -> Result<Map<String, Map<String, Option<String>>>, String> { + self.map = match self.parse(match async_fs::read_to_string(&path).await { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(s) => s, + }) { + Err(why) => { + return Err(format!( + "couldn't read {}: {}", + &path.as_ref().display(), + why + )) + } + Ok(map) => map, + }; + Ok(self.map.clone()) + } + + ///Writes the current configuation to the specified path asynchronously. If a file is not present, it is automatically created for you, if a file already + ///exists, it is truncated and the configuration is written to it. + /// + ///Usage is the same as `write`, but `.await` must be called after along with the usual async rules. + /// + ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. + pub async fn write_async<T: AsRef<Path>>(&self, path: T) -> std::io::Result<()> { + async_fs::write(path.as_ref(), self.unparse()).await + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..b313a56 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,154 @@ +/*! +This crate provides the `Ini` struct which implements a basic configuration language which provides a structure similar to whatβs found in Windows' `ini` files. +You can use this to write Rust programs which can be customized by end users easily. + +This is a simple configuration parsing utility with no dependencies built on Rust. It is inspired by Python's `configparser`. + +The current release is stable and changes will take place at a slower pace. We'll be keeping semver in mind for future releases as well. + +## π Quick Start + +A basic `ini`-syntax file (we say ini-syntax files because the files don't need to be necessarily `*.ini`) looks like this: +```INI +[DEFAULT] +key1 = value1 +pizzatime = yes +cost = 9 + +[topsecrets] +nuclear launch codes = topsecret + +[github.com] +User = QEDK +``` +Essentially, the syntax consists of sections, each of which can which contains keys with values. The `Ini` struct can read and write such values to +strings as well as files. + +## β Supported datatypes +`configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common +that it's a safe bet that some values need to be parsed in other types. For this, the `Ini` struct provides easy functions like `getint()`, `getuint()`, +`getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so +`true` is the same as `True` just like `TRUE`). The crate also provides a stronger `getboolcoerce()` function that parses more values (such as `T`, `yes` and `0`, all case-insensitively), the function's documentation will give you the exact details. +```rust +use configparser::ini::Ini; + +let mut config = Ini::new(); +config.read(String::from( + "[somesection] + someintvalue = 5")); +let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); +assert_eq!(my_value, 5); // value accessible! + +//You can ofcourse just choose to parse the values yourself: +let my_string = String::from("1984"); +let my_int = my_string.parse::<i32>().unwrap(); +``` + + +## π Supported `ini` file structure +A configuration file can consist of sections, each led by a `[section-name]` header, followed by key-value entries separated by a delimiter (`=` and `:`). By default, section names and key names are case-insensitive. Case-sensitivity can be enabled using the `Ini::new_cs()` constructor. All leading and trailing whitespace is removed from stored keys, values and section names. +Key values can be omitted, in which case the key-value delimiter +may also be left out (but this is different from putting a delimiter, we'll +explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the +API. Keep in mind that key-value pairs or section headers cannot span multiple lines. +Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). +Let's take for example: +```INI +[section headers are case-insensitive by default] +[ section headers are case-insensitive by default ] +are the section headers above same? = yes +sectionheaders_and_keysarestored_in_lowercase? = yes +keys_are_also_case_insensitive = Values are case sensitive +Case-sensitive_keys_and_sections = using a special constructor +you can also use colons : instead of the equal symbol +;anything after a comment symbol is ignored +#this is also a comment +spaces in keys=allowed ;and everything before this is still valid! +spaces in values=allowed as well +spaces around the delimiter = also OK + + +[All values are strings] +values like this= 0000 +or this= 0.999 +are they treated as numbers? = no +integers, floats and booleans are held as= strings + +[value-less?] +a_valueless_key_has_None +this key has an empty string value has Some("") = + + [indented sections] + can_values_be_as_well = True + purpose = formatting for readability + is_this_same = yes + is_this_same=yes + +``` +An important thing to note is that values with the same keys will get updated, this means that the last inserted key (whether that's a section header +or property key) is the one that remains in the `HashMap`. +The only bit of magic the API does is the section-less properties are put in a section called "default". You can configure this variable via the API. +Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. + +## Usage +Let's take another simple `ini` file and talk about working with it: +```INI +[topsecret] +KFC = the secret herb is orega- + +[values] +Uint = 31415 +``` +If you read the above sections carefully, you'll know that 1) all the keys are stored in lowercase, 2) `get()` can make access in a case-insensitive +manner and 3) we can use `getint()` to parse the `Int` value into an `i64`. Let's see that in action. + +```rust +use configparser::ini::Ini; +use std::error::Error; + +fn main() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new(); + + // You can easily load a file to get a clone of the map: + let map = config.load("tests/test.ini")?; + println!("{:?}", map); + // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() + + // If you want to access the value, then you can simply do: + let val = config.get("TOPSECRET", "KFC").unwrap(); + // Notice how get() can access indexes case-insensitively. + + assert_eq!(val, "the secret herb is orega-"); // value accessible! + + // What if you want remove KFC's secret recipe? Just use set(): + config.set("topsecret", "kfc", None); + + assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! + + // What if you want to get an unsigned integer? + let my_number = config.getuint("values", "Uint")?.unwrap(); + assert_eq!(my_number, 31415); // and we got it! + // The Ini struct provides more getters for primitive datatypes. + + // You can also access it like a normal hashmap: + let innermap = map["topsecret"].clone(); + // Remember that all indexes are stored in lowercase! + + // You can easily write the currently stored configuration to a file like: + config.write("output.ini"); + + // If you want to simply mutate the stored hashmap, you can use get_mut_map() + let map = config.get_mut_map(); + // You can then use normal HashMap functions on this map at your convenience. + // Remember that functions which rely on standard formatting might stop working + // if it's mutated differently. + + // If you want a case-sensitive map, just do: + let mut config = Ini::new_cs(); + // This automatically changes the behaviour of every function and parses the file as case-sensitive. + + Ok(()) +} +``` +*/ +pub mod ini; diff --git a/tests/test.ini b/tests/test.ini new file mode 100755 index 0000000..7da5603 --- /dev/null +++ b/tests/test.ini @@ -0,0 +1,19 @@ +defaultvalues=defaultvalues + +[topsecret] +KFC = the secret herb is orega- +colon:value after colon +Empty string = +None string +Password=[in-brackets] + + [ spacing ] + indented=indented +not indented = not indented + +[values] +Bool = True +Boolcoerce = 0 +Int = -31415 +Uint = 31415 +Float = 3.1415 diff --git a/tests/test.rs b/tests/test.rs new file mode 100755 index 0000000..b0f473c --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,348 @@ +use configparser::ini::Ini; +use std::error::Error; + +#[test] +#[allow(clippy::approx_constant)] +fn non_cs() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new(); + let map = config.load("tests/test.ini")?; + config.set_comment_symbols(&[';', '#', '!']); + let inpstring = config.read( + "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415" + .to_owned(), + )?; + assert_eq!(map, inpstring); + config.set("DEFAULT", "defaultvalues", Some("notdefault".to_owned())); + assert_eq!( + config.get("DEFAULT", "defaultvalues").unwrap(), + "notdefault" + ); + config.setstr("DEFAULT", "defaultvalues", Some("defaultvalues")); + assert_eq!( + config.get("DEFAULT", "defaultvalues").unwrap(), + "defaultvalues" + ); + config.setstr("DEFAULT", "defaultvalues", None); + config.write("output.ini")?; + let map2 = config.clone().load("output.ini")?; + assert_eq!(map2, *config.get_map_ref()); + let map3 = config.clone().read(config.writes())?; + assert_eq!(map2, map3); + assert_eq!(config.sections().len(), 4); + assert_eq!(config.get("DEFAULT", "defaultvalues"), None); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); + assert_eq!(config.get("topsecret", "None string"), None); + assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); + assert_eq!( + config.get("spacing", "not indented").unwrap(), + "not indented" + ); + assert_eq!( + config.get("topsecret", "colon").unwrap(), + "value after colon" + ); + assert!(config.getbool("values", "Bool")?.unwrap()); + assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); + assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); + assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); + assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); + assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); + assert_eq!( + map["default"]["defaultvalues"].clone().unwrap(), + "defaultvalues" + ); + assert_eq!( + map["topsecret"]["kfc"].clone().unwrap(), + "the secret herb is orega-" + ); + assert_eq!(map["topsecret"]["empty string"].clone().unwrap(), ""); + assert_eq!(map["topsecret"]["none string"], None); + assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); + assert_eq!( + map["spacing"]["not indented"].clone().unwrap(), + "not indented" + ); + let mut config2 = config.clone(); + let val = config2.remove_key("default", "defaultvalues"); + assert_eq!(val, Some(None)); + assert_eq!(config2.get("default", "defaultvalues"), None); + config2.remove_section("default"); + assert_eq!(config2.get("default", "nope"), None); + let mut_map = config.get_mut_map(); + mut_map.get_mut("topsecret").unwrap().insert( + String::from("none string"), + Some(String::from("None string")), + ); + assert_eq!( + mut_map["topsecret"]["none string"].clone().unwrap(), + "None string" + ); + mut_map.clear(); + config2.clear(); + assert_eq!(config.get_map_ref(), config2.get_map_ref()); + Ok(()) +} + +#[test] +#[allow(clippy::approx_constant)] +fn cs() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new_cs(); + let map = config.load("tests/test.ini")?; + config.set_comment_symbols(&[';', '#', '!']); + let inpstring = config.read( + "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415" + .to_owned(), + )?; + assert_eq!(map, inpstring); + config.set("default", "defaultvalues", Some("notdefault".to_owned())); + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "notdefault" + ); + config.setstr("default", "defaultvalues", Some("defaultvalues")); + assert_eq!( + config.get("default", "defaultvalues").unwrap(), + "defaultvalues" + ); + config.setstr("default", "defaultvalues", None); + config.write("output2.ini")?; + let map2 = config.clone().load("output2.ini")?; + assert_eq!(map2, *config.get_map_ref()); + let map3 = config.clone().read(config.writes())?; + assert_eq!(map2, map3); + assert_eq!(config.sections().len(), 4); + assert_eq!(config.get("default", "defaultvalues"), None); + assert_eq!( + config.get("topsecret", "KFC").unwrap(), + "the secret herb is orega-" + ); + assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); + assert_eq!(config.get("topsecret", "None string"), None); + assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); + assert_eq!( + config.get("spacing", "not indented").unwrap(), + "not indented" + ); + assert_eq!( + config.get("topsecret", "colon").unwrap(), + "value after colon" + ); + assert!(config.getbool("values", "Bool")?.unwrap()); + assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); + assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); + assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); + assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); + assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); + assert_eq!( + map["default"]["defaultvalues"].clone().unwrap(), + "defaultvalues" + ); + assert_eq!( + map["topsecret"]["KFC"].clone().unwrap(), + "the secret herb is orega-" + ); + assert_eq!(map["topsecret"]["Empty string"].clone().unwrap(), ""); + assert_eq!(map["topsecret"]["None string"], None); + assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); + assert_eq!( + map["spacing"]["not indented"].clone().unwrap(), + "not indented" + ); + let mut config2 = config.clone(); + let val = config2.remove_key("default", "defaultvalues"); + assert_eq!(val, Some(None)); + assert_eq!(config2.get("default", "defaultvalues"), None); + config2.remove_section("default"); + assert_eq!(config2.get("default", "nope"), None); + let mut_map = config.get_mut_map(); + mut_map.get_mut("topsecret").unwrap().insert( + String::from("none string"), + Some(String::from("None string")), + ); + assert_eq!( + mut_map["topsecret"]["none string"].clone().unwrap(), + "None string" + ); + mut_map.clear(); + config2.clear(); + assert_eq!(config.get_map_ref(), config2.get_map_ref()); + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn sort_on_write() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new_cs(); + config.load("tests/test.ini")?; + + assert_eq!( + config.writes(), + "defaultvalues=defaultvalues +[topsecret] +KFC=the secret herb is orega- +colon=value after colon +Empty string= +None string +Password=[in-brackets] +[spacing] +indented=indented +not indented=not indented +[values] +Bool=True +Boolcoerce=0 +Int=-31415 +Uint=31415 +Float=3.1415 +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "async-std")] +fn async_load_write() -> Result<(), Box<dyn Error>> { + const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues + [topsecret] + KFC = the secret herb is orega- + colon:value after colon + Empty string = + None string + Password=[in-brackets] + [ spacing ] + indented=indented + not indented = not indented ;testcomment + !modified comment + [values]#another comment + Bool = True + Boolcoerce = 0 + Int = -31415 + Uint = 31415 + Float = 3.1415"; + + let mut config = Ini::new(); + config.read(OUT_FILE_CONTENTS.to_owned())?; + config.write("output_sync.ini")?; + + async_std::task::block_on::<_, Result<_, String>>(async { + let mut config_async = Ini::new(); + config_async.read(OUT_FILE_CONTENTS.to_owned())?; + config_async + .write_async("output_async.ini") + .await + .map_err(|e| e.to_string())?; + Ok(()) + })?; + + let mut sync_content = Ini::new(); + sync_content.load("output_sync.ini")?; + + let async_content = async_std::task::block_on::<_, Result<_, String>>(async { + let mut async_content = Ini::new(); + async_content.load_async("output_async.ini").await?; + Ok(async_content) + })?; + + assert_eq!(sync_content, async_content); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn multiline_off() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new_cs(); + config.load("tests/test_multiline.ini")?; + + let map = config.get_map_ref(); + + let section = map.get("Section").unwrap(); + + assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); + assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); + assert_eq!(config.get("Section", "Key3").unwrap(), "this is a haiku"); + assert!(section.contains_key("spread across separate lines")); + assert!(section.contains_key("a single value")); + + assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); + + assert_eq!( + config.writes(), + "[Section] +Key1=Value1 +Key2=Value Two +Key3=this is a haiku +spread across separate lines +a single value +Key4=Four +" + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "indexmap")] +fn multiline_on() -> Result<(), Box<dyn Error>> { + let mut config = Ini::new_cs(); + config.set_multiline(true); + config.load("tests/test_multiline.ini")?; + + assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); + assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); + assert_eq!( + config.get("Section", "Key3").unwrap(), + "this is a haiku\nspread across separate lines\na single value" + ); + assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); + + assert_eq!( + config.writes(), + "[Section] +Key1=Value1 +Key2=Value Two +Key3=this is a haiku + spread across separate lines + a single value +Key4=Four +" + ); + + Ok(()) +} diff --git a/tests/test_multiline.ini b/tests/test_multiline.ini new file mode 100644 index 0000000..d980e99 --- /dev/null +++ b/tests/test_multiline.ini @@ -0,0 +1,7 @@ +[Section] +Key1: Value1 +Key2: Value Two +Key3: this is a haiku + spread across separate lines + a single value +Key4: Four
\ No newline at end of file |