Skip to content

Commit

Permalink
ObjectBuilder: add property_if(), property_if_some(), property_from_i…
Browse files Browse the repository at this point in the history
…ter() ...

... & property_if_not_empty()

This commit adds an `ObjectBuilderPropertySetter` `trait` which implements
functions to improve usability when setting properties.

**`property_if()`** allows setting a property from an `Option` only if a given
`predicate` evaluates to `true`. Otherwise, the `Object`'s default value or
any previously set value is kept.

**`property_if_some()`** allows setting a property from an `Option` only if the
`Option` is `Some`. Otherwise, the `Object`'s default value or any previously
set value is kept.

For instance, assuming an `Args` `struct` which was built from the application's
CLI arguments:

```rust
let args = Args {
    name: Some("test"),
    anwser: None,
};
```

... without this change, we need to write something like this:

```rust
let mut builder = Object::builder::<SimpleObject>();

if let Some(ref name) = args.name {
    builder = builder.property("name", name)
}

if let Some(ref answer) = args.answer {
    builder = builder.property("answer", &args.answer)
}

let obj = builder.build();
```

With `property_if_some()`, we can write:

```rust
let obj = Object::builder::<SimpleObject>()
    .property_if_some("name", &args.name)
    .property_if_some("answer", &args.answer)
    .build();
```

**`property_from_iter()`** allows building a collection-like `Value` property
from a collection of items. In GStreamer this allows things like:

```rust
let src = gst::ElementFactory::make("webrtcsrc")
    .property_from_iter::<gst::Array>("audio-codecs", ["L24", "OPUS"])
    .build()
    .unwrap();
```

**`property_if_not_empty()`** allows building a collection-like `Value` property
from a collection of items but does nothing if the provided collection if empty,
thus keeping the default value of the property, if any. In GStreamer this allows
things like:

```let src = gst::ElementFactory::make("webrtcsrc")
    .property_if_not_empty::<gst::Array>("audio-codecs", &args.audio_codecs)
    .build()
    .unwrap();
```
  • Loading branch information
fengalin committed Apr 30, 2024
1 parent 4bc38db commit 23be9af
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 21 deletions.
103 changes: 86 additions & 17 deletions glib/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
thread_guard::thread_id,
translate::*,
value::FromValue,
Closure, PtrSlice, RustClosure, SignalHandlerId, Type, Value,
Closure, PtrSlice, RustClosure, SendValue, SignalHandlerId, Type, Value,
};

// rustdoc-stripper-ignore-next
Expand Down Expand Up @@ -1525,9 +1525,92 @@ impl<'a, O: IsA<Object> + IsClass> ObjectBuilder<'a, O> {
}

// rustdoc-stripper-ignore-next
/// Set property `name` to the given value `value`.
/// Build the object with the provided properties.
///
/// # Panics
///
/// This panics if the object is not instantiable, doesn't have all the given properties or
/// property values of the wrong type are provided.
#[track_caller]
#[inline]
pub fn build(mut self) -> O {
let object = Object::with_mut_values(self.type_, &mut self.properties);
unsafe { object.unsafe_cast::<O>() }
}
}

pub trait ObjectBuilderPropertySetter<'a>: Sized {
// rustdoc-stripper-ignore-next
/// Sets property `name` to the given value `value`.
///
/// Overrides any default or previously defined value.
fn property(self, name: &'a str, value: impl Into<Value>) -> Self;

// rustdoc-stripper-ignore-next
/// Sets property `name` to the given inner value if the `predicate` evaluates to `true`.
///
/// This has no effect if the `predicate` evaluates to `false`,
/// i.e. default or previous value is kept.
#[inline]
fn property_if(self, name: &'a str, value: impl Into<Value>, predicate: bool) -> Self {
if predicate {
ObjectBuilderPropertySetter::property(self, name, value)
} else {
self
}
}

// rustdoc-stripper-ignore-next
/// Sets property `name` to the given inner value if `value` is `Some`.
///
/// This has no effect if the value is `None`, i.e. default or previous value is kept.
#[inline]
fn property_if_some(self, name: &'a str, value: Option<impl Into<Value>>) -> Self {
if let Some(value) = value {
ObjectBuilderPropertySetter::property(self, name, value)
} else {
self
}
}

// rustdoc-stripper-ignore-next
/// Sets property `name` using the given `ValueType` `V` built from `iter`'s the `Item`s.
///
/// Overrides any default or previously defined value.
#[inline]
fn property_from_iter<V: ValueType + Into<Value> + FromIterator<SendValue>>(
self,
name: &'a str,
iter: impl IntoIterator<Item = impl Into<SendValue>>,
) -> Self {
let iter = iter.into_iter().map(|item| item.into());
ObjectBuilderPropertySetter::property(self, name, V::from_iter(iter))
}

// rustdoc-stripper-ignore-next
/// Sets property `name` using the given `ValueType` `V` built from `iter`'s Item`s,
/// if `iter` is not empty.
///
/// This has no effect if `iter` is empty, i.e. previous property value is unchanged.
#[inline]
pub fn property(self, name: &'a str, value: impl Into<Value>) -> Self {
fn property_if_not_empty<V: ValueType + Into<Value> + FromIterator<SendValue>>(
self,
name: &'a str,
iter: impl IntoIterator<Item = impl Into<SendValue>>,
) -> Self {
let mut iter = iter.into_iter().peekable();
if iter.peek().is_some() {
let iter = iter.map(|item| item.into());
ObjectBuilderPropertySetter::property(self, name, V::from_iter(iter))
} else {
self
}
}
}

impl<'a, O: IsA<Object> + IsClass> ObjectBuilderPropertySetter<'a> for ObjectBuilder<'a, O> {
#[inline]
fn property(self, name: &'a str, value: impl Into<Value>) -> Self {
let ObjectBuilder {
type_,
mut properties,
Expand All @@ -1541,20 +1624,6 @@ impl<'a, O: IsA<Object> + IsClass> ObjectBuilder<'a, O> {
phantom: PhantomData,
}
}

// rustdoc-stripper-ignore-next
/// Build the object with the provided properties.
///
/// # Panics
///
/// This panics if the object is not instantiable, doesn't have all the given properties or
/// property values of the wrong type are provided.
#[track_caller]
#[inline]
pub fn build(mut self) -> O {
let object = Object::with_mut_values(self.type_, &mut self.properties);
unsafe { object.unsafe_cast::<O>() }
}
}

#[must_use = "if unused the property notifications will immediately be thawed"]
Expand Down
4 changes: 3 additions & 1 deletion glib/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
pub use crate::{
error::ErrorDomain,
gobject::traits::{DynamicObjectRegisterExt, TypeModuleExt, TypePluginExt},
object::{Cast, CastNone, IsA, ObjectClassExt, ObjectExt, ObjectType},
object::{
Cast, CastNone, IsA, ObjectBuilderPropertySetter, ObjectClassExt, ObjectExt, ObjectType,
},
param_spec::{HasParamSpec, ParamSpecBuilderExt, ParamSpecType},
types::{StaticType, StaticTypeExt},
value::{ToSendValue, ToValue, ValueType},
Expand Down
172 changes: 170 additions & 2 deletions glib/src/subclass/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,24 @@ mod test {

impl ObjectImpl for ChildObject {}

#[derive(Default)]
pub struct SimpleObject {
name: RefCell<Option<String>>,
construct_name: RefCell<Option<String>>,
constructed: RefCell<bool>,
answer: RefCell<i32>,
array: RefCell<Vec<String>>,
}

impl Default for SimpleObject {
fn default() -> Self {
SimpleObject {
name: Default::default(),
construct_name: Default::default(),
constructed: Default::default(),
answer: RefCell::new(42i32),
array: RefCell::new(vec!["default0".to_string(), "default1".to_string()]),
}
}
}

#[glib::object_subclass]
Expand All @@ -372,6 +385,10 @@ mod test {
.read_only()
.build(),
crate::ParamSpecObject::builder::<super::ChildObject>("child").build(),
crate::ParamSpecInt::builder("answer")
.default_value(42i32)
.build(),
crate::ParamSpecValueArray::builder("array").build(),
]
})
}
Expand Down Expand Up @@ -432,6 +449,20 @@ mod test {
"child" => {
// not stored, only used to test `set_property` with `Objects`
}
"answer" => {
let answer = value
.get()
.expect("type conformity checked by 'Object::set_property'");
self.answer.replace(answer);
}
"array" => {
let value = value
.get::<crate::ValueArray>()
.expect("type conformity checked by 'Object::set_property'");
let mut array = self.array.borrow_mut();
array.clear();
array.extend(value.iter().map(|v| v.get().unwrap()));
}
_ => unimplemented!(),
}
}
Expand All @@ -441,6 +472,8 @@ mod test {
"name" => self.name.borrow().to_value(),
"construct-name" => self.construct_name.borrow().to_value(),
"constructed" => self.constructed.borrow().to_value(),
"answer" => self.answer.borrow().to_value(),
"array" => crate::ValueArray::from_items(&*self.array.borrow()).to_value(),
_ => unimplemented!(),
}
}
Expand Down Expand Up @@ -511,11 +544,13 @@ mod test {
assert!(obj.type_().is_a(Dummy::static_type()));

let properties = obj.list_properties();
assert_eq!(properties.len(), 4);
assert_eq!(properties.len(), 6);
assert_eq!(properties[0].name(), "name");
assert_eq!(properties[1].name(), "construct-name");
assert_eq!(properties[2].name(), "constructed");
assert_eq!(properties[3].name(), "child");
assert_eq!(properties[4].name(), "answer");
assert_eq!(properties[5].name(), "array");
}

#[test]
Expand Down Expand Up @@ -565,6 +600,139 @@ mod test {
obj.set_property("child", &child);
}

#[test]
fn builder_property_if() {
use crate::ValueArray;

let array = ["val0", "val1"];
let obj = Object::builder::<SimpleObject>()
.property_if("name", "some name", true)
.property_if("answer", 21i32, 6 != 9)
.property_if(
"array",
ValueArray::from_items(["val0", "val1"]),
array.len() == 2,
)
.build();

assert_eq!(obj.property::<String>("name").as_str(), "some name");
assert_eq!(
obj.property::<Option<String>>("name").as_deref(),
Some("some name")
);
assert_eq!(obj.property::<i32>("answer"), 21);
assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(array));

let obj = Object::builder::<SimpleObject>()
.property_if("name", "some name", false)
.property_if("answer", 21i32, 6 == 9)
.property_if(
"array",
ValueArray::from_items(["val0", "val1"]),
array.len() == 4,
)
.build();

assert!(obj.property::<Option<String>>("name").is_none());
assert_eq!(obj.property::<i32>("answer"), 42);
assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(["default0", "default1"]));
}

#[test]
fn builder_property_if_some() {
use crate::ValueArray;

let array = ["val0", "val1"];
let obj = Object::builder::<SimpleObject>()
.property_if_some("name", Some("some name"))
.property_if_some("answer", Some(21i32))
.property_if_some("array", Some(ValueArray::from_items(["val0", "val1"])))
.build();

assert_eq!(obj.property::<String>("name").as_str(), "some name");
assert_eq!(
obj.property::<Option<String>>("name").as_deref(),
Some("some name")
);
assert_eq!(obj.property::<i32>("answer"), 21);
assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(array));

let obj = Object::builder::<SimpleObject>()
.property_if_some("name", Option::<&str>::None)
.property_if_some("answer", Option::<i32>::None)
.property_if_some("array", Option::<ValueArray>::None)
.build();

assert!(obj.property::<Option<String>>("name").is_none());
assert_eq!(obj.property::<i32>("answer"), 42);
assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(["default0", "default1"]));
}

#[test]
fn builder_property_from_iter() {
use crate::ValueArray;

let array = ["val0", "val1"];
let obj = Object::builder::<SimpleObject>()
.property_from_iter::<ValueArray>("array", &array)
.build();

assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(array));

let obj = Object::builder::<SimpleObject>()
.property_from_iter::<ValueArray>("array", Vec::<&str>::new())
.build();

assert!(obj.property::<ValueArray>("array").is_empty());
}

#[test]
fn builder_property_if_not_empty() {
use crate::ValueArray;

let array = ["val0", "val1"];
let obj = Object::builder::<SimpleObject>()
.property_if_not_empty::<ValueArray>("array", &array)
.build();

assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(array));

let empty_vec = Vec::<String>::new();
let obj = Object::builder::<SimpleObject>()
.property_if_not_empty::<ValueArray>("array", &empty_vec)
.build();

assert!(obj
.property::<ValueArray>("array")
.iter()
.map(|val| val.get::<&str>().unwrap())
.eq(["default0", "default1"]));
}

#[test]
#[should_panic = "property 'construct-name' of type 'SimpleObject' is not writable"]
fn test_set_property_non_writable() {
Expand Down
Loading

0 comments on commit 23be9af

Please sign in to comment.