Skip to content

Commit 5fa9546

Browse files
committed
feat: comfort API like string_by_key(key) takes a key like "remote.origin.url", add section_by_key("remote.origin") as well.
That way it's the most comfortable way to query values and very similar to how git does it, too. Additionally, sections can be obtained by section key, both mutably and immutably for completeness.
1 parent 0c98ec8 commit 5fa9546

File tree

12 files changed

+216
-2
lines changed

12 files changed

+216
-2
lines changed

git-config/src/file/access/comfort.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ impl<'event> File<'event> {
1818
self.string_filter(section_name, subsection_name, key, &mut |_| true)
1919
}
2020

21+
/// Like [`string()`][File::string()], but suitable for statically known `key`s like `remote.origin.url`.
22+
pub fn string_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<Cow<'_, BStr>> {
23+
self.string_filter_by_key(key, &mut |_| true)
24+
}
25+
2126
/// Like [`string()`][File::string()], but the section containing the returned value must pass `filter` as well.
2227
pub fn string_filter(
2328
&self,
@@ -29,6 +34,17 @@ impl<'event> File<'event> {
2934
self.raw_value_filter(section_name, subsection_name, key, filter).ok()
3035
}
3136

37+
/// Like [`string_filter()`][File::string_filter()], but suitable for statically known `key`s like `remote.origin.url`.
38+
pub fn string_filter_by_key<'a>(
39+
&self,
40+
key: impl Into<&'a BStr>,
41+
filter: &mut MetadataFilter,
42+
) -> Option<Cow<'_, BStr>> {
43+
let key = crate::parse::key(key)?;
44+
self.raw_value_filter(key.section_name, key.subsection_name, key.value_name, filter)
45+
.ok()
46+
}
47+
3248
/// Like [`value()`][File::value()], but returning `None` if the path wasn't found.
3349
///
3450
/// Note that this path is not vetted and should only point to resources which can't be used
@@ -44,6 +60,11 @@ impl<'event> File<'event> {
4460
self.path_filter(section_name, subsection_name, key, &mut |_| true)
4561
}
4662

63+
/// Like [`path()`][File::path()], but suitable for statically known `key`s like `remote.origin.url`.
64+
pub fn path_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<crate::Path<'_>> {
65+
self.path_filter_by_key(key, &mut |_| true)
66+
}
67+
4768
/// Like [`path()`][File::path()], but the section containing the returned value must pass `filter` as well.
4869
///
4970
/// This should be the preferred way of accessing paths as those from untrusted
@@ -62,6 +83,16 @@ impl<'event> File<'event> {
6283
.map(crate::Path::from)
6384
}
6485

86+
/// Like [`path_filter()`][File::path_filter()], but suitable for statically known `key`s like `remote.origin.url`.
87+
pub fn path_filter_by_key<'a>(
88+
&self,
89+
key: impl Into<&'a BStr>,
90+
filter: &mut MetadataFilter,
91+
) -> Option<crate::Path<'_>> {
92+
let key = crate::parse::key(key)?;
93+
self.path_filter(key.section_name, key.subsection_name, key.value_name, filter)
94+
}
95+
6596
/// Like [`value()`][File::value()], but returning `None` if the boolean value wasn't found.
6697
pub fn boolean(
6798
&self,
@@ -72,6 +103,11 @@ impl<'event> File<'event> {
72103
self.boolean_filter(section_name, subsection_name, key, &mut |_| true)
73104
}
74105

106+
/// Like [`boolean()`][File::boolean()], but suitable for statically known `key`s like `remote.origin.url`.
107+
pub fn boolean_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<Result<bool, value::Error>> {
108+
self.boolean_filter_by_key(key, &mut |_| true)
109+
}
110+
75111
/// Like [`boolean()`][File::boolean()], but the section containing the returned value must pass `filter` as well.
76112
pub fn boolean_filter(
77113
&self,
@@ -99,6 +135,16 @@ impl<'event> File<'event> {
99135
None
100136
}
101137

138+
/// Like [`boolean_filter()`][File::boolean_filter()], but suitable for statically known `key`s like `remote.origin.url`.
139+
pub fn boolean_filter_by_key<'a>(
140+
&self,
141+
key: impl Into<&'a BStr>,
142+
filter: &mut MetadataFilter,
143+
) -> Option<Result<bool, value::Error>> {
144+
let key = crate::parse::key(key)?;
145+
self.boolean_filter(key.section_name, key.subsection_name, key.value_name, filter)
146+
}
147+
102148
/// Like [`value()`][File::value()], but returning an `Option` if the integer wasn't found.
103149
pub fn integer(
104150
&self,
@@ -109,6 +155,11 @@ impl<'event> File<'event> {
109155
self.integer_filter(section_name, subsection_name, key, &mut |_| true)
110156
}
111157

158+
/// Like [`integer()`][File::integer()], but suitable for statically known `key`s like `remote.origin.url`.
159+
pub fn integer_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<Result<i64, value::Error>> {
160+
self.integer_filter_by_key(key, &mut |_| true)
161+
}
162+
112163
/// Like [`integer()`][File::integer()], but the section containing the returned value must pass `filter` as well.
113164
pub fn integer_filter(
114165
&self,
@@ -124,6 +175,16 @@ impl<'event> File<'event> {
124175
}))
125176
}
126177

178+
/// Like [`integer_filter()`][File::integer_filter()], but suitable for statically known `key`s like `remote.origin.url`.
179+
pub fn integer_filter_by_key<'a>(
180+
&self,
181+
key: impl Into<&'a BStr>,
182+
filter: &mut MetadataFilter,
183+
) -> Option<Result<i64, value::Error>> {
184+
let key = crate::parse::key(key)?;
185+
self.integer_filter(key.section_name, key.subsection_name, key.value_name, filter)
186+
}
187+
127188
/// Similar to [`values(…)`][File::values()] but returning strings if at least one of them was found.
128189
pub fn strings(
129190
&self,
@@ -134,6 +195,12 @@ impl<'event> File<'event> {
134195
self.raw_values(section_name, subsection_name, key).ok()
135196
}
136197

198+
/// Like [`strings()`][File::strings()], but suitable for statically known `key`s like `remote.origin.url`.
199+
pub fn strings_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<Vec<Cow<'_, BStr>>> {
200+
let key = crate::parse::key(key)?;
201+
self.strings(key.section_name, key.subsection_name, key.value_name)
202+
}
203+
137204
/// Similar to [`strings(…)`][File::strings()], but all values are in sections that passed `filter`.
138205
pub fn strings_filter(
139206
&self,
@@ -145,6 +212,16 @@ impl<'event> File<'event> {
145212
self.raw_values_filter(section_name, subsection_name, key, filter).ok()
146213
}
147214

215+
/// Like [`strings_filter()`][File::strings_filter()], but suitable for statically known `key`s like `remote.origin.url`.
216+
pub fn strings_filter_by_key<'a>(
217+
&self,
218+
key: impl Into<&'a BStr>,
219+
filter: &mut MetadataFilter,
220+
) -> Option<Vec<Cow<'_, BStr>>> {
221+
let key = crate::parse::key(key)?;
222+
self.strings_filter(key.section_name, key.subsection_name, key.value_name, filter)
223+
}
224+
148225
/// Similar to [`values(…)`][File::values()] but returning integers if at least one of them was found
149226
/// and if none of them overflows.
150227
pub fn integers(
@@ -156,6 +233,11 @@ impl<'event> File<'event> {
156233
self.integers_filter(section_name, subsection_name, key, &mut |_| true)
157234
}
158235

236+
/// Like [`integers()`][File::integers()], but suitable for statically known `key`s like `remote.origin.url`.
237+
pub fn integers_by_key<'a>(&self, key: impl Into<&'a BStr>) -> Option<Result<Vec<i64>, value::Error>> {
238+
self.integers_filter_by_key(key, &mut |_| true)
239+
}
240+
159241
/// Similar to [`integers(…)`][File::integers()] but all integers are in sections that passed `filter`
160242
/// and that are not overflowing.
161243
pub fn integers_filter(
@@ -179,4 +261,14 @@ impl<'event> File<'event> {
179261
.collect()
180262
})
181263
}
264+
265+
/// Like [`integers_filter()`][File::integers_filter()], but suitable for statically known `key`s like `remote.origin.url`.
266+
pub fn integers_filter_by_key<'a>(
267+
&self,
268+
key: impl Into<&'a BStr>,
269+
filter: &mut MetadataFilter,
270+
) -> Option<Result<Vec<i64>, value::Error>> {
271+
let key = crate::parse::key(key)?;
272+
self.integers_filter(key.section_name, key.subsection_name, key.value_name, filter)
273+
}
182274
}

git-config/src/file/access/mutate.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ impl<'event> File<'event> {
3131
.to_mut(nl))
3232
}
3333

34+
/// Returns the last found mutable section with a given `key`, identifying the name and subsection name like `core` or `remote.origin`.
35+
pub fn section_mut_by_key<'a, 'b>(
36+
&'a mut self,
37+
key: impl Into<&'b BStr>,
38+
) -> Result<SectionMut<'a, 'event>, lookup::existing::Error> {
39+
let key = section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
40+
self.section_mut(key.section_name, key.subsection_name)
41+
}
42+
3443
/// Return the mutable section identified by `id`, or `None` if it didn't exist.
3544
///
3645
/// Note that `id` is stable across deletions and insertions.
@@ -99,6 +108,17 @@ impl<'event> File<'event> {
99108
Ok(id.and_then(move |id| self.sections.get_mut(&id).map(move |s| s.to_mut(nl))))
100109
}
101110

111+
/// Like [`section_mut_filter()`][File::section_mut_filter()], but identifies the with a given `key`,
112+
/// like `core` or `remote.origin`.
113+
pub fn section_mut_filter_by_key<'a, 'b>(
114+
&'a mut self,
115+
key: impl Into<&'b BStr>,
116+
filter: &mut MetadataFilter,
117+
) -> Result<Option<file::SectionMut<'a, 'event>>, lookup::existing::Error> {
118+
let key = section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
119+
self.section_mut_filter(key.section_name, key.subsection_name, filter)
120+
}
121+
102122
/// Adds a new section. If a subsection name was provided, then
103123
/// the generated header will use the modern subsection syntax.
104124
/// Returns a reference to the new section for immediate editing.

git-config/src/file/access/read_only.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ impl<'event> File<'event> {
139139
.expect("section present as we take all"))
140140
}
141141

142+
/// Returns the last found immutable section with a given `key`, identifying the name and subsection name like `core`
143+
/// or `remote.origin`.
144+
pub fn section_by_key<'a>(
145+
&self,
146+
key: impl Into<&'a BStr>,
147+
) -> Result<&file::Section<'event>, lookup::existing::Error> {
148+
let key = crate::parse::section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
149+
self.section(key.section_name, key.subsection_name)
150+
}
151+
142152
/// Returns the last found immutable section with a given `name` and optional `subsection_name`, that matches `filter`.
143153
///
144154
/// If there are sections matching `section_name` and `subsection_name` but the `filter` rejects all of them, `Ok(None)`
@@ -161,6 +171,16 @@ impl<'event> File<'event> {
161171
}))
162172
}
163173

174+
/// Like [`section_filter()`][File::section_filter()], but identifies the section with `key` like `core` or `remote.origin`.
175+
pub fn section_filter_by_key<'a, 'b>(
176+
&'a self,
177+
key: impl Into<&'b BStr>,
178+
filter: &mut MetadataFilter,
179+
) -> Result<Option<&'a file::Section<'event>>, lookup::existing::Error> {
180+
let key = crate::parse::section::unvalidated::Key::parse(key).ok_or(lookup::existing::Error::KeyMissing)?;
181+
self.section_filter(key.section_name, key.subsection_name, filter)
182+
}
183+
164184
/// Gets all sections that match the provided `name`, ignoring any subsections.
165185
///
166186
/// # Examples

git-config/src/parse/section/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::parse::{Event, Section};
88
///
99
pub mod header;
1010

11+
pub(crate) mod unvalidated;
12+
1113
/// A container for events, avoiding heap allocations in typical files.
1214
pub type Events<'a> = SmallVec<[Event<'a>; 64]>;
1315

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use bstr::{BStr, ByteSlice};
2+
3+
/// An unvalidated parse result of a key for a section, parsing input like `remote.origin` or `core`.
4+
#[derive(Debug, PartialEq, Ord, PartialOrd, Eq, Hash, Clone, Copy)]
5+
pub struct Key<'a> {
6+
/// The name of the section, like `remote` in `remote.origin`.
7+
pub section_name: &'a str,
8+
/// The name of the sub-section, like `origin` in `remote.origin`.
9+
pub subsection_name: Option<&'a BStr>,
10+
}
11+
12+
impl<'a> Key<'a> {
13+
/// Parse `input` like `remote.origin` or `core` as a `Key` to make its section specific fields available,
14+
/// or `None` if there were not one or two tokens separated by `.`.
15+
/// Note that `input` isn't validated, and is `str` as ascii is a subset of UTF-8 which is required for any valid keys.
16+
pub fn parse(input: impl Into<&'a BStr>) -> Option<Self> {
17+
let input = input.into();
18+
let mut tokens = input.splitn(2, |b| *b == b'.');
19+
20+
Some(Key {
21+
section_name: tokens.next()?.to_str().ok()?,
22+
subsection_name: tokens.next().map(Into::into),
23+
})
24+
}
25+
}

git-config/src/parse/tests.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
11
mod section {
22

33
mod header {
4+
mod unvalidated {
5+
use crate::parse::section::unvalidated::Key;
6+
7+
#[test]
8+
fn section_name_only() {
9+
assert_eq!(
10+
Key::parse("core").unwrap(),
11+
Key {
12+
section_name: "core",
13+
subsection_name: None
14+
}
15+
);
16+
}
17+
18+
#[test]
19+
fn section_name_and_subsection() {
20+
assert_eq!(
21+
Key::parse("core.bare").unwrap(),
22+
Key {
23+
section_name: "core",
24+
subsection_name: Some("bare".into())
25+
}
26+
);
27+
}
28+
29+
#[test]
30+
fn section_name_and_subsection_with_separators() {
31+
assert_eq!(
32+
Key::parse("remote.https:///home/user.git").unwrap(),
33+
Key {
34+
section_name: "remote",
35+
subsection_name: Some("https:///home/user.git".into())
36+
}
37+
);
38+
}
39+
}
40+
441
mod write_to {
542
use std::borrow::Cow;
643

git-config/tests/file/access/read_only.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn get_value_for_all_provided_values() -> crate::Result {
3939

4040
assert!(!config.value::<Boolean>("core", None, "bool-explicit")?.0);
4141
assert!(!config.boolean("core", None, "bool-explicit").expect("exists")?);
42+
assert!(!config.boolean_by_key("core.bool-explicit").expect("exists")?);
4243

4344
assert!(
4445
config.value::<Boolean>("core", None, "bool-implicit").is_err(),
@@ -58,6 +59,10 @@ fn get_value_for_all_provided_values() -> crate::Result {
5859
&[cow_str("")],
5960
"unset values show up as empty within a string array"
6061
);
62+
assert_eq!(
63+
config.strings_by_key("core.bool-implicit").expect("present"),
64+
&[cow_str("")],
65+
);
6166

6267
assert_eq!(config.string("doesnt", None, "exist"), None);
6368

@@ -145,6 +150,8 @@ fn get_value_for_all_provided_values() -> crate::Result {
145150

146151
let actual = config.path("core", None, "location").expect("present");
147152
assert_eq!(&*actual, "~/tmp");
153+
let actual = config.path_by_key("core.location").expect("present");
154+
assert_eq!(&*actual, "~/tmp");
148155

149156
let actual = config.path("core", None, "location-quoted").expect("present");
150157
assert_eq!(&*actual, "~/quoted");

git-config/tests/file/init/comfort.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ fn from_git_dir() -> crate::Result {
4343
"value",
4444
"a value from the local repo configuration"
4545
);
46+
assert_eq!(config.string_by_key("a.local").expect("present").as_ref(), "value",);
4647
assert_eq!(
4748
config.string("a", None, "local-include").expect("present").as_ref(),
4849
"from-a.config",

git-config/tests/file/init/from_env.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn single_key_value_pair() -> crate::Result {
4444
let config = File::from_env(Default::default())?.unwrap();
4545
assert_eq!(config.raw_value("core", None, "key")?, Cow::<[u8]>::Borrowed(b"value"));
4646
assert_eq!(
47-
config.section("core", None)?.meta(),
47+
config.section_by_key("core")?.meta(),
4848
&git_config::file::Metadata::from(git_config::Source::Env),
4949
"source if configured correctly"
5050
);

git-config/tests/file/init/from_paths/includes/unconditional.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ fn respect_max_depth() -> crate::Result {
122122
let config =
123123
File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), follow_options())?.expect("non-empty");
124124
assert_eq!(config.integers("core", None, "i"), Some(Ok(vec![0, 1, 2, 3, 4])));
125+
assert_eq!(config.integers_by_key("core.i"), Some(Ok(vec![0, 1, 2, 3, 4])));
125126

126127
fn make_options(max_depth: u8, error_on_max_depth_exceeded: bool) -> init::Options<'static> {
127128
init::Options {
@@ -139,6 +140,7 @@ fn respect_max_depth() -> crate::Result {
139140
let options = make_options(1, false);
140141
let config = File::from_paths_metadata(into_meta(vec![dir.path().join("0")]), options)?.expect("non-empty");
141142
assert_eq!(config.integer("core", None, "i"), Some(Ok(1)));
143+
assert_eq!(config.integer_by_key("core.i"), Some(Ok(1)));
142144

143145
// with default max_allowed_depth of 10 and 4 levels of includes, last level is read
144146
let options = init::Options {

0 commit comments

Comments
 (0)