| 319 | } |
| 320 | |
| 321 | std::optional<DataProcessorService::ResolvedInput> DataProcessorService::resolveInputField( |
| 322 | const std::string& name) const { |
| 323 | // Exact whole-topic name → the topic's first leaf column. |
| 324 | if (const auto topic = resolveInputTopic(name)) { |
| 325 | return ResolvedInput{topic->first, topic->second, 0}; |
| 326 | } |
| 327 | // Otherwise treat the name as "<topic>/<field-path>": find the LONGEST live topic |
| 328 | // whose name prefixes `name` (topic names can themselves contain '/'), then map the |
| 329 | // trailing field path to its flattened-leaf column index. |
| 330 | const TopicStorage* best_storage = nullptr; |
| 331 | TopicId best_tid = 0; |
| 332 | DatasetId best_ds = 0; |
| 333 | std::string best_field; |
| 334 | std::size_t best_topic_len = 0; |
| 335 | for (const DatasetId ds : engine_.listDatasets()) { |
| 336 | for (const TopicId tid : engine_.listTopics(ds)) { |
| 337 | const TopicStorage* storage = engine_.getTopicStorage(tid); |
| 338 | if (storage == nullptr) { |
| 339 | continue; |
| 340 | } |
| 341 | const std::string& tn = storage->descriptor().name; |
| 342 | if (name.size() > tn.size() + 1 && name.compare(0, tn.size(), tn) == 0 && name[tn.size()] == '/' && |
| 343 | tn.size() > best_topic_len) { |
| 344 | best_topic_len = tn.size(); |
| 345 | best_storage = storage; |
| 346 | best_tid = tid; |
| 347 | best_ds = ds; |
| 348 | best_field = name.substr(tn.size() + 1); |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | if (best_storage == nullptr) { |
| 353 | return std::nullopt; |
| 354 | } |
| 355 | // Map the field path to its leaf column index. The dropped/display name separates |
| 356 | // nested fields with '/', but both the type tree (flattenFieldPaths) and the stored |
| 357 | // column descriptors use '.'-separated paths (e.g. "orientation.w"); normalize the |
| 358 | // queried field to '.' before matching so a nested input resolves, not just a flat |
| 359 | // one. The returned index is the engine's leaf-column order — exactly what |
| 360 | // add_mimo_transform reads per input. |
| 361 | std::string field_dotted = best_field; |
| 362 | std::replace(field_dotted.begin(), field_dotted.end(), '/', '.'); |
| 363 | const auto matches = [&](const std::string& path) { return path == best_field || path == field_dotted; }; |
| 364 | |
| 365 | // Schema-bearing topics (e.g. ROS/CDR): resolve through the TypeRegistry tree. |
| 366 | const PJ::SchemaId schema_id = best_storage->descriptor().schema_id; |
| 367 | if (schema_id != 0) { |
| 368 | if (const PJ::TypeTreeNode* root = engine_.typeRegistry().lookup(schema_id)) { |
| 369 | const std::vector<std::string> paths = PJ::flattenFieldPaths(*root); |
| 370 | for (std::size_t i = 0; i < paths.size(); ++i) { |
| 371 | if (matches(paths[i])) { |
| 372 | return ResolvedInput{best_tid, best_ds, i}; |
| 373 | } |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | // Schema-less topics (schema_id == 0, e.g. JSON scalar-only ingest): the leaf names |
| 378 | // live in the stored column descriptors, whose order is the engine column index. |
nothing calls this directly
no test coverage detected