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 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:

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

**`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:

```rust
let obj = Object::builder::<SimpleObject>()
    .property_if_not_empty::<ValueArray>("array", ["val0", "val1"])
    .build();
```
  • Loading branch information
fengalin committed Jun 14, 2024
1 parent 0a0d316 commit 014484b
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 4 deletions.
66 changes: 64 additions & 2 deletions glib/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1542,8 +1542,9 @@ impl<'a, O: IsA<Object> + IsClass> ObjectBuilder<'a, O> {
}

// rustdoc-stripper-ignore-next
/// Set property `name` to the given value `value`.
#[inline]
/// Sets property `name` to the given value `value`.
///
/// Overrides any default or previously defined value for `name`.
pub fn property(self, name: &'a str, value: impl Into<Value>) -> Self {
let ObjectBuilder {
type_,
Expand All @@ -1559,6 +1560,67 @@ impl<'a, O: IsA<Object> + IsClass> ObjectBuilder<'a, O> {
}
}

// 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 for `name` is kept.
#[inline]
pub fn property_if(self, name: &'a str, value: impl Into<Value>, predicate: bool) -> Self {
if predicate {
self.property(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 for `name` is kept.
#[inline]
pub fn property_if_some(self, name: &'a str, value: Option<impl Into<Value>>) -> Self {
if let Some(value) = value {
self.property(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 for `name`.
#[inline]
pub fn property_from_iter<V: ValueType + Into<Value> + FromIterator<Value>>(
self,
name: &'a str,
iter: impl IntoIterator<Item = impl Into<Value>>,
) -> Self {
let iter = iter.into_iter().map(|item| item.into());
self.property(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 for `name` is unchanged.
#[inline]
pub fn property_if_not_empty<V: ValueType + Into<Value> + FromIterator<Value>>(
self,
name: &'a str,
iter: impl IntoIterator<Item = impl Into<Value>>,
) -> Self {
let mut iter = iter.into_iter().peekable();
if iter.peek().is_some() {
let iter = iter.map(|item| item.into());
self.property(name, V::from_iter(iter))
} else {
self
}
}

// rustdoc-stripper-ignore-next
/// Build the object with the provided properties.
///
Expand Down
164 changes: 162 additions & 2 deletions glib/src/subclass/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,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 @@ -377,6 +390,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 @@ -437,6 +454,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 @@ -446,6 +477,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::new(self.array.borrow().iter()).to_value(),
_ => unimplemented!(),
}
}
Expand Down Expand Up @@ -523,11 +556,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 @@ -577,6 +612,131 @@ 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::new(["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::new(array), 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::new(array)))
.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

0 comments on commit 014484b

Please sign in to comment.