Skip to content

Commit

Permalink
Merge pull request #5618 from epage/ext
Browse files Browse the repository at this point in the history
feat(builder): Add Extension API
  • Loading branch information
epage authored Aug 2, 2024
2 parents 5f99b06 + ddd3b0c commit e5195fd
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 186 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ string = ["clap_builder/string"] # Allow runtime generated strings

# In-work features
unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"]
unstable-styles = ["clap_builder/unstable-styles"]
unstable-ext = []
unstable-styles = ["clap_builder/unstable-styles"] # deprecated

[lib]
bench = false
Expand Down
3 changes: 2 additions & 1 deletion clap_builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ string = [] # Allow runtime generated strings

# In-work features
unstable-v5 = ["deprecated"]
unstable-styles = ["color"]
unstable-ext = []
unstable-styles = ["color"] # deprecated

[lib]
bench = false
Expand Down
44 changes: 39 additions & 5 deletions clap_builder/src/builder/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use std::{

// Internal
use super::{ArgFlags, ArgSettings};
#[cfg(feature = "unstable-ext")]
use crate::builder::ext::Extension;
use crate::builder::ext::Extensions;
use crate::builder::ArgPredicate;
use crate::builder::IntoResettable;
use crate::builder::OsStr;
Expand Down Expand Up @@ -85,7 +88,7 @@ pub struct Arg {
pub(crate) terminator: Option<Str>,
pub(crate) index: Option<usize>,
pub(crate) help_heading: Option<Option<Str>>,
pub(crate) value_hint: Option<ValueHint>,
pub(crate) ext: Extensions,
}

/// # Basic API
Expand Down Expand Up @@ -869,6 +872,18 @@ impl Arg {
self.settings.unset(setting);
self
}

/// Extend [`Arg`] with [`ArgExt`] data
#[cfg(feature = "unstable-ext")]
pub fn add<T: ArgExt + Extension>(&mut self, tagged: T) -> bool {
self.ext.set(tagged)
}

/// Remove an [`ArgExt`]
#[cfg(feature = "unstable-ext")]
pub fn remove<T: ArgExt + Extension>(&mut self) -> Option<T> {
self.ext.remove::<T>()
}
}

/// # Value Handling
Expand Down Expand Up @@ -1291,7 +1306,15 @@ impl Arg {
/// ```
#[must_use]
pub fn value_hint(mut self, value_hint: impl IntoResettable<ValueHint>) -> Self {
self.value_hint = value_hint.into_resettable().into_option();
// HACK: we should use `Self::add` and `Self::remove` to type-check that `ArgExt` is used
match value_hint.into_resettable().into_option() {
Some(value_hint) => {
self.ext.set(value_hint);
}
None => {
self.ext.remove::<ValueHint>();
}
}
self
}

Expand Down Expand Up @@ -4022,7 +4045,8 @@ impl Arg {

/// Get the value hint of this argument
pub fn get_value_hint(&self) -> ValueHint {
self.value_hint.unwrap_or_else(|| {
// HACK: we should use `Self::add` and `Self::remove` to type-check that `ArgExt` is used
self.ext.get::<ValueHint>().copied().unwrap_or_else(|| {
if self.is_takes_value_set() {
let type_id = self.get_value_parser().type_id();
if type_id == AnyValueId::of::<std::path::PathBuf>() {
Expand Down Expand Up @@ -4210,6 +4234,12 @@ impl Arg {
pub fn is_ignore_case_set(&self) -> bool {
self.is_set(ArgSettings::IgnoreCase)
}

/// Access an [`ArgExt`]
#[cfg(feature = "unstable-ext")]
pub fn get<T: ArgExt + Extension>(&self) -> Option<&T> {
self.ext.get::<T>()
}
}

/// # Internally used only
Expand Down Expand Up @@ -4484,8 +4514,8 @@ impl fmt::Debug for Arg {
.field("terminator", &self.terminator)
.field("index", &self.index)
.field("help_heading", &self.help_heading)
.field("value_hint", &self.value_hint)
.field("default_missing_vals", &self.default_missing_vals);
.field("default_missing_vals", &self.default_missing_vals)
.field("ext", &self.ext);

#[cfg(feature = "env")]
{
Expand All @@ -4496,6 +4526,10 @@ impl fmt::Debug for Arg {
}
}

/// User-provided data that can be attached to an [`Arg`]
#[cfg(feature = "unstable-ext")]
pub trait ArgExt: Extension {}

// Flags
#[cfg(test)]
mod test {
Expand Down
196 changes: 17 additions & 179 deletions clap_builder/src/builder/ext.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,36 @@
use crate::util::AnyValue;
use crate::util::AnyValueId;
use crate::util::FlatMap;

#[derive(Default, Clone, Debug)]
pub(crate) struct Extensions {
extensions: FlatMap<AnyValueId, BoxedExtension>,
extensions: FlatMap<AnyValueId, AnyValue>,
}

impl Extensions {
#[allow(dead_code)]
pub(crate) fn get<T: Extension>(&self) -> Option<&T> {
let id = AnyValueId::of::<T>();
self.extensions.get(&id).map(|e| e.as_ref::<T>())
self.extensions.get(&id).map(|e| {
e.downcast_ref::<T>()
.expect("`Extensions` tracks values by type")
})
}

#[allow(dead_code)]
pub(crate) fn get_mut<T: Extension>(&mut self) -> Option<&mut T> {
let id = AnyValueId::of::<T>();
self.extensions.get_mut(&id).map(|e| e.as_mut::<T>())
}

#[allow(dead_code)]
pub(crate) fn get_or_insert_default<T: Extension + Default>(&mut self) -> &mut T {
let id = AnyValueId::of::<T>();
self.extensions
.entry(id)
.or_insert_with(|| BoxedExtension::new(T::default()))
.as_mut::<T>()
}

#[allow(dead_code)]
pub(crate) fn set<T: Extension + Into<BoxedEntry>>(&mut self, tagged: T) -> bool {
let BoxedEntry { id, value } = tagged.into();
pub(crate) fn set<T: Extension>(&mut self, tagged: T) -> bool {
let value = AnyValue::new(tagged);
let id = value.type_id();
self.extensions.insert(id, value).is_some()
}

#[allow(dead_code)]
pub(crate) fn remove<T: Extension>(&mut self) -> Option<Box<dyn Extension>> {
pub(crate) fn remove<T: Extension>(&mut self) -> Option<T> {
let id = AnyValueId::of::<T>();
self.extensions.remove(&id).map(BoxedExtension::into_inner)
self.extensions.remove(&id).map(|e| {
e.downcast_into::<T>()
.expect("`Extensions` tracks values by type")
})
}

pub(crate) fn update(&mut self, other: &Self) {
Expand All @@ -47,162 +40,7 @@ impl Extensions {
}
}

/// Supports conversion to `Any`. Traits to be extended by `impl_downcast!` must extend `Extension`.
pub(crate) trait Extension: std::fmt::Debug + Send + Sync + 'static {
/// Clone `&Box<dyn Trait>` (where `Trait: Extension`) to `Box<dyn Extension>`.
///
/// `Box<dyn Any>` can /// then be further `downcast` into
// `Box<ConcreteType>` where `ConcreteType` implements `Trait`.
fn clone_extension(&self) -> Box<dyn Extension>;
/// Convert `&Trait` (where `Trait: Extension`) to `&Any`.
///
/// This is needed since Rust cannot /// generate `&Any`'s vtable from
/// `&Trait`'s.
fn as_any(&self) -> &dyn std::any::Any;
/// Convert `&mut Trait` (where `Trait: Extension`) to `&Any`.
///
/// This is needed since Rust cannot /// generate `&mut Any`'s vtable from
/// `&mut Trait`'s.
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}

impl<T> Extension for T
where
T: Clone + std::fmt::Debug + Send + Sync + 'static,
{
fn clone_extension(&self) -> Box<dyn Extension> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

impl Clone for Box<dyn Extension> {
fn clone(&self) -> Self {
self.as_ref().clone_extension()
}
}

#[derive(Clone)]
#[repr(transparent)]
struct BoxedExtension(Box<dyn Extension>);

impl BoxedExtension {
fn new<T: Extension>(inner: T) -> Self {
Self(Box::new(inner))
}

fn into_inner(self) -> Box<dyn Extension> {
self.0
}

fn as_ref<T: Extension>(&self) -> &T {
self.0.as_ref().as_any().downcast_ref::<T>().unwrap()
}

fn as_mut<T: Extension>(&mut self) -> &mut T {
self.0.as_mut().as_any_mut().downcast_mut::<T>().unwrap()
}
}

impl std::fmt::Debug for BoxedExtension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
#[allow(unreachable_pub)]
pub trait Extension: std::fmt::Debug + Clone + std::any::Any + Send + Sync + 'static {}

#[derive(Clone)]
pub(crate) struct BoxedEntry {
id: AnyValueId,
value: BoxedExtension,
}

impl BoxedEntry {
pub(crate) fn new(r: impl Extension) -> Self {
let id = AnyValueId::from(&r);
let value = BoxedExtension::new(r);
BoxedEntry { id, value }
}
}

impl<R: Extension> From<R> for BoxedEntry {
fn from(inner: R) -> Self {
BoxedEntry::new(inner)
}
}

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

#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
struct Number(usize);

#[test]
fn get() {
let mut ext = Extensions::default();
ext.set(Number(10));
assert_eq!(ext.get::<Number>(), Some(&Number(10)));
}

#[test]
fn get_mut() {
let mut ext = Extensions::default();
ext.set(Number(10));
*ext.get_mut::<Number>().unwrap() = Number(20);
assert_eq!(ext.get::<Number>(), Some(&Number(20)));
}

#[test]
fn get_or_insert_default_empty() {
let mut ext = Extensions::default();
assert_eq!(ext.get_or_insert_default::<Number>(), &Number(0));
}

#[test]
fn get_or_insert_default_full() {
let mut ext = Extensions::default();
ext.set(Number(10));
assert_eq!(ext.get_or_insert_default::<Number>(), &Number(10));
}

#[test]
fn set() {
let mut ext = Extensions::default();
assert!(!ext.set(Number(10)));
assert_eq!(ext.get::<Number>(), Some(&Number(10)));
assert!(ext.set(Number(20)));
assert_eq!(ext.get::<Number>(), Some(&Number(20)));
}

#[test]
fn reset() {
let mut ext = Extensions::default();
assert_eq!(ext.get::<Number>(), None);

assert!(ext.remove::<Number>().is_none());
assert_eq!(ext.get::<Number>(), None);

assert!(!ext.set(Number(10)));
assert_eq!(ext.get::<Number>(), Some(&Number(10)));

assert!(ext.remove::<Number>().is_some());
assert_eq!(ext.get::<Number>(), None);
}

#[test]
fn update() {
let mut ext = Extensions::default();
assert_eq!(ext.get::<Number>(), None);

let mut new = Extensions::default();
assert!(!new.set(Number(10)));

ext.update(&new);
assert_eq!(ext.get::<Number>(), Some(&Number(10)));
}
}
impl<T> Extension for T where T: std::fmt::Debug + Clone + std::any::Any + Send + Sync + 'static {}
2 changes: 2 additions & 0 deletions clap_builder/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub mod styling;
pub use self::str::Str;
pub use action::ArgAction;
pub use arg::Arg;
#[cfg(feature = "unstable-ext")]
pub use arg::ArgExt;
pub use arg_group::ArgGroup;
pub use arg_predicate::ArgPredicate;
pub use command::Command;
Expand Down
3 changes: 3 additions & 0 deletions clap_builder/src/builder/value_hint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ pub enum ValueHint {
EmailAddress,
}

#[cfg(feature = "unstable-ext")]
impl crate::builder::ArgExt for ValueHint {}

impl FromStr for ValueHint {
type Err = String;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
Expand Down

0 comments on commit e5195fd

Please sign in to comment.