diff --git a/src/GNDStk/HDF5.hpp b/src/GNDStk/HDF5.hpp index 52949a9e7..802defb0c 100644 --- a/src/GNDStk/HDF5.hpp +++ b/src/GNDStk/HDF5.hpp @@ -9,6 +9,9 @@ class HDF5 { public: + static inline bool flat = true; + static inline bool typed = true; + // data HighFive::File *filePtr = nullptr; std::string fileName = ""; diff --git a/src/GNDStk/HDF5/src/write.hpp b/src/GNDStk/HDF5/src/write.hpp index 7f6df7c94..51f9c9b42 100644 --- a/src/GNDStk/HDF5/src/write.hpp +++ b/src/GNDStk/HDF5/src/write.hpp @@ -17,7 +17,7 @@ std::ostream &write(std::ostream &os, const bool decl = true) const if (empty()) { // This HDF5 object is empty. We'll place "stub" HDF5 output into // a temporary file, then copy the file's contents to the ostream. - // A temporary is used here, as it is elsewhere in our HDF handling, + // A temporary is used here, as it is elsewhere in our HDF5 handling, // because HighFive deals directly with files, not with streams. int stubDesc; const std::string stubName = temporaryName(stubDesc); diff --git a/src/GNDStk/Node/test/ctor.test.cpp b/src/GNDStk/Node/test/ctor.test.cpp index d36c8791f..b0b441d7d 100644 --- a/src/GNDStk/Node/test/ctor.test.cpp +++ b/src/GNDStk/Node/test/ctor.test.cpp @@ -111,8 +111,8 @@ SCENARIO("Testing GNDStk Node constructors") { // ------------------------ WHEN("A Node is constructed from just a name (no metadata/children)") { - Node n("NodeName"); - CHECK(n.name == "NodeName"); + Node n("MyName"); + CHECK(n.name == "MyName"); CHECK(n.metadata.size() == 0); CHECK(n.children.size() == 0); } diff --git a/src/GNDStk/convert/src/HDF5.hpp b/src/GNDStk/convert/src/HDF5.hpp index 2fedf3e25..de2c2d9a9 100644 --- a/src/GNDStk/convert/src/HDF5.hpp +++ b/src/GNDStk/convert/src/HDF5.hpp @@ -21,7 +21,7 @@ inline bool convert(const Node &node, HDF5 &h, const std::string &name) // Probably a regular Node... if (node.name != "") { - const bool ret = detail::node2hdf5(node,*h.filePtr); + const bool ret = detail::Node2HDF5(node,*h.filePtr); h.filePtr->flush(); return ret; } @@ -67,7 +67,7 @@ inline bool convert(const Node &node, HDF5 &h, const std::string &name) ); log::function(context); } - const bool ret = detail::node2hdf5(*c,*h.filePtr); + const bool ret = detail::Node2HDF5(*c,*h.filePtr); h.filePtr->flush(); if (!ret) return false; diff --git a/src/GNDStk/convert/src/Tree.hpp b/src/GNDStk/convert/src/Tree.hpp index d6abf4f00..8518ad6ce 100644 --- a/src/GNDStk/convert/src/Tree.hpp +++ b/src/GNDStk/convert/src/Tree.hpp @@ -252,7 +252,7 @@ inline bool convert(const HDF5 &h, Node &node, const bool decl) ? &node.add("#hdf5") // indicates that we built the object from an HDF5 : nullptr; - // empty hdf5 document? + // empty HDF5 document? if (h.empty()) return true; @@ -267,11 +267,11 @@ inline bool convert(const HDF5 &h, Node &node, const bool decl) // into the Node's "#hdf5" child that would have been created above if (decl) for (auto &attrName : group.listAttributeNames()) - if (!detail::hdf5attr2node(group.getAttribute(attrName),*declnode)) + if (!detail::HDF5attr2Node(group.getAttribute(attrName),*declnode)) return false; // visit the rest of "/" - if (!detail::hdf52node(group, "/", node, !decl)) + if (!detail::HDF52Node(group, "/", node, !decl)) return false; } catch (...) { log::function("convert(HDF5,Node)"); diff --git a/src/GNDStk/convert/src/detail-hdf52node.hpp b/src/GNDStk/convert/src/detail-hdf52node.hpp new file mode 100644 index 000000000..dad950ab4 --- /dev/null +++ b/src/GNDStk/convert/src/detail-hdf52node.hpp @@ -0,0 +1,276 @@ + +// ----------------------------------------------------------------------------- +// HDF5attr2Node +// For HighFive::Attribute +// ----------------------------------------------------------------------------- + +// Helper: attrTYPE2node +template +bool attrTYPE2node(const HighFive::Attribute &attr, NODE &node) +{ + if (attr.getDataType() == HighFive::AtomicType{}) { + const std::string attrName = attr.getName(); + const std::size_t attrSize = attr.getSpace().getElementCount(); + + // Scalar case. Includes bool. + if (attrSize == 1) { + T scalar; + attr.read(scalar); + node.add(attrName,scalar); + return true; + } + + // Vector case. EXcludes bool, as HighFive (perhaps HDF5 in general?) + // doesn't appear to support it in this case. Indeed, the body of the + // if-constexpr doesn't *compile* with bool. (Hence our if-constexpr.) + if constexpr (!std::is_same_v) { + std::vector vector; + vector.reserve(attrSize); + attr.read(vector); + node.add(attrName,vector); + return true; + } + } + + return false; +} + +// HDF5attr2Node +template +bool HDF5attr2Node(const HighFive::Attribute &attr, NODE &node) +{ + // HighFive's documentation leaves much to be desired. I used what I found + // in HighFive/include/highfive/bits/H5DataType_misc.hpp to get an idea of + // what attribute *types* are allowed. That file also had some handling of + // C-style fixed-length strings, as with char[length], and std::complex as + // well. It didn't have long double, which I'd have liked, but that's fine. + // I won't bother with fixed-length strings or with std::complex right now, + // but will support the rest. + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + if (attrTYPE2node(attr,node)) return true; + + log::error( + "HDF5 Attribute \"{}\"'s DataType \"{}\" is not handled at this time.", + attr.getName(), attr.getDataType().string()); + log::function("HDF5attr2Node(HighFive::Attribute, Node)"); + + return false; +} + + +// ----------------------------------------------------------------------------- +// HDF5data2node +// For HighFive::DataSet +// ----------------------------------------------------------------------------- + +// Helper: dataTYPE2node +template +bool dataTYPE2node(const HighFive::DataSet &data, NODE &node) +{ + if (data.getDataType() == HighFive::AtomicType{}) { + // Remarks as in the similar helper function attrTYPE2node()... + const std::size_t dataSize = data.getElementCount(); + + if (dataSize == 1) { + T scalar; + data.read(scalar); + node.add("#pcdata").add("#text",scalar); + return true; + } + + if constexpr (!std::is_same_v) { + std::vector vector; + vector.reserve(dataSize); + data.read(vector); + node.add("#pcdata").add("#text",vector); + return true; + } + } + + return false; +} + +// HDF5data2node +template +bool HDF5data2node( + const HighFive::DataSet &data, const std::string &dataName, + NODE &node +) { + // node name + node.name = dataName; + + // the data set's attributes + for (auto &attrName : data.listAttributeNames()) + if (!HDF5attr2Node(data.getAttribute(attrName), node)) + return false; + + // the data set's data + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + if (dataTYPE2node(data,node)) return true; + + log::error( + "HDF5 DataSet \"{}\"'s DataType \"{}\" is not handled at this time.", + dataName, data.getDataType().string()); + log::function("HDF5data2node(HighFive::DataSet, dataName, Node)"); + + return false; +} + + +// ----------------------------------------------------------------------------- +// HDF52Node +// ----------------------------------------------------------------------------- + +// Helper: error_HDF52Node +inline void error_HDF52Node(const std::string &message) +{ + log::error( + "Internal error in HDF52Node(HighFive::Group, std::string, Node):\n" + "Message: \"{}\".", + message + ); + throw std::exception{}; +} + +// HDF52Node +template +bool HDF52Node( + const HighFive::Group &group, const std::string &groupName, + NODE &node, const bool requireEmpty = true +) { + static const std::string context = + "HDF52Node(HighFive::Group, std::string, Node)"; + + // The node sent here should be fresh, ready to receive entries + if (requireEmpty && !node.empty()) + error_HDF52Node("!node.empty()"); + + // ------------------------ + // HDF5 group name + // ==> node name + // ------------------------ + + // if "/" then we're at the top-level node, which we call "" internally + if (groupName != "/") + node.name = groupName; + + // ------------------------ + // HDF5 attributes + // ==> metadata + // ------------------------ + + // if "/" then attributes were handled, in a special way, by the caller + if (groupName != "/") + for (auto &attrName : group.listAttributeNames()) + if (!HDF5attr2Node(group.getAttribute(attrName), node)) + return false; + + // ------------------------ + // HDF5 sub-groups + // ==> children + // ------------------------ + + for (auto &elemName : group.listObjectNames()) { + switch (group.getObjectType(elemName)) { + + // File + // NOT EXPECTED IN THIS CONTEXT + case HighFive::ObjectType::File : + error_HDF52Node("ObjectType \"File\" not expected here"); + break; + + // Group + // ACTION: call the present function recursively + case HighFive::ObjectType::Group : + try { + if (!HDF52Node(group.getGroup(elemName), elemName, node.add())) + return false; + } catch (...) { + log::function(context); + throw; + } + break; + + // UserDataType + // NOT HANDLED; perhaps we could provide something in the future + case HighFive::ObjectType::UserDataType : + error_HDF52Node("ObjectType \"UserDataType\" not handled"); + break; + + // DataSpace (not to be confused with Dataset) + // NOT EXPECTED IN THIS CONTEXT + case HighFive::ObjectType::DataSpace : + error_HDF52Node("ObjectType \"DataSpace\" not expected here"); + break; + + // Dataset + // ACTION: handle the DataSet's data + case HighFive::ObjectType::Dataset : + try { + if (!HDF5data2node( + group.getDataSet(elemName), + elemName, + node.add() + )) + return false; + } catch (...) { + log::function(context); + throw; + } + break; + + // Attribute + // NOT EXPECTED IN THIS CONTEXT + case HighFive::ObjectType::Attribute : + // group.listObjectNames() (used in the for-loop we're in right + // now) apparently doesn't include attribute names - which is fine, + // because we already handled attributes earlier. So, here, we just + // produce an error if ObjectType::Attribute inexplicably made an + // appearance here, where we don't expect it. + error_HDF52Node("ObjectType \"Attribute\" not expected here"); + break; + + // Other + // NOT HANDLED; we're not sure when this would arise + case HighFive::ObjectType::Other : + error_HDF52Node("ObjectType \"Other\" not handled"); + break; + + // default + // NOT HANDLED; presumably our switch has covered all bases already + default: + error_HDF52Node("ObjectType [unknown] not handled"); + break; + + } // switch + } // for + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail-json2node.hpp b/src/GNDStk/convert/src/detail-json2node.hpp new file mode 100644 index 000000000..56f2b7aab --- /dev/null +++ b/src/GNDStk/convert/src/detail-json2node.hpp @@ -0,0 +1,91 @@ + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// error_json2node +inline void error_json2node(const std::string &message) +{ + log::error( + "Internal error in json2node(nlohmann::ordered_json, Node):\n" + "Message: \"{}\".", + message + ); + throw std::exception{}; +} + + +// ----------------------------------------------------------------------------- +// json2node +// ----------------------------------------------------------------------------- + +// nlohmann::ordered_json::const_iterator ==> Node +// Why the iterator rather than the ordered_json object? I found that there +// were some seemingly funny semantics in the nlohmann JSON library. As we +// can see below, we have for example iter->is_object() (so, the -> operator, +// typical for iterators), but also iter.value() (the . operator - on an +// iterator). Similarly, also seen below, with the sub-elements. This is why +// we are, for now, writing our for-loops, here as well as in the functions +// that call this, in the older iterator form rather than the range-based-for +// form. Perhaps there's a way to reformulate all this in a shorter way, but +// this is what we have for now. + +template +bool json2node(const nlohmann::ordered_json::const_iterator &iter, NODE &node) +{ + static const std::string context = + "json2node(nlohmann::ordered_json::const_iterator, Node)"; + + // the node sent here should be fresh, ready to receive entries... + if (!node.empty()) + error_json2node("!node.empty()"); + + // non-object cases should have been handled + // before any caller calls this function... + if (!iter->is_object()) + error_json2node("!iter->is_object()"); + + // any "#attributes" key (a specially-named "child node" that we use in JSON + // in order to identify attributes) should have been handled in the caller... + if (iter.key() == "#attributes") + error_json2node("iter.key() == \"#attributes\""); + + // key,value ==> node name, JSON value to bring in + node.name = iter.key(); + const nlohmann::ordered_json &json = iter.value(); + + // elements + for (auto elem = json.begin(); elem != json.end(); ++elem) { + if (elem.key() == "#nodeName") { + // #nodeName? ...extract as current node's true name + node.name = elem->get(); + } else if (elem.key() == "#attributes") { + // #attributes? ...extract as current node's metadata + const auto &jsub = elem.value(); + for (auto attr = jsub.begin(); attr != jsub.end(); ++attr) + node.add(attr.key(), attr->get()); + } else if (elem->is_string()) { + // string? ...extract as metadata key/value pair + node.add(elem.key(), elem->get()); + } else if (elem->is_object()) { + // {} object? ...extract as normal child node + try { + if (!json2node(elem,node.add())) + return false; + } catch (...) { + log::function(context); + throw; + } + } else if (elem->is_null()) { + // null node? ...extract as normal (albeit empty) child node + // In GNDS, e.g. XML's or + node.add().name = elem.key(); + } else { + // no other cases are handled right now + error_json2node("unhandled JSON value type"); + } + } + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail-node2hdf5.hpp b/src/GNDStk/convert/src/detail-node2hdf5.hpp new file mode 100644 index 000000000..02e2e9614 --- /dev/null +++ b/src/GNDStk/convert/src/detail-node2hdf5.hpp @@ -0,0 +1,277 @@ + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// scalarAttr +template +HighFive::Attribute scalarAttr( + const std::string &key, const std::string &value, + OBJECT &hdf +) { + T scalar; + convert(value,scalar); + return hdf.createAttribute(key,scalar); +} + +// vectorAttr +template +HighFive::Attribute vectorAttr( + const std::string &key, const std::string &value, + OBJECT &hdf +) { + std::vector vector; + convert(value,vector); + return hdf.createAttribute(key,vector); +} + +// vecDataSet +template +HighFive::DataSet vecDataSet( + const std::string &key, const std::string &value, + OBJECT &hdf +) { + std::vector vector; + convert(value,vector); + return hdf.createDataSet(key,vector); +} + +// vecDataSet, w/ type string +template +HighFive::DataSet vecDataSet( + const std::string &key, const std::string &value, + OBJECT &hdf +) { + const std::string type = guessType(value); + return + type == "int" || type == "ints" ? + vecDataSet(key,value,hdf) : + type == "uint" || type == "uints" ? + vecDataSet(key,value,hdf) : + type == "long" || type == "longs" ? + vecDataSet(key,value,hdf) : + type == "ulong" || type == "ulongs" ? + vecDataSet(key,value,hdf) : + type == "double" || type == "doubles" ? + vecDataSet(key,value,hdf) : + // "string", "strings", or "" + vecDataSet(key,value,hdf) ; +} + + +// ----------------------------------------------------------------------------- +// typedHDF5 +// ----------------------------------------------------------------------------- + +template +void typedHDF5(const NODE &node, OBJECT &hdf) +{ + const std::string &parent = node.name; + + for (auto &meta : node.metadata) { + const std::string &key = meta.first; + const std::string &value = meta.second; + + // *** #cdata/#text + // *** #comment/#text + // ACTION: Write these as-is. That is, do NOT apply our type-guessing code + // to a comment, or to the contents of a block like those + // that we see in existing XML-format GNDS files. The type guesser would + // see words, and think "vector of [whitespace-separated] strings," which + // would be painfully wrong for what are clearly free-form strings. + if (key == "#text" && (parent == "#cdata" || parent == "#comment")) { + hdf.createAttribute(key,value); // just a simple string attribute + continue; + } + + // *** #pcdata/#text + // ACTION: Apply our type-guessing code, but write *vectors* only, never + // scalars. So, 10 would be interpreted as a vector with + // one element, NOT as a scalar; while 10 20 30 would be + // interpreted as a vector with three elements. What may look like scalars + // are folded into vectors because we think that reflects what nodes like + // this are intended to represent. (If something was really just a scalar, + // then surely it would be placed in the metadata, not in a node such as + // scalar.) + if constexpr (std::is_same_v) { + if (key == "#text" && parent == "#pcdata") { + vecDataSet(key,value,hdf); // DataSet will have name "#text" + continue; + } + } + + // *** key/#text not expected except as already handled + if (key == "#text") { + log::warning("Metadatum name \"#text\" not expected here; " + "writing anyway"); + log::function("detail::typedHDF5(Node named \"{}\", ...)", parent); + } + + // *** key/value + // General case + // ACTION: Apply our type-guessing code almost fully, except that for + // a metadatum="that looks like this", interpret "..." as essentially + // a single descriptive string. So, don't split it up into a vector. + const std::string type = guessType(value); + type == "int" ? scalarAttr(key,value,hdf) : + type == "ints" ? vectorAttr(key,value,hdf) : + type == "uint" ? scalarAttr(key,value,hdf) : + type == "uints" ? vectorAttr(key,value,hdf) : + type == "long" ? scalarAttr(key,value,hdf) : + type == "longs" ? vectorAttr(key,value,hdf) : + type == "ulong" ? scalarAttr(key,value,hdf) : + type == "ulongs" ? vectorAttr(key,value,hdf) : + type == "double" ? scalarAttr(key,value,hdf) : + type == "doubles" ? vectorAttr(key,value,hdf) : + /* else........ */ scalarAttr(key,value,hdf) ; + } +} + + +// ----------------------------------------------------------------------------- +// Meta2HDF5 +// ----------------------------------------------------------------------------- + +// Here, OBJECT hdf is either a HighFive::Group or a HighFive::DataSet +template +void Meta2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix) +{ + // #nodeName if appropriate (as with JSON, allows recovery of original name) + if (suffix != "") + hdf.createAttribute(std::string("#nodeName"), node.name); + + // Existing attributes + if (HDF5::typed) { + // Use our "guess what's in the string" code to try to infer what certain + // "string" values actually contain (a single int, say, or a vector of + // doubles). Then, use the inferred types in the HDF5 file. + typedHDF5(node,hdf); + } else { + // Write simple HDF5 where all data (metadata, and the content of "cdata" + // and "pcdata" nodes) end up being strings. Not even vectors of strings + // (as from H He Li ...), but single strings. + for (auto &meta : node.metadata) + hdf.createAttribute(meta.first, meta.second); + } +} + + +// ----------------------------------------------------------------------------- +// Node2HDF5 +// ----------------------------------------------------------------------------- + +// Here, OBJECT hdf is either a HighFive::File or a HighFive::Group +template +bool Node2HDF5(const NODE &node, OBJECT &hdf, const std::string &suffix = "") +{ + // As with JSON; see the remark in node2json() + const std::string nameOriginal = node.name; + const std::string nameSuffixed = node.name + suffix; + + // ------------------------ + // Specific cases + // ------------------------ + + if (HDF5::flat) { + // #cdata or #comment + // #text the only metadatum + // no children + // Reduce to: a string attribute, w/name == (#cdata or #comment) + suffix + // Brief: + // +-----------------+ +-----------+ + // | #cdata/#comment | ==> | Attribute | + // | #text | | value | + // | - | +-----------+ + // +-----------------+ + // + if ( + (nameOriginal == "#cdata" || nameOriginal == "#comment") && + node.children.size() == 0 && + node.metadata.size() == 1 && + node.metadata[0].first == "#text" + ) { + // attribute + hdf.createAttribute(nameSuffixed,node.metadata[0].second); + return true; + } + + // #pcdata + // #text the only metadatum + // no children + // Reduce to: a data set, w/name == #pcdata + suffix + // Brief: + // +----------+ +---------+ + // | #pcdata | ==> | DataSet | + // | #text | | data | + // +----------+ +---------+ + // + if (nameOriginal == "#pcdata" && + node.children.size() == 0 && + node.metadata.size() == 1 && + node.metadata[0].first == "#text" + ) { + // dataset + const std::string value = node.metadata[0].second; + HighFive::DataSet dataset = vecDataSet(nameSuffixed,value,hdf); + return true; + } + + // someName (think e.g. values, as in XML ) + // any number of metadata + // #pcdata the only child + // #text the only metadatum + // no children + // Reduce to: a data set, w/name == someName + suffix + // Brief: + // +--------------+ +---------------+ + // | someName | ==> | DataSet | + // | metadata | | Attributes | + // | #pcdata | | data | + // | #text | +---------------+ + // | - | + // +--------------+ + // + if (node.children.size() == 1 && + node.children[0]->name == "#pcdata" && + node.children[0]->metadata.size() == 1 && + node.children[0]->metadata[0].first == "#text" && + node.children[0]->children.size() == 0 + ) { + // dataset + const std::string value = node.children[0]->metadata[0].second; + HighFive::DataSet dataset = vecDataSet(nameSuffixed,value,hdf); + // metadata, then done; the #pcdata child was rolled into the data set + Meta2HDF5(node,dataset,suffix); + return true; + } + } + + // ------------------------ + // General case + // ------------------------ + + // subgroup + HighFive::Group group = hdf.createGroup(nameSuffixed); + + // metadata + Meta2HDF5(node,group,suffix); + + // children + // Logic is as with JSON; see the remark in node2json(). + std::map childNames; + for (auto &c : node.children) { + auto iter = childNames.find(c->name); + if (iter == childNames.end()) + childNames.insert({c->name,0}); + else + iter->second = 1; + } + for (auto &c : node.children) { + const std::size_t counter = childNames.find(c->name)->second++; + if (!Node2HDF5(*c, group, counter ? std::to_string(counter-1) : "")) + return false; + } + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail-node2json.hpp b/src/GNDStk/convert/src/detail-node2json.hpp new file mode 100644 index 000000000..2012d8d20 --- /dev/null +++ b/src/GNDStk/convert/src/detail-node2json.hpp @@ -0,0 +1,60 @@ + +// ----------------------------------------------------------------------------- +// node2json +// ----------------------------------------------------------------------------- + +template +bool node2json( + const NODE &node, nlohmann::ordered_json &j, + const std::string &suffix = "" +) { + // Original node name, and suffixed name. The latter is for handling child + // nodes of the same name under the same parent node, and includes a numeric + // suffix (so, name0, name1, etc.) in that scenario. This is needed for JSON + // because JSON doesn't support same-named child nodes. In cases where the + // name was unique to begin with, nameOriginal == nameSuffixed. + const std::string nameOriginal = node.name; + const std::string nameSuffixed = node.name + suffix; + + // Create new ordered_json in j + nlohmann::ordered_json &json = j[nameSuffixed]; + + // ------------------------ + // metadata ==> json + // ------------------------ + + if (suffix != "") + json["#nodeName"] = nameOriginal; + + for (auto &meta : node.metadata) + json["#attributes"][meta.first] = meta.second; + + // ------------------------ + // children ==> json + // ------------------------ + + // First, account for what children appear in the current node. If any child + // name appears multiple times, we must deal with that. For each represented + // child name, the map gets 0 if the name appears once, 1 if it appears more + // than once. Later, this 0/1 is used initially to make a boolean choice; + // then it serves as a counter to generate a 0-indexed numeric suffix that + // makes the child names unique: name0, name1, etc. + std::map childNames; + for (auto &c : node.children) { + auto iter = childNames.find(c->name); + if (iter == childNames.end()) + childNames.insert({c->name,0}); // once (so far) + else + iter->second = 1; // more than once + } + + // now revisit and process the child nodes + for (auto &c : node.children) { + const std::size_t counter = childNames.find(c->name)->second++; + if (!node2json(*c, json, counter ? std::to_string(counter-1) : "")) + return false; + } + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail-node2xml.hpp b/src/GNDStk/convert/src/detail-node2xml.hpp new file mode 100644 index 000000000..76f5c705f --- /dev/null +++ b/src/GNDStk/convert/src/detail-node2xml.hpp @@ -0,0 +1,110 @@ + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// check_special +template +bool check_special(const NODE &node, const std::string &label) +{ + if (node.children.size() != 0) { + log::error( + "Internal error in node2xml(Node, pugi::xml_node):\n" + "Ill-formed <" + label + "> node; " + "should have 0 children, but has {}.", + node.children.size() + ); + throw std::exception{}; + } + + if (node.metadata.size() != 1) { + log::error( + "Internal error in node2xml(Node, pugi::xml_node):\n" + "Ill-formed <" + label + "> node; " + "should have 1 metadatum, but has {}.", + node.metadata.size() + ); + throw std::exception{}; + } + + if (node.metadata.begin()->first != "#text") { + log::error( + "Internal error in node2xml(Node, pugi::xml_node):\n" + "Ill-formed <" + label + "> node; " + "should have metadatum key \"#text\", but has key \"{}\".", + node.metadata.begin()->first + ); + throw std::exception{}; + } + + return true; +} + +// write_cdata +template +bool write_cdata(const NODE &node, pugi::xml_node &xnode) +{ + if (!check_special(node,"#cdata")) return false; + xnode.append_child(pugi::node_cdata).set_value(node.meta("#text").data()); + return true; +} + +// write_pcdata +template +bool write_pcdata(const NODE &node, pugi::xml_node &xnode) +{ + if (!check_special(node,"#pcdata")) return false; + xnode.append_child(pugi::node_pcdata).set_value(node.meta("#text").data()); + return true; +} + +// write_comment +template +bool write_comment(const NODE &node, pugi::xml_node &xnode) +{ + if (!check_special(node,"#comment")) return false; + xnode.append_child(pugi::node_comment).set_value(node.meta("#text").data()); + return true; +} + + +// ----------------------------------------------------------------------------- +// node2xml +// ----------------------------------------------------------------------------- + +template +bool node2xml(const NODE &node, pugi::xml_node &x) +{ + static const std::string context = + "node2xml(Node, pugi::xml_node)"; + + // name + pugi::xml_node xnode = x.append_child(node.name.data()); + + // metadata + for (auto &meta : node.metadata) + xnode.append_attribute(meta.first.data()) = meta.second.data(); + + // children + for (auto &child : node.children) { + try { + // special element + if (child->name == "#cdata") + { if (write_cdata (*child,xnode)) continue; else return false; } + if (child->name == "#pcdata") + { if (write_pcdata (*child,xnode)) continue; else return false; } + if (child->name == "#comment") + { if (write_comment(*child,xnode)) continue; else return false; } + + // typical element + if (!node2xml(*child,xnode)) + return false; + } catch (...) { + log::function(context); + throw; + } + } + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail-xml2node.hpp b/src/GNDStk/convert/src/detail-xml2node.hpp new file mode 100644 index 000000000..27036d72e --- /dev/null +++ b/src/GNDStk/convert/src/detail-xml2node.hpp @@ -0,0 +1,145 @@ + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// error_xml2node +inline void error_xml2node(const std::string &type) +{ + log::error( + "Internal error in xml2node(pugi::xml_node, Node):\n" + "Type pugi::{} found, but not handled, as sub-element.", + type + ); + throw std::exception{}; +} + + +// ----------------------------------------------------------------------------- +// xml2node +// ----------------------------------------------------------------------------- + +/* +FYI, here's the pugixml code for pugi::xml_node_type: + +namespace pugi +{ + // Tree node types + enum xml_node_type + { + node_null, // Empty (null) node handle + node_document, // A document tree's absolute root + node_element, // Element tag, i.e. '' + node_pcdata, // Plain character data, i.e. 'foo' + node_cdata, // Character data, i.e. '' + node_comment, // Comment tag, i.e. '' + node_pi, // Processing instruction, i.e. '' + node_declaration, // Document declaration, i.e. '' + node_doctype // Document type declaration, i.e. '' + }; +} +*/ + +// pugi::xml_node ==> Node +template +bool xml2node(const pugi::xml_node &xnode, NODE &node) +{ + static const std::string context = + "xml2node(pugi::xml_node, Node)"; + + // check destination node + if (!node.empty()) { + log::error( + "Internal error in xml2node(pugi::xml_node, Node):\n" + "Destination Node is supposed to arrive here empty, but didn't." + ); + throw std::exception{}; + } + + // name + node.name = xnode.name(); + + // metadata + for (const pugi::xml_attribute &xattr : xnode.attributes()) + node.add(xattr.name(), xattr.value()); + + // children (sub-nodes) + for (const pugi::xml_node &xsub : xnode) { + + // ------------------------ + // not handled right now + // ------------------------ + + // I don't think that the following should ever appear in this context + if (xsub.type() == pugi::node_document) + error_xml2node("node_document"); + if (xsub.type() == pugi::node_declaration) + error_xml2node("node_declaration"); + + // For now I won't handle these; let's ensure that we don't see them + if (xsub.type() == pugi::node_null) + error_xml2node("node_null"); + if (xsub.type() == pugi::node_pi) + error_xml2node("node_pi"); + if (xsub.type() == pugi::node_doctype) + error_xml2node("node_doctype"); + + // ------------------------ + // element (typical case) + // ------------------------ + + if (xsub.type() == pugi::node_element) { + try { + if (!xml2node(xsub,node.add())) + return false; + } catch (...) { + log::function(context); + throw; + } + continue; + } + + // ------------------------ + // cdata, pcdata, comment + // ------------------------ + + // We'll store these in a special manner as children of the current node, + // reflecting how they arrived through pugi xml. Our manner of storing + // them will allow us to maintain their original ordering if, say, someone + // reads an XML, makes modest modifications or additions to data here and + // there, and then writes an XML back out. GNDS has no ordering, so doing + // this isn't necessary. It is, however, easy to handle, and users may + // appreciate that GNDStk doesn't toss comments, or mess with the ordering + // of cdata, pcdata, or comment nodes, either individually or together. + // Of note, all bets are off if someone converts to JSON and back, because + // the nlohmann JSON library reorders everything lexicographically. + + if (xsub.type() == pugi::node_cdata) { + node.add("#cdata").add("#text", xsub.value()); + continue; + } + + if (xsub.type() == pugi::node_pcdata) { + node.add("#pcdata").add("#text", xsub.value()); + continue; + } + + if (xsub.type() == pugi::node_comment) { + node.add("#comment").add("#text", xsub.value()); + continue; + } + + // ------------------------ + // well we missed something + // ------------------------ + + log::error( + "Internal error in xml2node(pugi::xml_node, Node):\n" + "Encountered a pugi:: node type that we don't know about." + ); + throw std::exception{}; + } + + // done + return true; +} diff --git a/src/GNDStk/convert/src/detail.hpp b/src/GNDStk/convert/src/detail.hpp index 0d10b4109..5b09f00d4 100644 --- a/src/GNDStk/convert/src/detail.hpp +++ b/src/GNDStk/convert/src/detail.hpp @@ -1,647 +1,23 @@ namespace detail { -// ----------------------------------------------------------------------------- -// node2json -// ----------------------------------------------------------------------------- - -template -bool node2json( - const NODE &node, nlohmann::ordered_json &j, - const std::string &suffix = "" -) { - // Original node name, and suffixed name. The latter is for handling child - // nodes of the same name under the same parent node, and includes a numeric - // suffix (so, name0, name1, etc.) in that scenario. This is needed for JSON - // because JSON doesn't support same-named child nodes. In cases where the - // name was unique to begin with, nameOriginal == nameSuffixed. - const std::string nameOriginal = node.name; - const std::string nameSuffixed = node.name + suffix; - - // Create new ordered_json in j - nlohmann::ordered_json &json = j[nameSuffixed]; - - // ------------------------ - // metadata ==> json - // ------------------------ - - if (suffix != "") - json["#nodeName"] = nameOriginal; - - for (auto &meta : node.metadata) - json["#attributes"][meta.first] = meta.second; - - // ------------------------ - // children ==> json - // ------------------------ - - // First, account for what children appear in the current node. If any child - // name appears multiple times, we must deal with that. For each represented - // child name, the map gets 0 if the name appears once, 1 if it appears more - // than once. Later, this 0/1 is used initially to make a boolean choice; - // then it serves as a counter to generate a 0-indexed numeric suffix that - // makes the child names unique: name0, name1, etc. - std::map childNames; - for (auto &c : node.children) { - auto iter = childNames.find(c->name); - if (iter == childNames.end()) - childNames.insert({c->name,0}); // once (so far) - else - iter->second = 1; // more than once - } - - // now revisit and process the child nodes - for (auto &c : node.children) { - const std::size_t counter = childNames.find(c->name)->second++; - if (!node2json(*c, json, counter ? std::to_string(counter-1) : "")) - return false; - } - - // done - return true; -} - - - -// ----------------------------------------------------------------------------- -// xml2node -// ----------------------------------------------------------------------------- - -/* -FYI, here's the pugixml code for pugi::xml_node_type: - -namespace pugi -{ - // Tree node types - enum xml_node_type - { - node_null, // Empty (null) node handle - node_document, // A document tree's absolute root - node_element, // Element tag, i.e. '' - node_pcdata, // Plain character data, i.e. 'foo' - node_cdata, // Character data, i.e. '' - node_comment, // Comment tag, i.e. '' - node_pi, // Processing instruction, i.e. '' - node_declaration, // Document declaration, i.e. '' - node_doctype // Document type declaration, i.e. '' - }; -} -*/ - -// Helper: error_xml2node -inline void error_xml2node(const std::string &type) -{ - log::error( - "Internal error in xml2node(pugi::xml_node, Node):\n" - "Type pugi::{} found, but not handled, as sub-element.", - type - ); - throw std::exception{}; -} - -// xml2node -// pugi::xml_node ==> Node -template -bool xml2node(const pugi::xml_node &xnode, NODE &node) -{ - static const std::string context = - "xml2node(pugi::xml_node, Node)"; - - // check destination node - if (!node.empty()) { - log::error( - "Internal error in xml2node(pugi::xml_node, Node):\n" - "Destination Node is supposed to arrive here empty, but didn't." - ); - throw std::exception{}; - } - - // name - node.name = xnode.name(); - - // metadata - for (const pugi::xml_attribute &xattr : xnode.attributes()) - node.add(xattr.name(), xattr.value()); - - // children (sub-nodes) - for (const pugi::xml_node &xsub : xnode) { - - // ------------------------ - // not handled right now - // ------------------------ - - // I don't think that the following should ever appear in this context - if (xsub.type() == pugi::node_document) - error_xml2node("node_document"); - if (xsub.type() == pugi::node_declaration) - error_xml2node("node_declaration"); - - // For now I won't handle these; let's ensure that we don't see them - if (xsub.type() == pugi::node_null) - error_xml2node("node_null"); - if (xsub.type() == pugi::node_pi) - error_xml2node("node_pi"); - if (xsub.type() == pugi::node_doctype) - error_xml2node("node_doctype"); - - // ------------------------ - // element (typical case) - // ------------------------ - - if (xsub.type() == pugi::node_element) { - try { - if (!xml2node(xsub,node.add())) - return false; - } catch (...) { - log::function(context); - throw; - } - continue; - } - - // ------------------------ - // cdata, pcdata, comment - // ------------------------ - - // We'll store these in a special manner as children of the current node, - // reflecting how they arrived through pugi xml. Our manner of storing - // them will allow us to maintain their original ordering if, say, someone - // reads an XML, makes modest modifications or additions to data here and - // there, and then writes an XML back out. GNDS has no ordering, so doing - // this isn't necessary. It is, however, easy to handle, and users may - // appreciate that GNDStk doesn't toss comments, or mess with the ordering - // of cdata, pcdata, or comment nodes, either individually or together. - // Of note, all bets are off if someone converts to JSON and back, because - // the nlohmann JSON library reorders everything lexicographically. - - if (xsub.type() == pugi::node_cdata) { - node.add("#cdata").add("#text", xsub.value()); - continue; - } - - if (xsub.type() == pugi::node_pcdata) { - node.add("#pcdata").add("#text", xsub.value()); - continue; - } - - if (xsub.type() == pugi::node_comment) { - node.add("#comment").add("#text", xsub.value()); - continue; - } - - // ------------------------ - // well we missed something - // ------------------------ - - log::error( - "Internal error in xml2node(pugi::xml_node, Node):\n" - "Encountered a pugi:: node type that we don't know about." - ); - throw std::exception{}; - } +// Node <==> XML +#include "detail-node2xml.hpp" +#include "detail-xml2node.hpp" - // done - return true; -} - - - -// ----------------------------------------------------------------------------- -// json2node -// ----------------------------------------------------------------------------- - -// nlohmann::ordered_json::const_iterator ==> Node -// Why the iterator rather than the ordered_json object? I found that there -// were some seemingly funny semantics in the nlohmann JSON library. As we -// can see below, we have for example iter->is_object() (so, the -> operator, -// typical for iterators), but also iter.value() (the . operator - on an -// iterator). Similarly, also seen below, with the sub-elements. This is why -// we are, for now, writing our for-loops, here as well as in the functions -// that call this, in the older iterator form rather than the range-based-for -// form. Perhaps there's a way to reformulate all this in a shorter way, but -// this is what we have for now. - -// Helper: error_json2node -inline void error_json2node(const std::string &message) -{ - log::error( - "Internal error in json2node(nlohmann::ordered_json, Node):\n" - "Message: \"{}\".", - message - ); - throw std::exception{}; -} - -// json2node -template -bool json2node(const nlohmann::ordered_json::const_iterator &iter, NODE &node) -{ - static const std::string context = - "json2node(nlohmann::ordered_json::const_iterator, Node)"; - - // the node sent here should be fresh, ready to receive entries... - if (!node.empty()) - error_json2node("!node.empty()"); - - // non-object cases should have been handled - // before any caller calls this function... - if (!iter->is_object()) - error_json2node("!iter->is_object()"); - - // any "#attributes" key (a specially-named "child node" that we use in JSON - // in order to identify attributes) should have been handled in the caller... - if (iter.key() == "#attributes") - error_json2node("iter.key() == \"#attributes\""); - - // key,value ==> node name, JSON value to bring in - node.name = iter.key(); - const nlohmann::ordered_json &json = iter.value(); - - // elements - for (auto elem = json.begin(); elem != json.end(); ++elem) { - if (elem.key() == "#nodeName") { - // #nodeName? ...extract as current node's true name - node.name = elem->get(); - } else if (elem.key() == "#attributes") { - // #attributes? ...extract as current node's metadata - const auto &jsub = elem.value(); - for (auto attr = jsub.begin(); attr != jsub.end(); ++attr) - node.add(attr.key(), attr->get()); - } else if (elem->is_string()) { - // string? ...extract as metadata key/value pair - node.add(elem.key(), elem->get()); - } else if (elem->is_object()) { - // {} object? ...extract as normal child node - try { - if (!json2node(elem,node.add())) - return false; - } catch (...) { - log::function(context); - throw; - } - } else if (elem->is_null()) { - // null node? ...extract as normal (albeit empty) child node - // In GNDS, e.g. XML's or - node.add().name = elem.key(); - } else { - // no other cases are handled right now - error_json2node("unhandled JSON value type"); - } - } - - // done - return true; -} - - - -// ----------------------------------------------------------------------------- -// node2hdf5 -// ----------------------------------------------------------------------------- - -// Here, OBJECT is either HighFive::File or HighFive::Group -template -bool node2hdf5(const NODE &node, OBJECT &h, const std::string &suffix = "") -{ - // As with JSON; see the comment in node2json() - const std::string nameOriginal = node.name; - const std::string nameSuffixed = node.name + suffix; - - // Create new group in h - HighFive::Group group = h.createGroup(nameSuffixed); - - // ------------------------ - // metadata ==> file/group - // ------------------------ - - // #nodeName if appropriate (as with JSON, allows recovery of original name) - if (suffix != "") - group.createAttribute(std::string("#nodeName"), nameOriginal); - - // existing attributes - for (auto &meta : node.metadata) - group.createAttribute(meta.first, meta.second); - - // todo - // Right now, we're doing a straight translation of our internal Node - // structure's string-based storage scheme, where everything (including - // in particular the text from XML CDATA and PCDATA nodes) is stored - // internally as std::strings. Of course, for HDF5 we'll actually want - // to recognize those constructs and write HDF5 DataSets instead! For - // now, we'll just get *something* working. - - // ------------------------ - // children ==> file/group - // ------------------------ - - // Logic as with JSON; see the comment in node2json() - std::map childNames; - for (auto &c : node.children) { - auto iter = childNames.find(c->name); - if (iter == childNames.end()) - childNames.insert({c->name,0}); // once (so far) - else - iter->second = 1; // more than once - } - - // Now revisit and process the child nodes - for (auto &c : node.children) { - const std::size_t counter = childNames.find(c->name)->second++; - if (!node2hdf5(*c, group, counter ? std::to_string(counter-1) : "")) - return false; - } - - // Done - return true; -} - - - -// ----------------------------------------------------------------------------- -// hdf5attr2node -// For HighFive::Attribute -// ----------------------------------------------------------------------------- - -// Helper: attrTYPE2node -template -bool attrTYPE2node(const HighFive::Attribute &attr, NODE &node) -{ - if (attr.getDataType() == HighFive::AtomicType{}) { - const std::string attrName = attr.getName(); - const std::size_t attrSize = attr.getSpace().getElementCount(); - - // Scalar case. Includes bool. - if (attrSize == 1) { - T scalar; - attr.read(scalar); - node.add(attrName,scalar); - return true; - } - - // Vector case. EXcludes bool, as HighFive (perhaps HDF5 in general?) - // doesn't appear to support it in this case. Indeed, the body of the - // if-constexpr doesn't *compile* with bool. (Hence our if-constexpr.) - if constexpr (!std::is_same_v) { - std::vector vector; - vector.reserve(attrSize); - attr.read(vector); - node.add(attrName,vector); - return true; - } - } - - return false; -} - -// hdf5attr2node -template -bool hdf5attr2node(const HighFive::Attribute &attr, NODE &node) -{ - // HighFive's documentation leaves much to be desired. I used what I found - // in HighFive/include/highfive/bits/H5DataType_misc.hpp to get an idea of - // what attribute *types* are allowed. That file also had some handling of - // C-style fixed-length strings, as with char[length], and std::complex as - // well. It didn't have long double, which I'd have liked, but that's fine. - // I won't bother with fixed-length strings or with std::complex right now, - // but will support the rest. - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - if (attrTYPE2node(attr,node)) return true; - - log::error( - "HDF5 Attribute \"{}\"'s DataType \"{}\" is not handled at this time.", - attr.getName(), attr.getDataType().string()); - log::function("hdf5attr2node(HighFive::Attribute, Node)"); - - return false; -} - - - -// ----------------------------------------------------------------------------- -// hdf5data2node -// For HighFive::DataSet -// ----------------------------------------------------------------------------- - -// Helper: dataTYPE2node -template -bool dataTYPE2node(const HighFive::DataSet &data, NODE &node) -{ - if (data.getDataType() == HighFive::AtomicType{}) { - // Comments as in the similar helper function attrTYPE2node()... - const std::size_t dataSize = data.getElementCount(); - - if (dataSize == 1) { - T scalar; - data.read(scalar); - node.add("#pcdata").add("#text",scalar); - return true; - } - - if constexpr (!std::is_same_v) { - std::vector vector; - vector.reserve(dataSize); - data.read(vector); - node.add("#pcdata").add("#text",vector); - return true; - } - } - - return false; -} - -// hdf5data2node -template -bool hdf5data2node( - const HighFive::DataSet &data, const std::string &dataName, - NODE &node -) { - // node name - node.name = dataName; - - // the data set's attributes - for (auto &attrName : data.listAttributeNames()) - if (!hdf5attr2node(data.getAttribute(attrName), node)) - return false; - - // the data set's data - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - if (dataTYPE2node(data,node)) return true; - - log::error( - "HDF5 DataSet \"{}\"'s DataType \"{}\" is not handled at this time.", - dataName, data.getDataType().string()); - log::function("hdf5data2node(HighFive::DataSet, dataName, Node)"); - - return false; -} - - - -// ----------------------------------------------------------------------------- -// hdf52node -// ----------------------------------------------------------------------------- - -// Helper: error_hdf52node -inline void error_hdf52node(const std::string &message) -{ - log::error( - "Internal error in hdf52node(HighFive::Group, std::string, Node):\n" - "Message: \"{}\".", - message - ); - throw std::exception{}; -} - -// hdf52node -template -bool hdf52node( - const HighFive::Group &group, const std::string &groupName, - NODE &node, const bool requireEmpty = true -) { - static const std::string context = - "hdf52node(HighFive::Group, std::string, Node)"; - - // The node sent here should be fresh, ready to receive entries - if (requireEmpty && !node.empty()) - error_hdf52node("!node.empty()"); - - // ------------------------ - // HDF5 group name - // ==> node name - // ------------------------ - - // if "/" then we're at the top-level node, which we call "" internally - if (groupName != "/") - node.name = groupName; - - // ------------------------ - // HDF5 attributes - // ==> metadata - // ------------------------ - - // if "/" then attributes were handled, in a special way, by the caller - if (groupName != "/") - for (auto &attrName : group.listAttributeNames()) - if (!hdf5attr2node(group.getAttribute(attrName), node)) - return false; - - // ------------------------ - // HDF5 sub-groups - // ==> children - // ------------------------ - - for (auto &elemName : group.listObjectNames()) { - switch (group.getObjectType(elemName)) { - - // File - // NOT EXPECTED IN THIS CONTEXT - case HighFive::ObjectType::File : - error_hdf52node("ObjectType \"File\" not expected here"); - break; - - // Group - // ACTION: call the present function recursively - case HighFive::ObjectType::Group : - try { - if (!hdf52node(group.getGroup(elemName), elemName, node.add())) - return false; - } catch (...) { - log::function(context); - throw; - } - break; - - // UserDataType - // NOT HANDLED; perhaps we could provide something in the future - case HighFive::ObjectType::UserDataType : - error_hdf52node("ObjectType \"UserDataType\" not handled"); - break; - - // DataSpace (not to be confused with Dataset) - // NOT EXPECTED IN THIS CONTEXT - case HighFive::ObjectType::DataSpace : - error_hdf52node("ObjectType \"DataSpace\" not expected here"); - break; - - // Dataset - // ACTION: handle the DataSet's data - case HighFive::ObjectType::Dataset : - try { - if (!hdf5data2node( - group.getDataSet(elemName), - elemName, - node.add() - )) - return false; - } catch (...) { - log::function(context); - throw; - } - break; - - // Attribute - // NOT EXPECTED IN THIS CONTEXT - case HighFive::ObjectType::Attribute : - // group.listObjectNames() (used in the for-loop we're in right - // now) apparently doesn't include attribute names - which is fine, - // because we already handled attributes earlier. So, here, we just - // produce an error if ObjectType::Attribute inexplicably made an - // appearance here, where we don't expect it. - error_hdf52node("ObjectType \"Attribute\" not expected here"); - break; - - // Other - // NOT HANDLED; we're not sure when this would arise - case HighFive::ObjectType::Other : - error_hdf52node("ObjectType \"Other\" not handled"); - break; - - // default - // NOT HANDLED; presumably our switch has covered all bases already - default: - error_hdf52node("ObjectType [unknown] not handled"); - break; - - } // switch - } // for - - // done - return true; -} +// Node <==> JSON +#include "detail-node2json.hpp" +#include "detail-json2node.hpp" +// Node <==> HDF5 +#include "detail-node2hdf5.hpp" +#include "detail-hdf52node.hpp" // ----------------------------------------------------------------------------- // node2node // ----------------------------------------------------------------------------- -// Node ==> Node template void node2node(const NODE &from, NODE &to) { @@ -660,113 +36,4 @@ void node2node(const NODE &from, NODE &to) node2node(*c, to.add()); } - - -// ----------------------------------------------------------------------------- -// node2xml -// ----------------------------------------------------------------------------- - -// Helper: check_special -template -bool check_special(const NODE &node, const std::string &label) -{ - if (node.children.size() != 0) { - log::error( - "Internal error in node2xml(Node, pugi::xml_node):\n" - "Ill-formed <" + label + "> node; " - "should have 0 children, but has {}.", - node.children.size() - ); - throw std::exception{}; - } - - if (node.metadata.size() != 1) { - log::error( - "Internal error in node2xml(Node, pugi::xml_node):\n" - "Ill-formed <" + label + "> node; " - "should have 1 metadatum, but has {}.", - node.metadata.size() - ); - throw std::exception{}; - } - - if (node.metadata.begin()->first != "#text") { - log::error( - "Internal error in node2xml(Node, pugi::xml_node):\n" - "Ill-formed <" + label + "> node; " - "should have metadatum key \"#text\", but has key \"{}\".", - node.metadata.begin()->first - ); - throw std::exception{}; - } - - return true; -} - -// Helper: write_cdata -template -bool write_cdata(const NODE &node, pugi::xml_node &xnode) -{ - if (!check_special(node,"#cdata")) return false; - xnode.append_child(pugi::node_cdata).set_value(node.meta("#text").data()); - return true; -} - -// Helper: write_pcdata -template -bool write_pcdata(const NODE &node, pugi::xml_node &xnode) -{ - if (!check_special(node,"#pcdata")) return false; - xnode.append_child(pugi::node_pcdata).set_value(node.meta("#text").data()); - return true; -} - -// Helper: write_comment -template -bool write_comment(const NODE &node, pugi::xml_node &xnode) -{ - if (!check_special(node,"#comment")) return false; - xnode.append_child(pugi::node_comment).set_value(node.meta("#text").data()); - return true; -} - - -// node2xml -template -bool node2xml(const NODE &node, pugi::xml_node &x) -{ - static const std::string context = - "node2xml(Node, pugi::xml_node)"; - - // name - pugi::xml_node xnode = x.append_child(node.name.data()); - - // metadata - for (auto &meta : node.metadata) - xnode.append_attribute(meta.first.data()) = meta.second.data(); - - // children - for (auto &child : node.children) { - try { - // special element - if (child->name == "#cdata") - { if (write_cdata (*child,xnode)) continue; else return false; } - if (child->name == "#pcdata") - { if (write_pcdata (*child,xnode)) continue; else return false; } - if (child->name == "#comment") - { if (write_comment(*child,xnode)) continue; else return false; } - - // typical element - if (!node2xml(*child,xnode)) - return false; - } catch (...) { - log::function(context); - throw; - } - } - - // done - return true; -} - } // namespace detail diff --git a/src/GNDStk/string2type.hpp b/src/GNDStk/string2type.hpp index 634c9ff62..4e9d9e9d9 100644 --- a/src/GNDStk/string2type.hpp +++ b/src/GNDStk/string2type.hpp @@ -55,20 +55,11 @@ inline void convert(std::istream &is, T &value) } } -/* -/// fixme Will probably need this eventually, to resolve a similar ambiguity -/// to the one I encountered with convert(string,ostream). I anticipate this -/// issue arising when node2hdf5() is outfitted with proper type awareness, -/// and in particular when we try to do a convert(string,vector), -/// which (see next commented block below) first converts the string to an -/// istringstream, then needs to resolve convert(i[string]stream,string). - // string inline void convert(std::istream &is, std::string &value) { is >> value; } -*/ // pair template diff --git a/src/GNDStk/string2type/test/string2type.test.cpp b/src/GNDStk/string2type/test/string2type.test.cpp index 6302388f5..d036e9a84 100644 --- a/src/GNDStk/string2type/test/string2type.test.cpp +++ b/src/GNDStk/string2type/test/string2type.test.cpp @@ -84,7 +84,7 @@ SCENARIO("Testing GNDStk convert(istream/string,type)") { CHECK(*iter++ == 300); } - THEN("It works for T == vector") { + THEN("It works for T == vector") { std::istringstream iss("1000 2000 3000"); std::vector container; convert(iss,container); @@ -93,6 +93,17 @@ SCENARIO("Testing GNDStk convert(istream/string,type)") { CHECK(container[1] == 2000); CHECK(container[2] == 3000); } + + THEN("It works for T == vector") { + std::istringstream iss("a bc def ghij"); + std::vector container; + convert(iss,container); + CHECK(container.size() == 4); + CHECK(container[0] == "a"); + CHECK(container[1] == "bc"); + CHECK(container[2] == "def"); + CHECK(container[3] == "ghij"); + } } // ------------------------ @@ -193,6 +204,27 @@ SCENARIO("Testing GNDStk convert(istream/string,type)") { convert(str,val); CHECK(val == 7.89L); } + + THEN("It works correctly for vector") { + const std::string str("1000 2000 3000"); + std::vector container; + convert(str,container); + CHECK(container.size() == 3); + CHECK(container[0] == 1000); + CHECK(container[1] == 2000); + CHECK(container[2] == 3000); + } + + THEN("It works correctly for vector") { + const std::string str("a bc def ghij"); + std::vector container; + convert(str,container); + CHECK(container.size() == 4); + CHECK(container[0] == "a"); + CHECK(container[1] == "bc"); + CHECK(container[2] == "def"); + CHECK(container[3] == "ghij"); + } } // WHEN } // SCENARIO