From 143c979faba32e38012b33ba9633234f824dbda5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:14:17 +0000 Subject: [PATCH 1/5] Initial plan From 5d1967273eb8a937fb9371e01bca90a2143eaf5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:20:37 +0000 Subject: [PATCH 2/5] Implement JSON-native templating with {!! ... !!} syntax - Add is_json_native_placeholder() and resolve_json_native() methods to TemplateEngine - Update BaseExecutor to detect and resolve JSON-native placeholders in dicts and lists - Add 18 unit tests for JSON-native resolution covering all types (bool, number, array, object, null) - Add 8 integration tests for HTTP execution with JSON-native placeholders - All 648 tests passing with no regressions Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- src/mcipy/executors/base.py | 22 ++- src/mcipy/templating.py | 63 +++++++ tests/test_execution.py | 330 ++++++++++++++++++++++++++++++++++ tests/unit/test_templating.py | 195 ++++++++++++++++++++ 4 files changed, 608 insertions(+), 2 deletions(-) diff --git a/src/mcipy/executors/base.py b/src/mcipy/executors/base.py index 27235fe..4740115 100644 --- a/src/mcipy/executors/base.py +++ b/src/mcipy/executors/base.py @@ -153,13 +153,22 @@ def _apply_basic_templating_to_dict( """ Apply basic templating to all string values in a dictionary. + Supports both standard {{...}} placeholders (resolved to strings) and + JSON-native {!!...!!} placeholders (resolved to native types). + Args: data: Dictionary to process (modified in-place) context: Context dictionary for template resolution """ for key, value in data.items(): if isinstance(value, str): - data[key] = self.template_engine.render_basic(value, context) + # Check if this is a JSON-native placeholder + if self.template_engine.is_json_native_placeholder(value): + # Resolve to native type + data[key] = self.template_engine.resolve_json_native(value, context) + else: + # Standard string templating + data[key] = self.template_engine.render_basic(value, context) elif isinstance(value, dict): self._apply_basic_templating_to_dict(value, context) elif isinstance(value, list): @@ -169,13 +178,22 @@ def _apply_basic_templating_to_list(self, data: list[Any], context: dict[str, An """ Apply basic templating to all string values in a list. + Supports both standard {{...}} placeholders (resolved to strings) and + JSON-native {!!...!!} placeholders (resolved to native types). + Args: data: List to process (modified in-place) context: Context dictionary for template resolution """ for i, value in enumerate(data): if isinstance(value, str): - data[i] = self.template_engine.render_basic(value, context) + # Check if this is a JSON-native placeholder + if self.template_engine.is_json_native_placeholder(value): + # Resolve to native type + data[i] = self.template_engine.resolve_json_native(value, context) + else: + # Standard string templating + data[i] = self.template_engine.render_basic(value, context) elif isinstance(value, dict): self._apply_basic_templating_to_dict(value, context) elif isinstance(value, list): diff --git a/src/mcipy/templating.py b/src/mcipy/templating.py index c024001..2c4071b 100644 --- a/src/mcipy/templating.py +++ b/src/mcipy/templating.py @@ -24,11 +24,74 @@ class TemplateEngine: Handles both basic placeholder substitution and advanced templating features like loops and conditional blocks. The engine supports: - Basic placeholders: {{props.propertyName}}, {{env.VAR_NAME}} + - JSON-native placeholders: {!!props.propertyName!!}, {!!env.VAR_NAME!!} - For loops: @for(i in range(0, 5)) ... @endfor - Foreach loops: @foreach(item in items) ... @endforeach - Control blocks: @if(condition) ... @elseif(condition) ... @else ... @endif """ + def is_json_native_placeholder(self, value: str) -> bool: + """ + Check if a string is a JSON-native placeholder (and only that). + + A valid JSON-native placeholder must be exactly in the format: + {!!path.to.value!!} with optional whitespace around the path. + + Args: + value: String to check + + Returns: + True if the string is exactly a JSON-native placeholder, False otherwise + """ + if not isinstance(value, str): + return False + + # Pattern: exactly {!! ... !!} with nothing before or after + pattern = r"^\{!!\s*([^}]+?)\s*!!\}$" + return bool(re.match(pattern, value)) + + def resolve_json_native(self, placeholder: str, context: dict[str, Any]) -> Any: + """ + Resolve a JSON-native placeholder to its native type. + + Extracts the path from {!!path!!} syntax and resolves it to the actual + value without converting to string. This preserves booleans, numbers, + arrays, objects, and null values. + + Args: + placeholder: The placeholder string (e.g., "{!!props.include_images!!}") + context: Context dictionary with 'props', 'env', and 'input' keys + + Returns: + The resolved value in its native type + + Raises: + TemplateError: If the placeholder is invalid or cannot be resolved + """ + # Validate it's a JSON-native placeholder + if not self.is_json_native_placeholder(placeholder): + raise TemplateError( + f"Invalid JSON-native placeholder format: '{placeholder}'. " + "Must be exactly {!!path!!} with no surrounding content." + ) + + # Extract the path from {!! ... !!} + pattern = r"^\{!!\s*([^}]+?)\s*!!\}$" + match = re.match(pattern, placeholder) + if not match: + raise TemplateError(f"Failed to extract path from placeholder: '{placeholder}'") + + path = match.group(1).strip() + + # Resolve the path to its native value + try: + value = self._resolve_placeholder(path, context) + return value + except Exception as e: + raise TemplateError( + f"Failed to resolve JSON-native placeholder '{placeholder}': {e}" + ) from e + def render_basic(self, template: str, context: dict[str, Any]) -> str: """ Perform basic placeholder substitution. diff --git a/tests/test_execution.py b/tests/test_execution.py index 3fa8a75..10a788e 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -419,3 +419,333 @@ def test_full_stack_all_executor_types(self): assert hasattr(result, "result") assert hasattr(result.result, "isError") assert hasattr(result.result, "content") + + +class TestJSONNativeResolutionIntegration: + """Integration tests for JSON-native {!! ... !!} resolution in execution.""" + + @pytest.fixture + def context_with_types(self): + """Fixture for context with various data types.""" + return { + "props": { + "include_images": True, + "case_sensitive": False, + "max_results": 100, + "quality": 0.95, + "urls": ["https://example.com/api", "https://test.com/api"], + "payload": {"key": "value", "count": 42}, + "tags": ["urgent", "review"], + "config": {"debug": False, "retries": 3}, + }, + "env": { + "FEATURE_ENABLED": True, + "MAX_TIMEOUT": 5000, + }, + "input": { + "include_images": True, + }, + } + + def test_http_json_body_with_boolean_native(self, context_with_types): + """Test HTTP execution with JSON body containing native boolean.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "include_images": "{!!props.include_images!!}", + "title": "{{props.title}}", # Standard string placeholder + }, + ) + context_with_types["props"]["title"] = "Test Report" + + config = HTTPExecutionConfig( + url="https://api.example.com/search", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify the JSON body was correctly constructed + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["include_images"] is True # Native boolean + assert isinstance(call_kwargs["json"]["include_images"], bool) + assert call_kwargs["json"]["title"] == "Test Report" # String + + def test_http_json_body_with_array_native(self, context_with_types): + """Test HTTP execution with JSON body containing native array.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "urls": "{!!props.urls!!}", + "tags": "{!!props.tags!!}", + }, + ) + + config = HTTPExecutionConfig( + url="https://api.example.com/batch", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify arrays are native + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["urls"] == [ + "https://example.com/api", + "https://test.com/api", + ] + assert isinstance(call_kwargs["json"]["urls"], list) + assert call_kwargs["json"]["tags"] == ["urgent", "review"] + assert isinstance(call_kwargs["json"]["tags"], list) + + def test_http_json_body_with_object_native(self, context_with_types): + """Test HTTP execution with JSON body containing native object.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "payload": "{!!props.payload!!}", + "config": "{!!props.config!!}", + }, + ) + + config = HTTPExecutionConfig( + url="https://api.example.com/data", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify objects are native + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["payload"] == {"key": "value", "count": 42} + assert isinstance(call_kwargs["json"]["payload"], dict) + assert call_kwargs["json"]["config"] == {"debug": False, "retries": 3} + assert isinstance(call_kwargs["json"]["config"], dict) + + def test_http_json_body_with_number_native(self, context_with_types): + """Test HTTP execution with JSON body containing native numbers.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "max_results": "{!!props.max_results!!}", + "quality": "{!!props.quality!!}", + }, + ) + + config = HTTPExecutionConfig( + url="https://api.example.com/query", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify numbers are native + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["max_results"] == 100 + assert isinstance(call_kwargs["json"]["max_results"], int) + assert call_kwargs["json"]["quality"] == 0.95 + assert isinstance(call_kwargs["json"]["quality"], float) + + def test_http_json_body_mixed_native_and_string(self, context_with_types): + """Test HTTP execution with mix of native and string placeholders.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "enabled": "{!!props.include_images!!}", # Native boolean + "count": "{!!props.max_results!!}", # Native number + "urls": "{!!props.urls!!}", # Native array + "name": "{{props.title}}", # String placeholder + "static": "fixed value", # No placeholder + }, + ) + context_with_types["props"]["title"] = "My Search" + + config = HTTPExecutionConfig( + url="https://api.example.com/complex", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify mixed types + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["enabled"] is True + assert isinstance(call_kwargs["json"]["enabled"], bool) + assert call_kwargs["json"]["count"] == 100 + assert isinstance(call_kwargs["json"]["count"], int) + assert isinstance(call_kwargs["json"]["urls"], list) + assert call_kwargs["json"]["name"] == "My Search" + assert isinstance(call_kwargs["json"]["name"], str) + assert call_kwargs["json"]["static"] == "fixed value" + + def test_http_json_body_nested_object_with_native(self, context_with_types): + """Test HTTP execution with nested objects containing native types.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "settings": { + "enabled": "{!!props.include_images!!}", + "max": "{!!props.max_results!!}", + "quality": "{!!props.quality!!}", + }, + "data": { + "tags": "{!!props.tags!!}", + }, + }, + ) + + config = HTTPExecutionConfig( + url="https://api.example.com/nested", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify nested native types + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["settings"]["enabled"] is True + assert isinstance(call_kwargs["json"]["settings"]["enabled"], bool) + assert call_kwargs["json"]["settings"]["max"] == 100 + assert call_kwargs["json"]["settings"]["quality"] == 0.95 + assert call_kwargs["json"]["data"]["tags"] == ["urgent", "review"] + assert isinstance(call_kwargs["json"]["data"]["tags"], list) + + def test_http_params_with_native_types(self, context_with_types): + """Test HTTP execution with query params containing native types.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + + config = HTTPExecutionConfig( + url="https://api.example.com/search", + method="GET", + params={ + "enabled": "{!!props.include_images!!}", + "max": "{!!props.max_results!!}", + }, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify params have native types + call_kwargs = mock_request.call_args[1] + assert call_kwargs["params"]["enabled"] is True + assert isinstance(call_kwargs["params"]["enabled"], bool) + assert call_kwargs["params"]["max"] == 100 + assert isinstance(call_kwargs["params"]["max"], int) + + def test_http_json_native_from_env(self, context_with_types): + """Test JSON-native resolution from environment variables.""" + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + body = HTTPBodyConfig( + type="json", + content={ + "feature_enabled": "{!!env.FEATURE_ENABLED!!}", + "timeout": "{!!env.MAX_TIMEOUT!!}", + }, + ) + + config = HTTPExecutionConfig( + url="https://api.example.com/config", + method="POST", + body=body, + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = executor.execute(config, context_with_types) + + assert not result.result.isError + + # Verify env values are native types + call_kwargs = mock_request.call_args[1] + assert call_kwargs["json"]["feature_enabled"] is True + assert isinstance(call_kwargs["json"]["feature_enabled"], bool) + assert call_kwargs["json"]["timeout"] == 5000 + assert isinstance(call_kwargs["json"]["timeout"], int) diff --git a/tests/unit/test_templating.py b/tests/unit/test_templating.py index 7148642..b2a561d 100644 --- a/tests/unit/test_templating.py +++ b/tests/unit/test_templating.py @@ -463,3 +463,198 @@ def test_whitespace_support_in_loop_variables(self, engine): # Should produce: "Alice,Alice;Bob,Bob;" assert result3 == "Alice,Alice;Bob,Bob;" + + +class TestJSONNativeResolution: + """Tests for JSON-native {!! ... !!} placeholder resolution.""" + + @pytest.fixture + def json_context(self) -> dict[str, Any]: + """Fixture for context with various data types.""" + return { + "props": { + "include_images": True, + "case_sensitive": False, + "max_results": 100, + "quality": 0.95, + "urls": ["https://example.com", "https://test.com"], + "payload": {"key": "value", "count": 42}, + "items": [1, 2, 3], + "metadata": {"name": "test", "enabled": True}, + "nothing": None, + }, + "env": { + "FEATURE_FLAG": True, + "MAX_RETRIES": 3, + "CONFIG": {"debug": False, "timeout": 30}, + }, + "input": { + "include_images": True, + }, + } + + def test_is_json_native_placeholder_valid(self, engine): + """Test detection of valid JSON-native placeholders.""" + assert engine.is_json_native_placeholder("{!!props.value!!}") is True + assert engine.is_json_native_placeholder("{!! props.value !!}") is True + assert engine.is_json_native_placeholder("{!!env.VAR!!}") is True + + def test_is_json_native_placeholder_invalid(self, engine): + """Test detection rejects invalid formats.""" + # Standard placeholders + assert engine.is_json_native_placeholder("{{props.value}}") is False + + # Mixed content + assert engine.is_json_native_placeholder("prefix {!!props.value!!}") is False + assert engine.is_json_native_placeholder("{!!props.value!!} suffix") is False + assert engine.is_json_native_placeholder("text {!!props.value!!} more") is False + + # Invalid syntax + assert engine.is_json_native_placeholder("{!props.value!}") is False + assert engine.is_json_native_placeholder("{!!props.value}") is False + assert engine.is_json_native_placeholder("props.value") is False + assert engine.is_json_native_placeholder("") is False + + # Non-string types + assert engine.is_json_native_placeholder(123) is False # pyright: ignore[reportArgumentType] + assert engine.is_json_native_placeholder(None) is False # pyright: ignore[reportArgumentType] + + def test_resolve_boolean_true(self, engine, json_context): + """Test resolving boolean true value.""" + result = engine.resolve_json_native("{!!props.include_images!!}", json_context) + assert result is True + assert isinstance(result, bool) + + def test_resolve_boolean_false(self, engine, json_context): + """Test resolving boolean false value.""" + result = engine.resolve_json_native("{!!props.case_sensitive!!}", json_context) + assert result is False + assert isinstance(result, bool) + + def test_resolve_number_integer(self, engine, json_context): + """Test resolving integer value.""" + result = engine.resolve_json_native("{!!props.max_results!!}", json_context) + assert result == 100 + assert isinstance(result, int) + + def test_resolve_number_float(self, engine, json_context): + """Test resolving float value.""" + result = engine.resolve_json_native("{!!props.quality!!}", json_context) + assert result == 0.95 + assert isinstance(result, float) + + def test_resolve_array(self, engine, json_context): + """Test resolving array value.""" + result = engine.resolve_json_native("{!!props.urls!!}", json_context) + assert result == ["https://example.com", "https://test.com"] + assert isinstance(result, list) + assert len(result) == 2 + + def test_resolve_array_of_numbers(self, engine, json_context): + """Test resolving array of numbers.""" + result = engine.resolve_json_native("{!!props.items!!}", json_context) + assert result == [1, 2, 3] + assert isinstance(result, list) + + def test_resolve_object(self, engine, json_context): + """Test resolving object value.""" + result = engine.resolve_json_native("{!!props.payload!!}", json_context) + assert result == {"key": "value", "count": 42} + assert isinstance(result, dict) + assert result["key"] == "value" + assert result["count"] == 42 + + def test_resolve_nested_object(self, engine, json_context): + """Test resolving nested object value.""" + result = engine.resolve_json_native("{!!props.metadata!!}", json_context) + assert result == {"name": "test", "enabled": True} + assert isinstance(result, dict) + + def test_resolve_null(self, engine, json_context): + """Test resolving null value.""" + result = engine.resolve_json_native("{!!props.nothing!!}", json_context) + assert result is None + + def test_resolve_from_env(self, engine, json_context): + """Test resolving from env context.""" + result = engine.resolve_json_native("{!!env.FEATURE_FLAG!!}", json_context) + assert result is True + assert isinstance(result, bool) + + result2 = engine.resolve_json_native("{!!env.MAX_RETRIES!!}", json_context) + assert result2 == 3 + assert isinstance(result2, int) + + result3 = engine.resolve_json_native("{!!env.CONFIG!!}", json_context) + assert result3 == {"debug": False, "timeout": 30} + assert isinstance(result3, dict) + + def test_resolve_from_input(self, engine, json_context): + """Test resolving from input context (alias for props).""" + result = engine.resolve_json_native("{!!input.include_images!!}", json_context) + assert result is True + assert isinstance(result, bool) + + def test_resolve_with_whitespace(self, engine, json_context): + """Test resolving with whitespace in placeholder.""" + result = engine.resolve_json_native("{!! props.include_images !!}", json_context) + assert result is True + + result2 = engine.resolve_json_native("{!! props.urls !!}", json_context) + assert result2 == ["https://example.com", "https://test.com"] + + def test_resolve_missing_path_error(self, engine, json_context): + """Test error when resolving missing path.""" + with pytest.raises(TemplateError) as exc_info: + engine.resolve_json_native("{!!props.missing!!}", json_context) + assert "not found" in str(exc_info.value).lower() + assert "props.missing" in str(exc_info.value) + + def test_resolve_invalid_format_error(self, engine, json_context): + """Test error when placeholder has invalid format.""" + with pytest.raises(TemplateError) as exc_info: + engine.resolve_json_native("{{props.value}}", json_context) + assert "invalid" in str(exc_info.value).lower() + + with pytest.raises(TemplateError) as exc_info: + engine.resolve_json_native("prefix {!!props.value!!}", json_context) + assert "invalid" in str(exc_info.value).lower() + assert "no surrounding content" in str(exc_info.value).lower() + + def test_resolve_mixed_content_error(self, engine, json_context): + """Test error when JSON-native placeholder has surrounding content.""" + # These should all raise errors because JSON-native must be the sole value + invalid_cases = [ + "text {!!props.value!!}", + "{!!props.value!!} text", + "prefix {!!props.value!!} suffix", + "The value is {!!props.value!!}", + ] + + for invalid_case in invalid_cases: + with pytest.raises(TemplateError) as exc_info: + engine.resolve_json_native(invalid_case, json_context) + assert "invalid" in str(exc_info.value).lower() + + def test_preserve_type_integrity(self, engine, json_context): + """Test that types are preserved exactly.""" + # Boolean should not become string + bool_result = engine.resolve_json_native("{!!props.include_images!!}", json_context) + assert bool_result is True + assert bool_result != "true" + assert bool_result != "True" + + # Number should not become string + num_result = engine.resolve_json_native("{!!props.max_results!!}", json_context) + assert num_result == 100 + assert num_result != "100" + + # Array should not become string + arr_result = engine.resolve_json_native("{!!props.urls!!}", json_context) + assert isinstance(arr_result, list) + assert arr_result != str(arr_result) + + # Object should not become string + obj_result = engine.resolve_json_native("{!!props.payload!!}", json_context) + assert isinstance(obj_result, dict) + assert obj_result != str(obj_result) From 1e488a404dd31d3a14ce07f3a2855bd35239f915 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:23:48 +0000 Subject: [PATCH 3/5] Add comprehensive documentation and manual test for JSON-native templating - Update schema_reference.md with detailed JSON-native placeholder documentation - Add examples for boolean, number, array, object, and null types - Document limitations, error cases, and when to use each syntax - Update input schema example to use JSON-native placeholders - Update PRD.md to include JSON-native templating - Add manual feature test demonstrating the difference between syntaxes - All tests passing (648 total) Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- PRD.md | 3 +- docs/schema_reference.md | 235 ++++++++++++++++- testsManual/test_json_native_feature.py | 323 ++++++++++++++++++++++++ 3 files changed, 554 insertions(+), 7 deletions(-) create mode 100644 testsManual/test_json_native_feature.py diff --git a/PRD.md b/PRD.md index 3da6f62..65c7a0a 100644 --- a/PRD.md +++ b/PRD.md @@ -322,7 +322,8 @@ Note: Include metadata field for infos like http status code, CLI exit code, etc Basic templating should be enabled in parts where we have templating such as execution part, headers and etc. Loops and Control blocks should be applied in large text parts, like text execution and while parsing a file in file execution flow. -- **Basic**: replaces placeholders like `{{props.propertyName}}` and `{{env.VAR_NAME}}` with values. +- **Basic**: replaces placeholders like `{{props.propertyName}}` and `{{env.VAR_NAME}}` with values (always as strings). +- **JSON-Native**: resolves placeholders like `{!!props.propertyName!!}` and `{!!env.VAR_NAME!!}` to native JSON types (boolean, number, array, object, null). Must be the sole value in a field. - **Loops**: For array and object props or env variables, Adapter should be able to parse `@for` -> `@endfor` and `@foreach` -> `@endforeach` - **Control Blocks**: Adapter should be able to use control blocks: `@if` -> `@elseif` -> `@else` -> `@endif` diff --git a/docs/schema_reference.md b/docs/schema_reference.md index 9b8345f..0dfc1f4 100644 --- a/docs/schema_reference.md +++ b/docs/schema_reference.md @@ -660,19 +660,36 @@ This behavior prevents template substitution errors for optional properties that "default": 100 }, "file_extensions": { - "type": "string", - "description": "Optional comma-separated list of file extensions" + "type": "array", + "description": "Optional list of file extensions", + "items": { + "type": "string" + } } }, "required": ["pattern", "directory"] }, "execution": { - "type": "text", - "text": "Searching '{{props.pattern}}' in {{props.directory}} (images: {{props.include_images}}, max: {{props.max_results}})" + "type": "http", + "method": "POST", + "url": "https://api.example.com/search", + "body": { + "type": "json", + "content": { + "pattern": "{{props.pattern}}", + "directory": "{{props.directory}}", + "include_images": "{!!props.include_images!!}", + "case_sensitive": "{!!props.case_sensitive!!}", + "max_results": "{!!props.max_results!!}", + "file_extensions": "{!!props.file_extensions!!}" + } + } } } ``` +**Note**: Use `{!! ... !!}` syntax for non-string types (boolean, number, array, object) to preserve their native JSON types. See [JSON-Native Placeholders](#json-native-placeholders) for more details. + **Execution with minimal properties:** ```python # Only required properties provided @@ -690,10 +707,12 @@ client.execute("search_files", properties={ client.execute("search_files", properties={ "pattern": "FIXME", "directory": "/tmp", - "include_images": true, - "max_results": 50 + "include_images": True, + "max_results": 50, + "file_extensions": [".py", ".js"] }) # Result: include_images=true, max_results=50 (overridden), case_sensitive=true (default) +# file_extensions=[".py", ".js"] (provided) ``` **Property Resolution Rules:** @@ -1175,6 +1194,210 @@ Replace placeholders with values from the context. --- +### JSON-Native Placeholders + +Resolve placeholders to their native JSON types (boolean, number, array, object, null) instead of strings. + +**Syntax**: `{!!path.to.value!!}` + +**Important**: JSON-native placeholders must be the **only** content in a field. They cannot be mixed with other text. + +#### Supported Types + +- **Boolean**: `true` or `false` (not `"true"` or `"false"`) +- **Number**: Integer or float (not string representation) +- **Array**: Native JSON array (not stringified) +- **Object**: Native JSON object (not stringified) +- **Null**: `null` value (not `"null"` string) + +#### Examples + +**Boolean Properties**: + +```json +{ + "execution": { + "type": "http", + "body": { + "type": "json", + "content": { + "include_images": "{!!props.include_images!!}", + "case_sensitive": "{!!props.case_sensitive!!}" + } + } + } +} +``` + +When executed with properties `{"include_images": true, "case_sensitive": false}`, the JSON body will be: + +```json +{ + "include_images": true, + "case_sensitive": false +} +``` + +**Array Properties**: + +```json +{ + "execution": { + "type": "http", + "body": { + "type": "json", + "content": { + "urls": "{!!props.urls!!}", + "tags": "{!!props.tags!!}" + } + } + } +} +``` + +When executed with properties `{"urls": ["https://a.com", "https://b.com"], "tags": ["urgent", "review"]}`, the JSON body will be: + +```json +{ + "urls": ["https://a.com", "https://b.com"], + "tags": ["urgent", "review"] +} +``` + +**Object Properties**: + +```json +{ + "execution": { + "type": "http", + "body": { + "type": "json", + "content": { + "config": "{!!props.config!!}", + "metadata": "{!!props.metadata!!}" + } + } + } +} +``` + +When executed with properties `{"config": {"debug": false, "retries": 3}, "metadata": {"version": "1.0"}}`, the JSON body will be: + +```json +{ + "config": { + "debug": false, + "retries": 3 + }, + "metadata": { + "version": "1.0" + } +} +``` + +**Number Properties**: + +```json +{ + "execution": { + "type": "http", + "body": { + "type": "json", + "content": { + "max_results": "{!!props.max_results!!}", + "quality": "{!!props.quality!!}" + } + } + } +} +``` + +When executed with properties `{"max_results": 100, "quality": 0.95}`, the JSON body will be: + +```json +{ + "max_results": 100, + "quality": 0.95 +} +``` + +**Mixed Native and String Placeholders**: + +```json +{ + "execution": { + "type": "http", + "body": { + "type": "json", + "content": { + "enabled": "{!!props.enabled!!}", + "count": "{!!props.count!!}", + "name": "{{props.name}}", + "description": "Search for {{props.query}}" + } + } + } +} +``` + +When executed with properties `{"enabled": true, "count": 50, "name": "My Search", "query": "testing"}`, the JSON body will be: + +```json +{ + "enabled": true, + "count": 50, + "name": "My Search", + "description": "Search for testing" +} +``` + +#### Limitations and Error Cases + +**✅ Valid Usage**: + +```json +{ + "enabled": "{!!props.enabled!!}", + "items": "{!!props.items!!}", + "config": "{!!env.CONFIG!!}" +} +``` + +**❌ Invalid Usage** (will raise errors): + +```json +{ + "message": "Status: {!!props.enabled!!}", + "url": "https://api.com/{!!props.path!!}", + "combined": "{!!props.value!!} and more text" +} +``` + +**Error Messages**: + +- **Mixed Content**: "Invalid JSON-native placeholder format: 'text {!!props.value!!}'. Must be exactly {!!path!!} with no surrounding content." +- **Missing Property**: "Failed to resolve JSON-native placeholder '{!!props.missing!!}': Path 'props.missing' not found in context" +- **Invalid Syntax**: "Invalid JSON-native placeholder format: '{{props.value}}'. Must be exactly {!!path!!} with no surrounding content." + +#### When to Use JSON-Native vs String Placeholders + +**Use `{!! ... !!}` when**: + +- Property must be a native boolean (`true`/`false`) in JSON +- Property must be a native number (integer or float) in JSON +- Property is an array that should remain an array in JSON +- Property is an object that should remain an object in JSON +- You need to preserve the exact type from input schema + +**Use `{{ ... }}` when**: + +- Building strings with multiple placeholders +- Concatenating values: `"User {{props.name}} has ID {{props.id}}"` +- Property should always be a string in the output +- Using in URLs, headers, or other string-only contexts + +--- + ### For Loops Iterate a fixed number of times using a range. diff --git a/testsManual/test_json_native_feature.py b/testsManual/test_json_native_feature.py new file mode 100644 index 0000000..b1743ce --- /dev/null +++ b/testsManual/test_json_native_feature.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +Manual test for JSON-native {!! ... !!} placeholder resolution. + +This test demonstrates the difference between standard {{...}} placeholders +(which always resolve to strings) and JSON-native {!!...!!} placeholders +(which preserve native types like boolean, number, array, object). + +Run this test manually: + uv run python testsManual/test_json_native_feature.py + +Expected output: + ✓ All tests demonstrating JSON-native resolution passed! +""" + +import json +from typing import Any +from unittest.mock import Mock, patch + +from mcipy.enums import ExecutionType +from mcipy.executors import ExecutorFactory +from mcipy.models import HTTPBodyConfig, HTTPExecutionConfig + + +def print_section(title: str) -> None: + """Print a section header.""" + print(f"\n{'=' * 70}") + print(f" {title}") + print(f"{'=' * 70}\n") + + +def print_result(test_name: str, body_sent: dict[str, Any]) -> None: + """Print test result showing the JSON body that was sent.""" + print(f"✓ {test_name}") + print(f" Body sent: {json.dumps(body_sent, indent=2)}") + print() + + +def test_boolean_resolution(): + """Test that boolean values are preserved as native booleans.""" + print_section("Test 1: Boolean Resolution") + + context = { + "props": {"include_images": True, "case_sensitive": False}, + "env": {}, + "input": {"include_images": True, "case_sensitive": False}, + } + + # Standard placeholders (converts to string) + print("Using standard {{...}} placeholders:") + body_standard = HTTPBodyConfig( + type="json", + content={ + "include_images": "{{props.include_images}}", # Will be "True" (string) + "case_sensitive": "{{props.case_sensitive}}", # Will be "False" (string) + }, + ) + config_standard = HTTPExecutionConfig( + url="https://api.example.com/test", method="POST", body=body_standard + ) + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + executor.execute(config_standard, context) + body_sent = mock_request.call_args[1]["json"] + + print(f" include_images type: {type(body_sent['include_images']).__name__}") + print(f" include_images value: {repr(body_sent['include_images'])}") + print(f" case_sensitive type: {type(body_sent['case_sensitive']).__name__}") + print(f" case_sensitive value: {repr(body_sent['case_sensitive'])}") + print() + + # Values are strings "True" and "False" + assert body_sent["include_images"] == "True" + assert body_sent["case_sensitive"] == "False" + assert isinstance(body_sent["include_images"], str) + assert isinstance(body_sent["case_sensitive"], str) + + # JSON-native placeholders (preserves native type) + print("Using JSON-native {!!...!!} placeholders:") + body_native = HTTPBodyConfig( + type="json", + content={ + "include_images": "{!!props.include_images!!}", # Will be true (boolean) + "case_sensitive": "{!!props.case_sensitive!!}", # Will be false (boolean) + }, + ) + config_native = HTTPExecutionConfig( + url="https://api.example.com/test", method="POST", body=body_native + ) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + executor.execute(config_native, context) + body_sent = mock_request.call_args[1]["json"] + + print(f" include_images type: {type(body_sent['include_images']).__name__}") + print(f" include_images value: {repr(body_sent['include_images'])}") + print(f" case_sensitive type: {type(body_sent['case_sensitive']).__name__}") + print(f" case_sensitive value: {repr(body_sent['case_sensitive'])}") + print() + + # Values are native booleans + assert body_sent["include_images"] is True + assert body_sent["case_sensitive"] is False + assert isinstance(body_sent["include_images"], bool) + assert isinstance(body_sent["case_sensitive"], bool) + + print_result("Boolean values preserved as native types", body_sent) + + +def test_array_and_object_resolution(): + """Test that arrays and objects are preserved as native types.""" + print_section("Test 2: Array and Object Resolution") + + context = { + "props": { + "urls": ["https://api1.com", "https://api2.com"], + "config": {"debug": False, "retries": 3, "timeout": 5000}, + }, + "env": {}, + "input": {}, + } + + body = HTTPBodyConfig( + type="json", + content={ + "urls": "{!!props.urls!!}", # Native array + "config": "{!!props.config!!}", # Native object + }, + ) + config = HTTPExecutionConfig( + url="https://api.example.com/test", method="POST", body=body + ) + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + executor.execute(config, context) + body_sent = mock_request.call_args[1]["json"] + + print(f" urls type: {type(body_sent['urls']).__name__}") + print(f" urls value: {body_sent['urls']}") + print(f" config type: {type(body_sent['config']).__name__}") + print(f" config value: {body_sent['config']}") + print() + + # Verify arrays and objects are native + assert isinstance(body_sent["urls"], list) + assert body_sent["urls"] == ["https://api1.com", "https://api2.com"] + assert isinstance(body_sent["config"], dict) + assert body_sent["config"]["debug"] is False + assert body_sent["config"]["retries"] == 3 + + print_result("Arrays and objects preserved as native types", body_sent) + + +def test_number_resolution(): + """Test that numbers are preserved as native integers and floats.""" + print_section("Test 3: Number Resolution") + + context = { + "props": {"max_results": 100, "quality": 0.95, "temperature": 72.5}, + "env": {}, + "input": {}, + } + + body = HTTPBodyConfig( + type="json", + content={ + "max_results": "{!!props.max_results!!}", # Native integer + "quality": "{!!props.quality!!}", # Native float + "temperature": "{!!props.temperature!!}", # Native float + }, + ) + config = HTTPExecutionConfig( + url="https://api.example.com/test", method="POST", body=body + ) + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + executor.execute(config, context) + body_sent = mock_request.call_args[1]["json"] + + print(f" max_results type: {type(body_sent['max_results']).__name__}") + print(f" max_results value: {body_sent['max_results']}") + print(f" quality type: {type(body_sent['quality']).__name__}") + print(f" quality value: {body_sent['quality']}") + print(f" temperature type: {type(body_sent['temperature']).__name__}") + print(f" temperature value: {body_sent['temperature']}") + print() + + # Verify numbers are native + assert isinstance(body_sent["max_results"], int) + assert body_sent["max_results"] == 100 + assert isinstance(body_sent["quality"], float) + assert body_sent["quality"] == 0.95 + assert isinstance(body_sent["temperature"], float) + assert body_sent["temperature"] == 72.5 + + print_result("Numbers preserved as native types", body_sent) + + +def test_mixed_placeholders(): + """Test mixing JSON-native and standard placeholders.""" + print_section("Test 4: Mixed Placeholders") + + context = { + "props": { + "enabled": True, + "count": 50, + "urls": ["https://a.com", "https://b.com"], + "name": "My Search", + "query": "testing", + }, + "env": {}, + "input": {}, + } + + body = HTTPBodyConfig( + type="json", + content={ + "enabled": "{!!props.enabled!!}", # Native boolean + "count": "{!!props.count!!}", # Native number + "urls": "{!!props.urls!!}", # Native array + "name": "{{props.name}}", # String placeholder + "description": "Search for {{props.query}}", # String with placeholder + "static": "fixed value", # No placeholder + }, + ) + config = HTTPExecutionConfig( + url="https://api.example.com/test", method="POST", body=body + ) + executor = ExecutorFactory.get_executor(ExecutionType.HTTP) + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + executor.execute(config, context) + body_sent = mock_request.call_args[1]["json"] + + print(" Mixed placeholder types in same JSON body:") + print(f" enabled: {body_sent['enabled']} (type: {type(body_sent['enabled']).__name__})") + print(f" count: {body_sent['count']} (type: {type(body_sent['count']).__name__})") + print(f" urls: {body_sent['urls']} (type: {type(body_sent['urls']).__name__})") + print(f" name: {body_sent['name']} (type: {type(body_sent['name']).__name__})") + print( + f" description: {body_sent['description']} (type: {type(body_sent['description']).__name__})" + ) + print(f" static: {body_sent['static']} (type: {type(body_sent['static']).__name__})") + print() + + # Verify mixed types + assert body_sent["enabled"] is True + assert isinstance(body_sent["enabled"], bool) + assert body_sent["count"] == 50 + assert isinstance(body_sent["count"], int) + assert isinstance(body_sent["urls"], list) + assert body_sent["name"] == "My Search" + assert isinstance(body_sent["name"], str) + assert body_sent["description"] == "Search for testing" + assert isinstance(body_sent["description"], str) + + print_result("Mixed native and string placeholders work together", body_sent) + + +def main(): + """Run all manual tests.""" + print("\n" + "=" * 70) + print(" JSON-Native Placeholder Resolution - Manual Feature Test") + print("=" * 70) + print("\nThis test demonstrates the {!!...!!} syntax for JSON-native resolution.") + print("Standard {{...}} placeholders always resolve to strings.") + print("JSON-native {!!...!!} placeholders preserve native types.\n") + + try: + test_boolean_resolution() + test_array_and_object_resolution() + test_number_resolution() + test_mixed_placeholders() + + print("\n" + "=" * 70) + print(" ✓ All tests demonstrating JSON-native resolution passed!") + print("=" * 70 + "\n") + + except AssertionError as e: + print(f"\n✗ Test failed: {e}\n") + raise + + +if __name__ == "__main__": + main() From e8916f9004a1aee2127070e8cab0762eb6dd18c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:28:47 +0000 Subject: [PATCH 4/5] Add comprehensive end-to-end manual test for JSON-native resolution - Create complete E2E test demonstrating all supported types - Test boolean, number (int/float), array, object, null, and string types - Verify type preservation through full MCIClient execution flow - Add clear visual output showing type verification - Test passes successfully demonstrating feature works end-to-end Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- testsManual/test_json_native_e2e.py | 259 ++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 testsManual/test_json_native_e2e.py diff --git a/testsManual/test_json_native_e2e.py b/testsManual/test_json_native_e2e.py new file mode 100644 index 0000000..449d82b --- /dev/null +++ b/testsManual/test_json_native_e2e.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +End-to-end integration test for JSON-native resolution feature. + +This test verifies that the feature works correctly when used through +the full MCIClient API, demonstrating a real-world use case. + +Run this test manually: + uv run python testsManual/test_json_native_e2e.py +""" + +import json +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +from mcipy import MCIClient + + +def test_e2e_json_native_api_call(): + """Test JSON-native resolution through full MCIClient execution flow.""" + print("\n" + "=" * 70) + print(" End-to-End Test: JSON-Native Resolution with MCIClient") + print("=" * 70 + "\n") + + # Create a temporary MCI schema file with JSON-native placeholders + schema = { + "schemaVersion": "1.0", + "metadata": { + "name": "Search API Test", + "version": "1.0.0", + }, + "tools": [ + { + "name": "search_files", + "description": "Search files with various filters", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query", + }, + "include_images": { + "type": "boolean", + "description": "Include image files", + }, + "case_sensitive": { + "type": "boolean", + "description": "Case-sensitive search", + }, + "max_results": { + "type": "number", + "description": "Maximum results", + }, + "quality": { + "type": "number", + "description": "Quality score (float)", + }, + "file_types": { + "type": "array", + "description": "File type filters", + "items": {"type": "string"}, + }, + "advanced_options": { + "type": "object", + "description": "Advanced search options", + }, + "tags": { + "type": "array", + "description": "Tags", + "items": {"type": "string"}, + }, + "metadata": { + "type": "object", + "description": "Metadata", + }, + "nothing": { + "type": "null", + "description": "Null value test", + }, + }, + "required": ["query"], + }, + "execution": { + "type": "http", + "method": "POST", + "url": "https://api.example.com/search", + "body": { + "type": "json", + "content": { + # String placeholders + "query": "{{props.query}}", + "description": "Search for {{props.query}} items", + # JSON-native placeholders for all types + "include_images": "{!!props.include_images!!}", + "case_sensitive": "{!!props.case_sensitive!!}", + "max_results": "{!!props.max_results!!}", + "quality": "{!!props.quality!!}", + "file_types": "{!!props.file_types!!}", + "advanced_options": "{!!props.advanced_options!!}", + "tags": "{!!props.tags!!}", + "metadata": "{!!props.metadata!!}", + "nothing": "{!!props.nothing!!}", + }, + }, + }, + } + ], + } + + # Write schema to temporary file + with tempfile.NamedTemporaryFile( + mode="w", suffix=".mci.json", delete=False + ) as f: + json.dump(schema, f) + schema_path = f.name + + try: + # Initialize MCIClient + client = MCIClient(json_file_path=schema_path, env_vars={}) + + # Execution with all property types + print("Executing tool with all JSON-native type examples") + print("-" * 70) + + properties = { + "query": "test search", + # Booleans + "include_images": True, + "case_sensitive": False, + # Numbers + "max_results": 50, + "quality": 0.95, + # Arrays + "file_types": [".py", ".js", ".md"], + "tags": ["urgent", "review", "testing"], + # Objects + "advanced_options": {"fuzzy": True, "language": "en", "limit": 100}, + "metadata": {"version": "1.0", "priority": 5, "active": True}, + # Null + "nothing": None, + } + + print(f"Input properties:\n{json.dumps(properties, indent=2)}\n") + + with patch("requests.request") as mock_request: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "results": ["file1.py", "file2.js"], + "count": 2, + } + mock_response.headers = {"Content-Type": "application/json"} + mock_response.raise_for_status = Mock() + mock_request.return_value = mock_response + + result = client.execute("search_files", properties=properties) + + # Verify execution succeeded + assert not result.result.isError, f"Execution failed: {result.result}" + + # Get the actual JSON body that was sent + call_kwargs = mock_request.call_args[1] + body_sent = call_kwargs["json"] + + print("JSON body sent to API:") + print(json.dumps(body_sent, indent=2)) + print() + + print("=" * 70) + print("Type Verification Results:") + print("=" * 70) + + # Verify String types (from {{...}}) + print("\n📝 String Placeholders ({{...}}):") + assert isinstance(body_sent["query"], str) + assert body_sent["query"] == "test search" + print(f" ✓ query: str = '{body_sent['query']}'") + + assert isinstance(body_sent["description"], str) + assert "Search for test search items" in body_sent["description"] + print(f" ✓ description: str = '{body_sent['description']}'") + + # Verify Boolean types (from {!!...!!}) + print("\n✓ Boolean Placeholders ({!!...!!}):") + assert isinstance(body_sent["include_images"], bool) + assert body_sent["include_images"] is True + print(f" ✓ include_images: bool = {body_sent['include_images']}") + + assert isinstance(body_sent["case_sensitive"], bool) + assert body_sent["case_sensitive"] is False + print(f" ✓ case_sensitive: bool = {body_sent['case_sensitive']}") + + # Verify Number types (from {!!...!!}) + print("\n🔢 Number Placeholders ({!!...!!}):") + assert isinstance(body_sent["max_results"], int) + assert body_sent["max_results"] == 50 + print(f" ✓ max_results: int = {body_sent['max_results']}") + + assert isinstance(body_sent["quality"], float) + assert body_sent["quality"] == 0.95 + print(f" ✓ quality: float = {body_sent['quality']}") + + # Verify Array types (from {!!...!!}) + print("\n📋 Array Placeholders ({!!...!!}):") + assert isinstance(body_sent["file_types"], list) + assert body_sent["file_types"] == [".py", ".js", ".md"] + print(f" ✓ file_types: list = {body_sent['file_types']}") + + assert isinstance(body_sent["tags"], list) + assert body_sent["tags"] == ["urgent", "review", "testing"] + print(f" ✓ tags: list = {body_sent['tags']}") + + # Verify Object types (from {!!...!!}) + print("\n🗂️ Object Placeholders ({!!...!!}):") + assert isinstance(body_sent["advanced_options"], dict) + assert body_sent["advanced_options"]["fuzzy"] is True + assert body_sent["advanced_options"]["language"] == "en" + print(f" ✓ advanced_options: dict = {json.dumps(body_sent['advanced_options'])}") + + assert isinstance(body_sent["metadata"], dict) + assert body_sent["metadata"]["version"] == "1.0" + assert body_sent["metadata"]["priority"] == 5 + assert body_sent["metadata"]["active"] is True + print(f" ✓ metadata: dict = {json.dumps(body_sent['metadata'])}") + + # Verify Null type (from {!!...!!}) + print("\n∅ Null Placeholder ({!!...!!}):") + assert body_sent["nothing"] is None + print(f" ✓ nothing: NoneType = {body_sent['nothing']}") + + print("\n" + "=" * 70) + print(" ✓ All Type Checks Passed!") + print("=" * 70) + + print("\n" + "=" * 70) + print(" ✓ End-to-End Test Passed Successfully!") + print("=" * 70) + print( + "\nConclusion: JSON-native resolution works correctly through the full" + ) + print( + "MCIClient execution flow, preserving all native JSON types:" + ) + print(" • Booleans (true/false)") + print(" • Numbers (integers and floats)") + print(" • Arrays") + print(" • Objects") + print(" • Null values") + print(" • Strings (via standard {{...}} syntax)\n") + + finally: + # Clean up temporary file + Path(schema_path).unlink() + + +if __name__ == "__main__": + test_e2e_json_native_api_call() From ca7ce9683ac22689cd59ee843f39db0f824c1cc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:56:13 +0000 Subject: [PATCH 5/5] Extract duplicated regex pattern as class constant - Add _JSON_NATIVE_PATTERN class constant to TemplateEngine - Replace inline pattern strings with self._JSON_NATIVE_PATTERN - Eliminates duplication between is_json_native_placeholder and resolve_json_native methods - All 648 tests passing, linting clean Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- src/mcipy/templating.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mcipy/templating.py b/src/mcipy/templating.py index 2c4071b..adb42ee 100644 --- a/src/mcipy/templating.py +++ b/src/mcipy/templating.py @@ -30,6 +30,9 @@ class TemplateEngine: - Control blocks: @if(condition) ... @elseif(condition) ... @else ... @endif """ + # Pattern for JSON-native placeholders: {!!path!!} with optional whitespace + _JSON_NATIVE_PATTERN = r"^\{!!\s*([^}]+?)\s*!!\}$" + def is_json_native_placeholder(self, value: str) -> bool: """ Check if a string is a JSON-native placeholder (and only that). @@ -47,8 +50,7 @@ def is_json_native_placeholder(self, value: str) -> bool: return False # Pattern: exactly {!! ... !!} with nothing before or after - pattern = r"^\{!!\s*([^}]+?)\s*!!\}$" - return bool(re.match(pattern, value)) + return bool(re.match(self._JSON_NATIVE_PATTERN, value)) def resolve_json_native(self, placeholder: str, context: dict[str, Any]) -> Any: """ @@ -76,8 +78,7 @@ def resolve_json_native(self, placeholder: str, context: dict[str, Any]) -> Any: ) # Extract the path from {!! ... !!} - pattern = r"^\{!!\s*([^}]+?)\s*!!\}$" - match = re.match(pattern, placeholder) + match = re.match(self._JSON_NATIVE_PATTERN, placeholder) if not match: raise TemplateError(f"Failed to extract path from placeholder: '{placeholder}'")