Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config access uses impl Key to allow type-safe keys #1236

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

bittrance
Copy link
Contributor

This PR changes the API of gix_config::File to use a trait (gix_config::Key) as key when requesting or setting configuration. This will allow gix to use its tree of structured and validating typed keys (gix::config::tree::sections::*) as keys directly, and also means that consumers of the gix crate to use type-safe config access. gix-config also provides an implementation for &str and &bstr so that constant strings can be used as keys. This is part of the work addressing #1125.

The PR is draft, as there remains various question marks, see specific comments in code below.

gix contains a lot of strings and format!() operations that could be changed to use type-safe keys, but that is left for a separate PR.

@bittrance bittrance changed the title Config access uses impl Key to allow structured keys Config access uses impl Key to allow type-safe keys Jan 5, 2024
Ok(section.set(key.try_into().map_err(section::key::Error::from)?, new_value.into()))
) -> Result<Option<Cow<'event, BStr>>, crate::file::set_raw_value::Error> {
let mut section = self.section_mut_or_create_new_filter(key.section_name(), key.subsection_name(), filter)?;
let key = key.name().to_owned().try_into().map_err(section::key::Error::from)?; // TODO Borrow, plz
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This to_owned() is not optimal, but it is not obvious to me how to formulate the required lifetimes.

match ((&input).section_name(), (&input).subsection_name()) {
(section, subsection) if !section.is_empty() => Filter {
name: section.into(),
subsection: subsection.map(ToOwned::to_owned),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exactly how the filter is used, so I'm not 100% sure this change retains the exact behavior of the filter in all corner cases.

key: impl AsRef<str>,
) -> Option<Cow<'_, BStr>> {
self.string_filter(section_name, subsection_name, key, &mut |_| true)
pub fn string<'a>(&self, key: impl Key<'a>) -> Option<Cow<'_, BStr>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rename these lifetimes to `'lookup'?

/// Parsed elements of a Git config key, like `remote.origin.url` or `core.bare`.
pub trait Key<'a> {
/// The name of the section key, like `url` in `remote.origin.url`.
fn name(&self) -> &'a str;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is name() to generic given that we implement it on &[b]str? .key_name() might be better?

.rev();
let key = section::Key(Cow::<BStr>::Borrowed(key.into()));
let key = section::Key(Cow::Borrowed(key.name().into())); // TODO Borrow, plz
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, this TODO is already addressed.

Copy link
Owner

@Byron Byron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing your amazing progress!

Before I can look at anything in detail, I'd need the commits split up so that…

  • …the Key related changes in gix-config are in one commit, whose subject is prefixed with fix!: or feat!: . Please write the subject and body so they can go into the changelog.
  • …all other changes to adapt to this change can go into its own commit without any special message requirements.

There is also this one big request of only modifying the string_by_key based access with the new trait. After all, the natural way of accessing the API is by passing triplets of section, subsection and 'key' (definitely open to a better term here).

That way, only string_by_key() has to change at all and that change would probably be compatible to most existing uses depending on the available implementations of Key for &BStr for example. I don't think there is a need to break the world here, and hope this could even work without making any breaking change.

Is that feasible at all?

@bittrance
Copy link
Contributor Author

…the Key related changes in gix-config are in one commit, whose subject is prefixed with fix!: or feat!: . Please write the subject and body so they can go into the changelog.

Is the purpose simply to have a feat! commit in the history? If so, I'd just split out adding the Key trait into a separate commit, no problem. The current commit looks like it does because I'm always striving for every commit to be viable (i.e. buildable and git-bisect:able) and still be meaningful.

There is also this one big request of only modifying the string_by_key based access with the new trait. After all, the natural way of accessing the API is by passing triplets of section, subsection and 'key' (definitely open to a better term here). That way, only string_by_key() has to change at all and that change would probably be compatible to most existing uses depending on the available implementations of Key for &BStr for example. I don't think there is a need to break the world here, and hope this could even work without making any breaking change.

I was thinking we should keep the API surface small by removing all the _by_key method so that only the impl Key versions remained (I prepared bittrance@c54bb3a for this purpose). My thought is that gix config API should encourage using type-safe access and I think config.string(format!("foo.{submod}.bar")) is more readable than config.string("foo", submod, "bar"), but I defer to your judgement on when it is worth making a breaking API change.

@Byron
Copy link
Owner

Byron commented Jan 5, 2024

Is the purpose simply to have a feat! commit in the history? If so, I'd just split out adding the Key trait into a separate commit, no problem. The current commit looks like it does because I'm always striving for every commit to be viable (i.e. buildable and git-bisect:able) and still be meaningful.

No, the purpose is to have a changelog that applies to the gix-config crate exclusively - otherwise cargo smart-release would think there are breaking changes in all of the crates whose code was adjusted, and it will claim new features in their changelogs, too.

That takes priority over each commit being green on CI, it's all trade-offs.

I was thinking we should keep the API surface small by removing all the _by_key method so that only the impl Key versions remained (I prepared bittrance@c54bb3a for this purpose). My thought is that gix config API should encourage using type-safe access and I think config.string(format!("foo.{submod}.bar")) is more readable than config.string("foo", submod, "bar"), but I defer to your judgement on when it is worth making a breaking API change.

Thanks for sharing your thoughts.

For a plumbing crate, it would already be more than fine. However, it's exposed in gix and thus it needs to provide more support for a nicer API, but that shouldn't supersede the 'API that is natural for the data', i.e. triplets as it's without overhead.

But since you would be touching all of these methods anyway, let's encourage the use of the potentially typesafe and nicer API by making their methods the shorter ones. This means, for example, to keep File::string(key: impl Key) as is, but have a longer method that takes the triplet, like File::string_by(section, subsection, key). The _by suffix is an example but something that seems to work nicely, and it seems better than my initial thought _by_triplet() which is punishingly long.
Please do feel free to experiment (and bikeshed) with the suffix though.
Finally, this also means that string() would call string_by().

I hope all that makes sense, please do let me know about any concern you might have as well.

@bittrance bittrance force-pushed the config-key-take-2 branch 2 times, most recently from f1d7e94 to 00f7c28 Compare January 30, 2024 18:35
gix-config/src/file/access/comfort.rs Outdated Show resolved Hide resolved
gix-config/src/file/access/comfort.rs Outdated Show resolved Hide resolved
gix-config/src/file/access/comfort.rs Outdated Show resolved Hide resolved
Copy link
Owner

@Byron Byron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the changes!

It's wonderful to see how the standard-API of gix-config can be enhanced by the Key trait. While looking at it more closely, I also noticed something else that I think can be fixed without too much overhead.

But if that work should ever appear overwhelming or boring because the API surface is quite massive, please let me know and I can help doing the refactoring. The last thing I'd want is for you to get exhausted due to all these comments of mine 😅.

Just let me know how I can help and I will try to make it happen.
Thanks again!


use crate::traits::Key;

impl Key for &String {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I see it, the way Key is designed requires some duplicate work. In order to make any call to one of the *_by(section, subsection, key) methods, one will have to do extra work in order to get the subsection name, even though what one is really doing is to split a string across . two or three times.

That operation, along with the potential for failure, are already captured in parse::key(). Thus Key would probably be better named ParseKey while providing only one method to obtain a parse::Key.

Doing this would probably also do away with the duplication in implementation, as it then all boils down to getting a &BStr for parsing a key.


assert_eq!("", "foo".section_name());
assert_eq!(None, "foo".subsection_name());
assert_eq!("foo", "foo".name());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my earlier comment, this is a good example on where the API of Key isn't currently suitable to capture what a Key is. Top-level keys like "foo" are not valid, they need a section name, and empty section names are not permitted (nor are empty keys).

Thus I think it's important to be able to express a parsing failure or 'invalid key' as part of the Key trait (or ParseKey trait as proposed).

@Byron
Copy link
Owner

Byron commented Apr 22, 2024

As a note, I plan to finish this PR myself once I get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants