diff --git a/src/linkml_map/transformer/object_transformer.py b/src/linkml_map/transformer/object_transformer.py index 973f741..8455f55 100644 --- a/src/linkml_map/transformer/object_transformer.py +++ b/src/linkml_map/transformer/object_transformer.py @@ -596,6 +596,9 @@ def _map_value_by_range( if source_class_slot.multivalued and isinstance(v, list): return [self.transform_enum(v1, any_of_enums, source_obj) for v1 in v] return self.transform_enum(v, any_of_enums, source_obj) + # No range and no any_of enums: nothing to recurse into for scalars + if not isinstance(v, (dict, list)): + return v if source_class_slot.multivalued: if isinstance(v, list): diff --git a/src/linkml_map/utils/dynamic_object.py b/src/linkml_map/utils/dynamic_object.py index f6752a3..47ae56c 100644 --- a/src/linkml_map/utils/dynamic_object.py +++ b/src/linkml_map/utils/dynamic_object.py @@ -3,6 +3,7 @@ from typing import Any from linkml_runtime import SchemaView +from linkml_runtime.utils.formatutils import camelcase class DynObj: @@ -47,5 +48,7 @@ def dynamic_object(obj: dict, sv: SchemaView, target: str): else: v = dynamic_object(v, sv, rng) attrs[k] = v - cls = type(target, (DynObj,), {}) + # Use camelcase so the Python type name matches ObjectIndex._class_map, + # which is keyed by SchemaView.class_name_mappings() (camelcase). + cls = type(camelcase(target), (DynObj,), {}) return cls(**attrs) diff --git a/tests/test_transformer/test_class_name_spaces.py b/tests/test_transformer/test_class_name_spaces.py new file mode 100644 index 0000000..7efd77e --- /dev/null +++ b/tests/test_transformer/test_class_name_spaces.py @@ -0,0 +1,130 @@ +"""Regression test for expr with spaces in source class names. + +When the source class name contains a space (e.g. "my record"), expr-based +slot derivations should still be able to access source object fields. + +See: https://github.com/linkml/linkml-map/issues/170 +""" + +import textwrap + +from linkml_runtime import SchemaView + +from linkml_map.transformer.object_transformer import ObjectTransformer + +SOURCE_SCHEMA = textwrap.dedent("""\ + id: https://example.org/source + name: source + prefixes: + linkml: https://w3id.org/linkml/ + imports: + - linkml:types + default_range: string + + classes: + my record: + attributes: + record_id: + identifier: true + label: {} + source_ref: {} +""") + +TARGET_SCHEMA = textwrap.dedent("""\ + id: https://example.org/target + name: target + prefixes: + linkml: https://w3id.org/linkml/ + imports: + - linkml:types + default_range: string + + classes: + Output: + attributes: + id: {} + name: {} + source: {} +""") + +INPUT_OBJ = { + "record_id": "rec-001", + "label": "Hello World", + "source_ref": "db:12345", +} + + +def _make_transformer(transform_spec: dict) -> ObjectTransformer: + """Build an ObjectTransformer from inline schemas and a transform dict.""" + transform_spec["source_schema"] = "https://example.org/source" + transform_spec["target_schema"] = "https://example.org/target" + tr = ObjectTransformer() + tr.source_schemaview = SchemaView(SOURCE_SCHEMA) + tr.target_schemaview = SchemaView(TARGET_SCHEMA) + tr.create_transformer_specification(transform_spec) + return tr + + +def test_expr_with_space_in_class_name_no_index(): + """expr should resolve source fields when class name has a space (no index).""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["id"] == "rec-001" + assert result["name"] == "Hello World" + assert result["source"] == "db:12345" + + +def test_expr_with_space_in_class_name_with_index(): + """expr should resolve source fields when class name has a space (with index).""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + tr.index(INPUT_OBJ, "my record") + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["id"] == "rec-001" + assert result["name"] == "Hello World" + assert result["source"] == "db:12345" + + +def test_expr_string_concat_with_space_in_class_name(): + """expr with string operations should work when class name has a space.""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label + ' (copy)'"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["name"] == "Hello World (copy)"