feat(cpp-object-oriented-modular-refactoring): step 11 - source-target-resolver

This commit is contained in:
KOKO\Mimi
2026-08-16 08:34:09 +09:00
parent 5430fffd62
commit a6324a9004
12 changed files with 714 additions and 273 deletions
+28
View File
@@ -0,0 +1,28 @@
#ifndef FESA_CORE_ASCII_H_
#define FESA_CORE_ASCII_H_
#include <cstdint>
#include <string_view>
#include "fesa/core/status.h"
namespace fesa {
/// @brief Converts one ASCII uppercase byte to lowercase.
/// @return The lowercase ASCII byte, or the input byte when it is not A-Z.
char AsciiLower(char value) noexcept;
/// @brief Compares two byte strings with ASCII-only case folding.
/// @return True when the strings have equal length and equal ASCII-folded
/// bytes.
bool AsciiCaseInsensitiveEquals(std::string_view lhs,
std::string_view rhs) noexcept;
/// @brief Parses a complete positive base-10 source label.
/// @return The positive label or an input failure for malformed, nonpositive,
/// or out-of-range text.
Result<std::int64_t> ParsePositiveSourceLabel(std::string_view text);
} // namespace fesa
#endif // FESA_CORE_ASCII_H_
@@ -0,0 +1,81 @@
#ifndef FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
#define FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
#include <cstddef>
#include <string>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/model/model_types.h"
namespace fesa {
class Domain;
/// @brief Selects the independent source node or element namespace.
enum class SourceEntityKind { kNode, kElement };
/// @brief Maps one direct label or named-target membership to stable identity.
/// @note An empty target_name denotes a direct source-label entry.
struct SourceTargetIndexEntry {
SourceEntityKind entity_kind;
std::string instance_name;
std::string target_name;
SourceEntityId source_id;
EntityIndex entity_index;
std::size_t declaration_order;
};
/// @brief Owns immutable compact source-target lookup entries.
class SourceTargetIndex {
public:
/// @brief Takes ownership of compact entries from a validated model
/// candidate.
explicit SourceTargetIndex(std::vector<SourceTargetIndexEntry> entries);
/// @brief Builds compact entries from an immutable semantic Domain.
/// @return An owning index that preserves Domain declaration order.
static SourceTargetIndex FromDomain(const Domain& domain);
/// @brief Returns owned entries without exposing mutable index state.
const std::vector<SourceTargetIndexEntry>& Entries() const noexcept;
private:
std::vector<SourceTargetIndexEntry> entries_;
};
/// @brief Describes one source target lookup.
struct SourceTargetQuery {
SourceEntityKind entity_kind;
std::string instance_name;
std::string target_name_or_label;
};
/// @brief Preserves both external source identity and stable internal index.
struct ResolvedSourceTarget {
SourceEntityId source_id;
EntityIndex entity_index;
};
/// @brief Resolves source labels and named targets without owning model state.
/// @note The referenced SourceTargetIndex must outlive this resolver.
class SourceTargetResolver {
public:
/// @brief Creates a non-owning resolver over an immutable index.
/// @param index Index whose lifetime must exceed the resolver lifetime.
explicit SourceTargetResolver(const SourceTargetIndex& index) noexcept;
/// @brief Resolves one query in stable declaration order.
/// @return Stable source targets, or a deterministic input diagnostic for an
/// invalid, missing, duplicate, or ambiguous target.
Result<std::vector<ResolvedSourceTarget>> Resolve(
const SourceTargetQuery& query) const;
private:
const SourceTargetIndex* index_;
};
} // namespace fesa
#endif // FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
+2
View File
@@ -10,6 +10,7 @@ add_library(
assembly/sparse_assembler.cpp assembly/sparse_assembler.cpp
build_info.cpp build_info.cpp
constraints/essential_constraints.cpp constraints/essential_constraints.cpp
core/ascii.cpp
core/diagnostic.cpp core/diagnostic.cpp
core/status.cpp core/status.cpp
elements/euler_beam_3d.cpp elements/euler_beam_3d.cpp
@@ -24,6 +25,7 @@ add_library(
math/vector.cpp math/vector.cpp
model/domain.cpp model/domain.cpp
model/shell_geometry.cpp model/shell_geometry.cpp
model/source_target_resolver.cpp
results/result_recovery.cpp results/result_recovery.cpp
solvers/linear/mkl_pardiso_solver.cpp solvers/linear/mkl_pardiso_solver.cpp
) )
+22 -65
View File
@@ -1,17 +1,15 @@
#include "fesa/assembly/load_assembler.h" #include "fesa/assembly/load_assembler.h"
#include <algorithm> #include <algorithm>
#include <charconv>
#include <cmath> #include <cmath>
#include <cstdint>
#include <limits> #include <limits>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <system_error>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "fesa/constraints/essential_constraints.h" #include "fesa/constraints/essential_constraints.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa { namespace fesa {
namespace { namespace {
@@ -27,28 +25,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location,
{{Severity::kError, code, location, keyword, identity, message}}); {{Severity::kError, code, location, keyword, identity, message}});
} }
char AsciiLower(const char value) {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
bool EqualName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(),
[](const char left_value, const char right_value) {
return AsciiLower(left_value) == AsciiLower(right_value);
});
}
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
}
/// @brief Checks that equation-space indices preserve stable full-DOF order. /// @brief Checks that equation-space indices preserve stable full-DOF order.
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) { bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
return std::adjacent_find( return std::adjacent_find(
@@ -122,52 +98,31 @@ Status ValidateDofOrder(const DofManager& dofs,
return Status::Ok(); return Status::Ok();
} }
Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain, Result<std::vector<EntityIndex>> ResolveTarget(
const NodalLoad& load) { const SourceTargetResolver& resolver, const Domain& domain,
std::vector<const NodeSet*> matching_sets; const NodalLoad& load) {
for (const auto& set : domain.NodeSets()) { auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
if (EqualName(set.name, load.target)) { if (!resolved.HasValue()) {
matching_sets.push_back(&set);
}
}
std::vector<EntityIndex> matching_nodes;
std::int64_t label = 0;
if (TryPositiveInteger(load.target, label)) {
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
if (domain.Nodes()[index].source_id.source_label == label) {
matching_nodes.push_back(static_cast<EntityIndex>(index));
}
}
}
if (matching_sets.size() > 1U || matching_nodes.size() > 1U ||
(!matching_sets.empty() && !matching_nodes.empty())) {
return Result<std::vector<EntityIndex>>::Failure( return Result<std::vector<EntityIndex>>::Failure(
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target, LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
"The load target must resolve unambiguously to one node or " "The load target must resolve unambiguously to one node or "
"one expanded node set.")); "one expanded node set."));
} }
if (!matching_sets.empty()) { std::vector<EntityIndex> nodes;
const auto& nodes = matching_sets.front()->node_indices; nodes.reserve(resolved.Value().size());
std::vector<unsigned char> seen(domain.Nodes().size(), 0U); std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
for (const EntityIndex node : nodes) { for (const auto& target : resolved.Value()) {
if (node >= domain.Nodes().size() || seen[node] != 0U) { const EntityIndex node = target.entity_index;
return Result<std::vector<EntityIndex>>::Failure(LoadFailure( if (node >= domain.Nodes().size() || seen[node] != 0U) {
"invalid-load-target", load.location, "CLOAD", load.target, return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
"The expanded node set must contain unique in-range stable node " "invalid-load-target", load.location, "CLOAD", load.target,
"identities.")); "The expanded node set must contain unique in-range stable node "
} "identities."));
seen[node] = 1U;
} }
return Result<std::vector<EntityIndex>>::Success(nodes); seen[node] = 1U;
nodes.push_back(node);
} }
if (!matching_nodes.empty()) { return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
return Result<std::vector<EntityIndex>>::Success(std::move(matching_nodes));
}
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
"invalid-load-target", load.location, "CLOAD", load.target,
"The load target must resolve to one semantic node or node set."));
} }
Status ValidateFiniteVector(const Vector& values, Status ValidateFiniteVector(const Vector& values,
@@ -285,6 +240,8 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
"invalid-load-order", model.Step().location, "CLOAD", model.Step().name, "invalid-load-order", model.Step().location, "CLOAD", model.Step().name,
"The active load view must include every sole-step load once.")); "The active load view must include every sole-step load once."));
} }
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver target_resolver{target_index};
Vector full_load{expected_full_count}; Vector full_load{expected_full_count};
// Active load indices are required to be the original source order; this // Active load indices are required to be the original source order; this
@@ -311,7 +268,7 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
load.target, "A nodal load magnitude must be finite.")); load.target, "A nodal load magnitude must be finite."));
} }
auto target = ResolveTarget(domain, load); auto target = ResolveTarget(target_resolver, domain, load);
if (!target.HasValue()) { if (!target.HasValue()) {
return Result<Vector>::Failure(target.GetStatus()); return Result<Vector>::Failure(target.GetStatus());
} }
+49
View File
@@ -0,0 +1,49 @@
#include "fesa/core/ascii.h"
#include <charconv>
#include <string>
#include <system_error>
namespace fesa {
char AsciiLower(const char value) noexcept {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
bool AsciiCaseInsensitiveEquals(const std::string_view lhs,
const std::string_view rhs) noexcept {
if (lhs.size() != rhs.size()) {
return false;
}
for (std::size_t index = 0U; index < lhs.size(); ++index) {
if (AsciiLower(lhs[index]) != AsciiLower(rhs[index])) {
return false;
}
}
return true;
}
Result<std::int64_t> ParsePositiveSourceLabel(const std::string_view text) {
std::int64_t value = 0;
const char* const storage = text.empty() ? "" : text.data();
const char* const first = storage;
const char* const last = storage + text.size();
const auto parsed = std::from_chars(first, last, value, 10);
if (parsed.ec == std::errc{} && parsed.ptr == last && value > 0) {
return Result<std::int64_t>::Success(value);
}
const std::string identity{text};
return Result<std::int64_t>::Failure(Status::Failure(
FailureCategory::kInput,
{{Severity::kError,
"invalid-source-label",
{{}, 0U},
"SOURCE_LABEL",
identity,
"A source label must be a complete positive base-10 integer."}}));
}
} // namespace fesa
+15 -40
View File
@@ -1,56 +1,29 @@
#include "fesa/fem/dof_manager.h" #include "fesa/fem/dof_manager.h"
#include <algorithm> #include <algorithm>
#include <charconv>
#include <stdexcept> #include <stdexcept>
#include <string>
#include <system_error>
#include <utility> #include <utility>
#include "fesa/model/source_target_resolver.h"
namespace fesa { namespace fesa {
namespace { namespace {
constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kDofsPerNode = 6U;
char AsciiLower(char value) {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
bool EqualName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(),
[](char left_value, char right_value) {
return AsciiLower(left_value) == AsciiLower(right_value);
});
}
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
}
std::vector<EntityIndex> ExpandBoundaryTarget( std::vector<EntityIndex> ExpandBoundaryTarget(
const Domain& domain, const BoundaryCondition& boundary) { const SourceTargetResolver& resolver, const BoundaryCondition& boundary) {
for (const auto& set : domain.NodeSets()) { auto resolved =
if (EqualName(set.name, boundary.target)) { resolver.Resolve({SourceEntityKind::kNode, "", boundary.target});
return set.node_indices; if (!resolved.HasValue()) {
} return {};
} }
std::vector<EntityIndex> indices;
std::int64_t source_label = 0; indices.reserve(resolved.Value().size());
if (TryPositiveInteger(boundary.target, source_label)) { for (const auto& target : resolved.Value()) {
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { indices.push_back(target.entity_index);
if (domain.Nodes()[node].source_id.source_label == source_label) {
return {static_cast<EntityIndex>(node)};
}
}
} }
return {}; return indices;
} }
template <std::size_t scatter_size> template <std::size_t scatter_size>
@@ -94,12 +67,14 @@ SparsePattern BuildSparsePattern(
Result<DofManager> DofManager::Create(const AnalysisModel& model) { Result<DofManager> DofManager::Create(const AnalysisModel& model) {
const Domain& domain = model.GetDomain(); const Domain& domain = model.GetDomain();
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver target_resolver{target_index};
const std::size_t full_count = domain.Nodes().size() * kDofsPerNode; const std::size_t full_count = domain.Nodes().size() * kDofsPerNode;
std::vector<std::optional<double>> prescribed_by_full_dof(full_count); std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) { for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
const auto& boundary = model.Step().boundaries.at(boundary_index); const auto& boundary = model.Step().boundaries.at(boundary_index);
const auto target = ExpandBoundaryTarget(domain, boundary); const auto target = ExpandBoundaryTarget(target_resolver, boundary);
for (const EntityIndex node : target) { for (const EntityIndex node : target) {
for (int component = boundary.first_dof; component <= boundary.last_dof; for (int component = boundary.first_dof; component <= boundary.last_dof;
++component) { ++component) {
+75 -106
View File
@@ -14,26 +14,14 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "fesa/core/ascii.h"
#include "fesa/math/vector3.h" #include "fesa/math/vector3.h"
#include "fesa/model/shell_geometry.h" #include "fesa/model/shell_geometry.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa { namespace fesa {
namespace { namespace {
std::string UppercaseAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
if (character >= 'a' && character <= 'z') {
return static_cast<char>(character - 'a' + 'A');
}
return character;
});
return value;
}
bool EqualName(const std::string& left, const std::string& right) {
return UppercaseAscii(left) == UppercaseAscii(right);
}
std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) { std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
while (!fields.empty() && fields.back().empty()) { while (!fields.empty() && fields.back().empty()) {
fields.pop_back(); fields.pop_back();
@@ -254,20 +242,6 @@ class MappingContext {
return true; return true;
} }
bool TryPositiveInteger(const std::string& text, std::int64_t& value) const {
if (text.empty()) {
return false;
}
char* end = nullptr;
errno = 0;
const long long parsed = std::strtoll(text.c_str(), &end, 10);
if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) {
return false;
}
value = static_cast<std::int64_t>(parsed);
return true;
}
bool ParseDouble(const std::string& text, double& value, bool ParseDouble(const std::string& text, double& value,
const SourceLocation& location, const std::string& keyword) { const SourceLocation& location, const std::string& keyword) {
if (text.empty()) { if (text.empty()) {
@@ -311,16 +285,17 @@ class MappingContext {
bool ContainsName(const std::vector<RawPart>& values, bool ContainsName(const std::vector<RawPart>& values,
const std::string& name) const { const std::string& name) const {
return std::any_of( return std::any_of(values.begin(), values.end(),
values.begin(), values.end(), [&name](const RawPart& value) {
[&name](const RawPart& value) { return EqualName(value.name, name); }); return AsciiCaseInsensitiveEquals(value.name, name);
});
} }
bool ContainsName(const std::vector<RawInstance>& values, bool ContainsName(const std::vector<RawInstance>& values,
const std::string& name) const { const std::string& name) const {
return std::any_of(values.begin(), values.end(), return std::any_of(values.begin(), values.end(),
[&name](const RawInstance& value) { [&name](const RawInstance& value) {
return EqualName(value.name, name); return AsciiCaseInsensitiveEquals(value.name, name);
}); });
} }
@@ -328,14 +303,14 @@ class MappingContext {
const std::string& name) const { const std::string& name) const {
return std::any_of(values.begin(), values.end(), return std::any_of(values.begin(), values.end(),
[&name](const RawMaterial& value) { [&name](const RawMaterial& value) {
return EqualName(value.name, name); return AsciiCaseInsensitiveEquals(value.name, name);
}); });
} }
bool ContainsSetName(const std::vector<RawSet>& sets, bool ContainsSetName(const std::vector<RawSet>& sets,
const std::string& name) const { const std::string& name) const {
const auto matches = [&name](const RawSet& set) { const auto matches = [&name](const RawSet& set) {
return EqualName(set.name, name); return AsciiCaseInsensitiveEquals(set.name, name);
}; };
return std::any_of(sets.begin(), sets.end(), matches); return std::any_of(sets.begin(), sets.end(), matches);
} }
@@ -344,7 +319,7 @@ class MappingContext {
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(), return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
[node_set, &name](const RawAssemblySet& set) { [node_set, &name](const RawAssemblySet& set) {
return set.is_node_set == node_set && return set.is_node_set == node_set &&
EqualName(set.name, name); AsciiCaseInsensitiveEquals(set.name, name);
}); });
} }
@@ -674,15 +649,15 @@ class MappingContext {
RawElement::Type element_type{}; RawElement::Type element_type{};
ElementFamily element_family{}; ElementFamily element_family{};
std::size_t expected_field_count = 0U; std::size_t expected_field_count = 0U;
if (EqualName(*type, "B33")) { if (AsciiCaseInsensitiveEquals(*type, "B33")) {
element_type = RawElement::Type::kB33; element_type = RawElement::Type::kB33;
element_family = ElementFamily::kBeam; element_family = ElementFamily::kBeam;
expected_field_count = 3U; expected_field_count = 3U;
} else if (EqualName(*type, "S4")) { } else if (AsciiCaseInsensitiveEquals(*type, "S4")) {
element_type = RawElement::Type::kS4; element_type = RawElement::Type::kS4;
element_family = ElementFamily::kShell; element_family = ElementFamily::kShell;
expected_field_count = 5U; expected_field_count = 5U;
} else if (EqualName(*type, "S4R")) { } else if (AsciiCaseInsensitiveEquals(*type, "S4R")) {
element_type = RawElement::Type::kS4r; element_type = RawElement::Type::kS4r;
element_family = ElementFamily::kShell; element_family = ElementFamily::kShell;
expected_field_count = 5U; expected_field_count = 5U;
@@ -803,9 +778,7 @@ class MappingContext {
return; return;
} }
if (block.data[0].fields.size() == 2U) { if (block.data[0].fields.size() == 2U) {
std::int64_t ignored_integration_points = 0; if (!ParsePositiveSourceLabel(block.data[0].fields[1]).HasValue()) {
if (!TryPositiveInteger(block.data[0].fields[1],
ignored_integration_points)) {
InputFailure("unsupported-shell-section-option", block.data[0].location, InputFailure("unsupported-shell-section-option", block.data[0].location,
block.canonical_name, block.data[0].fields[1], block.canonical_name, block.data[0].fields[1],
"The optional shell integration-point field must be a " "The optional shell integration-point field must be a "
@@ -932,7 +905,7 @@ class MappingContext {
if (element_set == nullptr || material == nullptr || section == nullptr) { if (element_set == nullptr || material == nullptr || section == nullptr) {
return; return;
} }
if (!EqualName(*section, "GENERAL")) { if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
InputFailure("unsupported-section-formulation", block.location, InputFailure("unsupported-section-formulation", block.location,
block.canonical_name, *section, block.canonical_name, *section,
"Only SECTION=GENERAL belongs to the approved subset."); "Only SECTION=GENERAL belongs to the approved subset.");
@@ -940,7 +913,8 @@ class MappingContext {
} }
if (std::any_of(part.sections.begin(), part.sections.end(), if (std::any_of(part.sections.begin(), part.sections.end(),
[element_set](const RawSection& existing) { [element_set](const RawSection& existing) {
return EqualName(existing.element_set_name, *element_set); return AsciiCaseInsensitiveEquals(
existing.element_set_name, *element_set);
})) { })) {
InputFailure("duplicate-entity", block.location, block.canonical_name, InputFailure("duplicate-entity", block.location, block.canonical_name,
*element_set, *element_set,
@@ -1332,7 +1306,7 @@ class MappingContext {
"NLGEOM requires NO in the approved subset."); "NLGEOM requires NO in the approved subset.");
return; return;
} }
if (!EqualName(*nlgeom->value, "NO")) { if (!AsciiCaseInsensitiveEquals(*nlgeom->value, "NO")) {
if (model_element_family_ == ElementFamily::kShell) { if (model_element_family_ == ElementFamily::kShell) {
InputFailure("unsupported-nonlinear-geometry", block.location, InputFailure("unsupported-nonlinear-geometry", block.location,
block.canonical_name, *nlgeom->value, block.canonical_name, *nlgeom->value,
@@ -1510,22 +1484,24 @@ class MappingContext {
const RawPart* FindPart(const std::string& name) const { const RawPart* FindPart(const std::string& name) const {
const auto found = std::find_if( const auto found = std::find_if(
parts_.begin(), parts_.end(), parts_.begin(), parts_.end(), [&name](const RawPart& part) {
[&name](const RawPart& part) { return EqualName(part.name, name); }); return AsciiCaseInsensitiveEquals(part.name, name);
});
return found == parts_.end() ? nullptr : &*found; return found == parts_.end() ? nullptr : &*found;
} }
const RawInstance* FindInstance(const std::string& name) const { const RawInstance* FindInstance(const std::string& name) const {
const auto found = std::find_if(instances_.begin(), instances_.end(), const auto found =
[&name](const RawInstance& instance) { std::find_if(instances_.begin(), instances_.end(),
return EqualName(instance.name, name); [&name](const RawInstance& instance) {
}); return AsciiCaseInsensitiveEquals(instance.name, name);
});
return found == instances_.end() ? nullptr : &*found; return found == instances_.end() ? nullptr : &*found;
} }
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const { std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const {
for (std::size_t index = 0U; index < materials_.size(); ++index) { for (std::size_t index = 0U; index < materials_.size(); ++index) {
if (EqualName(materials_[index].name, name)) { if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) {
return static_cast<EntityIndex>(index); return static_cast<EntityIndex>(index);
} }
} }
@@ -1534,9 +1510,10 @@ class MappingContext {
const RawSet* FindSet(const std::vector<RawSet>& sets, const RawSet* FindSet(const std::vector<RawSet>& sets,
const std::string& name) const { const std::string& name) const {
const auto found = std::find_if( const auto found =
sets.begin(), sets.end(), std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) {
[&name](const RawSet& set) { return EqualName(set.name, name); }); return AsciiCaseInsensitiveEquals(set.name, name);
});
return found == sets.end() ? nullptr : &*found; return found == sets.end() ? nullptr : &*found;
} }
@@ -2000,7 +1977,8 @@ class MappingContext {
const auto definition_instance = std::find_if( const auto definition_instance = std::find_if(
definition_.instances.begin(), definition_.instances.end(), definition_.instances.begin(), definition_.instances.end(),
[&raw_instance](const InstanceDefinition& instance) { [&raw_instance](const InstanceDefinition& instance) {
return EqualName(instance.name, raw_instance->name); return AsciiCaseInsensitiveEquals(instance.name,
raw_instance->name);
}); });
if (definition_instance == definition_.instances.end()) { if (definition_instance == definition_.instances.end()) {
InputFailure("unresolved-reference", raw_set.location, InputFailure("unresolved-reference", raw_set.location,
@@ -2010,11 +1988,11 @@ class MappingContext {
} }
if (raw_set.is_node_set) { if (raw_set.is_node_set) {
definition_.node_sets.erase( definition_.node_sets.erase(
std::remove_if(definition_.node_sets.begin(), std::remove_if(
definition_.node_sets.end(), definition_.node_sets.begin(), definition_.node_sets.end(),
[&raw_set](const NodeSet& set) { [&raw_set](const NodeSet& set) {
return EqualName(set.name, raw_set.name); return AsciiCaseInsensitiveEquals(set.name, raw_set.name);
}), }),
definition_.node_sets.end()); definition_.node_sets.end());
NodeSet set{ NodeSet set{
raw_set.name, definition_instance->name, {}, raw_set.location}; raw_set.name, definition_instance->name, {}, raw_set.location};
@@ -2040,7 +2018,8 @@ class MappingContext {
std::remove_if(definition_.element_sets.begin(), std::remove_if(definition_.element_sets.begin(),
definition_.element_sets.end(), definition_.element_sets.end(),
[&raw_set](const ElementSet& set) { [&raw_set](const ElementSet& set) {
return EqualName(set.name, raw_set.name); return AsciiCaseInsensitiveEquals(set.name,
raw_set.name);
}), }),
definition_.element_sets.end()); definition_.element_sets.end());
ElementSet set{ ElementSet set{
@@ -2066,68 +2045,57 @@ class MappingContext {
} }
} }
std::optional<std::vector<EntityIndex>> ResolveNodeTarget( /// @brief Builds one stable node-target index from the completed candidate.
const std::string& target, const SourceLocation& location, SourceTargetIndex BuildSourceTargetIndex() const {
const std::string& keyword) { std::vector<SourceTargetIndexEntry> entries;
std::vector<const NodeSet*> matching_sets; std::size_t declaration_order = 0U;
for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) {
const auto& source_id = definition_.nodes[node].source_id;
entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "",
source_id, static_cast<EntityIndex>(node),
declaration_order++});
}
for (const auto& set : definition_.node_sets) { for (const auto& set : definition_.node_sets) {
if (EqualName(set.name, target)) { for (const EntityIndex node : set.node_indices) {
matching_sets.push_back(&set); const auto& source_id = definition_.nodes[node].source_id;
entries.push_back({SourceEntityKind::kNode,
set.instance_name.value_or(source_id.instance_name),
set.name, source_id, node, declaration_order++});
} }
} }
return SourceTargetIndex{std::move(entries)};
}
std::vector<EntityIndex> matching_nodes; std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
std::int64_t label = 0; const SourceTargetResolver& resolver, const std::string& target,
if (TryPositiveInteger(target, label)) { const SourceLocation& location, const std::string& keyword) {
for (std::size_t index = 0U; index < definition_.nodes.size(); ++index) { auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
if (definition_.nodes[index].source_id.source_label == label) { if (!resolved.HasValue()) {
matching_nodes.push_back(static_cast<EntityIndex>(index));
}
}
}
// A token is resolved only after both approved interpretations have
// been considered; declaration order never gives a node set priority
// over an equally valid direct source-label target.
if (matching_sets.size() > 1U) {
InputFailure( InputFailure(
"unresolved-reference", location, keyword, target, "unresolved-reference", location, keyword, target,
"The node-set target is ambiguous across identity instances."); "The boundary or load target must resolve unambiguously to one "
"node or one node set.");
return std::nullopt; return std::nullopt;
} }
if (matching_nodes.size() > 1U) { std::vector<EntityIndex> indices;
InputFailure("unresolved-reference", location, keyword, target, indices.reserve(resolved.Value().size());
"The direct node-label target is ambiguous across identity " for (const auto& entry : resolved.Value()) {
"instances."); indices.push_back(entry.entity_index);
return std::nullopt;
} }
if (matching_sets.size() == 1U && matching_nodes.size() == 1U) { return indices;
InputFailure("unresolved-reference", location, keyword, target,
"The target is ambiguous between a node-set name and a "
"direct node label.");
return std::nullopt;
}
if (matching_sets.size() == 1U) {
return matching_sets.front()->node_indices;
}
if (matching_nodes.size() == 1U) {
return matching_nodes;
}
InputFailure(
"unresolved-reference", location, keyword, target,
"The boundary or load target must resolve to one node or node set.");
return std::nullopt;
} }
void FinalizeStep() { void FinalizeStep() {
std::vector<BoundaryCondition> boundaries = model_boundaries_; std::vector<BoundaryCondition> boundaries = model_boundaries_;
boundaries.insert(boundaries.end(), step_.boundaries.begin(), boundaries.insert(boundaries.end(), step_.boundaries.begin(),
step_.boundaries.end()); step_.boundaries.end());
const SourceTargetIndex target_index = BuildSourceTargetIndex();
const SourceTargetResolver target_resolver{target_index};
std::map<std::pair<EntityIndex, int>, double> prescribed_values; std::map<std::pair<EntityIndex, int>, double> prescribed_values;
for (const auto& boundary : boundaries) { for (const auto& boundary : boundaries) {
auto target = auto target = ResolveNodeTarget(target_resolver, boundary.target,
ResolveNodeTarget(boundary.target, boundary.location, "BOUNDARY"); boundary.location, "BOUNDARY");
if (!target) { if (!target) {
return; return;
} }
@@ -2148,7 +2116,8 @@ class MappingContext {
} }
} }
for (const auto& load : step_.loads) { for (const auto& load : step_.loads) {
if (!ResolveNodeTarget(load.target, load.location, "CLOAD")) { if (!ResolveNodeTarget(target_resolver, load.target, load.location,
"CLOAD")) {
return; return;
} }
} }
+227
View File
@@ -0,0 +1,227 @@
#include "fesa/model/source_target_resolver.h"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "fesa/core/ascii.h"
#include "fesa/model/domain.h"
namespace fesa {
namespace {
struct Match {
const SourceTargetIndexEntry* entry;
bool named_target;
std::size_t input_order;
};
bool InstanceMatches(const std::string& query_instance,
const std::string& entry_instance) {
return query_instance.empty() ||
AsciiCaseInsensitiveEquals(query_instance, entry_instance);
}
bool LooksLikeInteger(const std::string& text) {
if (text.empty()) {
return false;
}
std::size_t offset = 0U;
if (text.front() == '+' || text.front() == '-') {
offset = 1U;
}
if (offset == text.size()) {
return false;
}
return std::all_of(
text.begin() + static_cast<std::ptrdiff_t>(offset), text.end(),
[](const char value) { return value >= '0' && value <= '9'; });
}
Diagnostic TargetDiagnostic(const std::string& code,
const std::string& identity,
const std::string& message) {
return {Severity::kError, code, {{}, 0U}, "SOURCE_TARGET", identity, message};
}
Result<std::vector<ResolvedSourceTarget>> TargetFailure(
const std::string& code, const std::string& identity,
const std::string& message) {
return Result<std::vector<ResolvedSourceTarget>>::Failure(Status::Failure(
FailureCategory::kInput, {TargetDiagnostic(code, identity, message)}));
}
bool SpansInstances(const std::vector<Match>& matches, const bool named) {
const SourceTargetIndexEntry* first = nullptr;
for (const auto& match : matches) {
if (match.named_target != named) {
continue;
}
if (first == nullptr) {
first = match.entry;
} else if (!AsciiCaseInsensitiveEquals(first->instance_name,
match.entry->instance_name)) {
return true;
}
}
return false;
}
} // namespace
SourceTargetIndex::SourceTargetIndex(
std::vector<SourceTargetIndexEntry> entries)
: entries_{std::move(entries)} {}
SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) {
std::vector<SourceTargetIndexEntry> entries;
std::size_t declaration_order = 0U;
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
const auto& node = domain.Nodes()[index];
entries.push_back({SourceEntityKind::kNode, node.source_id.instance_name,
"", node.source_id, static_cast<EntityIndex>(index),
declaration_order++});
}
for (const auto& set : domain.NodeSets()) {
for (const EntityIndex node_index : set.node_indices) {
SourceEntityId source_id{set.instance_name.value_or(""), 0, ""};
if (node_index < domain.Nodes().size()) {
source_id = domain.Nodes()[node_index].source_id;
}
entries.push_back({SourceEntityKind::kNode,
set.instance_name.value_or(source_id.instance_name),
set.name, std::move(source_id), node_index,
declaration_order++});
}
}
for (std::size_t index = 0U; index < domain.Elements().size(); ++index) {
const auto& element = domain.Elements()[index];
entries.push_back({SourceEntityKind::kElement,
element.source_id.instance_name, "", element.source_id,
static_cast<EntityIndex>(index), declaration_order++});
}
for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) {
const auto& element = domain.ShellElements()[index];
entries.push_back({SourceEntityKind::kElement,
element.source_id.instance_name, "", element.source_id,
static_cast<EntityIndex>(index), declaration_order++});
}
for (const auto& set : domain.ElementSets()) {
for (const EntityIndex element_index : set.element_indices) {
SourceEntityId source_id{set.instance_name.value_or(""), 0, ""};
if (domain.ShellElements().empty() &&
element_index < domain.Elements().size()) {
source_id = domain.Elements()[element_index].source_id;
} else if (domain.Elements().empty() &&
element_index < domain.ShellElements().size()) {
source_id = domain.ShellElements()[element_index].source_id;
}
entries.push_back({SourceEntityKind::kElement,
set.instance_name.value_or(source_id.instance_name),
set.name, std::move(source_id), element_index,
declaration_order++});
}
}
return SourceTargetIndex{std::move(entries)};
}
const std::vector<SourceTargetIndexEntry>& SourceTargetIndex::Entries()
const noexcept {
return entries_;
}
SourceTargetResolver::SourceTargetResolver(
const SourceTargetIndex& index) noexcept
: index_{&index} {}
Result<std::vector<ResolvedSourceTarget>> SourceTargetResolver::Resolve(
const SourceTargetQuery& query) const {
const auto parsed_label =
ParsePositiveSourceLabel(query.target_name_or_label);
std::vector<Match> matches;
for (std::size_t input_order = 0U; input_order < index_->Entries().size();
++input_order) {
const auto& entry = index_->Entries()[input_order];
if (entry.entity_kind != query.entity_kind ||
!InstanceMatches(query.instance_name, entry.instance_name)) {
continue;
}
const bool named = !entry.target_name.empty() &&
AsciiCaseInsensitiveEquals(entry.target_name,
query.target_name_or_label);
const bool direct = entry.target_name.empty() && parsed_label.HasValue() &&
entry.source_id.source_label == parsed_label.Value();
if (named || direct) {
matches.push_back({&entry, named, input_order});
}
}
if (matches.empty()) {
if (!parsed_label.HasValue() &&
LooksLikeInteger(query.target_name_or_label)) {
return TargetFailure(
"invalid-source-target", query.target_name_or_label,
"A direct source target must be a positive base-10 label.");
}
return TargetFailure(
"unresolved-source-target", query.target_name_or_label,
"The source target does not resolve in its namespace.");
}
const bool has_named =
std::any_of(matches.begin(), matches.end(),
[](const Match& match) { return match.named_target; });
const bool has_direct =
std::any_of(matches.begin(), matches.end(),
[](const Match& match) { return !match.named_target; });
if ((has_named && has_direct) || SpansInstances(matches, true) ||
SpansInstances(matches, false)) {
return TargetFailure(
"ambiguous-source-target", query.target_name_or_label,
"The source target has more than one valid identity interpretation.");
}
std::stable_sort(
matches.begin(), matches.end(), [](const Match& lhs, const Match& rhs) {
if (lhs.entry->declaration_order != rhs.entry->declaration_order) {
return lhs.entry->declaration_order < rhs.entry->declaration_order;
}
return lhs.input_order < rhs.input_order;
});
std::vector<Diagnostic> duplicate_diagnostics;
std::vector<EntityIndex> seen_indices;
for (const auto& match : matches) {
const auto duplicate = std::find(seen_indices.begin(), seen_indices.end(),
match.entry->entity_index);
if (duplicate != seen_indices.end()) {
duplicate_diagnostics.push_back(TargetDiagnostic(
"duplicate-source-target", query.target_name_or_label,
"Duplicate resolved source target " +
match.entry->source_id.source_label_text + "."));
} else {
seen_indices.push_back(match.entry->entity_index);
}
}
if (!duplicate_diagnostics.empty()) {
return Result<std::vector<ResolvedSourceTarget>>::Failure(Status::Failure(
FailureCategory::kInput, std::move(duplicate_diagnostics)));
}
if (!has_named && seen_indices.size() > 1U) {
return TargetFailure(
"ambiguous-source-target", query.target_name_or_label,
"The direct source label resolves to multiple semantic entities.");
}
std::vector<ResolvedSourceTarget> resolved;
resolved.reserve(matches.size());
for (const auto& match : matches) {
resolved.push_back({match.entry->source_id, match.entry->entity_index});
}
return Result<std::vector<ResolvedSourceTarget>>::Success(
std::move(resolved));
}
} // namespace fesa
+21 -62
View File
@@ -2,21 +2,19 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <charconv>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <limits> #include <limits>
#include <optional> #include <optional>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <system_error>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "fesa/elements/euler_beam_3d.h" #include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/mitc4_shell.h" #include "fesa/elements/mitc4_shell.h"
#include "fesa/math/vector3.h" #include "fesa/math/vector3.h"
#include "fesa/model/source_target_resolver.h"
namespace fesa { namespace fesa {
namespace { namespace {
@@ -324,70 +322,29 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
return Status::Ok(); return Status::Ok();
} }
char AsciiLower(const char value) { Result<std::vector<EntityIndex>> ResolveLoadTarget(
if (value >= 'A' && value <= 'Z') { const SourceTargetResolver& resolver, const Domain& domain,
return static_cast<char>(value + ('a' - 'A')); const NodalLoad& load) {
} auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
return value; if (!resolved.HasValue()) {
}
bool EqualName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(),
[](const char left_value, const char right_value) {
return AsciiLower(left_value) == AsciiLower(right_value);
});
}
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
}
Result<std::vector<EntityIndex>> ResolveLoadTarget(const Domain& domain,
const NodalLoad& load) {
std::vector<const NodeSet*> sets;
for (const auto& set : domain.NodeSets()) {
if (EqualName(set.name, load.target)) {
sets.push_back(&set);
}
}
std::vector<EntityIndex> nodes;
std::int64_t source_label = 0;
if (TryPositiveInteger(load.target, source_label)) {
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
if (domain.Nodes()[node].source_id.source_label == source_label) {
nodes.push_back(static_cast<EntityIndex>(node));
}
}
}
if (sets.size() > 1U || nodes.size() > 1U ||
(!sets.empty() && !nodes.empty())) {
return RecoveryResultFailure<std::vector<EntityIndex>>( return RecoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity", load.location, load.target, "invalid-node-station-entity", load.location, load.target,
"A station-eligibility load target must resolve unambiguously."); "A station-eligibility load target must resolve unambiguously.");
} }
if (!sets.empty()) { std::vector<EntityIndex> nodes;
std::vector<unsigned char> seen(domain.Nodes().size(), 0U); nodes.reserve(resolved.Value().size());
for (const EntityIndex node : sets.front()->node_indices) { std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
if (node >= domain.Nodes().size() || seen[node] != 0U) { for (const auto& target : resolved.Value()) {
return RecoveryResultFailure<std::vector<EntityIndex>>( const EntityIndex node = target.entity_index;
"invalid-node-station-entity", load.location, load.target, if (node >= domain.Nodes().size() || seen[node] != 0U) {
"A station-eligibility node set must contain unique valid nodes."); return RecoveryResultFailure<std::vector<EntityIndex>>(
} "invalid-node-station-entity", load.location, load.target,
seen[node] = 1U; "A station-eligibility node set must contain unique valid nodes.");
} }
return Result<std::vector<EntityIndex>>::Success( seen[node] = 1U;
sets.front()->node_indices); nodes.push_back(node);
} }
if (!nodes.empty()) { return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
}
return RecoveryResultFailure<std::vector<EntityIndex>>(
"invalid-node-station-entity", load.location, load.target,
"A station-eligibility load target must resolve to a node or node set.");
} }
bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale, bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale,
@@ -879,6 +836,8 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
"invalid-node-station-entity", model.Step().location, model.Step().name, "invalid-node-station-entity", model.Step().location, model.Step().name,
"The active load view must preserve every sole-step load."); "The active load view must preserve every sole-step load.");
} }
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
const SourceTargetResolver target_resolver{target_index};
for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) { for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) {
const EntityIndex load_index = model.ActiveLoads()[order]; const EntityIndex load_index = model.ActiveLoads()[order];
if (load_index != order || load_index >= model.Step().loads.size()) { if (load_index != order || load_index >= model.Step().loads.size()) {
@@ -893,7 +852,7 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
"nonfinite-node-station-value", load.location, load.target, "nonfinite-node-station-value", load.location, load.target,
"Station eligibility requires finite concentrated loads."); "Station eligibility requires finite concentrated loads.");
} }
auto targets = ResolveLoadTarget(domain, load); auto targets = ResolveLoadTarget(target_resolver, domain, load);
if (!targets.HasValue()) { if (!targets.HasValue()) {
return Result<std::vector<NodeStationResultRow>>::Failure( return Result<std::vector<NodeStationResultRow>>::Failure(
targets.GetStatus()); targets.GetStatus());
+2
View File
@@ -9,6 +9,7 @@ add_executable(
unit/assembly/load_assembler_test.cpp unit/assembly/load_assembler_test.cpp
unit/assembly/sparse_assembler_test.cpp unit/assembly/sparse_assembler_test.cpp
unit/constraints/essential_constraints_test.cpp unit/constraints/essential_constraints_test.cpp
unit/core/ascii_test.cpp
unit/core/diagnostic_test.cpp unit/core/diagnostic_test.cpp
unit/core/source_identity_test.cpp unit/core/source_identity_test.cpp
unit/core/status_test.cpp unit/core/status_test.cpp
@@ -27,6 +28,7 @@ add_executable(
unit/model/domain_test.cpp unit/model/domain_test.cpp
unit/model/model_types_test.cpp unit/model/model_types_test.cpp
unit/model/shell_geometry_test.cpp unit/model/shell_geometry_test.cpp
unit/model/source_target_resolver_test.cpp
unit/results/result_records_test.cpp unit/results/result_records_test.cpp
unit/results/result_recovery_test.cpp unit/results/result_recovery_test.cpp
unit/results/results_writer_test.cpp unit/results/results_writer_test.cpp
+57
View File
@@ -0,0 +1,57 @@
#include "fesa/core/ascii.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
TEST(Ascii, LowercasesOnlyAsciiUppercaseBytes) {
EXPECT_EQ(fesa::AsciiLower('A'), 'a');
EXPECT_EQ(fesa::AsciiLower('Z'), 'z');
EXPECT_EQ(fesa::AsciiLower('a'), 'a');
EXPECT_EQ(fesa::AsciiLower('0'), '0');
const char non_ascii = static_cast<char>(0xc0U);
EXPECT_EQ(fesa::AsciiLower(non_ascii), non_ascii);
}
TEST(Ascii, ComparesNamesWithoutLocaleOrUnicodeFolding) {
EXPECT_TRUE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "bEaM-1"));
EXPECT_FALSE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "Beam-10"));
const std::string uppercase_byte{static_cast<char>(0xc0U)};
const std::string lowercase_byte{static_cast<char>(0xe0U)};
EXPECT_FALSE(
fesa::AsciiCaseInsensitiveEquals(uppercase_byte, lowercase_byte));
}
TEST(Ascii, ParsesOnlyCompletePositiveBaseTenSourceLabels) {
auto one = fesa::ParsePositiveSourceLabel("1");
auto leading_zeroes = fesa::ParsePositiveSourceLabel("00042");
auto maximum = fesa::ParsePositiveSourceLabel(
std::to_string((std::numeric_limits<std::int64_t>::max)()));
ASSERT_TRUE(one.HasValue());
EXPECT_EQ(one.Value(), 1);
ASSERT_TRUE(leading_zeroes.HasValue());
EXPECT_EQ(leading_zeroes.Value(), 42);
ASSERT_TRUE(maximum.HasValue());
EXPECT_EQ(maximum.Value(), (std::numeric_limits<std::int64_t>::max)());
const std::string overflow =
std::to_string((std::numeric_limits<std::int64_t>::max)()) + "0";
const std::vector<std::string> invalid_values{"", "0", "-1", "+", "+1",
" 1", "1 ", "1x", overflow};
for (const std::string& invalid : invalid_values) {
SCOPED_TRACE(invalid);
auto result = fesa::ParsePositiveSourceLabel(invalid);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
"invalid-source-label");
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].entity_identity, invalid);
}
}
@@ -0,0 +1,135 @@
#include "fesa/model/source_target_resolver.h"
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace {
fesa::SourceTargetIndexEntry Entry(const fesa::SourceEntityKind entity_kind,
std::string instance_name,
std::string target_name,
const std::int64_t source_label,
std::string source_label_text,
const fesa::EntityIndex entity_index,
const std::size_t declaration_order) {
fesa::SourceEntityId source_id{instance_name, source_label,
std::move(source_label_text)};
return {entity_kind,
std::move(instance_name),
std::move(target_name),
std::move(source_id),
entity_index,
declaration_order};
}
std::vector<fesa::EntityIndex> Indices(
const std::vector<fesa::ResolvedSourceTarget>& targets) {
std::vector<fesa::EntityIndex> indices;
indices.reserve(targets.size());
for (const auto& target : targets) {
indices.push_back(target.entity_index);
}
return indices;
}
void ExpectFailure(const fesa::SourceTargetIndex& index,
fesa::SourceTargetQuery query, const std::string& code) {
const auto result = fesa::SourceTargetResolver{index}.Resolve(query);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code, code);
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].entity_identity,
query.target_name_or_label);
}
} // namespace
TEST(SourceTargetResolver,
SeparatesNamespacesAndInstancesAndExpandsInDeclarationOrder) {
using fesa::SourceEntityKind;
fesa::SourceTargetIndex index{{
Entry(SourceEntityKind::kNode, "Beam-1", "Ends", 20, "0020", 1U, 30U),
Entry(SourceEntityKind::kElement, "Beam-1", "Ends", 700, "0700", 4U, 10U),
Entry(SourceEntityKind::kNode, "Beam-1", "Ends", 10, "0010", 0U, 20U),
Entry(SourceEntityKind::kNode, "Beam-2", "Ends", 10, "0010", 2U, 40U),
Entry(SourceEntityKind::kNode, "Beam-1", "", 10, "0010", 0U, 0U),
}};
const fesa::SourceTargetResolver resolver{index};
auto nodes = resolver.Resolve({SourceEntityKind::kNode, "bEaM-1", "eNdS"});
ASSERT_TRUE(nodes.HasValue());
EXPECT_EQ(Indices(nodes.Value()), (std::vector<fesa::EntityIndex>{0U, 1U}));
EXPECT_EQ(nodes.Value()[0U].source_id.instance_name, "Beam-1");
EXPECT_EQ(nodes.Value()[0U].source_id.source_label_text, "0010");
auto elements =
resolver.Resolve({SourceEntityKind::kElement, "BEAM-1", "ends"});
ASSERT_TRUE(elements.HasValue());
EXPECT_EQ(Indices(elements.Value()), (std::vector<fesa::EntityIndex>{4U}));
auto second_instance =
resolver.Resolve({SourceEntityKind::kNode, "beam-2", "ENDS"});
ASSERT_TRUE(second_instance.HasValue());
EXPECT_EQ(Indices(second_instance.Value()),
(std::vector<fesa::EntityIndex>{2U}));
auto direct = resolver.Resolve({SourceEntityKind::kNode, "Beam-1", "10"});
ASSERT_TRUE(direct.HasValue());
EXPECT_EQ(Indices(direct.Value()), (std::vector<fesa::EntityIndex>{0U}));
}
TEST(SourceTargetResolver, RejectsMissingAmbiguousAndNonpositiveTargets) {
using fesa::SourceEntityKind;
const fesa::SourceTargetIndex index{{
Entry(SourceEntityKind::kNode, "Beam-1", "", 10, "10", 0U, 0U),
Entry(SourceEntityKind::kNode, "Beam-2", "", 10, "10", 1U, 1U),
Entry(SourceEntityKind::kNode, "Beam-1", "10", 20, "20", 2U, 2U),
Entry(SourceEntityKind::kNode, "Beam-1", "Shared", 10, "10", 0U, 3U),
Entry(SourceEntityKind::kNode, "Beam-2", "Shared", 10, "10", 1U, 4U),
}};
ExpectFailure(index, {SourceEntityKind::kNode, "", "Missing"},
"unresolved-source-target");
ExpectFailure(index, {SourceEntityKind::kNode, "", "0"},
"invalid-source-target");
ExpectFailure(index, {SourceEntityKind::kNode, "", "-7"},
"invalid-source-target");
ExpectFailure(index, {SourceEntityKind::kNode, "", "10"},
"ambiguous-source-target");
ExpectFailure(index, {SourceEntityKind::kNode, "", "Shared"},
"ambiguous-source-target");
ExpectFailure(index, {SourceEntityKind::kNode, "Beam-1", "10"},
"ambiguous-source-target");
}
TEST(SourceTargetResolver,
ReportsDuplicateMembershipsInDeterministicDeclarationOrder) {
using fesa::SourceEntityKind;
const fesa::SourceTargetIndex index{{
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 10, "10", 0U, 1U),
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 10, "10", 0U, 9U),
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 20, "20", 1U, 2U),
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 20, "20", 1U, 7U),
}};
const auto result = fesa::SourceTargetResolver{index}.Resolve(
{SourceEntityKind::kNode, "Beam-1", "duplicate"});
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 2U);
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
"duplicate-source-target");
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].message,
"Duplicate resolved source target 20.");
EXPECT_EQ(result.GetStatus().Diagnostics()[1U].code,
"duplicate-source-target");
EXPECT_EQ(result.GetStatus().Diagnostics()[1U].message,
"Duplicate resolved source target 10.");
}