Skip to content

Commit

Permalink
feature: List Contents of File
Browse files Browse the repository at this point in the history
Added the ability to list the groups and channels in the file.

Refs: #35
  • Loading branch information
JamesMc86 committed Dec 7, 2023
1 parent 31bfa8e commit 986eb66
Show file tree
Hide file tree
Showing 6 changed files with 517 additions and 10 deletions.
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ pub enum TdmsError {
SegmentAddressOverflow,
#[error("The segment ToC expects a data block but no data channels are present. The file is likely corrupt.")]
SegmentTocDataBlockWithoutDataChannels,
#[error("Attempted to parse an invalid object path. {0}")]
InvalidObjectPath(String),
#[error("Attempted to parse an valid but unsuitable path to a channel. {0}")]
InvalidChannelPath(String),
}
179 changes: 177 additions & 2 deletions src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ use std::{
path::Path,
};

use crate::index::Index;
use crate::io::writer::{LittleEndianWriter, TdmsWriter};
use crate::meta_data::Segment;
use crate::{error::TdmsError, PropertyPath, PropertyValue};
use crate::{index::Index, ChannelPath};
use crate::{
io::writer::{LittleEndianWriter, TdmsWriter},
paths::path_group_name,
};
pub use file_writer::TdmsFileWriter;

/// A TDMS file.
Expand Down Expand Up @@ -115,6 +118,38 @@ impl<F: Write + Read + Seek + std::fmt::Debug> TdmsFile<F> {
self.index.get_object_properties(object_path)
}

/// Read all groups in the file.
///
/// Returns an iterator to the paths for each group.
pub fn read_groups<'a>(&'a self) -> impl Iterator<Item = PropertyPath> + 'a {
// We cannot guarantee a seperate path for the group has been written
// as they are implicitly included in the channel path as well.
// Therefore extract all possible group names from all paths and deduplicate.
// Use a btreeset to deduplicate the paths.
let mut groups = std::collections::BTreeSet::new();

let paths = self.index.all_paths();
for path in paths {
let group_name = path_group_name(path);
if let Some(group_name) = group_name {
groups.insert(group_name);
}
}

groups.into_iter().map(PropertyPath::group)
}

/// Read all the channels in a group.
///
/// Returns an iterator to the paths for each channel.
pub fn read_channels_in_group<'a: 'c, 'b: 'c, 'c>(
&'a self,
group: &'b PropertyPath,
) -> impl Iterator<Item = ChannelPath> + 'c {
let paths = self.index.paths_starting_with(group.path());
paths.filter_map(|path| ChannelPath::try_from(path).ok())
}

/// Get a writer for the TDMS data so that you can write data.
///
/// While this is in use you will not be able to access the read API.
Expand Down Expand Up @@ -153,13 +188,153 @@ mod tests {

use std::io::Cursor;

use crate::DataLayout;

use super::*;

fn new_empty_file() -> TdmsFile<Cursor<Vec<u8>>> {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
TdmsFile::new(cursor).unwrap()
}

#[test]
fn test_can_load_empty_buffer() {
let buffer = Vec::new();
let mut cursor = Cursor::new(buffer);
let result = build_index(&mut cursor);
assert!(result.is_ok());
}

#[test]
fn test_list_groups_with_properties_single() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_properties(
&PropertyPath::group("group"),
&[("name", PropertyValue::String("my_channel".to_string()))],
)
.unwrap();

drop(writer);
let groups: Vec<_> = file.read_groups().collect();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], PropertyPath::group("group"));
}

#[test]
fn test_list_groups_with_properties_multiple() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_properties(
&PropertyPath::group("group"),
&[("name", PropertyValue::String("my_channel".to_string()))],
)
.unwrap();
writer
.write_properties(
&PropertyPath::group("group2"),
&[("name", PropertyValue::String("my_channel".to_string()))],
)
.unwrap();

drop(writer);
let groups: Vec<_> = file.read_groups().collect();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0], PropertyPath::group("group"));
assert_eq!(groups[1], PropertyPath::group("group2"));
}

#[test]
fn test_list_implicit_groups_from_channels() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_channels(
&[ChannelPath::new("group", "channel")],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
DataLayout::Interleaved,
)
.unwrap();

drop(writer);
let groups: Vec<_> = file.read_groups().collect();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0], PropertyPath::group("group"));
}

#[test]
fn test_list_channels_in_group_single() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_channels(
&[ChannelPath::new("group", "channel")],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
DataLayout::Interleaved,
)
.unwrap();

drop(writer);
let channels: Vec<_> = file
.read_channels_in_group(&PropertyPath::group("group"))
.collect();
assert_eq!(channels.len(), 1);
assert_eq!(channels[0], ChannelPath::new("group", "channel"));
}

#[test]
fn test_list_channels_in_group_multiple() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_channels(
&[ChannelPath::new("group", "channel")],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
DataLayout::Interleaved,
)
.unwrap();
writer
.write_channels(
&[ChannelPath::new("group", "channel2")],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
DataLayout::Interleaved,
)
.unwrap();

drop(writer);
let channels: Vec<_> = file
.read_channels_in_group(&PropertyPath::group("group"))
.collect();
assert_eq!(channels.len(), 2);
assert_eq!(channels[0], ChannelPath::new("group", "channel"));
assert_eq!(channels[1], ChannelPath::new("group", "channel2"));
}

#[test]
fn test_list_channels_in_group_none() {
let mut file = new_empty_file();

let mut writer = file.writer().unwrap();
writer
.write_channels(
&[ChannelPath::new("group", "channel")],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
DataLayout::Interleaved,
)
.unwrap();

drop(writer);
let channels: Vec<_> = file
.read_channels_in_group(&PropertyPath::group("group2"))
.collect();
assert_eq!(channels.len(), 0);
}
}
6 changes: 3 additions & 3 deletions src/index/building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
raw_data::DataBlock,
};

use super::{DataFormat, DataLocation, ObjectData, Objectindex};
use super::{DataFormat, DataLocation, ObjectData, ObjectIndex};

/// Data cached for the current "active" objects which are the objects
/// that we are expecting data in the next data block.
Expand Down Expand Up @@ -40,14 +40,14 @@ impl ActiveObject {
}

/// Fetch the corresponding [`ObjectData`] for the active object.
fn get_object_data<'c>(&self, index: &'c Objectindex) -> &'c ObjectData {
fn get_object_data<'c>(&self, index: &'c ObjectIndex) -> &'c ObjectData {
index
.get(&self.path)
.expect("Should always have a registered version of active object")
}

/// Fetch the corresponding [`ObjectData`] for the active object in a mutable form.
fn get_object_data_mut<'c>(&self, index: &'c mut Objectindex) -> &'c mut ObjectData {
fn get_object_data_mut<'c>(&self, index: &'c mut ObjectIndex) -> &'c mut ObjectData {
index
.get_mut(&self.path)
.expect("Should always have a registered version of active object")
Expand Down
10 changes: 5 additions & 5 deletions src/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod building;
mod querying;
mod writing;

use std::collections::HashMap;
use std::collections::BTreeMap;

use crate::error::TdmsError;
use crate::meta_data::{ObjectMetaData, RawDataIndex, RawDataMeta};
Expand Down Expand Up @@ -56,7 +56,7 @@ impl From<DataFormat> for RawDataIndex {
#[derive(Clone, PartialEq, Debug)]
struct ObjectData {
path: String,
properties: HashMap<String, PropertyValue>,
properties: BTreeMap<String, PropertyValue>,
data_locations: Vec<DataLocation>,
latest_data_format: Option<DataFormat>,
}
Expand All @@ -66,7 +66,7 @@ impl ObjectData {
fn from_metadata(meta: &ObjectMetaData) -> Self {
let mut new = Self {
path: meta.path.clone(),
properties: HashMap::new(),
properties: BTreeMap::new(),
data_locations: vec![],
latest_data_format: None,
};
Expand Down Expand Up @@ -105,12 +105,12 @@ impl ObjectData {
}

/// The inner format for registering the objects.
type Objectindex = HashMap<String, ObjectData>;
type ObjectIndex = BTreeMap<String, ObjectData>;

#[derive(Default, Debug, Clone)]
pub struct Index {
active_objects: Vec<building::ActiveObject>,
objects: Objectindex,
objects: ObjectIndex,
data_blocks: Vec<DataBlock>,
next_segment_start: u64,
}
Expand Down
Loading

0 comments on commit 986eb66

Please sign in to comment.