diff --git a/lib/wp/meson.build b/lib/wp/meson.build
index 53351e1201fd163aac046710048c06afac9185e8..7de804a27f2ff6eb1be4afc1232235aa400efeca 100644
--- a/lib/wp/meson.build
+++ b/lib/wp/meson.build
@@ -14,6 +14,7 @@ wp_lib_sources = files(
   'link.c',
   'module.c',
   'node.c',
+  'object-interest.c',
   'object-manager.c',
   'policy.c',
   'port.c',
@@ -46,6 +47,7 @@ wp_lib_headers = files(
   'link.h',
   'module.h',
   'node.h',
+  'object-interest.h',
   'object-manager.h',
   'policy.h',
   'port.h',
diff --git a/lib/wp/object-interest.c b/lib/wp/object-interest.c
new file mode 100644
index 0000000000000000000000000000000000000000..e8fee86d824aff45f8451a31d68171e6ed86218c
--- /dev/null
+++ b/lib/wp/object-interest.c
@@ -0,0 +1,830 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * SECTION: object-interest
+ * @title: Object Interest
+ */
+
+#define G_LOG_DOMAIN "wp-object-interest"
+
+#include "object-interest.h"
+#include "debug.h"
+#include "error.h"
+#include "private.h"
+
+struct constraint
+{
+  WpConstraintType type;
+  WpConstraintVerb verb;
+  gchar subject_type; /* a basic GVariantType as a single char */
+  gchar *subject;
+  GVariant *value;
+};
+
+struct _WpObjectInterest
+{
+  gboolean valid;
+  GType gtype;
+  struct pw_array constraints;
+};
+
+/**
+ * WpObjectInterest:
+ * An object interest is a helper that is used in #WpObjectManager to
+ * declare interest in certain kinds of objects.
+ *
+ * An interest is defined by a #GType and a set of constraints on the object's
+ * properties. An object "matches" the interest if it is of the specified
+ * #GType (either the same type or a descendant of it) and all the constraints
+ * are satisfied.
+ */
+G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
+                     wp_object_interest_copy, wp_object_interest_free)
+
+/**
+ * wp_object_interest_new:
+ * @gtype: the type of the object to declare interest in
+ * @...: a set of constraints, terminated with %NULL
+ *
+ * Creates a new interest that declares interest in objects of the specified
+ * @gtype, with the constraints specified in the variable arguments.
+ *
+ * The variable arguments should be a list of constraints terminated with %NULL,
+ * where each constraint consists of the following arguments:
+ *  - a #WpConstraintType: the constraint type
+ *  - a `const gchar *`: the subject name
+ *  - a `const gchar *`: the format string
+ *  - 0 or more arguments according to the format string
+ *
+ * The format string is interpreted as follows:
+ *  - the first character is the constraint verb:
+ *     - `=`: %WP_CONSTRAINT_VERB_EQUALS
+ *     - `c`: %WP_CONSTRAINT_VERB_IN_LIST
+ *     - `~`: %WP_CONSTRAINT_VERB_IN_RANGE
+ *     - `#`: %WP_CONSTRAINT_VERB_MATCHES
+ *     - `+`: %WP_CONSTRAINT_VERB_IS_PRESENT
+ *     - `-`: %WP_CONSTRAINT_VERB_IS_ABSENT
+ *  - the rest of the characters are interpreted as a #GVariant format string,
+ *    as it would be used in g_variant_new()
+ *
+ * The rest of this function's arguments up to the start of the next constraint
+ * depend on the #GVariant format part of the format string and are used to
+ * construct a #GVariant for the constraint's value argument.
+ *
+ * For further reading on the constraint's arguments, see
+ * wp_object_interest_add_constraint()
+ *
+ * For example, this interest matches objects that are descendands of #WpProxy
+ * with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
+ * called "format.dsp" that contains the string "audio" somewhere in the value
+ * and with a pipewire property "port.name" being present (with any value):
+ * |[
+ * interest = wp_object_interest_new (WP_TYPE_PROXY,
+ *     WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "~(uu)", 0, 100,
+ *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "format.dsp", "#s", "*audio*",
+ *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.name", "+",
+ *     NULL);
+ * ]|
+ *
+ * Returns: (transfer full): the new object interest
+ */
+WpObjectInterest *
+wp_object_interest_new (GType gtype, ...)
+{
+  WpObjectInterest *self;
+  va_list args;
+  va_start (args, gtype);
+  self = wp_object_interest_new_valist (gtype, &args);
+  va_end (args);
+  return self;
+}
+
+/**
+ * wp_object_interest_new_valist:
+ * @gtype: the type of the object to declare interest in
+ * @args: pointer to va_list containing the constraints
+ *
+ * va_list version of wp_object_interest_new()
+ *
+ * Returns: (transfer full): the new object interest
+ */
+WpObjectInterest *
+wp_object_interest_new_valist (GType gtype, va_list *args)
+{
+  WpObjectInterest *self = wp_object_interest_new_type (gtype);
+  WpConstraintType type;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  for (type = va_arg (*args, WpConstraintType);
+       type != WP_CONSTRAINT_TYPE_NONE;
+       type = va_arg (*args, WpConstraintType))
+  {
+    const gchar *subject, *format;
+    WpConstraintVerb verb = 0;
+    GVariant *value = NULL;
+
+    subject = va_arg (*args, const gchar *);
+    g_return_val_if_fail (subject != NULL, NULL);
+
+    format = va_arg (*args, const gchar *);
+    g_return_val_if_fail (format != NULL, NULL);
+
+    verb = format[0];
+    if (verb != 0 && format[1] != '\0')
+      value = g_variant_new_va (format + 1, NULL, args);
+
+    wp_object_interest_add_constraint (self, type, subject, verb, value);
+  }
+  return self;
+}
+
+/**
+ * wp_object_interest_new_type: (rename-to wp_object_interest_new)
+ * @gtype: the type of the object to declare interest in
+ *
+ * Creates a new interest that declares interest in objects of the specified
+ * @gtype, without any property constraints. To add property constraints,
+ * you can call wp_object_interest_add_constraint() afterwards.
+ *
+ * Returns: (transfer full): the new object interest
+ */
+WpObjectInterest *
+wp_object_interest_new_type (GType gtype)
+{
+  WpObjectInterest *self = g_slice_new0 (WpObjectInterest);
+  g_return_val_if_fail (self != NULL, NULL);
+  self->gtype = gtype;
+  pw_array_init (&self->constraints, sizeof (struct constraint));
+  return self;
+}
+
+/**
+ * wp_object_interest_add_constraint:
+ * @self: the object interest
+ * @type: the constraint type
+ * @subject: the subject that the constraint applies to
+ * @verb: the operation that is performed to check the constraint
+ * @value: (transfer floating)(nullable): the value to check for
+ *
+ * Adds a constraint to this interest. Constraints consist of a @type,
+ * a @subject, a @verb and, depending on the @verb, a @value.
+ *
+ * Constraints are almost like a spoken language sentence that declare a
+ * condition that must be true in order to consider that an object can match
+ * this interest. For instance, a constraint can be "pipewire property
+ * 'object.id' equals 10". This would be translated to:
+ * |[
+ * wp_object_interest_add_constraint (i,
+ *    WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id",
+ *    WP_CONSTRAINT_VERB_EQUALS, g_variant_new_int (10));
+ * ]|
+ *
+ * Some verbs require a @value and some others do not. For those that do,
+ * the @value must be of a specific type:
+ *  - %WP_CONSTRAINT_VERB_EQUALS: @value can be a string, a (u)int32,
+ *    a (u)int64, a double or a boolean. The @subject value must equal this
+ *    value for the constraint to be satisfied
+ *  - %WP_CONSTRAINT_VERB_IN_LIST: @value must be a tuple that contains any
+ *    number of items of the same type; the items can be string, (u)int32,
+ *    (u)int64 or double. These items make a list that the @subject's value
+ *    will be checked against. If any of the items equals the @subject value,
+ *    the constraint is satisfied
+ *  - %WP_CONSTRAINT_VERB_IN_RANGE: @value must be a tuple that contains exactly
+ *    2 numbers of the same type ((u)int32, (u)int64 or double), meaning the
+ *    minimum and maximum (inclusive) of the range. If the @subject value is a
+ *    number within this range, the constraint is satisfied
+ *  - %WP_CONSTRAINT_VERB_MATCHES: @value must be a string that defines a
+ *    pattern usable with #GPatternSpec. If the @subject value matches this
+ *    pattern, the constraint is satisfied
+ *
+ * In case the type of the @subject value is not the same type as the one
+ * requested by the type of the @value, the @subject value is converted.
+ * For #GObject properties, this conversion is done using g_value_transform(),
+ * so limitations of this function apply. In the case of PipeWire properties,
+ * which are *always* strings, conversion is done as follows:
+ *  - to boolean: `"true"` or `"1"` means %TRUE, `"false"` or `"0"` means %FALSE
+ *  - to int / uint / int64 / uint64: One of the `strtol()` family of functions
+ *    is used to convert, using base 10
+ *  - to double: `strtod()` is used
+ *
+ * This method does not fail if invalid arguments are given. However,
+ * wp_object_interest_validate() should be called after adding all the
+ * constraints on an interest in order to catch errors.
+ */
+void
+wp_object_interest_add_constraint (WpObjectInterest * self,
+    WpConstraintType type, const gchar * subject,
+    WpConstraintVerb verb, GVariant * value)
+{
+  struct constraint *c;
+
+  g_return_if_fail (self != NULL);
+
+  c = pw_array_add (&self->constraints, sizeof (struct constraint));
+  g_return_if_fail (c != NULL);
+  c->type = type;
+  c->verb = verb;
+  /* subject_type is filled in by _validate() */
+  c->subject_type = '\0';
+  c->subject = g_strdup (subject);
+  c->value = value ? g_variant_ref_sink (value) : NULL;
+
+  /* mark as invalid to force validation */
+  self->valid = FALSE;
+}
+
+/**
+ * wp_object_interest_copy:
+ * @self: the object interest to copy
+ *
+ * Returns: (transfer full): a deep copy of @self
+ */
+WpObjectInterest *
+wp_object_interest_copy (WpObjectInterest * self)
+{
+  WpObjectInterest *copy;
+  struct constraint *c, *cc;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  copy = wp_object_interest_new_type (self->gtype);
+  g_return_val_if_fail (copy != NULL, NULL);
+
+  pw_array_ensure_size (&copy->constraints, self->constraints.size);
+  pw_array_for_each (c, &self->constraints) {
+    cc = pw_array_add (&self->constraints, sizeof (struct constraint));
+    g_return_val_if_fail (cc != NULL, NULL);
+    cc->type = c->type;
+    cc->verb = c->verb;
+    cc->subject_type = c->subject_type;
+    cc->subject = g_strdup (c->subject);
+    cc->value = c->value ? g_variant_ref (c->value) : NULL;
+  }
+  copy->valid = self->valid;
+
+  return copy;
+}
+
+/**
+ * wp_object_interest_free:
+ * @self: (transfer full): the object interest to free
+ *
+ * Releases @self and all the memory associated with it
+ */
+void
+wp_object_interest_free (WpObjectInterest * self)
+{
+  struct constraint *c;
+
+  g_return_if_fail (self != NULL);
+
+  pw_array_for_each (c, &self->constraints) {
+    g_clear_pointer (&c->subject, g_free);
+    g_clear_pointer (&c->value, g_variant_unref);
+  }
+  pw_array_clear (&self->constraints);
+  g_slice_free (WpObjectInterest, self);
+}
+
+/**
+ * wp_object_interest_validate:
+ * @self: the object interest to validate
+ * @error: (out) (optional): the error, in case validation failed
+ *
+ * Validates the interest, ensuring that the interest #GType is a valid
+ * object and that all the constraints have been expressed properly.
+ *
+ * Returns: %TRUE if the interest is valid and can be used in a match,
+ *   %FALSE otherwise
+ */
+gboolean
+wp_object_interest_validate (WpObjectInterest * self, GError ** error)
+{
+  struct constraint *c;
+  gboolean is_proxy;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  /* if already validated, we are done */
+  if (self->valid)
+    return TRUE;
+
+  if (!G_TYPE_IS_OBJECT (self->gtype)) {
+    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+        "type '%s' is not a GObject", g_type_name (self->gtype));
+    return FALSE;
+  }
+
+  is_proxy = g_type_is_a (self->gtype, WP_TYPE_PROXY);
+
+  pw_array_for_each (c, &self->constraints) {
+    const GVariantType *value_type = NULL;
+
+    if (c->type <= WP_CONSTRAINT_TYPE_NONE ||
+        c->type > WP_CONSTRAINT_TYPE_G_PROPERTY) {
+      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+          "invalid constraint type %d", c->type);
+      return FALSE;
+    }
+
+    if (!is_proxy && (c->type == WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY ||
+            c->type == WP_CONSTRAINT_TYPE_PW_PROPERTY)) {
+      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+          "constraint type %d cannot apply to non-WpProxy type '%s'",
+          c->type, g_type_name (self->gtype));
+      return FALSE;
+    }
+
+    if (!c->subject) {
+      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+          "constraint subject cannot be NULL");
+      return FALSE;
+    }
+
+    switch (c->verb) {
+      case WP_CONSTRAINT_VERB_EQUALS:
+      case WP_CONSTRAINT_VERB_IN_LIST:
+      case WP_CONSTRAINT_VERB_IN_RANGE:
+      case WP_CONSTRAINT_VERB_MATCHES:
+        if (!c->value) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "verb %d (%c) requires a value", c->verb, (gchar) c->verb);
+          return FALSE;
+        }
+        value_type = g_variant_get_type (c->value);
+        break;
+
+      case WP_CONSTRAINT_VERB_IS_PRESENT:
+      case WP_CONSTRAINT_VERB_IS_ABSENT:
+        if (c->value) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "verb %d (%c) should not have a value", c->verb, (gchar) c->verb);
+          return FALSE;
+        }
+        break;
+
+      default:
+        g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+            "invalid constraint verb %d (%c)", c->verb, (gchar) c->verb);
+        return FALSE;
+    }
+
+    switch (c->verb) {
+      case WP_CONSTRAINT_VERB_EQUALS:
+        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_BOOLEAN) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "WP_CONSTRAINT_VERB_EQUALS requires a basic GVariant type"
+              " (actual type was '%s')", g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        break;
+      case WP_CONSTRAINT_VERB_IN_LIST: {
+        const GVariantType *tuple_type;
+
+        if (!g_variant_type_is_definite (value_type) ||
+            !g_variant_type_is_tuple (value_type)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "WP_CONSTRAINT_VERB_IN_LIST requires a tuple GVariant type"
+              " (actual type was '%s')", g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        for (tuple_type = value_type = g_variant_type_first (value_type);
+            tuple_type != NULL;
+            tuple_type = g_variant_type_next (tuple_type)) {
+          if (!g_variant_type_equal (tuple_type, value_type)) {
+            g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+                "tuple must contain children of the same type"
+                " (mismatching type was '%s' at '%.*s')",
+                g_variant_get_type_string (c->value),
+                (int) g_variant_type_get_string_length (tuple_type),
+                g_variant_type_peek_string (tuple_type));
+            return FALSE;
+          }
+        }
+
+        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
+            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "list tuple must contain string, (u)int32, (u)int64 or double"
+              " (mismatching type was '%s' at '%.*s')",
+              g_variant_get_type_string (c->value),
+              (int) g_variant_type_get_string_length (value_type),
+              g_variant_type_peek_string (value_type));
+          return FALSE;
+        }
+
+        break;
+      }
+      case WP_CONSTRAINT_VERB_IN_RANGE: {
+        const GVariantType *tuple_type;
+
+        if (!g_variant_type_is_definite (value_type) ||
+            !g_variant_type_is_tuple (value_type)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "range requires a tuple GVariant type (actual type was '%s')",
+              g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        tuple_type = value_type = g_variant_type_first (value_type);
+        if (!tuple_type) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "range requires a non-empty tuple (actual type was '%s')",
+              g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        if (!g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT32) &&
+            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT32) &&
+            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT64) &&
+            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT64) &&
+            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_DOUBLE)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "range tuple must contain (u)int32, (u)int64 or double"
+              " (mismatching type was '%s' at '%.*s')",
+              g_variant_get_type_string (c->value),
+              (int) g_variant_type_get_string_length (tuple_type),
+              g_variant_type_peek_string (tuple_type));
+          return FALSE;
+        }
+
+        tuple_type = g_variant_type_next (tuple_type);
+        if (!tuple_type || !g_variant_type_equal (tuple_type, value_type)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "range tuple must contain 2 children of the same type"
+              " (mismatching type was '%s' at '%.*s')",
+              g_variant_get_type_string (c->value),
+              (int) g_variant_type_get_string_length (tuple_type),
+              g_variant_type_peek_string (tuple_type));
+          return FALSE;
+        }
+
+        tuple_type = g_variant_type_next (tuple_type);
+        if (tuple_type) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "range tuple must contain exactly 2 children, not more"
+              " (mismatching type was '%s')",
+              g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        break;
+      }
+      case WP_CONSTRAINT_VERB_MATCHES:
+        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING)) {
+          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+              "WP_CONSTRAINT_VERB_MATCHES requires a string GVariant"
+              " (actual type was '%s')", g_variant_get_type_string (c->value));
+          return FALSE;
+        }
+
+        break;
+      case WP_CONSTRAINT_VERB_IS_PRESENT:
+      case WP_CONSTRAINT_VERB_IS_ABSENT:
+        break;
+      default:
+        g_return_val_if_reached (FALSE);
+    }
+
+    /* cache the type that the property must have */
+    if (value_type)
+      c->subject_type = *g_variant_type_peek_string (value_type);
+  }
+
+  return (self->valid = TRUE);
+}
+
+G_GNUC_CONST static GType
+subject_type_to_gtype (gchar type)
+{
+  switch (type) {
+    case 'b': return G_TYPE_BOOLEAN;
+    case 'i': return G_TYPE_INT;
+    case 'u': return G_TYPE_UINT;
+    case 'x': return G_TYPE_INT64;
+    case 't': return G_TYPE_UINT64;
+    case 'd': return G_TYPE_DOUBLE;
+    case 's': return G_TYPE_STRING;
+    default: g_return_val_if_reached (G_TYPE_INVALID);
+  }
+}
+
+static inline gboolean
+property_string_to_gvalue (gchar subj_type, const gchar * str, GValue * val)
+{
+  g_value_init (val, subject_type_to_gtype (subj_type));
+
+  switch (subj_type) {
+    case 'b':
+      if (!strcmp (str, "true") || !strcmp (str, "1"))
+        g_value_set_boolean (val, TRUE);
+      else if (!strcmp (str, "false") || !strcmp (str, "0"))
+        g_value_set_boolean (val, FALSE);
+      else {
+        wp_trace ("failed to convert '%s' to boolean", str);
+        return FALSE;
+      }
+      break;
+    case 's':
+      g_value_set_static_string (val, str);
+      break;
+
+#define CASE_NUMBER(l, T, convert) \
+    case l: { \
+      g##T number; \
+      errno = 0; \
+      number = convert; \
+      if (errno != 0) { \
+        wp_trace ("failed to convert '%s' to " #T, str); \
+        return FALSE; \
+      } \
+      g_value_set_##T (val, number); \
+      break; \
+    }
+    CASE_NUMBER ('i', int, strtol (str, NULL, 10))
+    CASE_NUMBER ('u', uint, strtoul (str, NULL, 10))
+    CASE_NUMBER ('x', int64, strtoll (str, NULL, 10))
+    CASE_NUMBER ('t', uint64, strtoull (str, NULL, 10))
+    CASE_NUMBER ('d', double, strtod (str, NULL))
+#undef CASE_NUMBER
+    default:
+      g_return_val_if_reached (FALSE);
+  }
+  return TRUE;
+}
+
+static inline gboolean
+constraint_verb_equals (gchar subj_type, const GValue * subj_val,
+    GVariant * check_val)
+{
+  switch (subj_type) {
+    case 'd': {
+      gdouble a = g_value_get_double (subj_val);
+      gdouble b = g_variant_get_double (check_val);
+      return G_APPROX_VALUE (a, b, FLT_EPSILON);
+    }
+    case 's':
+      return !g_strcmp0 (g_value_get_string (subj_val),
+                         g_variant_get_string (check_val, NULL));
+#define CASE_BASIC(l, T, R) \
+    case l: \
+      return (g_value_get_##T (subj_val) == g_variant_get_##R (check_val));
+    CASE_BASIC ('b', boolean, boolean)
+    CASE_BASIC ('i', int, int32)
+    CASE_BASIC ('u', uint, uint32)
+    CASE_BASIC ('x', int64, int64)
+    CASE_BASIC ('t', uint64, uint64)
+#undef CASE_BASIC
+    default:
+      g_return_val_if_reached (FALSE);
+  }
+}
+
+static inline gboolean
+constraint_verb_matches (gchar subj_type, const GValue * subj_val,
+    GVariant * check_val)
+{
+  switch (subj_type) {
+    case 's':
+      return g_pattern_match_simple (g_variant_get_string (check_val, NULL),
+                                     g_value_get_string (subj_val));
+    default:
+      g_return_val_if_reached (FALSE);
+  }
+  return TRUE;
+}
+
+static inline gboolean
+constraint_verb_in_list (gchar subj_type, const GValue * subj_val,
+    GVariant * check_val)
+{
+  GVariantIter iter;
+  g_autoptr (GVariant) child = NULL;
+
+  g_variant_iter_init (&iter, check_val);
+  while ((child = g_variant_iter_next_value (&iter))) {
+    if (constraint_verb_equals (subj_type, subj_val, child))
+      return TRUE;
+    g_variant_unref (child);
+  }
+  return FALSE;
+}
+
+static inline gboolean
+constraint_verb_in_range (gchar subj_type, const GValue * subj_val,
+    GVariant * check_val)
+{
+  switch (subj_type) {
+#define CASE_RANGE(l, t, T) \
+    case l: { \
+      g##T val, min, max; \
+      g_variant_get (check_val, "("#t#t")", &min, &max); \
+      val = g_value_get_##T (subj_val); \
+      if (val < min || val > max) \
+        return FALSE; \
+      break; \
+    }
+    CASE_RANGE('i', i, int)
+    CASE_RANGE('u', u, uint)
+    CASE_RANGE('x', x, int64)
+    CASE_RANGE('t', t, uint64)
+    CASE_RANGE('d', d, double)
+#undef CASE_RANGE
+    default:
+      g_return_val_if_reached (FALSE);
+  }
+  return TRUE;
+}
+
+/**
+ * wp_object_interest_matches:
+ * @self: the object interest
+ * @object: the target object to check for a match
+ *
+ * Checks if the specified @object matches the type and all the constraints
+ * that are described in @self
+ *
+ * Equivalent to `wp_object_interest_matches_full (self,
+ * G_OBJECT_TYPE (object), object, NULL, NULL)`
+ *
+ * Returns: %TRUE if the object matches, %FALSE otherwise
+ */
+gboolean
+wp_object_interest_matches (WpObjectInterest * self, gpointer object)
+{
+  return wp_object_interest_matches_full (self, G_OBJECT_TYPE (object),
+      object, NULL, NULL);
+}
+
+/**
+ * wp_object_interest_matches_full:
+ * @self: the object interest
+ * @object_type: the type to be checked against the interest's type
+ * @object: (type GObject)(transfer none)(nullable): the object to be used for
+ *   checking constraints of type %WP_CONSTRAINT_TYPE_G_PROPERTY
+ * @pw_props: (transfer none)(nullable): the properties to be used for
+ *   checking constraints of type %WP_CONSTRAINT_TYPE_PW_PROPERTY
+ * @pw_global_props: (transfer none)(nullable): the properties to be used for
+ *   checking constraints of type %WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY
+ *
+ * A low-level version of wp_object_interest_matches(). In this version,
+ * the object's type is directly given in @object_type and is not inferred
+ * from the @object. @object is only used to check for constraints against
+ * #GObject properties.
+ *
+ * @pw_props and @pw_global_props are used to check constraints against
+ * PipeWire object properties and global properties, respectively.
+ *
+ * @object, @pw_props and @pw_global_props may be %NULL, but in case there
+ * are any constraints that require them, the match will fail.
+ * As a special case, if @object is not %NULL and is a subclass of #WpProxy,
+ * then @pw_props and @pw_global_props, if required, will be internally
+ * retrieved from @object by calling wp_proxy_get_properties() and
+ * wp_proxy_get_global_properties() respectively.
+ *
+ * Returns: %TRUE if the the type matches this interest and the properties
+ *   match the constraints, %FALSE otherwise
+ */
+gboolean
+wp_object_interest_matches_full (WpObjectInterest * self,
+    GType object_type, gpointer object, WpProperties * pw_props,
+    WpProperties * pw_global_props)
+{
+  g_autoptr (WpProperties) props = NULL;
+  g_autoptr (WpProperties) global_props = NULL;
+  g_autoptr (GError) error = NULL;
+  struct constraint *c;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (G_UNLIKELY (!wp_object_interest_validate (self, &error))) {
+    wp_critical_boxed (WP_TYPE_OBJECT_INTEREST, self, "validation failed: %s",
+        error->message);
+    return FALSE;
+  }
+
+  /* check if the GType matches */
+  if (!g_type_is_a (object_type, self->gtype))
+    return FALSE;
+
+  /* prepare for constraint lookups on proxy properties */
+  if (object && g_type_is_a (object_type, WP_TYPE_PROXY)) {
+    WpProxy *p = WP_PROXY (object);
+
+    if (!pw_global_props)
+      pw_global_props = global_props = wp_proxy_get_global_properties (p);
+
+    if (!pw_props && wp_proxy_get_features (p) & WP_PROXY_FEATURE_INFO)
+      pw_props = props = wp_proxy_get_properties (p);
+  }
+
+  /* check all constraints; if any of them fails at any point, fail the match */
+  pw_array_for_each (c, &self->constraints) {
+    WpProperties *lookup_props = pw_global_props;
+    g_auto (GValue) value = G_VALUE_INIT;
+    gboolean exists = FALSE;
+
+    /* collect, check & convert the subject property */
+    switch (c->type) {
+      case WP_CONSTRAINT_TYPE_PW_PROPERTY:
+        lookup_props = pw_props;
+        G_GNUC_FALLTHROUGH;
+
+      case WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY: {
+        const gchar *lookup_str = NULL;
+
+        if (lookup_props)
+          exists = !!(lookup_str = wp_properties_get (lookup_props, c->subject));
+
+        if (exists && c->subject_type)
+          property_string_to_gvalue (c->subject_type, lookup_str, &value);
+        break;
+      }
+      case WP_CONSTRAINT_TYPE_G_PROPERTY: {
+        GType value_type;
+
+        if (object)
+          exists = !!g_object_class_find_property (G_OBJECT_GET_CLASS (object), c->subject);
+
+        if (exists && c->subject_type) {
+          g_object_get_property (object, c->subject, &value);
+          value_type = G_VALUE_TYPE (&value);
+
+          /* transform if not compatible */
+          if (value_type != subject_type_to_gtype (c->subject_type)) {
+            if (g_value_type_transformable (value_type,
+                    subject_type_to_gtype (c->subject_type))) {
+              g_auto (GValue) orig = G_VALUE_INIT;
+              g_value_init (&orig, value_type);
+              g_value_copy (&value, &orig);
+              g_value_unset (&value);
+              g_value_init (&value, subject_type_to_gtype (c->subject_type));
+              g_value_transform (&orig, &value);
+            }
+            else
+              return FALSE;
+          }
+        }
+
+        break;
+      }
+      default:
+        g_return_val_if_reached (FALSE);
+    }
+
+    /* match the subject to the constraint's value,
+       according to the operation defined by the verb */
+    switch (c->verb) {
+      case WP_CONSTRAINT_VERB_EQUALS:
+        if (!exists ||
+            !constraint_verb_equals (c->subject_type, &value, c->value))
+          return FALSE;
+        break;
+      case WP_CONSTRAINT_VERB_MATCHES:
+        if (!exists ||
+            !constraint_verb_matches (c->subject_type, &value, c->value))
+          return FALSE;
+        break;
+      case WP_CONSTRAINT_VERB_IN_LIST:
+        if (!exists ||
+            !constraint_verb_in_list (c->subject_type, &value, c->value))
+          return FALSE;
+        break;
+      case WP_CONSTRAINT_VERB_IN_RANGE:
+        if (!exists ||
+            !constraint_verb_in_range (c->subject_type, &value, c->value))
+          return FALSE;
+        break;
+      case WP_CONSTRAINT_VERB_IS_PRESENT:
+        if (!exists)
+          return FALSE;
+        break;
+      case WP_CONSTRAINT_VERB_IS_ABSENT:
+        if (exists)
+          return FALSE;
+        break;
+      default:
+        g_return_val_if_reached (FALSE);
+    }
+  }
+  return TRUE;
+}
diff --git a/lib/wp/object-interest.h b/lib/wp/object-interest.h
new file mode 100644
index 0000000000000000000000000000000000000000..b52853c71f2f14884280c66d6d952d020f2232dc
--- /dev/null
+++ b/lib/wp/object-interest.h
@@ -0,0 +1,105 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef __WIREPLUMBER_OBJECT_INTEREST_H__
+#define __WIREPLUMBER_OBJECT_INTEREST_H__
+
+#include <glib-object.h>
+#include "defs.h"
+#include "properties.h"
+
+G_BEGIN_DECLS
+
+/**
+ * WpConstraintType:
+ * @WP_CONSTRAINT_TYPE_NONE: invalid constraint type
+ * @WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY: constraint applies
+ *   to a PipeWire global property of the object (the ones returned by
+ *   wp_proxy_get_global_properties())
+ * @WP_CONSTRAINT_TYPE_PW_PROPERTY: constraint applies
+ *   to a PipeWire property of the object (the ones returned by
+ *   wp_proxy_get_properties())
+ * @WP_CONSTRAINT_TYPE_G_PROPERTY: constraint applies to a #GObject
+ *   property of the object
+ */
+typedef enum {
+  WP_CONSTRAINT_TYPE_NONE = 0,
+  WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
+  WP_CONSTRAINT_TYPE_PW_PROPERTY,
+  WP_CONSTRAINT_TYPE_G_PROPERTY,
+} WpConstraintType;
+
+/**
+ * WpConstraintVerb:
+ * @WP_CONSTRAINT_VERB_EQUALS: `=` the subject's value must equal the
+ *   constraint's value
+ * @WP_CONSTRAINT_VERB_IN_LIST: `c` the subject's value must equal at least
+ *   one of the values in the list given as the constraint's value
+ * @WP_CONSTRAINT_VERB_IN_RANGE: `~` the subject's value must be a number
+ *   in the range defined by the constraint's value
+ * @WP_CONSTRAINT_VERB_MATCHES: `#` the subject's value must match the
+ *   pattern specified in the constraint's value
+ * @WP_CONSTRAINT_VERB_IS_PRESENT: `+` the subject property must exist
+ * @WP_CONSTRAINT_VERB_IS_ABSENT: `-` the subject property must not exist
+ */
+typedef enum {
+  WP_CONSTRAINT_VERB_EQUALS = '=',
+  WP_CONSTRAINT_VERB_IN_LIST = 'c',
+  WP_CONSTRAINT_VERB_IN_RANGE = '~',
+  WP_CONSTRAINT_VERB_MATCHES = '#',
+  WP_CONSTRAINT_VERB_IS_PRESENT = '+',
+  WP_CONSTRAINT_VERB_IS_ABSENT = '-',
+} WpConstraintVerb;
+
+/**
+ * WP_TYPE_OBJECT_INTEREST:
+ *
+ * The #WpObjectInterest #GType
+ */
+#define WP_TYPE_OBJECT_INTEREST (wp_object_interest_get_type ())
+WP_API
+GType wp_object_interest_get_type (void) G_GNUC_CONST;
+
+typedef struct _WpObjectInterest WpObjectInterest;
+
+WP_API
+WpObjectInterest * wp_object_interest_new (GType gtype, ...) G_GNUC_NULL_TERMINATED;
+
+WP_API
+WpObjectInterest * wp_object_interest_new_valist (GType gtype, va_list * args);
+
+WP_API
+WpObjectInterest * wp_object_interest_new_type (GType gtype);
+
+WP_API
+void wp_object_interest_add_constraint (WpObjectInterest * self,
+    WpConstraintType type, const gchar * subject,
+    WpConstraintVerb verb, GVariant * value);
+
+WP_API
+WpObjectInterest * wp_object_interest_copy (WpObjectInterest * self);
+
+WP_API
+void wp_object_interest_free (WpObjectInterest * self);
+
+WP_API
+gboolean wp_object_interest_validate (WpObjectInterest * self, GError ** error);
+
+WP_API
+gboolean wp_object_interest_matches (WpObjectInterest * self, gpointer object);
+
+WP_API
+gboolean wp_object_interest_matches_full (WpObjectInterest * self,
+    GType object_type, gpointer object, WpProperties * pw_props,
+    WpProperties * pw_global_props);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_free)
+
+G_END_DECLS
+
+#endif
diff --git a/lib/wp/wp.h b/lib/wp/wp.h
index d9c377d880fab5d8f20131f07a8f3c62706093c9..1a678cda816629aca79fe0620afd460d90f384bb 100644
--- a/lib/wp/wp.h
+++ b/lib/wp/wp.h
@@ -21,6 +21,7 @@
 #include "link.h"
 #include "module.h"
 #include "node.h"
+#include "object-interest.h"
 #include "object-manager.h"
 #include "policy.h"
 #include "port.h"
diff --git a/tests/wp/meson.build b/tests/wp/meson.build
index a6e30e16a067f787f79e98c20496e7af6b194f65..61323ee32e8e9466e154c534e07f83731578265d 100644
--- a/tests/wp/meson.build
+++ b/tests/wp/meson.build
@@ -16,6 +16,13 @@ test(
   env: common_env,
 )
 
+test(
+  'test-object-interest',
+  executable('test-object-interest', 'object-interest.c',
+      dependencies: common_deps, c_args: common_args),
+  env: common_env,
+)
+
 test(
   'test-properties',
   executable('test-properties', 'properties.c',
diff --git a/tests/wp/object-interest.c b/tests/wp/object-interest.c
new file mode 100644
index 0000000000000000000000000000000000000000..9a14bc0604ab5f1d743912d5b86f8fa3698be41e
--- /dev/null
+++ b/tests/wp/object-interest.c
@@ -0,0 +1,843 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+
+enum {
+  PROP_0,
+  PROP_TEST_STRING,
+  PROP_TEST_INT,
+  PROP_TEST_UINT,
+  PROP_TEST_INT64,
+  PROP_TEST_UINT64,
+  PROP_TEST_FLOAT,
+  PROP_TEST_DOUBLE,
+  PROP_TEST_BOOLEAN,
+};
+
+struct _TestObjA
+{
+  GObject parent;
+  gchar *test_string;
+  gint test_int;
+  guint test_uint;
+  gint64 test_int64;
+  guint64 test_uint64;
+  gfloat test_float;
+  gdouble test_double;
+  gboolean test_boolean;
+};
+
+#define TEST_TYPE_A (test_obj_a_get_type ())
+G_DECLARE_FINAL_TYPE (TestObjA, test_obj_a, TEST, OBJ_A, GObject)
+G_DEFINE_TYPE (TestObjA, test_obj_a, G_TYPE_OBJECT)
+
+static void
+test_obj_a_init (TestObjA * self)
+{
+}
+
+static void
+test_obj_a_finalize (GObject * object)
+{
+  TestObjA *self = TEST_OBJ_A (object);
+  g_free (self->test_string);
+  G_OBJECT_CLASS (test_obj_a_parent_class)->finalize (object);
+}
+
+static void
+test_obj_a_get_property (GObject * object, guint id, GValue * value,
+    GParamSpec * pspec)
+{
+  TestObjA *self = TEST_OBJ_A (object);
+
+  switch (id) {
+    case PROP_TEST_STRING:
+      g_value_set_string (value, self->test_string);
+      break;
+    case PROP_TEST_INT:
+      g_value_set_int (value, self->test_int);
+      break;
+    case PROP_TEST_UINT:
+      g_value_set_uint (value, self->test_uint);
+      break;
+    case PROP_TEST_INT64:
+      g_value_set_int64 (value, self->test_int64);
+      break;
+    case PROP_TEST_UINT64:
+      g_value_set_uint64 (value, self->test_uint64);
+      break;
+    case PROP_TEST_FLOAT:
+      g_value_set_float (value, self->test_float);
+      break;
+    case PROP_TEST_DOUBLE:
+      g_value_set_double (value, self->test_double);
+      break;
+    case PROP_TEST_BOOLEAN:
+      g_value_set_boolean (value, self->test_boolean);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
+      break;
+  }
+}
+
+static void
+test_obj_a_set_property (GObject * object, guint id, const GValue * value,
+    GParamSpec * pspec)
+{
+  TestObjA *self = TEST_OBJ_A (object);
+
+  switch (id) {
+    case PROP_TEST_STRING:
+      self->test_string = g_value_dup_string (value);
+      break;
+    case PROP_TEST_INT:
+      self->test_int = g_value_get_int (value);
+      break;
+    case PROP_TEST_UINT:
+      self->test_uint = g_value_get_uint (value);
+      break;
+    case PROP_TEST_INT64:
+      self->test_int64 = g_value_get_int64 (value);
+      break;
+    case PROP_TEST_UINT64:
+      self->test_uint64 = g_value_get_uint64 (value);
+      break;
+    case PROP_TEST_FLOAT:
+      self->test_float = g_value_get_float (value);
+      break;
+    case PROP_TEST_DOUBLE:
+      self->test_double = g_value_get_double (value);
+      break;
+    case PROP_TEST_BOOLEAN:
+      self->test_boolean = g_value_get_boolean (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
+      break;
+  }
+}
+
+static void
+test_obj_a_class_init (TestObjAClass * klass)
+{
+  GObjectClass *obj_class = (GObjectClass *) klass;
+
+  obj_class->finalize = test_obj_a_finalize;
+  obj_class->get_property = test_obj_a_get_property;
+  obj_class->set_property = test_obj_a_set_property;
+
+  g_object_class_install_property (obj_class, PROP_TEST_STRING,
+      g_param_spec_string ("test-string", "test-string", "blurb", NULL,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_INT,
+      g_param_spec_int ("test-int", "test-int", "blurb",
+          G_MININT, G_MAXINT, 0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_UINT,
+      g_param_spec_uint ("test-uint", "test-uint", "blurb",
+          0, G_MAXUINT, 0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_INT64,
+      g_param_spec_int64 ("test-int64", "test-int64", "blurb",
+          G_MININT64, G_MAXINT64, 0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_UINT64,
+      g_param_spec_uint64 ("test-uint64", "test-uint64", "blurb",
+          0, G_MAXUINT64, 0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_FLOAT,
+      g_param_spec_float ("test-float", "test-float", "blurb",
+          -20.0f, 20.0f, 0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_DOUBLE,
+      g_param_spec_double ("test-double", "test-double", "blurb",
+          -20.0, 20.0, 0.0,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (obj_class, PROP_TEST_BOOLEAN,
+      g_param_spec_boolean ("test-boolean", "test-boolean", "blurb", FALSE,
+          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+struct _TestObjB
+{
+  TestObjA parent;
+};
+
+#define TEST_TYPE_B (test_obj_b_get_type ())
+G_DECLARE_FINAL_TYPE (TestObjB, test_obj_b, TEST, OBJ_B, TestObjA)
+G_DEFINE_TYPE (TestObjB, test_obj_b, TEST_TYPE_A)
+
+static void
+test_obj_b_init (TestObjB * self)
+{
+}
+
+static void
+test_obj_b_class_init (TestObjBClass * klass)
+{
+}
+
+typedef struct {
+  GObject *object;
+} TestFixture;
+
+static void
+test_object_interest_setup (TestFixture * f, gconstpointer data)
+{
+  f->object = g_object_new (TEST_TYPE_B,
+      "test-string", "toast",
+      "test-int", -30,
+      "test-uint", 50,
+      "test-int64", G_GINT64_CONSTANT (-0x1d636b02300a7aa7),
+      "test-uint64", G_GUINT64_CONSTANT (0x1d636b02300a7aa7),
+      "test-float", 3.14f,
+      "test-double", 3.1415926545897932384626433,
+      "test-boolean", TRUE,
+      NULL);
+  g_assert_nonnull (f->object);
+}
+
+static void
+test_object_interest_teardown (TestFixture * f, gconstpointer data)
+{
+  g_clear_object (&f->object);
+}
+
+#define TEST_EXPECT_MATCH(interest) \
+  G_STMT_START { \
+    g_autoptr (GError) error = NULL; \
+    gboolean ret; \
+    \
+    g_assert_nonnull (interest); \
+    \
+    ret = wp_object_interest_validate (interest, &error); \
+    g_assert_no_error (error); \
+    g_assert_true (ret); \
+    \
+    g_assert_true (wp_object_interest_matches (interest, f->object)); \
+    \
+    g_clear_pointer (&interest, wp_object_interest_free); \
+  } G_STMT_END
+
+#define TEST_EXPECT_NO_MATCH(interest) \
+  G_STMT_START { \
+    g_autoptr (GError) error = NULL; \
+    gboolean ret; \
+    \
+    g_assert_nonnull (interest); \
+    \
+    ret = wp_object_interest_validate (interest, &error); \
+    g_assert_no_error (error); \
+    g_assert_true (ret); \
+    \
+    g_assert_false (wp_object_interest_matches (interest, f->object)); \
+    \
+    g_clear_pointer (&interest, wp_object_interest_free); \
+  } G_STMT_END
+
+#define TEST_EXPECT_MATCH_WP_PROPS(interest, props, global_props) \
+  G_STMT_START { \
+    g_autoptr (GError) error = NULL; \
+    gboolean ret; \
+    \
+    g_assert_nonnull (interest); \
+    \
+    ret = wp_object_interest_validate (interest, &error); \
+    g_assert_no_error (error); \
+    g_assert_true (ret); \
+    \
+    g_assert_true (wp_object_interest_matches_full (interest, \
+            WP_TYPE_PROXY, NULL, props, global_props)); \
+    \
+    g_clear_pointer (&interest, wp_object_interest_free); \
+  } G_STMT_END
+
+#define TEST_EXPECT_NO_MATCH_WP_PROPS(interest, props, global_props) \
+  G_STMT_START { \
+    g_autoptr (GError) error = NULL; \
+    gboolean ret; \
+    \
+    g_assert_nonnull (interest); \
+    \
+    ret = wp_object_interest_validate (interest, &error); \
+    g_assert_no_error (error); \
+    g_assert_true (ret); \
+    \
+    g_assert_false (wp_object_interest_matches_full (interest, \
+            WP_TYPE_PROXY, NULL, props, global_props)); \
+    \
+    g_clear_pointer (&interest, wp_object_interest_free); \
+  } G_STMT_END
+
+#define TEST_EXPECT_VALIDATION_ERROR(interest) \
+  G_STMT_START { \
+    g_autoptr (GError) error = NULL; \
+    gboolean ret; \
+    \
+    g_assert_nonnull (interest); \
+    \
+    ret = wp_object_interest_validate (interest, &error); \
+    g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT); \
+    g_assert_false (ret); \
+    \
+    g_clear_pointer (&interest, wp_object_interest_free); \
+  } G_STMT_END
+
+static void
+test_object_interest_unconstrained (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new_type (TEST_TYPE_A);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new_type (WP_TYPE_PROXY);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_constraint_equals (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "toast", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "fail", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "=i", -30, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "=i", 100, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 100, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
+      "=x", G_GINT64_CONSTANT (-0x1d636b02300a7aa7), NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
+      "=x", G_GINT64_CONSTANT (100), NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
+      "=t", G_GUINT64_CONSTANT (0x1d636b02300a7aa7), NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
+      "=t", G_GUINT64_CONSTANT (100), NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
+      "=d", 3.1415926545897932384626433, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "=d", 3.14, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "=d", 3.14, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "=d", 1.0, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "=b", TRUE, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "=b", FALSE, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "=d", 3.1415926545897932384626433,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "toast",
+      NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
+      "=d", 3.1415926545897932384626433,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "FAIL",
+      NULL);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_constraint_list (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string",
+      "c(sss)", "success", "toast", "test", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string",
+      "c(ss)", "not-a-toast", "fail", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "c(iii)", -30, 20, -10, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "c(i)", 100, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(uu)", 100, 50, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 100, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "c(xx)",
+      G_GINT64_CONSTANT (100), G_GINT64_CONSTANT (-0x1d636b02300a7aa7), NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
+      "c(x)", G_GINT64_CONSTANT (100), NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
+      "c(t)", G_GUINT64_CONSTANT (0x1d636b02300a7aa7), NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
+      "c(t)", G_GUINT64_CONSTANT (100), NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
+      "c(dd)", 2.0, 3.1415926545897932384626433, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "c(d)", 3.14, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "c(dd)", 2.0, 3.14, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "c(dd)", 1.0, 2.0, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
+      "c(d)", 3.1415926545897932384626433,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 50,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "c(ss)", "random", "toast",
+      NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
+      "c(d)", 3.1415926545897932384626433,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 50,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "c(s)", "FAIL",
+      NULL);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_constraint_range (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "~(ii)", -40, 20, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "~(ii)", 10, 100, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 40, 100, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 100, 150, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "~(xx)",
+      G_GINT64_CONSTANT (-0x1d636b02300a7aaa),
+      G_GINT64_CONSTANT (-0x1d636b02300a7aa0),
+      NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "~(xx)",
+      G_GINT64_CONSTANT (0),
+      G_GINT64_CONSTANT (100),
+      NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64", "~(tt)",
+      G_GUINT64_CONSTANT (0x1d636b02300a7aa0),
+      G_GUINT64_CONSTANT (0x1d636b02300a7aaa),
+      NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64", "~(tt)",
+      G_GUINT64_CONSTANT (0),
+      G_GUINT64_CONSTANT (100),
+      NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 2.0, 4.0, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", -1.0, 3.14, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "~(dd)", 2.0, 4.0, NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "~(dd)", -1.0, 3.13, NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 0.0, 10.0,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 0, 100,
+      NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 10.0, 20.0,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 0, 100,
+      NULL);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_constraint_matches (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "to*", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "t*st", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "*a?t", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "egg*", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "t?est", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_constraint_present_absent (TestFixture * f,
+    gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "+", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "toast", "+", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "toast", "-", NULL);
+  TEST_EXPECT_MATCH (i);
+
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "-", NULL);
+  TEST_EXPECT_NO_MATCH (i);
+}
+
+static void
+test_object_interest_pw_props (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpProperties) props = NULL;
+  g_autoptr (WpProperties) global_props = NULL;
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  props = wp_properties_new (
+      "object.id", "10",
+      "port.name", "test",
+      "port.physical", "true",
+      "audio.channel", "FR",
+      "audio.volume", "0.8",
+      "format.dsp", "32 bit float mono audio",
+      NULL);
+
+  global_props = wp_properties_new (
+      "object.id", "10",
+      "format.dsp", "32 bit float mono audio",
+      NULL);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(ii)", 0, 100, NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=i", 11, NULL);
+  TEST_EXPECT_NO_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "format.dsp", "#s", "*audio*", NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.physical", "=b", TRUE, NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.channel", "c(sss)",
+      "MONO", "FL", "FR", NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.volume", "=d", 0.8, NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.volume", "~(dd)", 0.0, 0.5,
+      NULL);
+  TEST_EXPECT_NO_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=i", 10,
+      NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "+",
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "format.dsp", "+",
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "port.name", "-",
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "port.physical", "-",
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.name", "+",
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.physical", "+",
+      NULL);
+  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
+}
+
+static void
+test_object_interest_validate (TestFixture * f, gconstpointer data)
+{
+  g_autoptr (WpObjectInterest) i = NULL;
+
+  /* invalid type */
+  i = wp_object_interest_new (WP_TYPE_PROXY, 32, "object.id", "+", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* non-WpProxy type with pw property constraint */
+  i = wp_object_interest_new (TEST_TYPE_A,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "+", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* bad verb; the varargs constructor would assert here */
+  i = wp_object_interest_new_type (WP_TYPE_PROXY);
+  wp_object_interest_add_constraint (i, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+      "object.id", 0, g_variant_new_string ("10"));
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* no subject; the varargs version would assert here */
+  i = wp_object_interest_new_type (WP_TYPE_PROXY);
+  wp_object_interest_add_constraint (i, WP_CONSTRAINT_TYPE_PW_PROPERTY,
+      NULL, WP_CONSTRAINT_VERB_EQUALS, g_variant_new_int32 (10));
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* no value for verb that requires it */
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "#", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* value given for verb that doesn't require it */
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "+s", "10", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "-s", "10", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* tuple required */
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "ci", 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~i", 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* invalid value type */
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=y", (guchar) 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=n", (gint16) 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=q", (guint16) 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c(bb)", TRUE, FALSE, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(ss)", "0", "20", NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "#i", 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+
+  /* tuple with different types */
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c(si)", "9", 10, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+  i = wp_object_interest_new (WP_TYPE_PROXY,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(iu)", -10, 20, NULL);
+  TEST_EXPECT_VALIDATION_ERROR (i);
+}
+
+int
+main (int argc, char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
+
+  g_test_add ("/wp/object-interest/unconstrained",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_unconstrained,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/equals",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_constraint_equals,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/list",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_constraint_list,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/range",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_constraint_range,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/matches",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_constraint_matches,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/present-absent",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_constraint_present_absent,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/pw-props",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_pw_props,
+      test_object_interest_teardown);
+
+  g_test_add ("/wp/object-interest/validate",
+      TestFixture, NULL,
+      test_object_interest_setup,
+      test_object_interest_validate,
+      test_object_interest_teardown);
+
+  return g_test_run ();
+}