Skip to content

Commit 42d940b

Browse files
committed
fix: add proper serialization/validation for auth and auth_tools fields
- Add field_serializer and field_validator for auth_tools in TextCallTemplate - Add field_serializer and field_validator for both auth and auth_tools in HttpCallTemplate - Use AuthSerializer.validate_dict() for proper dict-to-Auth conversion - Add comprehensive test coverage for auth_tools serialization - Ensures dict configurations preserve all critical authentication fields - All 155 tests pass with proper field validation
1 parent 1589bdf commit 42d940b

File tree

3 files changed

+90
-6
lines changed

3 files changed

+90
-6
lines changed

plugins/communication_protocols/http/src/utcp_http/http_call_template.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from utcp.data.call_template import CallTemplate, CallTemplateSerializer
2-
from utcp.data.auth import Auth
2+
from utcp.data.auth import Auth, AuthSerializer
33
from utcp.interfaces.serializer import Serializer
44
from utcp.exceptions import UtcpSerializerValidationError
55
import traceback
6-
from typing import Optional, Dict, List, Literal
7-
from pydantic import Field
6+
from typing import Optional, Dict, List, Literal, Any
7+
from pydantic import Field, field_serializer, field_validator
88

99
class HttpCallTemplate(CallTemplate):
1010
"""REQUIRED
@@ -108,6 +108,44 @@ class HttpCallTemplate(CallTemplate):
108108
body_field: Optional[str] = Field(default="body", description="The name of the single input field to be sent as the request body.")
109109
header_fields: Optional[List[str]] = Field(default=None, description="List of input fields to be sent as request headers.")
110110

111+
@field_serializer('auth')
112+
def serialize_auth(self, auth: Optional[Auth]) -> Optional[dict]:
113+
"""Serialize auth to dictionary."""
114+
if auth is None:
115+
return None
116+
return AuthSerializer().to_dict(auth)
117+
118+
@field_validator('auth', mode='before')
119+
@classmethod
120+
def validate_auth(cls, v: Any) -> Optional[Auth]:
121+
"""Validate and deserialize auth from dictionary."""
122+
if v is None:
123+
return None
124+
if isinstance(v, Auth):
125+
return v
126+
if isinstance(v, dict):
127+
return AuthSerializer().validate_dict(v)
128+
raise ValueError(f"auth must be None, Auth instance, or dict, got {type(v)}")
129+
130+
@field_serializer('auth_tools')
131+
def serialize_auth_tools(self, auth_tools: Optional[Auth]) -> Optional[dict]:
132+
"""Serialize auth_tools to dictionary."""
133+
if auth_tools is None:
134+
return None
135+
return AuthSerializer().to_dict(auth_tools)
136+
137+
@field_validator('auth_tools', mode='before')
138+
@classmethod
139+
def validate_auth_tools(cls, v: Any) -> Optional[Auth]:
140+
"""Validate and deserialize auth_tools from dictionary."""
141+
if v is None:
142+
return None
143+
if isinstance(v, Auth):
144+
return v
145+
if isinstance(v, dict):
146+
return AuthSerializer().validate_dict(v)
147+
raise ValueError(f"auth_tools must be None, Auth instance, or dict, got {type(v)}")
148+
111149

112150
class HttpCallTemplateSerializer(Serializer[HttpCallTemplate]):
113151
"""REQUIRED

plugins/communication_protocols/text/src/utcp_text/text_call_template.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Literal, Optional
2-
from pydantic import Field
1+
from typing import Literal, Optional, Any
2+
from pydantic import Field, field_serializer, field_validator
33

44
from utcp.data.call_template import CallTemplate
5-
from utcp.data.auth import Auth
5+
from utcp.data.auth import Auth, AuthSerializer
66
from utcp.interfaces.serializer import Serializer
77
from utcp.exceptions import UtcpSerializerValidationError
88
import traceback
@@ -26,6 +26,25 @@ class TextCallTemplate(CallTemplate):
2626
auth: None = None
2727
auth_tools: Optional[Auth] = Field(None, description="Authentication to apply to generated tools from OpenAPI specs.")
2828

29+
@field_serializer('auth_tools')
30+
def serialize_auth_tools(self, auth_tools: Optional[Auth]) -> Optional[dict]:
31+
"""Serialize auth_tools to dictionary."""
32+
if auth_tools is None:
33+
return None
34+
return AuthSerializer().to_dict(auth_tools)
35+
36+
@field_validator('auth_tools', mode='before')
37+
@classmethod
38+
def validate_auth_tools(cls, v: Any) -> Optional[Auth]:
39+
"""Validate and deserialize auth_tools from dictionary."""
40+
if v is None:
41+
return None
42+
if isinstance(v, Auth):
43+
return v
44+
if isinstance(v, dict):
45+
return AuthSerializer().validate_dict(v)
46+
raise ValueError(f"auth_tools must be None, Auth instance, or dict, got {type(v)}")
47+
2948

3049
class TextCallTemplateSerializer(Serializer[TextCallTemplate]):
3150
"""REQUIRED

plugins/communication_protocols/text/tests/test_text_communication_protocol.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,30 @@ async def test_text_call_template_with_auth_tools():
372372

373373
assert template.auth_tools == auth_tools
374374
assert template.auth is None # auth should still be None for file access
375+
376+
377+
@pytest.mark.asyncio
378+
async def test_text_call_template_auth_tools_serialization():
379+
"""Test that auth_tools field properly serializes and validates from dict."""
380+
# Test creation from dict
381+
template_dict = {
382+
"name": "test-template",
383+
"call_template_type": "text",
384+
"file_path": "test.json",
385+
"auth_tools": {
386+
"auth_type": "api_key",
387+
"api_key": "test-key",
388+
"var_name": "Authorization",
389+
"location": "header"
390+
}
391+
}
392+
393+
template = TextCallTemplate(**template_dict)
394+
assert template.auth_tools is not None
395+
assert template.auth_tools.api_key == "test-key"
396+
assert template.auth_tools.var_name == "Authorization"
397+
398+
# Test serialization to dict
399+
serialized = template.model_dump()
400+
assert serialized["auth_tools"]["auth_type"] == "api_key"
401+
assert serialized["auth_tools"]["api_key"] == "test-key"

0 commit comments

Comments
 (0)