From 6b1dbc6672fb13d1dc2e0e30ae3e443aac1a8f77 Mon Sep 17 00:00:00 2001
From: Oliver Ruebel <oruebel@users.noreply.github.com>
Date: Tue, 21 Jan 2025 14:48:12 -0800
Subject: [PATCH 1/5] Add codecov badge to README.md (#135)

* Add codecov badge to README.md

* exclude tests from coverage trace

* add outputs for debugging

* update coverage workflow

* debug coverage dir

* debug coverage dir

* modify exclusion criteria

* revert coverage trace command

---------

Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com>
---
 README.md            | 3 ++-
 cmake/coverage.cmake | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 249d6d96..cd231d3f 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
 [![Codespell](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/codespell.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/codespell.yml)
 [![Lint](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/lint.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/lint.yml)
 [![Docs build](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/doxygen-gh-pages.yml/badge.svg)](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/doxygen-gh-pages.yml)
+[![Coverage](https://codecov.io/github/NeurodataWithoutBorders/aqnwb/coverage.svg?branch=main)](https://app.codecov.io/github/NeurodataWithoutBorders/aqnwb?branch=main)
 
 [![Docs](https://img.shields.io/badge/AqNWB-Docs-8A2BE2?style=flat)](https://neurodatawithoutborders.github.io/aqnwb/)
 [![Code Stats](https://img.shields.io/badge/AqNWB-Code%20Statistics-8A2BE2?style=flat)](https://nwb-overview.readthedocs.io/en/latest/nwb-project-analytics/docs/source/code_stat_pages/AqNWB_stats.html)
@@ -30,4 +31,4 @@ See the [AqNWB Documentation](https://neurodatawithoutborders.github.io/aqnwb/)
 
 For more information about the license, contributing guidelines, code of conduct
 and other  relevant documentation for developers please see the
-[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html).
\ No newline at end of file
+[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html).
diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake
index 6c4b03e2..868e5dc7 100644
--- a/cmake/coverage.cmake
+++ b/cmake/coverage.cmake
@@ -4,7 +4,7 @@
 # customization issues
 set(
     COVERAGE_TRACE_COMMAND
-    lcov -c -q
+    lcov -c --verbose
     -o "${PROJECT_BINARY_DIR}/coverage.info"
     -d "${PROJECT_BINARY_DIR}"
     --include "${PROJECT_SOURCE_DIR}/src/*"

From d76f8b7b384ee6dcd2fb775ac9c4d0bd709f0d4b Mon Sep 17 00:00:00 2001
From: Steph Prince <40640337+stephprince@users.noreply.github.com>
Date: Tue, 21 Jan 2025 18:26:26 -0500
Subject: [PATCH 2/5] Add AnnotationSeries data type (#141)

* add initial AnnotationSeries implementation

* add convenience functions, remove dtype option

* add draft of tests

* update writeDataBlock error message

* update AnnotationSeries methods for vlen strings

* add tests

* fix formatting

* Update src/nwb/misc/AnnotationSeries.hpp

Co-authored-by: Oliver Ruebel <oruebel@users.noreply.github.com>

* update read example in tests

---------

Co-authored-by: Oliver Ruebel <oruebel@users.noreply.github.com>
---
 CMakeLists.txt                    |  1 +
 src/io/hdf5/HDF5RecordingData.cpp |  3 +-
 src/nwb/NWBFile.cpp               | 30 +++++++++++++
 src/nwb/NWBFile.hpp               | 14 ++++++
 src/nwb/RecordingContainers.cpp   | 17 +++++++
 src/nwb/RecordingContainers.hpp   | 16 +++++++
 src/nwb/misc/AnnotationSeries.cpp | 71 ++++++++++++++++++++++++++++++
 src/nwb/misc/AnnotationSeries.hpp | 71 ++++++++++++++++++++++++++++++
 tests/CMakeLists.txt              |  1 +
 tests/testMisc.cpp                | 73 +++++++++++++++++++++++++++++++
 tests/testNWBFile.cpp             | 55 +++++++++++++++++++++++
 11 files changed, 351 insertions(+), 1 deletion(-)
 create mode 100644 src/nwb/misc/AnnotationSeries.cpp
 create mode 100644 src/nwb/misc/AnnotationSeries.hpp
 create mode 100644 tests/testMisc.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8fbe6787..f2287f36 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,6 +32,7 @@ add_library(
     src/nwb/ecephys/SpikeEventSeries.cpp
     src/nwb/file/ElectrodeGroup.cpp
     src/nwb/file/ElectrodeTable.cpp
+    src/nwb/misc/AnnotationSeries.cpp
     src/nwb/hdmf/base/Container.cpp
     src/nwb/hdmf/base/Data.cpp
     src/nwb/hdmf/table/DynamicTable.cpp
diff --git a/src/io/hdf5/HDF5RecordingData.cpp b/src/io/hdf5/HDF5RecordingData.cpp
index c7f6d8b6..7fc80e90 100644
--- a/src/io/hdf5/HDF5RecordingData.cpp
+++ b/src/io/hdf5/HDF5RecordingData.cpp
@@ -53,7 +53,8 @@ Status HDF5RecordingData::writeDataBlock(
         || type.type == BaseDataType::Type::T_STR)
     {
       std::cerr << "HDF5RecordingData::writeDataBlock called for string data, "
-                   "use HDF5RecordingData::writeStringDataBlock instead."
+                   "use HDF5RecordingData::writeDataBlock with a string array "
+                   "data input instead of void* data."
                 << std::endl;
       return Status::Failure;
     }
diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp
index 4d3b4743..dc23df75 100644
--- a/src/nwb/NWBFile.cpp
+++ b/src/nwb/NWBFile.cpp
@@ -16,6 +16,7 @@
 #include "nwb/ecephys/ElectricalSeries.hpp"
 #include "nwb/ecephys/SpikeEventSeries.hpp"
 #include "nwb/file/ElectrodeGroup.hpp"
+#include "nwb/misc/AnnotationSeries.hpp"
 #include "spec/core.hpp"
 #include "spec/hdmf_common.hpp"
 #include "spec/hdmf_experimental.hpp"
@@ -298,6 +299,35 @@ Status NWBFile::createSpikeEventSeries(
   return Status::Success;
 }
 
+Status NWBFile::createAnnotationSeries(std::vector<std::string> recordingNames,
+                                       RecordingContainers* recordingContainers,
+                                       std::vector<SizeType>& containerIndexes)
+{
+  if (!m_io->canModifyObjects()) {
+    return Status::Failure;
+  }
+
+  for (size_t i = 0; i < recordingNames.size(); ++i) {
+    const std::string& recordingName = recordingNames[i];
+
+    std::string annotationSeriesPath =
+        AQNWB::mergePaths(acquisitionPath, recordingName);
+
+    // Setup annotation series datasets
+    auto annotationSeries =
+        std::make_unique<AnnotationSeries>(annotationSeriesPath, m_io);
+    annotationSeries->initialize(
+        "Stores user annotations made during an experiment",
+        "no comments",
+        SizeArray {0},
+        SizeArray {CHUNK_XSIZE});
+    recordingContainers->addContainer(std::move(annotationSeries));
+    containerIndexes.push_back(recordingContainers->size() - 1);
+  }
+
+  return Status::Success;
+}
+
 template<SizeType N>
 void NWBFile::cacheSpecifications(
     const std::string& specPath,
diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp
index a9e014ce..69ed79bf 100644
--- a/src/nwb/NWBFile.hpp
+++ b/src/nwb/NWBFile.hpp
@@ -131,6 +131,20 @@ class NWBFile : public Container
       RecordingContainers* recordingContainers = nullptr,
       std::vector<SizeType>& containerIndexes = emptyContainerIndexes);
 
+  /** @brief Create AnnotationSeries objects to record data into.
+   * Created objects are stored in recordingContainers.
+   * @param recordingNames vector indicating the names of the AnnotationSeries
+   * within the acquisition group
+   * @param recordingContainers The container to store the created TimeSeries.
+   * @param containerIndexes The indexes of the containers added to
+   * recordingContainers
+   * @return Status The status of the object creation operation.
+   */
+  Status createAnnotationSeries(
+      std::vector<std::string> recordingNames,
+      RecordingContainers* recordingContainers = nullptr,
+      std::vector<SizeType>& containerIndexes = emptyContainerIndexes);
+
 protected:
   /**
    * @brief Creates the default file structure.
diff --git a/src/nwb/RecordingContainers.cpp b/src/nwb/RecordingContainers.cpp
index dc21d29e..f65b785e 100644
--- a/src/nwb/RecordingContainers.cpp
+++ b/src/nwb/RecordingContainers.cpp
@@ -4,6 +4,7 @@
 #include "nwb/ecephys/ElectricalSeries.hpp"
 #include "nwb/ecephys/SpikeEventSeries.hpp"
 #include "nwb/hdmf/base/Container.hpp"
+#include "nwb/misc/AnnotationSeries.hpp"
 
 using namespace AQNWB::NWB;
 // Recording Container
@@ -86,3 +87,19 @@ Status RecordingContainers::writeSpikeEventData(const SizeType& containerInd,
   return ses->writeSpike(
       numSamples, numChannels, data, timestamps, controlInput);
 }
+
+Status RecordingContainers::writeAnnotationSeriesData(
+    const SizeType& containerInd,
+    const SizeType& numSamples,
+    const std::vector<std::string> data,
+    const void* timestamps,
+    const void* controlInput)
+{
+  AnnotationSeries* as =
+      dynamic_cast<AnnotationSeries*>(getContainer(containerInd));
+
+  if (as == nullptr)
+    return Status::Failure;
+
+  return as->writeAnnotation(numSamples, data, timestamps, controlInput);
+}
\ No newline at end of file
diff --git a/src/nwb/RecordingContainers.hpp b/src/nwb/RecordingContainers.hpp
index 33f2b1da..f57bcf0d 100644
--- a/src/nwb/RecordingContainers.hpp
+++ b/src/nwb/RecordingContainers.hpp
@@ -111,6 +111,22 @@ class RecordingContainers
                              const void* timestamps,
                              const void* controlInput = nullptr);
 
+  /**
+   * @brief Write AnnotationSeries data to a recordingContainer dataset.
+   * @param containerInd The index of the AnnotationSeries dataset within the
+   * AnnotationSeries containers.
+   * @param numSamples Number of samples in the time for the single event.
+   * @param data A vector of strings of data to write.
+   * @param timestamps A pointer to the timestamps block
+   * @param controlInput A pointer to the control block data (optional)
+   * @return The status of the write operation.
+   */
+  Status writeAnnotationSeriesData(const SizeType& containerInd,
+                                   const SizeType& numSamples,
+                                   const std::vector<std::string> data,
+                                   const void* timestamps,
+                                   const void* controlInput = nullptr);
+
   /**
    * @brief Get the number of recording containers
    */
diff --git a/src/nwb/misc/AnnotationSeries.cpp b/src/nwb/misc/AnnotationSeries.cpp
new file mode 100644
index 00000000..5e5db340
--- /dev/null
+++ b/src/nwb/misc/AnnotationSeries.cpp
@@ -0,0 +1,71 @@
+
+#include "nwb/misc/AnnotationSeries.hpp"
+
+#include "Utils.hpp"
+
+using namespace AQNWB::NWB;
+
+// AnnotationSeries
+// Initialize the static registered_ member to trigger registration
+REGISTER_SUBCLASS_IMPL(AnnotationSeries)
+
+/** Constructor */
+AnnotationSeries::AnnotationSeries(const std::string& path,
+                                   std::shared_ptr<IO::BaseIO> io)
+    : TimeSeries(path, io)
+{
+}
+
+/** Destructor */
+AnnotationSeries::~AnnotationSeries() {}
+
+/** Initialization function*/
+void AnnotationSeries::initialize(const std::string& description,
+                                  const std::string& comments,
+                                  const SizeArray& dsetSize,
+                                  const SizeArray& chunkSize)
+{
+  TimeSeries::initialize(
+      IO::BaseDataType::V_STR,  // fixed to string according to schema
+      "n/a",  // unit fixed to "n/a"
+      description,
+      comments,
+      dsetSize,
+      chunkSize,
+      1.0f,  // conversion fixed to 1.0, since unit is n/a
+      -1.0f,  // resolution fixed to -1.0
+      0.0f);  // offset fixed to 0.0, since unit is n/a
+}
+
+Status AnnotationSeries::writeAnnotation(const SizeType& numSamples,
+                                         std::vector<std::string> dataInput,
+                                         const void* timestampsInput,
+                                         const void* controlInput)
+{
+  std::vector<SizeType> dataShape = {numSamples};
+  std::vector<SizeType> positionOffset = {this->m_samplesRecorded};
+
+  // Write timestamps
+  Status tsStatus = Status::Success;
+  tsStatus = this->timestamps->writeDataBlock(
+      dataShape, positionOffset, this->timestampsType, timestampsInput);
+
+  // Write the data
+  Status dataStatus = this->data->writeDataBlock(
+      dataShape, positionOffset, this->m_dataType, dataInput);
+
+  // Write the control data if it exists
+  if (controlInput != nullptr) {
+    tsStatus = this->control->writeDataBlock(
+        dataShape, positionOffset, this->controlType, controlInput);
+  }
+
+  // track samples recorded
+  m_samplesRecorded += numSamples;
+
+  if ((dataStatus != Status::Success) || (tsStatus != Status::Success)) {
+    return Status::Failure;
+  } else {
+    return Status::Success;
+  }
+}
\ No newline at end of file
diff --git a/src/nwb/misc/AnnotationSeries.hpp b/src/nwb/misc/AnnotationSeries.hpp
new file mode 100644
index 00000000..a9ac2ac1
--- /dev/null
+++ b/src/nwb/misc/AnnotationSeries.hpp
@@ -0,0 +1,71 @@
+
+#pragma once
+
+#include <string>
+
+#include "Utils.hpp"
+#include "io/BaseIO.hpp"
+#include "io/ReadIO.hpp"
+#include "nwb/base/TimeSeries.hpp"
+
+namespace AQNWB::NWB
+{
+/**
+ * @brief TimeSeries storing text-based records about the experiment.
+ */
+class AnnotationSeries : public TimeSeries
+{
+public:
+  // Register the AnnotationSeries
+  REGISTER_SUBCLASS(AnnotationSeries, "core")
+
+  /**
+   * @brief Constructor.
+   * @param path The location of the AnnotationSeries in the file.
+   * @param io A shared pointer to the IO object.
+   */
+  AnnotationSeries(const std::string& path, std::shared_ptr<IO::BaseIO> io);
+
+  /**
+   * @brief Destructor
+   */
+  ~AnnotationSeries();
+
+  /**
+   * @brief Initializes the AnnotationSeries
+   * @param description The description of the AnnotationSeries.
+   * @param dsetSize Initial size of the main dataset. This must be a vector
+   *                 with one element specifying the length in time.
+   * @param chunkSize Chunk size to use.
+   */
+  void initialize(const std::string& description,
+                  const std::string& comments,
+                  const SizeArray& dsetSize,
+                  const SizeArray& chunkSize);
+
+  /**
+   * @brief Writes a channel to an AnnotationSeries dataset.
+   * @param numSamples The number of samples to write (length in time).
+   * @param dataInput A vector of strings.
+   * @param timestampsInput A pointer to the timestamps block.
+   * @param controlInput A pointer to the control block data (optional)
+   * @return The status of the write operation.
+   */
+  Status writeAnnotation(const SizeType& numSamples,
+                         const std::vector<std::string> dataInput,
+                         const void* timestampsInput,
+                         const void* controlInput = nullptr);
+
+  DEFINE_FIELD(readData,
+               DatasetField,
+               std::string,
+               "data",
+               Annotations made during an experiment.)
+
+private:
+  /**
+   * @brief The number of samples already written per channel.
+   */
+  SizeType m_samplesRecorded = 0;
+};
+}  // namespace AQNWB::NWB
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c5288219..8615085e 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -18,6 +18,7 @@ add_executable(aqnwb_test
     testElementIdentifiers.cpp
     testFile.cpp
     testHDF5IO.cpp
+    testMisc.cpp
     testNWBFile.cpp
     testRecordingWorkflow.cpp
     testRegisteredType.cpp
diff --git a/tests/testMisc.cpp b/tests/testMisc.cpp
new file mode 100644
index 00000000..0770e1fc
--- /dev/null
+++ b/tests/testMisc.cpp
@@ -0,0 +1,73 @@
+#include <H5Cpp.h>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include "Types.hpp"
+#include "Utils.hpp"
+#include "io/BaseIO.hpp"
+#include "nwb/misc/AnnotationSeries.hpp"
+#include "testUtils.hpp"
+
+using namespace AQNWB;
+
+TEST_CASE("AnnotationSeries", "[misc]")
+{
+  // setup recording info
+  SizeType numSamples = 3;
+  std::string dataPath = "/annotations";
+  std::vector<std::string> mockAnnotations = {
+      "Subject moved",
+      "Break started",
+      "Break ended",
+  };
+  std::vector<double> mockTimestamps = getMockTimestamps(numSamples, 1);
+  std::vector<double> mockTimestamps2 = mockTimestamps;
+  for (double& value : mockTimestamps2) {
+    value += 5;
+  }
+
+  SECTION("test writing annotations")
+  {
+    // setup io object
+    std::string path = getTestFilePath("AnnotationSeries.h5");
+    std::shared_ptr<BaseIO> io = createIO("HDF5", path);
+    io->open();
+
+    // setup annotation series
+    NWB::AnnotationSeries as = NWB::AnnotationSeries(dataPath, io);
+    as.initialize(
+        "Test annotations", "Test comments", SizeArray {0}, SizeArray {1});
+
+    // write annotations multiple times to test adding to same dataset
+    Status writeStatus =
+        as.writeAnnotation(numSamples, mockAnnotations, mockTimestamps.data());
+    REQUIRE(writeStatus == Status::Success);
+    Status writeStatus2 =
+        as.writeAnnotation(numSamples, mockAnnotations, mockTimestamps2.data());
+    REQUIRE(writeStatus2 == Status::Success);
+    io->flush();
+
+    // Read annotations back from file
+    std::vector<std::string> expectedAnnotations = mockAnnotations;
+    expectedAnnotations.insert(expectedAnnotations.end(),
+                               mockAnnotations.begin(),
+                               mockAnnotations.end());
+    std::vector<std::string> dataOut(expectedAnnotations.size());
+
+    auto readDataWrapper = as.readData();
+    auto readAnnotationsDataTyped = readDataWrapper->values();
+    REQUIRE(readAnnotationsDataTyped.data == expectedAnnotations);
+
+    // Read timestamps
+    std::vector<double> expectedTimestamps = mockTimestamps;
+    expectedTimestamps.insert(expectedTimestamps.end(),
+                              mockTimestamps2.begin(),
+                              mockTimestamps2.end());
+    std::vector<double> timestampsOut(expectedTimestamps.size());
+
+    auto readTimestampsWrapper = as.readTimestamps();
+    auto readTimestampsDataTyped = readTimestampsWrapper->values();
+    REQUIRE_THAT(readTimestampsDataTyped.data,
+                 Catch::Matchers::Approx(expectedTimestamps));
+  }
+}
diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp
index 35939c3f..9bbd8f29 100644
--- a/tests/testNWBFile.cpp
+++ b/tests/testNWBFile.cpp
@@ -10,6 +10,7 @@
 #include "nwb/RecordingContainers.hpp"
 #include "nwb/base/TimeSeries.hpp"
 #include "nwb/ecephys/SpikeEventSeries.hpp"
+#include "nwb/misc/AnnotationSeries.hpp"
 #include "testUtils.hpp"
 
 using namespace AQNWB;
@@ -164,6 +165,60 @@ TEST_CASE("createMultipleEcephysDatasets", "[nwb]")
   nwbfile.finalize();
 }
 
+TEST_CASE("createAnnotationSeries", "[nwb]")
+{
+  std::string filename = getTestFilePath("createAnnotationSeries.nwb");
+
+  // initialize nwbfile object and create base structure
+  std::shared_ptr<IO::HDF5::HDF5IO> io =
+      std::make_shared<IO::HDF5::HDF5IO>(filename);
+  io->open();
+  NWB::NWBFile nwbfile(io);
+  nwbfile.initialize(generateUuid());
+
+  // create Annotation Series
+  std::vector<std::string> mockAnnotationNames = {"annotations1",
+                                                  "annotations2"};
+  std::unique_ptr<NWB::RecordingContainers> recordingContainers =
+      std::make_unique<NWB::RecordingContainers>();
+  Status resultCreate = nwbfile.createAnnotationSeries(
+      mockAnnotationNames, recordingContainers.get());
+  REQUIRE(resultCreate == Status::Success);
+
+  // start recording
+  Status resultStart = io->startRecording();
+  REQUIRE(resultStart == Status::Success);
+
+  // write annotation data
+  std::vector<std::string> mockAnnotations = {
+      "Start recording", "Subject moved", "End recording"};
+  std::vector<double> mockTimestamps = {0.1, 0.5, 1.0};
+  std::vector<SizeType> positionOffset = {0};
+  SizeType dataShape = mockAnnotations.size();
+
+  // write to both annotation series
+  recordingContainers->writeAnnotationSeriesData(
+      0, dataShape, mockAnnotations, mockTimestamps.data());
+  recordingContainers->writeAnnotationSeriesData(
+      1, dataShape, mockAnnotations, mockTimestamps.data());
+
+  // test searching for all AnnotationSeries objects
+  std::unordered_set<std::string> typesToSearch = {"core::AnnotationSeries"};
+  std::unordered_map<std::string, std::string> found_types =
+      io->findTypes("/", typesToSearch, IO::SearchMode::CONTINUE_ON_TYPE);
+  REQUIRE(found_types.size()
+          == 2);  // We should have annotations1 and annotations2
+  for (const auto& pair : found_types) {
+    // only AnnotationSeries should be found
+    REQUIRE(pair.second == "core::AnnotationSeries");
+    // only annotations1 or annotations2 should be in the list
+    REQUIRE(((pair.first == "/acquisition/annotations1")
+             || (pair.first == "/acquisition/annotations2")));
+  }
+
+  nwbfile.finalize();
+}
+
 TEST_CASE("setCanModifyObjectsMode", "[nwb]")
 {
   std::string filename = getTestFilePath("testCanModifyObjectsMode.nwb");

From 5100ad21a1ef15cfe90b8d179c3ac2b450fe83c1 Mon Sep 17 00:00:00 2001
From: Oliver Ruebel <oruebel@users.noreply.github.com>
Date: Wed, 22 Jan 2025 16:43:09 -0800
Subject: [PATCH 3/5] Update docs/pages/userdocs/read.dox

Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com>
---
 docs/pages/userdocs/read.dox | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/pages/userdocs/read.dox b/docs/pages/userdocs/read.dox
index 38156850..10f27958 100644
--- a/docs/pages/userdocs/read.dox
+++ b/docs/pages/userdocs/read.dox
@@ -308,7 +308,7 @@
  * - \ref AQNWB::IO::BaseIO "BaseIO", \ref AQNWB::IO::HDF5::HDF5IO "HDF5IO" are responsible for
  *   i) reading type attribute and group information, ii) searching the file for typed objects via
  *   \ref AQNWB::IO::BaseIO::findTypes "findTypes()" methods, and iii) retrieving the paths of all
- *   object associated with a storage objects (e.g., a Group) via  \ref AQNWB::IO::BaseIO::getStorageObjects "getStoragebjects()"
+ *   object associated with a storage object (e.g., a Group) via  \ref AQNWB::IO::BaseIO::getStorageObjects "getStorageObjects()"
  *
  * \subsubsection read_design_wrapper_registeredType RegisteredType
  *

From 67bc2cd483862f15ead95bbad59409c923cee891 Mon Sep 17 00:00:00 2001
From: Oliver Ruebel <oruebel@users.noreply.github.com>
Date: Wed, 22 Jan 2025 16:43:25 -0800
Subject: [PATCH 4/5] Update src/Types.hpp

Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com>
---
 src/Types.hpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Types.hpp b/src/Types.hpp
index 17ebbdfc..6d45f767 100644
--- a/src/Types.hpp
+++ b/src/Types.hpp
@@ -39,7 +39,7 @@ class Types
    *  \brief Helper struct to check if a value is a data field, i.e.,
    * Dataset or Attribute
    *
-   * This function is used to enforce constrains on templated functions that
+   * This function is used to enforce constraints on templated functions that
    * should only be callable for valid StorageObjectType values
    */
   template<StorageObjectType T>

From 19fecb4225c58b70e8448124878a537fd8b72d92 Mon Sep 17 00:00:00 2001
From: Oliver Ruebel <oruebel@lbl.gov>
Date: Wed, 22 Jan 2025 23:30:16 -0800
Subject: [PATCH 5/5] Add additional test case for HDF5IO::getStorageObjects

---
 tests/testHDF5IO.cpp | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/tests/testHDF5IO.cpp b/tests/testHDF5IO.cpp
index 72aacf11..f50831f9 100644
--- a/tests/testHDF5IO.cpp
+++ b/tests/testHDF5IO.cpp
@@ -142,6 +142,38 @@ TEST_CASE("getStorageObjects", "[hdf5io]")
     REQUIRE(groupContent.size() == 0);
   }
 
+  SECTION("attribute")
+  {
+    int attrData1 = 42;
+    hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/", "attr1");
+    auto attributeContent = hdf5io.getStorageObjects("/attr1");
+    REQUIRE(attributeContent.size() == 0);
+  }
+
+  SECTION("dataset w/o attribute")
+  {
+    // Dataset without attributes
+    hdf5io.createArrayDataSet(
+        BaseDataType::I32, SizeArray {0}, SizeArray {1}, "/data");
+    auto datasetContent = hdf5io.getStorageObjects("/data");
+    REQUIRE(datasetContent.size() == 0);
+
+    // Dataset with attribute
+    int attrData1 = 42;
+    hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/data", "attr1");
+    auto dataContent2 = hdf5io.getStorageObjects("/data");
+    REQUIRE(dataContent2.size() == 1);
+    REQUIRE(
+        dataContent2[0]
+        == std::make_pair(std::string("attr1"), StorageObjectType::Attribute));
+  }
+
+  SECTION("invalid path")
+  {
+    auto invalidPathContent = hdf5io.getStorageObjects("/invalid/path");
+    REQUIRE(invalidPathContent.size() == 0);
+  }
+
   SECTION("group with datasets, subgroups, and attributes")
   {
     hdf5io.createGroup("/data");