From 3afe0fda0ca6e539070424cbeff406d015e73c2d Mon Sep 17 00:00:00 2001
From: "E. Madison Bray" <676149+embray@users.noreply.github.com>
Date: Tue, 15 Sep 2026 12:33:27 +0200
Subject: [PATCH] feat: add asdf_file_find(_ex) and the asdf_find(_ex) macro
gh-246
---
abi/libasdf-x86_64.abi | 31 +++++++++----
changes/246.feature | 7 +++
configure.ac | 16 ++++---
docs/usage/examples.rst | 6 +--
docs/usage/ndarrays.rst | 8 ++--
include/asdf/file.h | 100 ++++++++++++++++++++++++++++++++++++++++
src/file.c | 25 ++++++++--
src/file.h | 2 -
tests/test-file.c | 37 +++++++++++++--
9 files changed, 199 insertions(+), 33 deletions(-)
create mode 100644 changes/246.feature
diff --git a/abi/libasdf-x86_64.abi b/abi/libasdf-x86_64.abi
index 12562cc6..8a7f57b4 100644
--- a/abi/libasdf-x86_64.abi
+++ b/abi/libasdf-x86_64.abi
@@ -60,6 +60,8 @@
+
+
@@ -2435,8 +2437,11 @@
+
+
+
@@ -2446,8 +2451,6 @@
-
-
@@ -2814,6 +2817,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2957,11 +2977,9 @@
-
-
@@ -2970,7 +2988,6 @@
-
@@ -3710,10 +3727,6 @@
-
-
-
-
diff --git a/changes/246.feature b/changes/246.feature
new file mode 100644
index 00000000..17383248
--- /dev/null
+++ b/changes/246.feature
@@ -0,0 +1,7 @@
+Added ``asdf_file_find`` and ``asdf_file_find_ex`` functions, corresponding to
+``asdf_value_find`` and ``asdf_value_find_ex``, the only difference being that
+they take the ``asdf_file_t *`` as their first argument as a shortcut for
+searching from the root of the tree.
+
+Also adds convenience macros ``asdf_find`` and ``asdf_find_ex`` which are
+generic in the first argument (can be either a file or a value).
diff --git a/configure.ac b/configure.ac
index e4029957..df1b9654 100644
--- a/configure.ac
+++ b/configure.ac
@@ -23,12 +23,16 @@ AC_INIT(
#
# History:
#
-# - 0:0:0 (0.1.0): initial default
-# - 1:0:1 (0.2.0): added asdf_free(). Also changed max_depth from ssize_t
-# to asdf_depth_t (int64_t): identical on LP64, but wider
-# on ILP32, where it is an interface change rather than an
-# addition. age was deliberately NOT reset, knowingly
-# breaking 32-bit targets, which are not actively supported.
+# - 0:0:0 (0.1.0): Initial default
+# - 1:0:1 (0.2.0): Added asdf_free().
+#
+# Changed max_depth from ssize_t to asdf_depth_t (int64_t):
+# identical on LP64, but wider on ILP32, where it is an
+# interface change rather than an addition. age was
+# deliberately NOT reset, knowingly breaking 32-bit
+# targets, which are not actively supported.
+#
+# Added asdf_file_find and asdf_file_find_ex.
LIBASDF_VERSION_INFO=1:0:1
AC_SUBST([LIBASDF_VERSION_INFO])
diff --git a/docs/usage/examples.rst b/docs/usage/examples.rst
index 58df9a1f..7271e84e 100644
--- a/docs/usage/examples.rst
+++ b/docs/usage/examples.rst
@@ -75,8 +75,7 @@ the ASDF tree, as well as extract block data. Inline comments provide further e
asdf_meta_destroy(meta);
// Find the first ndarray in the file, if any
- asdf_value_t *root = asdf_get_value(file, "");
- asdf_value_t *value = asdf_value_find(root, asdf_value_is_ndarray);
+ asdf_value_t *value = asdf_find(file, asdf_value_is_ndarray);
if (!value) {
fprintf(stderr, "no ndarray found in the file\n");
@@ -96,9 +95,8 @@ the ASDF tree, as well as extract block data. Inline comments provide further e
printf("Using ndarray at: %s\n", asdf_value_path(value));
printf("Number of data dimensions: %d\n", ndarray->ndim);
- // The generic value wrappers are no longer needed and should be freed.
+ // The generic value wrapper is no longer needed and should be freed.
asdf_value_destroy(value);
- asdf_value_destroy(root);
// Get just a raw pointer to the ndarray data block (if uncompressed).
// Optionally returns the size in bytes as well
diff --git a/docs/usage/ndarrays.rst b/docs/usage/ndarrays.rst
index 7350249b..ffa1830c 100644
--- a/docs/usage/ndarrays.rst
+++ b/docs/usage/ndarrays.rst
@@ -93,13 +93,12 @@ you are done:
asdf_ndarray_destroy(array);
If you do not know the path in advance you can search the tree for the first
-ndarray with `asdf_value_find` and the ``asdf_value_is_ndarray`` predicate, then
-cast the matching value with `asdf_value_as_ndarray`:
+ndarray with `asdf_find` and the ``asdf_value_is_ndarray`` predicate, then cast
+the matching value with `asdf_value_as_ndarray`:
.. code:: c
- asdf_value_t *root = asdf_get_value(file, "");
- asdf_value_t *found = asdf_value_find(root, asdf_value_is_ndarray);
+ asdf_value_t *found = asdf_find(file, asdf_value_is_ndarray);
asdf_ndarray_t *array = NULL;
if (found && asdf_value_as_ndarray(found, &array) == ASDF_VALUE_OK) {
@@ -107,7 +106,6 @@ cast the matching value with `asdf_value_as_ndarray`:
}
asdf_value_destroy(found);
- asdf_value_destroy(root);
See :ref:`values` for more on generic value handles and tree traversal.
diff --git a/include/asdf/file.h b/include/asdf/file.h
index 16dbabc3..3af53dca 100644
--- a/include/asdf/file.h
+++ b/include/asdf/file.h
@@ -947,6 +947,106 @@ asdf_set_mapping(asdf_file_t *file, const char *path, asdf_mapping_t *mapping);
ASDF_EXPORT asdf_value_err_t
asdf_set_sequence(asdf_file_t *file, const char *path, asdf_sequence_t *sequence);
+
+/**
+ * .. _file-traversal:
+ *
+ * Tree traversal
+ * --------------
+ */
+
+// clang-format off
+
+/**
+ * Traverse the tree breadth-first starting from ``root`` and return the first
+ * value matching ``pred``
+ *
+ * The caller owns the returned `asdf_value_t *` and must eventually destroy it
+ * with `asdf_value_destroy`. Returns ``NULL`` if no matching value was found.
+ *
+ * This is a macro that can take either an `asdf_file_t *` or `asdf_value_t *`
+ * as the root from which to traverse. In the former case, this is equivalent
+ * to starting the traversal from the root of the tree (via `asdf_file_find`).
+ *
+ * :param root: `asdf_value_t *` handle for the root node to search from or
+ * an `asdf_file_t *`.
+ * :param pred: A predicate function to match the value to return; see
+ * `asdf_value_pred_t`
+ * :return: The first matching `asdf_value_t *`, or ``NULL`` if not found
+ */
+#define asdf_find(root, pred) /* NOLINT(readability-identifier-naming) */ \
+ _Generic( \
+ (root), \
+ asdf_file_t *: asdf_file_find, \
+ asdf_value_t *: asdf_value_find)(root, pred)
+
+/**
+ * Extended version of `asdf_find` with additional traversal options
+ *
+ * Like `asdf_find` but allows controlling traversal order, which
+ * container types to descend into, and the maximum search depth.
+ *
+ * :param root: `asdf_value_t *` handle for the root node to search from
+ * or an `asdf_file_t *`
+ * :param pred: A predicate function to match the value to return; see
+ * `asdf_value_pred_t`
+ * :param depth_first: If ``true`` descend the tree in depth-first order;
+ * otherwise the tree is traversed breadth-first
+ * :param descend_pred: Optional predicate (``NULL`` means descend into all
+ * containers) controlling which containers are descended into
+ * :param max_depth: Maximum depth to descend; ``-1`` means no limit
+ * :return: The first matching `asdf_value_t *`, or ``NULL`` if not found
+ */
+#define asdf_find_ex(root, pred, depth_first, descend_pred, max_depth) /* NOLINT(readability-identifier-naming) */ \
+ _Generic( \
+ (root), \
+ asdf_file_t *: asdf_file_find_ex, \
+ asdf_value_t *: asdf_value_find_ex)(root, pred, depth_first, descend_pred, max_depth)
+
+// clang-format on
+
+/**
+ * Traverse the tree breadth-first starting from root of the given file's tree
+ * and return the first value matching ``pred``
+ *
+ * This is a shorthand for calling `asdf_value_find` starting from the root
+ * of the tree, equivalent to:
+ *
+ * .. code:: c
+ *
+ * asdf_value_t *root = asdf_get_value(file, "");
+ * asdf_value_t *found = asdf_value_find(root, pred);
+ *
+ * :param root: `asdf_file_t *` handle for the file to search
+ * :param pred: A predicate function to match the value to return; see
+ * `asdf_value_pred_t`
+ * :return: The first matching `asdf_value_t *`, or ``NULL`` if not found
+ */
+ASDF_EXPORT asdf_value_t *asdf_file_find(asdf_file_t *file, asdf_value_pred_t pred);
+
+/**
+ * Extended version of `asdf_file_find` with additional traversal options
+ *
+ * This is just the file-level companion to `asdf_file_find`, similarly to
+ * `asdf_value_find_ex`.
+ *
+ * :param root: `asdf_file_t *` handle for the file to search
+ * :param pred: A predicate function to match the value to return; see
+ * `asdf_value_pred_t`
+ * :param depth_first: If ``true`` descend the tree in depth-first order;
+ * otherwise the tree is traversed breadth-first
+ * :param descend_pred: Optional predicate (``NULL`` means descend into all
+ * containers) controlling which containers are descended into
+ * :param max_depth: Maximum depth to descend; ``-1`` means no limit
+ * :return: The first matching `asdf_value_t *`, or ``NULL`` if not found
+ */
+ASDF_EXPORT asdf_value_t *asdf_file_find_ex(
+ asdf_file_t *file,
+ asdf_value_pred_t pred,
+ bool depth_first,
+ asdf_value_pred_t descend_pred,
+ asdf_depth_t max_depth);
+
ASDF_END_DECLS
#endif /* ASDF_FILE_H */
diff --git a/src/file.c b/src/file.c
index 75c08740..b2bf4836 100644
--- a/src/file.c
+++ b/src/file.c
@@ -5,11 +5,8 @@
#include
#include
#include
-#include
#include
-#include
-
#include "block.h"
#include "context.h"
#include "core/asdf.h"
@@ -1019,3 +1016,25 @@ asdf_value_err_t asdf_set_sequence(asdf_file_t *file, const char *path, asdf_seq
asdf_sequence_destroy(sequence);
return err;
}
+
+
+asdf_value_t *asdf_file_find(asdf_file_t *file, asdf_value_pred_t pred) {
+ return asdf_file_find_ex(file, pred, false, NULL, -1);
+}
+
+
+asdf_value_t *asdf_file_find_ex(
+ asdf_file_t *file,
+ asdf_value_pred_t pred,
+ bool depth_first,
+ asdf_value_pred_t descend_pred,
+ asdf_depth_t max_depth) {
+ asdf_value_t *root = asdf_get_value(file, "");
+
+ if (UNLIKELY(!root))
+ return NULL;
+
+ asdf_value_t *found = asdf_value_find_ex(root, pred, depth_first, descend_pred, max_depth);
+ asdf_value_destroy(root);
+ return found;
+}
diff --git a/src/file.h b/src/file.h
index 19a34604..5ea60ced 100644
--- a/src/file.h
+++ b/src/file.h
@@ -2,8 +2,6 @@
#include
-#include
-
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
diff --git a/tests/test-file.c b/tests/test-file.c
index d94e3e08..268dbb6e 100644
--- a/tests/test-file.c
+++ b/tests/test-file.c
@@ -1,6 +1,7 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* for memmem */
#endif
+#include
#include
#include
#include
@@ -9,9 +10,7 @@
#include
#include
-#include
-
-#include
+#include "stc/cstr.h"
#include "asdf/emitter.h"
#include "asdf/error.h"
@@ -869,6 +868,35 @@ MU_TEST(test_asdf_free) {
}
+static bool value_find_pred_b(asdf_value_t *value) {
+ const char *str = NULL;
+ asdf_value_err_t err = asdf_value_as_string0(value, &str);
+
+ if (ASDF_VALUE_OK != err)
+ return false;
+
+ return strcmp(str, "b") == 0;
+}
+
+
+MU_TEST(test_asdf_file_find) {
+ const char *filename = get_fixture_file_path("nested.asdf");
+ asdf_file_t *file = asdf_open(filename, "r");
+ assert_not_null(file);
+ asdf_value_t *val = asdf_file_find(file, value_find_pred_b);
+ assert_not_null(val);
+ const char *str = NULL;
+ // BFS should find the value "b" at the top-level first
+ assert_string_equal(asdf_value_path(val), "/b");
+ asdf_value_err_t err = asdf_value_as_string0(val, &str);
+ assert_int(err, ==, ASDF_VALUE_OK);
+ assert_string_equal(str, "b");
+ asdf_value_destroy(val);
+ asdf_close(file);
+ return MUNIT_OK;
+}
+
+
MU_TEST_SUITE(
file,
MU_RUN_TEST(test_asdf_open_file),
@@ -902,7 +930,8 @@ MU_TEST_SUITE(
MU_RUN_TEST(write_custom_tag_handle),
MU_RUN_TEST(write_to_nonexistent_file),
MU_RUN_TEST(test_asdf_set_value_double_free),
- MU_RUN_TEST(test_asdf_free)
+ MU_RUN_TEST(test_asdf_free),
+ MU_RUN_TEST(test_asdf_file_find)
);