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) );