@@ -68,9 +68,176 @@ class UiPathRuntimeSchema(BaseModel):
6868 model_config = COMMON_MODEL_SCHEMA
6969
7070
71+ def _get_job_attachment_definition () -> dict [str , Any ]:
72+ """Get the job-attachment definition schema for UiPath attachments.
73+
74+ Returns:
75+ The JSON schema definition for a UiPath job attachment.
76+ """
77+ return {
78+ "type" : "object" ,
79+ "required" : ["ID" ],
80+ "x-uipath-resource-kind" : "JobAttachment" ,
81+ "properties" : {
82+ "ID" : {"type" : "string" },
83+ "FullName" : {"type" : "string" },
84+ "MimeType" : {"type" : "string" },
85+ "Metadata" : {
86+ "type" : "object" ,
87+ "additionalProperties" : {"type" : "string" },
88+ },
89+ },
90+ }
91+
92+
93+ def transform_attachment_refs (schema : dict [str , Any ]) -> dict [str , Any ]:
94+ """Transform UiPathAttachment references in a JSON schema to use $ref.
95+
96+ This function recursively traverses a JSON schema and replaces any objects
97+ with title="UiPathAttachment" with a $ref to "#/definitions/job-attachment",
98+ adding the job-attachment definition to the schema's definitions section.
99+
100+ Args:
101+ schema: The JSON schema to transform (will not be modified in-place).
102+
103+ Returns:
104+ A new schema with UiPathAttachment references replaced by $ref.
105+
106+ Example:
107+ >>> schema = {
108+ ... "type": "object",
109+ ... "properties": {
110+ ... "file": {
111+ ... "title": "UiPathAttachment",
112+ ... "type": "object",
113+ ... "properties": {...}
114+ ... }
115+ ... }
116+ ... }
117+ >>> result = transform_attachment_refs(schema)
118+ >>> result["properties"]["file"]
119+ {"$ref": "#/definitions/job-attachment"}
120+ >>> "job-attachment" in result["definitions"]
121+ True
122+ """
123+ import copy
124+
125+ result = copy .deepcopy (schema )
126+ has_attachments = False
127+
128+ def transform_recursive (obj : Any ) -> Any :
129+ """Recursively transform the schema object."""
130+ nonlocal has_attachments
131+
132+ if isinstance (obj , dict ):
133+ if obj .get ("title" ) == "UiPathAttachment" and obj .get ("type" ) == "object" :
134+ has_attachments = True
135+ return {"$ref" : "#/definitions/job-attachment" }
136+
137+ return {key : transform_recursive (value ) for key , value in obj .items ()}
138+
139+ elif isinstance (obj , list ):
140+ return [transform_recursive (item ) for item in obj ]
141+
142+ else :
143+ # Return primitive values as-is
144+ return obj
145+
146+ result = transform_recursive (result )
147+
148+ # add the job-attachment definition if any are present
149+ if has_attachments :
150+ if "definitions" not in result :
151+ result ["definitions" ] = {}
152+ result ["definitions" ]["job-attachment" ] = _get_job_attachment_definition ()
153+
154+ return result
155+
156+
157+ def resolve_refs (schema , root = None , visited = None ):
158+ """Recursively resolves $ref references in a JSON schema, handling circular references.
159+
160+ Returns:
161+ tuple: (resolved_schema, has_circular_dependency)
162+ """
163+ if root is None :
164+ root = schema
165+
166+ if visited is None :
167+ visited = set ()
168+
169+ has_circular = False
170+
171+ if isinstance (schema , dict ):
172+ if "$ref" in schema :
173+ ref_path = schema ["$ref" ]
174+
175+ if ref_path in visited :
176+ # Circular dependency detected
177+ return {
178+ "type" : "object" ,
179+ "description" : f"Circular reference to { ref_path } " ,
180+ }, True
181+
182+ visited .add (ref_path )
183+
184+ # Resolve the reference
185+ ref_parts = ref_path .lstrip ("#/" ).split ("/" )
186+ ref_schema = root
187+ for part in ref_parts :
188+ ref_schema = ref_schema .get (part , {})
189+
190+ result , circular = resolve_refs (ref_schema , root , visited )
191+ has_circular = has_circular or circular
192+
193+ # Remove from visited after resolution (allows the same ref in different branches)
194+ visited .discard (ref_path )
195+
196+ return result , has_circular
197+
198+ resolved_dict = {}
199+ for k , v in schema .items ():
200+ resolved_value , circular = resolve_refs (v , root , visited )
201+ resolved_dict [k ] = resolved_value
202+ has_circular = has_circular or circular
203+ return resolved_dict , has_circular
204+
205+ elif isinstance (schema , list ):
206+ resolved_list = []
207+ for item in schema :
208+ resolved_item , circular = resolve_refs (item , root , visited )
209+ resolved_list .append (resolved_item )
210+ has_circular = has_circular or circular
211+ return resolved_list , has_circular
212+
213+ return schema , False
214+
215+
216+ def process_nullable_types (
217+ schema : dict [str , Any ] | list [Any ] | Any ,
218+ ) -> dict [str , Any ] | list [Any ]:
219+ """Process the schema to handle nullable types by removing anyOf with null and keeping the base type."""
220+ if isinstance (schema , dict ):
221+ if "anyOf" in schema and len (schema ["anyOf" ]) == 2 :
222+ types = [t .get ("type" ) for t in schema ["anyOf" ]]
223+ if "null" in types :
224+ non_null_type = next (
225+ t for t in schema ["anyOf" ] if t .get ("type" ) != "null"
226+ )
227+ return non_null_type
228+
229+ return {k : process_nullable_types (v ) for k , v in schema .items ()}
230+ elif isinstance (schema , list ):
231+ return [process_nullable_types (item ) for item in schema ]
232+ return schema
233+
234+
71235__all__ = [
72236 "UiPathRuntimeSchema" ,
73237 "UiPathRuntimeGraph" ,
74238 "UiPathRuntimeNode" ,
75239 "UiPathRuntimeEdge" ,
240+ "process_nullable_types" ,
241+ "resolve_refs" ,
242+ "transform_attachment_refs" ,
76243]
0 commit comments