workato-platform-cli 1.0.0rc5.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. workato_platform_cli/__init__.py +135 -0
  2. workato_platform_cli/_version.py +34 -0
  3. workato_platform_cli/cli/__init__.py +126 -0
  4. workato_platform_cli/cli/commands/__init__.py +0 -0
  5. workato_platform_cli/cli/commands/api_clients.py +627 -0
  6. workato_platform_cli/cli/commands/api_collections.py +497 -0
  7. workato_platform_cli/cli/commands/assets.py +82 -0
  8. workato_platform_cli/cli/commands/connections.py +1205 -0
  9. workato_platform_cli/cli/commands/connectors/__init__.py +0 -0
  10. workato_platform_cli/cli/commands/connectors/command.py +178 -0
  11. workato_platform_cli/cli/commands/connectors/connector_manager.py +351 -0
  12. workato_platform_cli/cli/commands/data_tables.py +345 -0
  13. workato_platform_cli/cli/commands/guide.py +315 -0
  14. workato_platform_cli/cli/commands/init.py +229 -0
  15. workato_platform_cli/cli/commands/profiles.py +364 -0
  16. workato_platform_cli/cli/commands/projects/__init__.py +0 -0
  17. workato_platform_cli/cli/commands/projects/command.py +513 -0
  18. workato_platform_cli/cli/commands/projects/project_manager.py +338 -0
  19. workato_platform_cli/cli/commands/properties.py +174 -0
  20. workato_platform_cli/cli/commands/pull.py +327 -0
  21. workato_platform_cli/cli/commands/push/__init__.py +0 -0
  22. workato_platform_cli/cli/commands/push/command.py +320 -0
  23. workato_platform_cli/cli/commands/recipes/__init__.py +0 -0
  24. workato_platform_cli/cli/commands/recipes/command.py +847 -0
  25. workato_platform_cli/cli/commands/recipes/validator.py +1740 -0
  26. workato_platform_cli/cli/commands/workspace.py +73 -0
  27. workato_platform_cli/cli/containers.py +80 -0
  28. workato_platform_cli/cli/resources/data/connection-data.json +7364 -0
  29. workato_platform_cli/cli/resources/data/picklist-data.json +3706 -0
  30. workato_platform_cli/cli/resources/docs/README.md +178 -0
  31. workato_platform_cli/cli/resources/docs/actions.md +452 -0
  32. workato_platform_cli/cli/resources/docs/block-structure.md +424 -0
  33. workato_platform_cli/cli/resources/docs/connections-parameters.md +11946 -0
  34. workato_platform_cli/cli/resources/docs/data-mapping.md +779 -0
  35. workato_platform_cli/cli/resources/docs/formulas/array-list-formulas.md +1276 -0
  36. workato_platform_cli/cli/resources/docs/formulas/conditions.md +102 -0
  37. workato_platform_cli/cli/resources/docs/formulas/date-formulas.md +798 -0
  38. workato_platform_cli/cli/resources/docs/formulas/number-formulas.md +507 -0
  39. workato_platform_cli/cli/resources/docs/formulas/other-formulas.md +419 -0
  40. workato_platform_cli/cli/resources/docs/formulas/string-formulas.md +1353 -0
  41. workato_platform_cli/cli/resources/docs/formulas.md +214 -0
  42. workato_platform_cli/cli/resources/docs/naming-conventions.md +163 -0
  43. workato_platform_cli/cli/resources/docs/recipe-deployment-workflow.md +352 -0
  44. workato_platform_cli/cli/resources/docs/recipe-fundamentals.md +179 -0
  45. workato_platform_cli/cli/resources/docs/triggers.md +360 -0
  46. workato_platform_cli/cli/utils/__init__.py +10 -0
  47. workato_platform_cli/cli/utils/config/__init__.py +33 -0
  48. workato_platform_cli/cli/utils/config/manager.py +1001 -0
  49. workato_platform_cli/cli/utils/config/models.py +89 -0
  50. workato_platform_cli/cli/utils/config/profiles.py +491 -0
  51. workato_platform_cli/cli/utils/config/workspace.py +113 -0
  52. workato_platform_cli/cli/utils/exception_handler.py +531 -0
  53. workato_platform_cli/cli/utils/gitignore.py +32 -0
  54. workato_platform_cli/cli/utils/ignore_patterns.py +44 -0
  55. workato_platform_cli/cli/utils/spinner.py +63 -0
  56. workato_platform_cli/cli/utils/version_checker.py +237 -0
  57. workato_platform_cli/client/__init__.py +0 -0
  58. workato_platform_cli/client/workato_api/__init__.py +202 -0
  59. workato_platform_cli/client/workato_api/api/__init__.py +15 -0
  60. workato_platform_cli/client/workato_api/api/api_platform_api.py +2875 -0
  61. workato_platform_cli/client/workato_api/api/connections_api.py +1807 -0
  62. workato_platform_cli/client/workato_api/api/connectors_api.py +840 -0
  63. workato_platform_cli/client/workato_api/api/data_tables_api.py +604 -0
  64. workato_platform_cli/client/workato_api/api/export_api.py +621 -0
  65. workato_platform_cli/client/workato_api/api/folders_api.py +621 -0
  66. workato_platform_cli/client/workato_api/api/packages_api.py +1197 -0
  67. workato_platform_cli/client/workato_api/api/projects_api.py +590 -0
  68. workato_platform_cli/client/workato_api/api/properties_api.py +620 -0
  69. workato_platform_cli/client/workato_api/api/recipes_api.py +1379 -0
  70. workato_platform_cli/client/workato_api/api/users_api.py +285 -0
  71. workato_platform_cli/client/workato_api/api_client.py +807 -0
  72. workato_platform_cli/client/workato_api/api_response.py +21 -0
  73. workato_platform_cli/client/workato_api/configuration.py +601 -0
  74. workato_platform_cli/client/workato_api/docs/APIPlatformApi.md +844 -0
  75. workato_platform_cli/client/workato_api/docs/ApiClient.md +46 -0
  76. workato_platform_cli/client/workato_api/docs/ApiClientApiCollectionsInner.md +30 -0
  77. workato_platform_cli/client/workato_api/docs/ApiClientApiPoliciesInner.md +30 -0
  78. workato_platform_cli/client/workato_api/docs/ApiClientCreateRequest.md +46 -0
  79. workato_platform_cli/client/workato_api/docs/ApiClientListResponse.md +32 -0
  80. workato_platform_cli/client/workato_api/docs/ApiClientResponse.md +29 -0
  81. workato_platform_cli/client/workato_api/docs/ApiCollection.md +38 -0
  82. workato_platform_cli/client/workato_api/docs/ApiCollectionCreateRequest.md +32 -0
  83. workato_platform_cli/client/workato_api/docs/ApiEndpoint.md +41 -0
  84. workato_platform_cli/client/workato_api/docs/ApiKey.md +36 -0
  85. workato_platform_cli/client/workato_api/docs/ApiKeyCreateRequest.md +32 -0
  86. workato_platform_cli/client/workato_api/docs/ApiKeyListResponse.md +32 -0
  87. workato_platform_cli/client/workato_api/docs/ApiKeyResponse.md +29 -0
  88. workato_platform_cli/client/workato_api/docs/Asset.md +39 -0
  89. workato_platform_cli/client/workato_api/docs/AssetReference.md +37 -0
  90. workato_platform_cli/client/workato_api/docs/Connection.md +44 -0
  91. workato_platform_cli/client/workato_api/docs/ConnectionCreateRequest.md +35 -0
  92. workato_platform_cli/client/workato_api/docs/ConnectionUpdateRequest.md +34 -0
  93. workato_platform_cli/client/workato_api/docs/ConnectionsApi.md +526 -0
  94. workato_platform_cli/client/workato_api/docs/ConnectorAction.md +33 -0
  95. workato_platform_cli/client/workato_api/docs/ConnectorVersion.md +32 -0
  96. workato_platform_cli/client/workato_api/docs/ConnectorsApi.md +249 -0
  97. workato_platform_cli/client/workato_api/docs/CreateExportManifestRequest.md +29 -0
  98. workato_platform_cli/client/workato_api/docs/CreateFolderRequest.md +30 -0
  99. workato_platform_cli/client/workato_api/docs/CustomConnector.md +35 -0
  100. workato_platform_cli/client/workato_api/docs/CustomConnectorCodeResponse.md +29 -0
  101. workato_platform_cli/client/workato_api/docs/CustomConnectorCodeResponseData.md +29 -0
  102. workato_platform_cli/client/workato_api/docs/CustomConnectorListResponse.md +29 -0
  103. workato_platform_cli/client/workato_api/docs/DataTable.md +34 -0
  104. workato_platform_cli/client/workato_api/docs/DataTableColumn.md +37 -0
  105. workato_platform_cli/client/workato_api/docs/DataTableColumnRequest.md +37 -0
  106. workato_platform_cli/client/workato_api/docs/DataTableCreateRequest.md +31 -0
  107. workato_platform_cli/client/workato_api/docs/DataTableCreateResponse.md +29 -0
  108. workato_platform_cli/client/workato_api/docs/DataTableListResponse.md +29 -0
  109. workato_platform_cli/client/workato_api/docs/DataTableRelation.md +30 -0
  110. workato_platform_cli/client/workato_api/docs/DataTablesApi.md +172 -0
  111. workato_platform_cli/client/workato_api/docs/DeleteProject403Response.md +29 -0
  112. workato_platform_cli/client/workato_api/docs/Error.md +29 -0
  113. workato_platform_cli/client/workato_api/docs/ExportApi.md +175 -0
  114. workato_platform_cli/client/workato_api/docs/ExportManifestRequest.md +35 -0
  115. workato_platform_cli/client/workato_api/docs/ExportManifestResponse.md +29 -0
  116. workato_platform_cli/client/workato_api/docs/ExportManifestResponseResult.md +36 -0
  117. workato_platform_cli/client/workato_api/docs/Folder.md +35 -0
  118. workato_platform_cli/client/workato_api/docs/FolderAssetsResponse.md +29 -0
  119. workato_platform_cli/client/workato_api/docs/FolderAssetsResponseResult.md +29 -0
  120. workato_platform_cli/client/workato_api/docs/FolderCreationResponse.md +35 -0
  121. workato_platform_cli/client/workato_api/docs/FoldersApi.md +176 -0
  122. workato_platform_cli/client/workato_api/docs/ImportResults.md +32 -0
  123. workato_platform_cli/client/workato_api/docs/OAuthUrlResponse.md +29 -0
  124. workato_platform_cli/client/workato_api/docs/OAuthUrlResponseData.md +29 -0
  125. workato_platform_cli/client/workato_api/docs/OpenApiSpec.md +30 -0
  126. workato_platform_cli/client/workato_api/docs/PackageDetailsResponse.md +35 -0
  127. workato_platform_cli/client/workato_api/docs/PackageDetailsResponseRecipeStatusInner.md +30 -0
  128. workato_platform_cli/client/workato_api/docs/PackageResponse.md +33 -0
  129. workato_platform_cli/client/workato_api/docs/PackagesApi.md +364 -0
  130. workato_platform_cli/client/workato_api/docs/PicklistRequest.md +30 -0
  131. workato_platform_cli/client/workato_api/docs/PicklistResponse.md +29 -0
  132. workato_platform_cli/client/workato_api/docs/PlatformConnector.md +36 -0
  133. workato_platform_cli/client/workato_api/docs/PlatformConnectorListResponse.md +32 -0
  134. workato_platform_cli/client/workato_api/docs/Project.md +32 -0
  135. workato_platform_cli/client/workato_api/docs/ProjectsApi.md +173 -0
  136. workato_platform_cli/client/workato_api/docs/PropertiesApi.md +186 -0
  137. workato_platform_cli/client/workato_api/docs/Recipe.md +58 -0
  138. workato_platform_cli/client/workato_api/docs/RecipeConfigInner.md +33 -0
  139. workato_platform_cli/client/workato_api/docs/RecipeConnectionUpdateRequest.md +30 -0
  140. workato_platform_cli/client/workato_api/docs/RecipeListResponse.md +29 -0
  141. workato_platform_cli/client/workato_api/docs/RecipeStartResponse.md +31 -0
  142. workato_platform_cli/client/workato_api/docs/RecipesApi.md +367 -0
  143. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionCreateRequest.md +34 -0
  144. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionResponse.md +29 -0
  145. workato_platform_cli/client/workato_api/docs/RuntimeUserConnectionResponseData.md +30 -0
  146. workato_platform_cli/client/workato_api/docs/SuccessResponse.md +29 -0
  147. workato_platform_cli/client/workato_api/docs/UpsertProjectPropertiesRequest.md +29 -0
  148. workato_platform_cli/client/workato_api/docs/User.md +48 -0
  149. workato_platform_cli/client/workato_api/docs/UsersApi.md +84 -0
  150. workato_platform_cli/client/workato_api/docs/ValidationError.md +30 -0
  151. workato_platform_cli/client/workato_api/docs/ValidationErrorErrorsValue.md +28 -0
  152. workato_platform_cli/client/workato_api/exceptions.py +216 -0
  153. workato_platform_cli/client/workato_api/models/__init__.py +83 -0
  154. workato_platform_cli/client/workato_api/models/api_client.py +185 -0
  155. workato_platform_cli/client/workato_api/models/api_client_api_collections_inner.py +89 -0
  156. workato_platform_cli/client/workato_api/models/api_client_api_policies_inner.py +89 -0
  157. workato_platform_cli/client/workato_api/models/api_client_create_request.py +138 -0
  158. workato_platform_cli/client/workato_api/models/api_client_list_response.py +101 -0
  159. workato_platform_cli/client/workato_api/models/api_client_response.py +91 -0
  160. workato_platform_cli/client/workato_api/models/api_collection.py +110 -0
  161. workato_platform_cli/client/workato_api/models/api_collection_create_request.py +97 -0
  162. workato_platform_cli/client/workato_api/models/api_endpoint.py +117 -0
  163. workato_platform_cli/client/workato_api/models/api_key.py +102 -0
  164. workato_platform_cli/client/workato_api/models/api_key_create_request.py +93 -0
  165. workato_platform_cli/client/workato_api/models/api_key_list_response.py +101 -0
  166. workato_platform_cli/client/workato_api/models/api_key_response.py +91 -0
  167. workato_platform_cli/client/workato_api/models/asset.py +124 -0
  168. workato_platform_cli/client/workato_api/models/asset_reference.py +110 -0
  169. workato_platform_cli/client/workato_api/models/connection.py +173 -0
  170. workato_platform_cli/client/workato_api/models/connection_create_request.py +99 -0
  171. workato_platform_cli/client/workato_api/models/connection_update_request.py +97 -0
  172. workato_platform_cli/client/workato_api/models/connector_action.py +100 -0
  173. workato_platform_cli/client/workato_api/models/connector_version.py +99 -0
  174. workato_platform_cli/client/workato_api/models/create_export_manifest_request.py +91 -0
  175. workato_platform_cli/client/workato_api/models/create_folder_request.py +89 -0
  176. workato_platform_cli/client/workato_api/models/custom_connector.py +117 -0
  177. workato_platform_cli/client/workato_api/models/custom_connector_code_response.py +91 -0
  178. workato_platform_cli/client/workato_api/models/custom_connector_code_response_data.py +87 -0
  179. workato_platform_cli/client/workato_api/models/custom_connector_list_response.py +95 -0
  180. workato_platform_cli/client/workato_api/models/data_table.py +107 -0
  181. workato_platform_cli/client/workato_api/models/data_table_column.py +125 -0
  182. workato_platform_cli/client/workato_api/models/data_table_column_request.py +130 -0
  183. workato_platform_cli/client/workato_api/models/data_table_create_request.py +99 -0
  184. workato_platform_cli/client/workato_api/models/data_table_create_response.py +91 -0
  185. workato_platform_cli/client/workato_api/models/data_table_list_response.py +95 -0
  186. workato_platform_cli/client/workato_api/models/data_table_relation.py +90 -0
  187. workato_platform_cli/client/workato_api/models/delete_project403_response.py +87 -0
  188. workato_platform_cli/client/workato_api/models/error.py +87 -0
  189. workato_platform_cli/client/workato_api/models/export_manifest_request.py +107 -0
  190. workato_platform_cli/client/workato_api/models/export_manifest_response.py +91 -0
  191. workato_platform_cli/client/workato_api/models/export_manifest_response_result.py +112 -0
  192. workato_platform_cli/client/workato_api/models/folder.py +110 -0
  193. workato_platform_cli/client/workato_api/models/folder_assets_response.py +91 -0
  194. workato_platform_cli/client/workato_api/models/folder_assets_response_result.py +95 -0
  195. workato_platform_cli/client/workato_api/models/folder_creation_response.py +110 -0
  196. workato_platform_cli/client/workato_api/models/import_results.py +93 -0
  197. workato_platform_cli/client/workato_api/models/o_auth_url_response.py +91 -0
  198. workato_platform_cli/client/workato_api/models/o_auth_url_response_data.py +87 -0
  199. workato_platform_cli/client/workato_api/models/open_api_spec.py +96 -0
  200. workato_platform_cli/client/workato_api/models/package_details_response.py +126 -0
  201. workato_platform_cli/client/workato_api/models/package_details_response_recipe_status_inner.py +99 -0
  202. workato_platform_cli/client/workato_api/models/package_response.py +109 -0
  203. workato_platform_cli/client/workato_api/models/picklist_request.py +89 -0
  204. workato_platform_cli/client/workato_api/models/picklist_response.py +88 -0
  205. workato_platform_cli/client/workato_api/models/platform_connector.py +116 -0
  206. workato_platform_cli/client/workato_api/models/platform_connector_list_response.py +101 -0
  207. workato_platform_cli/client/workato_api/models/project.py +93 -0
  208. workato_platform_cli/client/workato_api/models/recipe.py +174 -0
  209. workato_platform_cli/client/workato_api/models/recipe_config_inner.py +100 -0
  210. workato_platform_cli/client/workato_api/models/recipe_connection_update_request.py +89 -0
  211. workato_platform_cli/client/workato_api/models/recipe_list_response.py +95 -0
  212. workato_platform_cli/client/workato_api/models/recipe_start_response.py +91 -0
  213. workato_platform_cli/client/workato_api/models/runtime_user_connection_create_request.py +97 -0
  214. workato_platform_cli/client/workato_api/models/runtime_user_connection_response.py +91 -0
  215. workato_platform_cli/client/workato_api/models/runtime_user_connection_response_data.py +89 -0
  216. workato_platform_cli/client/workato_api/models/success_response.py +87 -0
  217. workato_platform_cli/client/workato_api/models/upsert_project_properties_request.py +88 -0
  218. workato_platform_cli/client/workato_api/models/user.py +151 -0
  219. workato_platform_cli/client/workato_api/models/validation_error.py +102 -0
  220. workato_platform_cli/client/workato_api/models/validation_error_errors_value.py +143 -0
  221. workato_platform_cli/client/workato_api/rest.py +213 -0
  222. workato_platform_cli/client/workato_api/test/__init__.py +0 -0
  223. workato_platform_cli/client/workato_api/test/test_api_client.py +94 -0
  224. workato_platform_cli/client/workato_api/test/test_api_client_api_collections_inner.py +52 -0
  225. workato_platform_cli/client/workato_api/test/test_api_client_api_policies_inner.py +52 -0
  226. workato_platform_cli/client/workato_api/test/test_api_client_create_request.py +75 -0
  227. workato_platform_cli/client/workato_api/test/test_api_client_list_response.py +114 -0
  228. workato_platform_cli/client/workato_api/test/test_api_client_response.py +104 -0
  229. workato_platform_cli/client/workato_api/test/test_api_collection.py +72 -0
  230. workato_platform_cli/client/workato_api/test/test_api_collection_create_request.py +57 -0
  231. workato_platform_cli/client/workato_api/test/test_api_endpoint.py +75 -0
  232. workato_platform_cli/client/workato_api/test/test_api_key.py +64 -0
  233. workato_platform_cli/client/workato_api/test/test_api_key_create_request.py +56 -0
  234. workato_platform_cli/client/workato_api/test/test_api_key_list_response.py +78 -0
  235. workato_platform_cli/client/workato_api/test/test_api_key_response.py +68 -0
  236. workato_platform_cli/client/workato_api/test/test_api_platform_api.py +101 -0
  237. workato_platform_cli/client/workato_api/test/test_asset.py +67 -0
  238. workato_platform_cli/client/workato_api/test/test_asset_reference.py +62 -0
  239. workato_platform_cli/client/workato_api/test/test_connection.py +81 -0
  240. workato_platform_cli/client/workato_api/test/test_connection_create_request.py +59 -0
  241. workato_platform_cli/client/workato_api/test/test_connection_update_request.py +56 -0
  242. workato_platform_cli/client/workato_api/test/test_connections_api.py +73 -0
  243. workato_platform_cli/client/workato_api/test/test_connector_action.py +59 -0
  244. workato_platform_cli/client/workato_api/test/test_connector_version.py +58 -0
  245. workato_platform_cli/client/workato_api/test/test_connectors_api.py +52 -0
  246. workato_platform_cli/client/workato_api/test/test_create_export_manifest_request.py +88 -0
  247. workato_platform_cli/client/workato_api/test/test_create_folder_request.py +53 -0
  248. workato_platform_cli/client/workato_api/test/test_custom_connector.py +76 -0
  249. workato_platform_cli/client/workato_api/test/test_custom_connector_code_response.py +54 -0
  250. workato_platform_cli/client/workato_api/test/test_custom_connector_code_response_data.py +52 -0
  251. workato_platform_cli/client/workato_api/test/test_custom_connector_list_response.py +82 -0
  252. workato_platform_cli/client/workato_api/test/test_data_table.py +88 -0
  253. workato_platform_cli/client/workato_api/test/test_data_table_column.py +72 -0
  254. workato_platform_cli/client/workato_api/test/test_data_table_column_request.py +64 -0
  255. workato_platform_cli/client/workato_api/test/test_data_table_create_request.py +82 -0
  256. workato_platform_cli/client/workato_api/test/test_data_table_create_response.py +90 -0
  257. workato_platform_cli/client/workato_api/test/test_data_table_list_response.py +94 -0
  258. workato_platform_cli/client/workato_api/test/test_data_table_relation.py +54 -0
  259. workato_platform_cli/client/workato_api/test/test_data_tables_api.py +45 -0
  260. workato_platform_cli/client/workato_api/test/test_delete_project403_response.py +51 -0
  261. workato_platform_cli/client/workato_api/test/test_error.py +52 -0
  262. workato_platform_cli/client/workato_api/test/test_export_api.py +45 -0
  263. workato_platform_cli/client/workato_api/test/test_export_manifest_request.py +69 -0
  264. workato_platform_cli/client/workato_api/test/test_export_manifest_response.py +68 -0
  265. workato_platform_cli/client/workato_api/test/test_export_manifest_response_result.py +66 -0
  266. workato_platform_cli/client/workato_api/test/test_folder.py +64 -0
  267. workato_platform_cli/client/workato_api/test/test_folder_assets_response.py +80 -0
  268. workato_platform_cli/client/workato_api/test/test_folder_assets_response_result.py +78 -0
  269. workato_platform_cli/client/workato_api/test/test_folder_creation_response.py +64 -0
  270. workato_platform_cli/client/workato_api/test/test_folders_api.py +45 -0
  271. workato_platform_cli/client/workato_api/test/test_import_results.py +58 -0
  272. workato_platform_cli/client/workato_api/test/test_o_auth_url_response.py +54 -0
  273. workato_platform_cli/client/workato_api/test/test_o_auth_url_response_data.py +52 -0
  274. workato_platform_cli/client/workato_api/test/test_open_api_spec.py +54 -0
  275. workato_platform_cli/client/workato_api/test/test_package_details_response.py +64 -0
  276. workato_platform_cli/client/workato_api/test/test_package_details_response_recipe_status_inner.py +52 -0
  277. workato_platform_cli/client/workato_api/test/test_package_response.py +58 -0
  278. workato_platform_cli/client/workato_api/test/test_packages_api.py +59 -0
  279. workato_platform_cli/client/workato_api/test/test_picklist_request.py +53 -0
  280. workato_platform_cli/client/workato_api/test/test_picklist_response.py +52 -0
  281. workato_platform_cli/client/workato_api/test/test_platform_connector.py +94 -0
  282. workato_platform_cli/client/workato_api/test/test_platform_connector_list_response.py +106 -0
  283. workato_platform_cli/client/workato_api/test/test_project.py +57 -0
  284. workato_platform_cli/client/workato_api/test/test_projects_api.py +45 -0
  285. workato_platform_cli/client/workato_api/test/test_properties_api.py +45 -0
  286. workato_platform_cli/client/workato_api/test/test_recipe.py +124 -0
  287. workato_platform_cli/client/workato_api/test/test_recipe_config_inner.py +55 -0
  288. workato_platform_cli/client/workato_api/test/test_recipe_connection_update_request.py +54 -0
  289. workato_platform_cli/client/workato_api/test/test_recipe_list_response.py +134 -0
  290. workato_platform_cli/client/workato_api/test/test_recipe_start_response.py +54 -0
  291. workato_platform_cli/client/workato_api/test/test_recipes_api.py +59 -0
  292. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_create_request.py +59 -0
  293. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_response.py +56 -0
  294. workato_platform_cli/client/workato_api/test/test_runtime_user_connection_response_data.py +54 -0
  295. workato_platform_cli/client/workato_api/test/test_success_response.py +52 -0
  296. workato_platform_cli/client/workato_api/test/test_upsert_project_properties_request.py +52 -0
  297. workato_platform_cli/client/workato_api/test/test_user.py +85 -0
  298. workato_platform_cli/client/workato_api/test/test_users_api.py +38 -0
  299. workato_platform_cli/client/workato_api/test/test_validation_error.py +52 -0
  300. workato_platform_cli/client/workato_api/test/test_validation_error_errors_value.py +50 -0
  301. workato_platform_cli/client/workato_api_README.md +205 -0
  302. workato_platform_cli-1.0.0rc5.dev5.dist-info/METADATA +185 -0
  303. workato_platform_cli-1.0.0rc5.dev5.dist-info/RECORD +306 -0
  304. workato_platform_cli-1.0.0rc5.dev5.dist-info/WHEEL +4 -0
  305. workato_platform_cli-1.0.0rc5.dev5.dist-info/entry_points.txt +2 -0
  306. workato_platform_cli-1.0.0rc5.dev5.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,1740 @@
1
+ import json
2
+ import time
3
+
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
10
+
11
+ from workato_platform_cli import Workato
12
+ from workato_platform_cli.client.workato_api.models.platform_connector import (
13
+ PlatformConnector,
14
+ )
15
+
16
+
17
+ class Keyword(str, Enum):
18
+ """Recipe keywords that define the structure"""
19
+
20
+ TRIGGER = "trigger"
21
+ ACTION = "action"
22
+ IF = "if"
23
+ ELSIF = "elsif"
24
+ ELSE = "else"
25
+ FOREACH = "foreach"
26
+ REPEAT = "repeat"
27
+ WHILE_CONDITION = "while_condition"
28
+ TRY = "try"
29
+ CATCH = "catch"
30
+ STOP = "stop"
31
+ APPLICATION = "application"
32
+
33
+
34
+ class ErrorType(str, Enum):
35
+ """Validation error types"""
36
+
37
+ SYNTAX_INVALID = "syntax_invalid"
38
+ LINE_SYNTAX_INVALID = "line_syntax_invalid"
39
+ LINE_ATTR_INVALID = "line_attr_invalid"
40
+ INPUT_FIELD_INVALID = "input_field_invalid"
41
+ INPUT_EXPR_INVALID = "input_expr_invalid"
42
+ INPUT_UNKNOWN_DATA_PILL = "input_unknown_data_pill"
43
+ INPUT_FIELD_BLANK = "input_field_blank"
44
+ INPUT_VALUE_INVALID = "input_value_invalid"
45
+ INPUT_INVALID_BY_ADAPTER = "input_invalid_by_adapter"
46
+ DEPENDS_ON_INVALID = "depends_on_invalid"
47
+ EXTERNAL_INPUT_INVALID = "external_input_invalid"
48
+ DYNAMIC_FIELD_MAPPING_INVALID = "dynamic_field_mapping_invalid"
49
+ EXTENDED_SCHEMA_INVALID = "extended_schema_invalid"
50
+ STRUCTURE_INVALID = "structure_invalid"
51
+ INPUT_MODE_INCONSISTENT = "input_mode_inconsistent"
52
+ ARRAY_MAPPING_INVALID = "array_mapping_invalid"
53
+ FORMULA_SYNTAX_INVALID = "formula_syntax_invalid"
54
+ BLOCK_NUMBERING_INVALID = "block_numbering_invalid"
55
+
56
+
57
+ @dataclass
58
+ class ValidationError:
59
+ """Individual validation error"""
60
+
61
+ field_label: str | None = None
62
+ value: Any = None
63
+ message: str = ""
64
+ field_path: list[str] | None = None
65
+ error_type: ErrorType | None = None
66
+ line_number: int | None = None
67
+
68
+
69
+ @dataclass
70
+ class ValidationResult:
71
+ """Complete validation result"""
72
+
73
+ is_valid: bool
74
+ errors: list[ValidationError]
75
+ warnings: list[ValidationError] = field(default_factory=list)
76
+
77
+
78
+ class RecipeLine(BaseModel):
79
+ """Base recipe line structure"""
80
+
81
+ model_config = ConfigDict(extra="allow")
82
+
83
+ number: int
84
+ keyword: Keyword
85
+ uuid: str
86
+ as_: str | None = Field(None, alias="as")
87
+ input: dict[str, Any] | None = None
88
+ block: list["RecipeLine"] | None = None
89
+ comment: str | None = None
90
+ custom_title: str | None = None
91
+ description: str | None = None
92
+ hidden_config_fields: list[str] | None = None
93
+ name: str | None = None
94
+ provider: str | None = None
95
+ title: str | None = None
96
+ visible_config_fields: list[str] | None = None
97
+ wizard_finished: bool | None = Field(None, alias="wizardFinished")
98
+ mask_data: bool | None = Field(None, alias="mask_data")
99
+ skip: bool | None = None
100
+ toggle_cfg: dict[str, Any] | None = Field(None, alias="toggleCfg")
101
+ dynamic_pick_list_selection: dict[str, Any] | None = Field(
102
+ None, alias="dynamicPickListSelection"
103
+ )
104
+ extended_input_schema: list[dict[str, Any]] | None = None
105
+ extended_output_schema: list[dict[str, Any]] | None = None
106
+ requirements: dict[str, Any] | None = None
107
+ external_input_definition: dict[str, Any] | None = None
108
+ filter: dict[str, Any] | None = None
109
+ batch_size: int | None = Field(None, alias="batch_size")
110
+ clear_scope: bool | None = Field(None, alias="clear_scope")
111
+ repeat_mode: str | None = Field(None, alias="repeat_mode")
112
+ source: str | None = None
113
+ format_version: str | None = Field(None, alias="format_version")
114
+ job_report_config: dict[str, Any] | None = None
115
+ job_report_schema: list[dict[str, Any]] | None = None
116
+ param: dict[str, Any] | None = None
117
+ parameters_schema: list[dict[str, Any]] | None = None
118
+ unfinished: bool | None = None
119
+
120
+ @field_validator("as_")
121
+ @classmethod
122
+ def validate_as_length(cls, v: str) -> str:
123
+ if v and len(v) > 48:
124
+ raise ValueError("'as' field must be 48 characters or less")
125
+ return v
126
+
127
+ @field_validator("uuid")
128
+ @classmethod
129
+ def validate_uuid_length(cls, v: str) -> str:
130
+ if v and len(v) > 36:
131
+ raise ValueError("UUID must be 36 characters or less")
132
+ return v
133
+
134
+ @field_validator("job_report_schema", "job_report_config")
135
+ @classmethod
136
+ def validate_job_report_size(cls, v: list[dict[str, Any]]) -> list[dict[str, Any]]:
137
+ if v and len(v) > 10:
138
+ raise ValueError("Job report schema/config must be 10 items or less")
139
+ return v
140
+
141
+
142
+ class RecipeAccountId(BaseModel):
143
+ zip_name: str
144
+ name: str
145
+ folder: str
146
+
147
+
148
+ class RecipeConfig(BaseModel):
149
+ keyword: Keyword
150
+ provider: str
151
+ skip_validation: bool | None = None
152
+ account_id: RecipeAccountId | None = None
153
+
154
+
155
+ class Recipe(BaseModel):
156
+ name: str
157
+ description: str | None = None
158
+ version: int | None = None
159
+ private: bool
160
+ concurrency: int
161
+ code: RecipeLine
162
+ config: list[RecipeConfig]
163
+
164
+
165
+ class RecipeStructure(BaseModel):
166
+ """Complete recipe structure"""
167
+
168
+ root: RecipeLine
169
+
170
+ @model_validator(mode="after")
171
+ def validate_recipe_structure(self) -> "RecipeStructure":
172
+ """Validate the overall recipe structure"""
173
+ if not self.root:
174
+ return self
175
+
176
+ errors: list[ValidationError] = []
177
+
178
+ # Must start with trigger
179
+ if self.root.keyword != Keyword.TRIGGER:
180
+ errors.append(
181
+ ValidationError(
182
+ message="Recipe must start with a trigger",
183
+ error_type=ErrorType.SYNTAX_INVALID,
184
+ line_number=self.root.number,
185
+ )
186
+ )
187
+
188
+ # Validate structure recursively
189
+ structure_errors = self._validate_structure_recursive(self.root, [])
190
+ errors.extend(structure_errors)
191
+
192
+ if errors:
193
+ raise ValueError(
194
+ f"Recipe structure validation failed: {len(errors)} errors found"
195
+ )
196
+
197
+ return self
198
+
199
+ @classmethod
200
+ def _validate_structure_recursive(
201
+ cls, line: RecipeLine, path: list[int]
202
+ ) -> list[ValidationError]:
203
+ """Recursively validate recipe structure"""
204
+ errors = []
205
+ current_path = path + [line.number]
206
+
207
+ # Validate line-specific rules
208
+ line_errors = cls._validate_line_structure(line, current_path)
209
+ errors.extend(line_errors)
210
+
211
+ # Validate children
212
+ if line.block:
213
+ for child in line.block:
214
+ child_errors = cls._validate_structure_recursive(child, current_path)
215
+ errors.extend(child_errors)
216
+
217
+ return errors
218
+
219
+ @classmethod
220
+ def _validate_line_structure(
221
+ cls, line: RecipeLine, path: list[int]
222
+ ) -> list[ValidationError]:
223
+ """Validate specific line structure rules"""
224
+ errors = []
225
+
226
+ # Validate keyword-specific rules
227
+ if line.keyword == Keyword.IF:
228
+ errors.extend(cls._validate_if_structure(line, path))
229
+ elif line.keyword == Keyword.FOREACH:
230
+ errors.extend(cls._validate_foreach_structure(line, path))
231
+ elif line.keyword == Keyword.REPEAT:
232
+ errors.extend(cls._validate_repeat_structure(line, path))
233
+ elif line.keyword == Keyword.TRY:
234
+ errors.extend(cls._validate_try_structure(line, path))
235
+ elif line.keyword == Keyword.ACTION:
236
+ errors.extend(cls._validate_action_structure(line, path))
237
+
238
+ return errors
239
+
240
+ @classmethod
241
+ def _validate_if_structure(
242
+ cls, line: RecipeLine, path: list[int]
243
+ ) -> list[ValidationError]:
244
+ """Validate IF block structure"""
245
+ errors = []
246
+
247
+ if not line.block:
248
+ errors.append(
249
+ ValidationError(
250
+ message="IF statement must have a block",
251
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
252
+ line_number=line.number,
253
+ )
254
+ )
255
+ return errors
256
+
257
+ # Validate IF block structure: [top_level_lines*, elsif_lines*, else_line?]
258
+ block = line.block
259
+ i = 0
260
+
261
+ # top_level_lines*
262
+ while i < len(block) and block[i].keyword in [
263
+ Keyword.ACTION,
264
+ Keyword.IF,
265
+ Keyword.FOREACH,
266
+ Keyword.REPEAT,
267
+ Keyword.TRY,
268
+ Keyword.STOP,
269
+ ]:
270
+ i += 1
271
+
272
+ # elsif_lines*
273
+ while i < len(block) and block[i].keyword == Keyword.ELSIF:
274
+ i += 1
275
+
276
+ # else_line?
277
+ if i < len(block) and block[i].keyword == Keyword.ELSE:
278
+ i += 1
279
+
280
+ if i < len(block):
281
+ errors.append(
282
+ ValidationError(
283
+ message=f"Unexpected line type '{block[i].keyword}' in IF block",
284
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
285
+ line_number=block[i].number,
286
+ )
287
+ )
288
+
289
+ return errors
290
+
291
+ @classmethod
292
+ def _validate_foreach_structure(
293
+ cls, line: RecipeLine, path: list[int]
294
+ ) -> list[ValidationError]:
295
+ """Validate FOREACH structure"""
296
+ errors = []
297
+
298
+ if not line.source:
299
+ errors.append(
300
+ ValidationError(
301
+ message="FOREACH must have a source",
302
+ error_type=ErrorType.LINE_ATTR_INVALID,
303
+ line_number=line.number,
304
+ )
305
+ )
306
+
307
+ return errors
308
+
309
+ @classmethod
310
+ def _validate_repeat_structure(
311
+ cls, line: RecipeLine, path: list[int]
312
+ ) -> list[ValidationError]:
313
+ """Validate REPEAT structure"""
314
+ errors = []
315
+
316
+ if not line.block:
317
+ errors.append(
318
+ ValidationError(
319
+ message="REPEAT must have a block",
320
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
321
+ line_number=line.number,
322
+ )
323
+ )
324
+
325
+ return errors
326
+
327
+ @classmethod
328
+ def _validate_try_structure(
329
+ cls, line: RecipeLine, path: list[int]
330
+ ) -> list[ValidationError]:
331
+ """Validate TRY block structure"""
332
+ errors = []
333
+
334
+ if not line.block:
335
+ errors.append(
336
+ ValidationError(
337
+ message="TRY statement must have a block",
338
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
339
+ line_number=line.number,
340
+ )
341
+ )
342
+ return errors
343
+
344
+ # Validate TRY block structure: [top_level_lines*, catch_line?]
345
+ block = line.block
346
+ i = 0
347
+
348
+ # top_level_lines*
349
+ while i < len(block) and block[i].keyword in [
350
+ Keyword.ACTION,
351
+ Keyword.IF,
352
+ Keyword.FOREACH,
353
+ Keyword.REPEAT,
354
+ Keyword.TRY,
355
+ Keyword.STOP,
356
+ ]:
357
+ i += 1
358
+
359
+ # catch_line?
360
+ if i < len(block) and block[i].keyword == Keyword.CATCH:
361
+ i += 1
362
+
363
+ if i < len(block):
364
+ errors.append(
365
+ ValidationError(
366
+ message=f"Unexpected line type '{block[i].keyword}' in TRY block",
367
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
368
+ line_number=block[i].number,
369
+ )
370
+ )
371
+
372
+ return errors
373
+
374
+ @classmethod
375
+ def _validate_action_structure(
376
+ cls, line: RecipeLine, path: list[int]
377
+ ) -> list[ValidationError]:
378
+ """Validate ACTION structure"""
379
+ errors = []
380
+
381
+ if line.block:
382
+ errors.append(
383
+ ValidationError(
384
+ message="ACTION should not have a block",
385
+ error_type=ErrorType.LINE_SYNTAX_INVALID,
386
+ line_number=line.number,
387
+ )
388
+ )
389
+
390
+ return errors
391
+
392
+
393
+ class RecipeValidator:
394
+ """Main recipe validator class"""
395
+
396
+ def __init__(
397
+ self,
398
+ workato_api_client: Workato,
399
+ ):
400
+ self.workato_api_client = workato_api_client
401
+ self.known_adapters: set[str] = set()
402
+ self.known_data_pills: set[str] = set()
403
+ self.connection_configs: dict[str, Any] = {}
404
+ self.platform_connectors: dict[str, Any] = {}
405
+ self.custom_connectors: dict[str, Any] = {}
406
+ self.connector_metadata: dict[str, dict[str, Any]] = {}
407
+ self.current_recipe_root: RecipeLine | None = None
408
+
409
+ # Cache settings
410
+ self._cache_file = (
411
+ Path.home() / ".workato_platform" / "cli" / "connector_cache.json"
412
+ )
413
+ self._cache_ttl_hours = 24 # Cache for 24 hours
414
+ self._last_cache_update = None
415
+ self._connectors_loaded = False
416
+
417
+ # Connector data will be loaded lazily when first needed
418
+
419
+ async def _ensure_connectors_loaded(self) -> None:
420
+ """Ensure connector metadata is loaded (either from cache or API)"""
421
+ if not self._connectors_loaded:
422
+ await self._load_builtin_connectors()
423
+ self._connectors_loaded = True
424
+
425
+ def _load_cached_connectors(self) -> bool:
426
+ """Load connector metadata from cache if available and not expired"""
427
+ try:
428
+ if not self._cache_file.exists():
429
+ return False
430
+
431
+ # Check cache age
432
+ cache_age = time.time() - self._cache_file.stat().st_mtime
433
+
434
+ if cache_age > (self._cache_ttl_hours * 3600):
435
+ return False # Cache expired
436
+
437
+ # Load from cache
438
+ with open(self._cache_file) as f:
439
+ cache_data = json.load(f)
440
+
441
+ self.known_adapters = set(cache_data.get("known_adapters", []))
442
+ self.connector_metadata = cache_data.get("connector_metadata", {})
443
+ self._last_cache_update = cache_data.get("last_update", 0)
444
+
445
+ return True
446
+
447
+ except json.JSONDecodeError:
448
+ return False
449
+
450
+ def _save_connectors_to_cache(self) -> None:
451
+ """Save connector metadata to cache file"""
452
+ try:
453
+ # Ensure cache directory exists
454
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
455
+
456
+ cache_data = {
457
+ "known_adapters": list(self.known_adapters),
458
+ "connector_metadata": self.connector_metadata,
459
+ "last_update": time.time(),
460
+ "cache_version": "1.0",
461
+ }
462
+
463
+ with open(self._cache_file, "w") as f:
464
+ json.dump(cache_data, f, indent=2)
465
+
466
+ except (OSError, PermissionError):
467
+ return
468
+
469
+ async def validate_recipe(self, recipe_data: dict[str, Any]) -> ValidationResult:
470
+ """Main validation entry point"""
471
+ # Ensure connectors are loaded before validation
472
+ await self._ensure_connectors_loaded()
473
+
474
+ errors: list[ValidationError] = []
475
+ warnings: list[ValidationError] = []
476
+
477
+ try:
478
+ # Extract the recipe code structure
479
+ if "code" not in recipe_data:
480
+ errors.append(
481
+ ValidationError(
482
+ message="Recipe must have a 'code' field",
483
+ error_type=ErrorType.SYNTAX_INVALID,
484
+ )
485
+ )
486
+ return ValidationResult(
487
+ is_valid=False, errors=errors, warnings=warnings
488
+ )
489
+
490
+ # Parse and validate basic structure
491
+ recipe = RecipeStructure(root=RecipeLine(**recipe_data["code"]))
492
+
493
+ # Set the current recipe root for cross-reference validation
494
+ self.current_recipe_root = recipe.root
495
+
496
+ # Validate unique 'as' values across all recipe steps
497
+ as_errors = self._validate_unique_as_values(recipe.root)
498
+ errors.extend(as_errors)
499
+
500
+ # Validate block structure and numbering rules
501
+ structure_errors = self._validate_block_structure(recipe.root)
502
+ errors.extend(structure_errors)
503
+
504
+ # Validate providers and trigger/action names
505
+ provider_errors = self._validate_providers(recipe.root)
506
+ errors.extend(provider_errors)
507
+
508
+ # Validate references and dependencies with step context
509
+ step_context: dict[str, Any] = {} # Track available data sources
510
+ ref_errors = self._validate_references_with_context(
511
+ recipe.root, step_context
512
+ )
513
+ errors.extend(ref_errors)
514
+
515
+ # Validate input/output schemas
516
+ schema_errors = self._validate_schemas(recipe.root)
517
+ errors.extend(schema_errors)
518
+
519
+ # Validate expressions
520
+ expr_errors = self._validate_expressions(recipe.root)
521
+ errors.extend(expr_errors)
522
+
523
+ # Validate array mappings and data pill structures
524
+ array_errors = self._validate_array_mappings(recipe.root)
525
+ errors.extend(array_errors)
526
+
527
+ # Validate input modes and formulas
528
+ input_mode_errors = self._validate_input_modes(recipe.root)
529
+ errors.extend(input_mode_errors)
530
+
531
+ # Validate config section coverage
532
+ config_errors = self._validate_config_coverage(
533
+ recipe.root, recipe_data.get("config", [])
534
+ )
535
+ errors.extend(config_errors)
536
+
537
+ except Exception as e:
538
+ errors.append(
539
+ ValidationError(
540
+ message=f"Recipe parsing failed: {str(e)}",
541
+ error_type=ErrorType.SYNTAX_INVALID,
542
+ )
543
+ )
544
+
545
+ return ValidationResult(
546
+ is_valid=len(errors) == 0, errors=errors, warnings=warnings or []
547
+ )
548
+
549
+ async def _load_builtin_connectors(self) -> None:
550
+ """Load connector metadata from cache or Workato API"""
551
+ # Try to load from cache first
552
+ if self._load_cached_connectors():
553
+ return # Successfully loaded from cache
554
+
555
+ # Fetch platform connectors with pagination
556
+ all_connectors: list[PlatformConnector] = []
557
+ page: int = 1
558
+ per_page = 100
559
+
560
+ while True:
561
+ response = (
562
+ await self.workato_api_client.connectors_api.list_platform_connectors(
563
+ page=page,
564
+ per_page=per_page,
565
+ )
566
+ )
567
+
568
+ connectors = response.items
569
+
570
+ if not connectors:
571
+ break
572
+
573
+ all_connectors.extend(connectors)
574
+
575
+ # If we got fewer than per_page results, we're on the last page
576
+ if len(connectors) < per_page:
577
+ break
578
+
579
+ page += 1
580
+
581
+ for platform_connector in all_connectors:
582
+ provider_name = platform_connector.name.lower()
583
+ self.known_adapters.add(provider_name)
584
+
585
+ # Convert List[ConnectorAction] to dict for JSON serialization
586
+ # and validation. The validation logic expects dicts (calls .keys()),
587
+ # so we convert: List[ConnectorAction] -> {action.name: action.to_dict()}
588
+ self.connector_metadata[provider_name] = {
589
+ "type": "platform",
590
+ "name": platform_connector.name,
591
+ "deprecated": platform_connector.deprecated,
592
+ "categories": platform_connector.categories,
593
+ "triggers": {
594
+ trigger.name: trigger.to_dict()
595
+ for trigger in platform_connector.triggers
596
+ },
597
+ "actions": {
598
+ action.name: action.to_dict()
599
+ for action in platform_connector.actions
600
+ },
601
+ }
602
+
603
+ # Fetch custom connectors
604
+ custom_connectors_response = (
605
+ await self.workato_api_client.connectors_api.list_custom_connectors()
606
+ )
607
+
608
+ for custom_connector in custom_connectors_response.result:
609
+ provider_name = custom_connector.name.lower()
610
+ code_response = (
611
+ await self.workato_api_client.connectors_api.get_custom_connector_code(
612
+ custom_connector.id
613
+ )
614
+ )
615
+ self.known_adapters.add(provider_name)
616
+ # Note: Custom connector trigger/action parsing is not implemented
617
+ # Using empty dicts for consistency with platform connector structure
618
+ self.connector_metadata[provider_name] = {
619
+ "type": "custom",
620
+ "name": custom_connector.name,
621
+ "code": code_response.data.code,
622
+ "categories": [], # Custom connectors don't have categories
623
+ "triggers": {}, # Not Implemented - would need to parse code
624
+ "actions": {}, # Not Implemented - would need to parse code
625
+ }
626
+
627
+ # Save to cache for next time
628
+ self._save_connectors_to_cache()
629
+
630
+ def _validate_providers(self, line: RecipeLine) -> list[ValidationError]:
631
+ """Validate provider names and trigger/action names"""
632
+ errors = []
633
+
634
+ # Validate provider if present
635
+ if line.provider:
636
+ provider_name = line.provider.lower()
637
+
638
+ # Check if provider is known
639
+ if self.known_adapters and provider_name not in self.known_adapters:
640
+ errors.append(
641
+ ValidationError(
642
+ message=(
643
+ f"Unknown provider '{line.provider}'. Provider not found in"
644
+ " available connectors."
645
+ ),
646
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
647
+ field_label="provider",
648
+ line_number=line.number,
649
+ )
650
+ )
651
+
652
+ # Validate trigger/action name if we have metadata
653
+ if line.keyword not in [Keyword.TRIGGER, Keyword.ACTION]:
654
+ return errors
655
+
656
+ if provider_name in self.connector_metadata:
657
+ connector_meta = self.connector_metadata[provider_name]
658
+
659
+ if line.keyword == Keyword.TRIGGER:
660
+ available_triggers = connector_meta.get("triggers", {})
661
+ if available_triggers and line.name not in available_triggers:
662
+ trigger_names = ", ".join(list(available_triggers.keys()))
663
+ errors.append(
664
+ ValidationError(
665
+ message=(
666
+ f"Unknown trigger '{line.name}' for provider "
667
+ f"'{line.provider}'. Available "
668
+ f"triggers: {trigger_names}"
669
+ ),
670
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
671
+ field_label="name",
672
+ line_number=line.number,
673
+ )
674
+ )
675
+
676
+ elif line.keyword == Keyword.ACTION:
677
+ available_actions = connector_meta.get("actions", {})
678
+ if available_actions and line.name not in available_actions:
679
+ action_names = ", ".join(list(available_actions.keys()))
680
+ errors.append(
681
+ ValidationError(
682
+ message=(
683
+ f"Unknown action '{line.name}' for provider "
684
+ f"'{line.provider}'. Available "
685
+ f"actions: {action_names}"
686
+ ),
687
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
688
+ field_label="name",
689
+ line_number=line.number,
690
+ )
691
+ )
692
+
693
+ # Recursively validate children
694
+ if line.block:
695
+ for child in line.block:
696
+ child_errors = self._validate_providers(child)
697
+ errors.extend(child_errors)
698
+
699
+ return errors
700
+
701
+ def _validate_unique_as_values(self, line: RecipeLine) -> list[ValidationError]:
702
+ """Validate that all 'as' values are unique across the recipe"""
703
+ errors = []
704
+ # as_value -> {"line_number": int, "provider": str, "keyword": str}
705
+ as_tracker: dict[str, dict[str, Any]] = {}
706
+
707
+ def collect_as_values(
708
+ current_line: RecipeLine,
709
+ path: list[int] | None = None,
710
+ ) -> None:
711
+ """Recursively collect all 'as' values and their locations"""
712
+ if path is None:
713
+ path = []
714
+
715
+ current_path = path + [current_line.number]
716
+
717
+ # Check if this line has an 'as' value
718
+ if current_line.as_:
719
+ as_value = current_line.as_
720
+
721
+ if as_value in as_tracker:
722
+ # Found duplicate!
723
+ existing_info = as_tracker[as_value]
724
+ current_keyword = (
725
+ current_line.keyword.value
726
+ if hasattr(current_line.keyword, "value")
727
+ else str(current_line.keyword)
728
+ )
729
+
730
+ errors.append(
731
+ ValidationError(
732
+ message=(
733
+ f"Duplicate 'as' value '{as_value}' found. First used "
734
+ f"in {existing_info['keyword']} step "
735
+ f"{existing_info['line_number']} "
736
+ f"({existing_info['provider']}), also used in "
737
+ f"{current_keyword} step {current_line.number} "
738
+ f"({current_line.provider})"
739
+ ),
740
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
741
+ field_path=["as"],
742
+ line_number=current_line.number,
743
+ )
744
+ )
745
+ else:
746
+ # Record this 'as' value
747
+ current_keyword = (
748
+ current_line.keyword.value
749
+ if hasattr(current_line.keyword, "value")
750
+ else str(current_line.keyword)
751
+ )
752
+
753
+ as_tracker[as_value] = {
754
+ "line_number": current_line.number,
755
+ "provider": current_line.provider or "unknown",
756
+ "keyword": current_keyword,
757
+ "path": current_path,
758
+ }
759
+
760
+ # Recursively check children
761
+ if current_line.block:
762
+ for child in current_line.block:
763
+ collect_as_values(child, current_path)
764
+
765
+ # Collect all 'as' values starting from root
766
+ collect_as_values(line)
767
+
768
+ return errors
769
+
770
+ def _validate_references_with_context(
771
+ self, line: RecipeLine, step_context: dict[str, Any]
772
+ ) -> list[ValidationError]:
773
+ """Validate references between steps with context tracking"""
774
+ errors: list[ValidationError] = []
775
+
776
+ # Add current step to context if it has an 'as' name
777
+ # Note: Duplicate 'as' values are now caught early by _validate_unique_as_values
778
+ if line.as_ and line.provider:
779
+ step_context[line.as_] = {
780
+ "provider": line.provider,
781
+ "keyword": line.keyword.value
782
+ if hasattr(line.keyword, "value")
783
+ else str(line.keyword),
784
+ "number": line.number,
785
+ "name": line.name,
786
+ }
787
+
788
+ # Validate data pill references in input
789
+ if line.input:
790
+ pill_errors = self._validate_data_pill_references_with_context(
791
+ line.input, line.number, step_context
792
+ )
793
+ errors.extend(pill_errors)
794
+
795
+ # Validate data pill references in conditional input
796
+ # (for if/elsif/while_condition)
797
+ if (
798
+ hasattr(line, "input")
799
+ and line.input
800
+ and line.keyword in [Keyword.IF, Keyword.ELSIF]
801
+ ):
802
+ pill_errors = self._validate_data_pill_references_with_context(
803
+ line.input, line.number, step_context
804
+ )
805
+ errors.extend(pill_errors)
806
+
807
+ # Validate children with updated context
808
+ if line.block:
809
+ # For repeat blocks, add special repeat context
810
+ if line.keyword == Keyword.REPEAT and line.as_:
811
+ repeat_context = step_context.copy()
812
+ repeat_context[line.as_] = {
813
+ "provider": "repeat",
814
+ "keyword": "repeat",
815
+ "number": line.number,
816
+ "name": "repeat_processor",
817
+ }
818
+ for child in line.block:
819
+ child_errors = self._validate_references_with_context(
820
+ child, repeat_context
821
+ )
822
+ errors.extend(child_errors)
823
+ else:
824
+ for child in line.block:
825
+ child_errors = self._validate_references_with_context(
826
+ child, step_context
827
+ )
828
+ errors.extend(child_errors)
829
+
830
+ return errors
831
+
832
+ def _validate_data_pill_references_with_context(
833
+ self, input_data: dict[str, Any], line_number: int, step_context: dict[str, Any]
834
+ ) -> list[ValidationError]:
835
+ """Validate data pill references in input fields with step context"""
836
+ errors: list[ValidationError] = []
837
+
838
+ def check_value(value: Any, field_path: list[str]) -> None:
839
+ if isinstance(value, str):
840
+ # Look for data pill patterns like #{_('data.provider.as.field')}
841
+ pill_matches = self._extract_data_pills(value)
842
+ for pill in pill_matches:
843
+ if not self._is_valid_data_pill(pill):
844
+ errors.append(
845
+ ValidationError(
846
+ message=(
847
+ f"Invalid data pill format: '{pill}'. Expected "
848
+ f"format: data.provider.as.field"
849
+ ),
850
+ error_type=ErrorType.INPUT_UNKNOWN_DATA_PILL,
851
+ field_path=field_path,
852
+ line_number=line_number,
853
+ )
854
+ )
855
+ else:
856
+ # Validate cross-reference
857
+ validation_error = self._validate_data_pill_cross_reference(
858
+ pill, line_number, step_context, field_path
859
+ )
860
+ if validation_error:
861
+ errors.append(validation_error)
862
+ elif isinstance(value, dict):
863
+ for key, val in value.items():
864
+ check_value(val, field_path + [key])
865
+ elif type(value).__name__ == "list":
866
+ for i, val in enumerate(value):
867
+ check_value(val, field_path + [str(i)])
868
+
869
+ check_value(input_data, [])
870
+ return errors
871
+
872
+ def _validate_data_pill_cross_reference(
873
+ self,
874
+ pill: str,
875
+ line_number: int,
876
+ step_context: dict[str, Any],
877
+ field_path: list[str],
878
+ ) -> ValidationError | None:
879
+ """Validate that data pill references point to valid previous steps"""
880
+ # Parse data pill: data.provider.as.field_path
881
+ parts = pill.split(".")
882
+ if len(parts) < 4:
883
+ return None
884
+
885
+ provider = parts[1]
886
+ as_name = parts[2]
887
+
888
+ # Check if the referenced step exists
889
+ if as_name not in step_context:
890
+ available_steps = list(step_context.keys())
891
+ suggestion = (
892
+ f" Available steps: {', '.join(available_steps[:5])}"
893
+ if available_steps
894
+ else ""
895
+ )
896
+ return ValidationError(
897
+ message=f"Data pill references unknown step '{as_name}'.{suggestion}",
898
+ error_type=ErrorType.INPUT_UNKNOWN_DATA_PILL,
899
+ field_path=field_path,
900
+ line_number=line_number,
901
+ )
902
+
903
+ # Validate provider matches
904
+ step_info = step_context[as_name]
905
+ if step_info["provider"] != provider:
906
+ return ValidationError(
907
+ message=(
908
+ f"Data pill provider mismatch: step '{as_name}' uses "
909
+ f"provider '{step_info['provider']}', not '{provider}'"
910
+ ),
911
+ error_type=ErrorType.INPUT_UNKNOWN_DATA_PILL,
912
+ field_path=field_path,
913
+ line_number=line_number,
914
+ )
915
+
916
+ return None
917
+
918
+ def _validate_data_pill_references(
919
+ self, input_data: dict[str, Any], line_number: int
920
+ ) -> list[ValidationError]:
921
+ """Legacy method - kept for compatibility"""
922
+ return self._validate_data_pill_references_with_context(
923
+ input_data, line_number, {}
924
+ )
925
+
926
+ def _extract_data_pills(self, text: str | None) -> list[str]:
927
+ """Extract data pill references from text"""
928
+ import re
929
+
930
+ if not isinstance(text, str):
931
+ return []
932
+
933
+ # Match Workato data pill formats: #{_dp('data.path')}
934
+ # or legacy #{_('data.path')}
935
+ pattern = r"#\{_(?:dp)?\(['\"]([^'\"]+)['\"]\)\}"
936
+ return re.findall(pattern, text)
937
+
938
+ def _is_valid_data_pill(self, pill: str) -> bool:
939
+ """Check if data pill reference is valid"""
940
+ # Validate data pill format: data.provider.as.field_path
941
+ if not pill.startswith("data."):
942
+ return False
943
+
944
+ parts = pill.split(".")
945
+ if len(parts) < 4: # Need at least: data.provider.as.field
946
+ return False
947
+
948
+ # Extract components
949
+ prefix = parts[0] # should be 'data'
950
+ provider = parts[1]
951
+ as_name = parts[2]
952
+ # field_path is parts[3:]
953
+
954
+ return prefix == "data" and provider is not None and as_name is not None
955
+
956
+ def _validate_schemas(self, line: RecipeLine) -> list[ValidationError]:
957
+ """Validate input/output schemas"""
958
+ errors: list[ValidationError] = []
959
+
960
+ # Validate children
961
+ if line.block:
962
+ for child in line.block:
963
+ child_errors = self._validate_schemas(child)
964
+ errors.extend(child_errors)
965
+
966
+ return errors
967
+
968
+ def _validate_expressions(self, line: RecipeLine) -> list[ValidationError]:
969
+ """Validate expressions in recipe"""
970
+ errors: list[ValidationError] = []
971
+
972
+ # Validate expressions in input fields
973
+ if line.input:
974
+ expr_errors = self._validate_input_expressions(line.input, line.number)
975
+ errors.extend(expr_errors)
976
+
977
+ # Validate children
978
+ if line.block:
979
+ for child in line.block:
980
+ child_errors = self._validate_expressions(child)
981
+ errors.extend(child_errors)
982
+
983
+ return errors
984
+
985
+ def _validate_input_expressions(
986
+ self, input_data: dict[str, Any], line_number: int
987
+ ) -> list[ValidationError]:
988
+ """Validate expressions in input fields"""
989
+ errors = []
990
+
991
+ def check_expression(value: Any, field_path: list[str]) -> None:
992
+ if (
993
+ isinstance(value, str)
994
+ and self._is_expression(value)
995
+ and not self._is_valid_expression(value)
996
+ ):
997
+ errors.append(
998
+ ValidationError(
999
+ message=f"Invalid expression syntax: {value}",
1000
+ error_type=ErrorType.INPUT_EXPR_INVALID,
1001
+ field_path=field_path,
1002
+ line_number=line_number,
1003
+ )
1004
+ )
1005
+ elif isinstance(value, dict):
1006
+ for key, val in value.items():
1007
+ check_expression(val, field_path + [key])
1008
+ elif type(value).__name__ == "list":
1009
+ for i, val in enumerate(value):
1010
+ check_expression(val, field_path + [str(i)])
1011
+
1012
+ check_expression(input_data, [])
1013
+ return errors
1014
+
1015
+ def _is_expression(self, text: str | None) -> bool:
1016
+ """Check if text is an expression"""
1017
+ if not isinstance(text, str):
1018
+ return False
1019
+
1020
+ stripped = text.strip()
1021
+ # Basic expression detection covering formulas, Jinja, and data pills
1022
+ return stripped.startswith("=") or "{{" in stripped or "#{_" in stripped
1023
+
1024
+ def _is_valid_expression(self, expression: str) -> bool:
1025
+ """Validate expression syntax"""
1026
+ # Basic expression validation - could be enhanced
1027
+ # For now, just check if it's not empty
1028
+ return bool(expression and expression.strip())
1029
+
1030
+ def _validate_config_coverage(
1031
+ self, line: RecipeLine, config: list[dict[str, Any]]
1032
+ ) -> list[ValidationError]:
1033
+ """Validate that all providers used in recipe have config entries"""
1034
+ errors = []
1035
+
1036
+ # Collect all providers used in the recipe
1037
+ used_providers: set[str] = set()
1038
+ self._collect_providers(line, used_providers)
1039
+
1040
+ # Collect providers declared in config
1041
+ config_providers: set[str] = set()
1042
+ for config_entry in config:
1043
+ if isinstance(config_entry, dict) and config_entry.get("provider"):
1044
+ config_providers.add(config_entry["provider"])
1045
+
1046
+ builtin_connectors = {
1047
+ key
1048
+ for key, meta in self.connector_metadata.items()
1049
+ if key != "workato_app" and "Workato" not in meta["categories"]
1050
+ }
1051
+
1052
+ # Check for missing config entries (excluding built-in connectors)
1053
+ missing_providers = used_providers - config_providers - builtin_connectors
1054
+ for provider in missing_providers:
1055
+ errors.append(
1056
+ ValidationError(
1057
+ message=(
1058
+ f"Provider '{provider}' is used in recipe but missing from "
1059
+ f"config section"
1060
+ ),
1061
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1062
+ field_path=["config"],
1063
+ )
1064
+ )
1065
+
1066
+ return errors
1067
+
1068
+ def _collect_providers(self, line: RecipeLine, providers: set[str]) -> None:
1069
+ """Recursively collect all providers used in the recipe"""
1070
+ if line.provider:
1071
+ providers.add(line.provider)
1072
+
1073
+ if line.block:
1074
+ for child in line.block:
1075
+ self._collect_providers(child, providers)
1076
+
1077
+ def _validate_generic_schema_usage(self, line: RecipeLine) -> list[ValidationError]:
1078
+ """Generic schema validation based on actual usage patterns for any provider"""
1079
+ errors = []
1080
+
1081
+ # Check if this step is referenced by others - needs extended_output_schema
1082
+ if self._step_is_referenced(line) and (
1083
+ not hasattr(line, "extended_output_schema")
1084
+ or not line.extended_output_schema
1085
+ ):
1086
+ errors.append(
1087
+ ValidationError(
1088
+ message=(
1089
+ f"Step {line.number} ({line.provider}) requires "
1090
+ f"'extended_output_schema' because it's referenced by "
1091
+ f"other steps"
1092
+ ),
1093
+ error_type=ErrorType.EXTENDED_SCHEMA_INVALID,
1094
+ line_number=line.number,
1095
+ field_path=["extended_output_schema"],
1096
+ )
1097
+ )
1098
+
1099
+ return errors
1100
+
1101
+ def _is_control_block(self, line: RecipeLine) -> bool:
1102
+ """Check if a step is a control block that doesn't need input schemas"""
1103
+ # Control blocks are if, while, repeat, etc. - they use data pills for
1104
+ # conditions but don't process input data that needs schemas
1105
+ return line.keyword in [
1106
+ Keyword.IF,
1107
+ Keyword.ELSIF,
1108
+ Keyword.WHILE_CONDITION,
1109
+ Keyword.REPEAT,
1110
+ ]
1111
+
1112
+ def _step_uses_data_pills(self, line: RecipeLine) -> bool:
1113
+ """Check if a step uses data pill references in its input"""
1114
+ if not line.input:
1115
+ return False
1116
+
1117
+ # Convert input to string to search for data pill patterns
1118
+ input_str = str(line.input)
1119
+
1120
+ # Look for data pill patterns: #{_('...')} or #{...}
1121
+ import re
1122
+
1123
+ data_pill_patterns = [
1124
+ r"#\{\s*_\([^)]+\)\s*\}", # #{_('data.provider.as.field')}
1125
+ r"#\{[^}]+\}", # #{simple_reference}
1126
+ ]
1127
+
1128
+ return any(re.search(pattern, input_str) for pattern in data_pill_patterns)
1129
+
1130
+ def _step_is_referenced(self, target_line: RecipeLine) -> bool:
1131
+ """Check if this step is referenced by other steps via data pills"""
1132
+ if not target_line.as_:
1133
+ return False # Can't be referenced without an 'as' value
1134
+
1135
+ if not self.current_recipe_root:
1136
+ return False
1137
+
1138
+ # We need to check the entire recipe tree for references to this step
1139
+ # This requires access to the root of the recipe
1140
+ return self._find_references_to_step(self.current_recipe_root, target_line)
1141
+
1142
+ def _find_references_to_step(
1143
+ self, search_root: RecipeLine, target_line: RecipeLine
1144
+ ) -> bool:
1145
+ """Recursively search for data pill references to a specific step"""
1146
+ if not target_line.as_ or not target_line.provider:
1147
+ return False
1148
+
1149
+ # Pattern to look for: data.{provider}.{as_value}
1150
+ reference_pattern = f"data.{target_line.provider}.{target_line.as_}"
1151
+
1152
+ return self._search_for_reference_pattern(search_root, reference_pattern)
1153
+
1154
+ def _search_for_reference_pattern(self, line: RecipeLine, pattern: str) -> bool:
1155
+ """Recursively search for a data pill reference pattern"""
1156
+ # Check current line's input for the pattern
1157
+ if line.input:
1158
+ input_str = str(line.input)
1159
+ if pattern in input_str:
1160
+ return True
1161
+
1162
+ # Check children
1163
+ if line.block:
1164
+ for child in line.block:
1165
+ if self._search_for_reference_pattern(child, pattern):
1166
+ return True
1167
+
1168
+ return False
1169
+
1170
+ def _validate_array_mappings(self, line: RecipeLine) -> list[ValidationError]:
1171
+ """Validate array mapping structures and data pill patterns"""
1172
+ errors = []
1173
+
1174
+ if line.input:
1175
+ errors.extend(self._validate_data_pill_structures(line.input, line.number))
1176
+ errors.extend(self._validate_array_consistency(line.input, line.number))
1177
+
1178
+ # Check children
1179
+ if line.block:
1180
+ for child in line.block:
1181
+ child_errors = self._validate_array_mappings(child)
1182
+ errors.extend(child_errors)
1183
+
1184
+ return errors
1185
+
1186
+ def _validate_data_pill_structures(
1187
+ self, input_data: dict[str, Any], line_number: int
1188
+ ) -> list[ValidationError]:
1189
+ """Validate data pill JSON structures in _dp() patterns"""
1190
+ errors: list[ValidationError] = []
1191
+ import json
1192
+ import re
1193
+
1194
+ def check_value(value: Any, field_path: list[str] | None = None) -> None:
1195
+ if field_path is None:
1196
+ field_path = []
1197
+ if isinstance(value, str):
1198
+ # Find all _dp() patterns
1199
+ dp_patterns = re.findall(r"#\{_dp\(\'([^\']+)\'\)\}", value)
1200
+ for dp_json in dp_patterns:
1201
+ # Check if this is simple syntax (data.provider.as.field)
1202
+ if dp_json.startswith("data."):
1203
+ # Simple syntax - validate the data path structure
1204
+ path_parts = dp_json.split(".")
1205
+ if len(path_parts) < 4: # data.provider.as.field (minimum)
1206
+ errors.append(
1207
+ ValidationError(
1208
+ message=(
1209
+ f"Simple data pill syntax must have at least 4 "
1210
+ "parts: data.provider.as.field in step "
1211
+ f"{line_number}"
1212
+ ),
1213
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1214
+ line_number=line_number,
1215
+ field_path=field_path + ["_dp"],
1216
+ )
1217
+ )
1218
+ else:
1219
+ # Validate provider/as reference exists
1220
+ provider = path_parts[1]
1221
+ as_value = path_parts[2]
1222
+ if not self._step_exists(provider, as_value):
1223
+ errors.append(
1224
+ ValidationError(
1225
+ message=(
1226
+ f"Simple data pill references non-existent "
1227
+ "step: provider='{provider}', "
1228
+ f"as='{as_value}' in step {line_number}"
1229
+ ),
1230
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1231
+ line_number=line_number,
1232
+ field_path=field_path + ["_dp"],
1233
+ )
1234
+ )
1235
+ else:
1236
+ # Complex JSON syntax - parse and validate
1237
+ try:
1238
+ # Parse the JSON structure
1239
+ dp_data = json.loads(dp_json)
1240
+
1241
+ # Validate required fields based on pill_type
1242
+ pill_type = dp_data.get("pill_type")
1243
+
1244
+ # All data pills must have pill_type
1245
+ if not pill_type:
1246
+ errors.append(
1247
+ ValidationError(
1248
+ message=(
1249
+ "Data pill missing required field "
1250
+ f"'pill_type' in step {line_number}"
1251
+ ),
1252
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1253
+ line_number=line_number,
1254
+ field_path=field_path + ["_dp"],
1255
+ )
1256
+ )
1257
+ else:
1258
+ # Validate based on pill_type
1259
+ if pill_type in ("output", "refs"):
1260
+ # Output/refs pills need provider, line, path
1261
+ required = ["provider", "line", "path"]
1262
+ for field in required:
1263
+ if field not in dp_data:
1264
+ errors.append(
1265
+ ValidationError(
1266
+ message=(
1267
+ f"Data pill with pill_type "
1268
+ f"'{pill_type}' missing "
1269
+ f"required field '{field}' "
1270
+ f"in step {line_number}"
1271
+ ),
1272
+ error_type=(
1273
+ ErrorType.INPUT_INVALID_BY_ADAPTER
1274
+ ),
1275
+ line_number=line_number,
1276
+ field_path=field_path + ["_dp"],
1277
+ )
1278
+ )
1279
+ elif pill_type == "project_property":
1280
+ # Project property pills need property_name
1281
+ if "property_name" not in dp_data:
1282
+ errors.append(
1283
+ ValidationError(
1284
+ message=(
1285
+ "Data pill with pill_type "
1286
+ "'project_property' missing "
1287
+ f"required field 'property_name' "
1288
+ f"in step {line_number}"
1289
+ ),
1290
+ error_type=(
1291
+ ErrorType.INPUT_INVALID_BY_ADAPTER
1292
+ ),
1293
+ line_number=line_number,
1294
+ field_path=field_path + ["_dp"],
1295
+ )
1296
+ )
1297
+ # Other pill types (e.g., "lookup", "variable")
1298
+ # can be added here as needed
1299
+
1300
+ # Validate provider/line references exist (only for
1301
+ # output/refs pills)
1302
+ if (
1303
+ pill_type in ("output", "refs")
1304
+ and "provider" in dp_data
1305
+ and "line" in dp_data
1306
+ and not self._step_exists(
1307
+ dp_data["provider"], dp_data["line"]
1308
+ )
1309
+ ):
1310
+ errors.append(
1311
+ ValidationError(
1312
+ message=(
1313
+ f"Data pill references non-existent step: "
1314
+ f"provider='{dp_data['provider']}', "
1315
+ f"line='{dp_data['line']}' in step "
1316
+ f"{line_number}"
1317
+ ),
1318
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1319
+ line_number=line_number,
1320
+ field_path=field_path + ["_dp"],
1321
+ )
1322
+ )
1323
+
1324
+ # Validate path is an array (only for output/refs pills)
1325
+ if (
1326
+ pill_type in ("output", "refs")
1327
+ and "path" in dp_data
1328
+ and not isinstance(dp_data["path"], list)
1329
+ ):
1330
+ errors.append(
1331
+ ValidationError(
1332
+ message=(
1333
+ f"Data pill 'path' must be an array in "
1334
+ f"step {line_number}"
1335
+ ),
1336
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1337
+ line_number=line_number,
1338
+ field_path=field_path + ["_dp", "path"],
1339
+ )
1340
+ )
1341
+
1342
+ except json.JSONDecodeError:
1343
+ errors.append(
1344
+ ValidationError(
1345
+ message=(
1346
+ f"Invalid JSON in data pill _dp() pattern "
1347
+ f"in step {line_number}"
1348
+ ),
1349
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1350
+ line_number=line_number,
1351
+ field_path=field_path + ["_dp"],
1352
+ )
1353
+ )
1354
+
1355
+ elif isinstance(value, dict):
1356
+ for key, subvalue in value.items():
1357
+ check_value(subvalue, field_path + [key])
1358
+
1359
+ elif isinstance(value, list):
1360
+ for i, subvalue in enumerate(value):
1361
+ check_value(subvalue, field_path + [str(i)])
1362
+
1363
+ check_value(input_data)
1364
+ return errors
1365
+
1366
+ def _validate_array_consistency(
1367
+ self, input_data: dict[str, Any], line_number: int
1368
+ ) -> list[ValidationError]:
1369
+ """Validate array structure consistency (____source + element mappings)"""
1370
+ errors = []
1371
+
1372
+ def check_array_structure(
1373
+ obj: Any, field_path: list[str] | None = None
1374
+ ) -> None:
1375
+ if field_path is None:
1376
+ field_path = []
1377
+ if isinstance(obj, dict):
1378
+ # Check if this looks like an array mapping structure
1379
+ if "____source" in obj:
1380
+ source_path = self._extract_path_from_dp(obj["____source"])
1381
+
1382
+ # Check if there are any individual field mappings
1383
+ has_element_mappings = False
1384
+
1385
+ # Check other fields for consistent element mappings
1386
+ for key, value in obj.items():
1387
+ if (
1388
+ key != "____source"
1389
+ and isinstance(value, str)
1390
+ and "_dp(" in value
1391
+ ):
1392
+ has_element_mappings = True
1393
+ element_path = self._extract_path_from_dp(value)
1394
+
1395
+ # Validate element path consistency
1396
+ if (
1397
+ source_path
1398
+ and element_path
1399
+ and not self._is_valid_element_path(
1400
+ source_path, element_path
1401
+ )
1402
+ ):
1403
+ errors.append(
1404
+ ValidationError(
1405
+ message=(
1406
+ f"Array element path inconsistent with "
1407
+ f"source path in step {line_number}. "
1408
+ f"Source: {source_path}, Element: "
1409
+ f"{element_path}"
1410
+ ),
1411
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1412
+ line_number=line_number,
1413
+ field_path=field_path + [key],
1414
+ )
1415
+ )
1416
+
1417
+ # Warn if ____source exists but no individual field mappings found
1418
+ if not has_element_mappings:
1419
+ # Check if there are any non-____source fields that could be
1420
+ # element mappings
1421
+ other_fields = [k for k in obj if k != "____source"]
1422
+ if other_fields:
1423
+ errors.append(
1424
+ ValidationError(
1425
+ message=(
1426
+ f"Array mapping with ____source found but no "
1427
+ f"individual field mappings using _dp() with "
1428
+ f"current_item in step {line_number}. Consider "
1429
+ f"mapping individual fields: "
1430
+ f"{', '.join(other_fields[:3])}"
1431
+ ),
1432
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1433
+ line_number=line_number,
1434
+ field_path=field_path + ["____source"],
1435
+ )
1436
+ )
1437
+ else:
1438
+ errors.append(
1439
+ ValidationError(
1440
+ message=(
1441
+ f"Array mapping with ____source found but "
1442
+ "no individual field mappings in step "
1443
+ f"{line_number}. Array elements cannot be "
1444
+ "accessed without field mappings."
1445
+ ),
1446
+ error_type=ErrorType.INPUT_INVALID_BY_ADAPTER,
1447
+ line_number=line_number,
1448
+ field_path=field_path + ["____source"],
1449
+ )
1450
+ )
1451
+
1452
+ # Recursively check nested objects
1453
+ for key, value in obj.items():
1454
+ check_array_structure(value, field_path + [key])
1455
+
1456
+ elif isinstance(obj, list):
1457
+ for i, item in enumerate(obj):
1458
+ check_array_structure(item, field_path + [str(i)])
1459
+
1460
+ check_array_structure(input_data)
1461
+ return errors
1462
+
1463
+ def _step_exists(self, provider: str, as_value: str) -> bool:
1464
+ """Check if a step with given provider and as_value exists in the recipe"""
1465
+ if not hasattr(self, "current_recipe_root") or not self.current_recipe_root:
1466
+ return True # Skip validation if we don't have recipe context
1467
+
1468
+ return (
1469
+ self._find_step_by_as(self.current_recipe_root, provider, as_value)
1470
+ is not None
1471
+ )
1472
+
1473
+ def _find_step_by_as(
1474
+ self, line: RecipeLine, provider: str, as_value: str
1475
+ ) -> RecipeLine | None:
1476
+ """Find a step by provider and as_value"""
1477
+ if line.provider == provider and line.as_ == as_value:
1478
+ return line
1479
+
1480
+ if line.block:
1481
+ for child in line.block:
1482
+ result = self._find_step_by_as(child, provider, as_value)
1483
+ if result:
1484
+ return result
1485
+
1486
+ return None
1487
+
1488
+ def _extract_path_from_dp(self, dp_string: str) -> list[Any]:
1489
+ """Extract path array from _dp() string"""
1490
+ import json
1491
+ import re
1492
+
1493
+ match = re.search(r"#\{_dp\(\'([^\']+)\'\)\}", dp_string)
1494
+ if match:
1495
+ try:
1496
+ dp_data = json.loads(match.group(1))
1497
+ return dp_data.get("path", []) or []
1498
+ except json.JSONDecodeError:
1499
+ pass
1500
+ return []
1501
+
1502
+ def _is_valid_element_path(self, source_path: list, element_path: list) -> bool:
1503
+ """Check if element path is a valid extension of source path"""
1504
+ if not source_path or not element_path:
1505
+ return True # Skip validation if we can't parse paths
1506
+
1507
+ # Element path should start with source path, then have current_item,
1508
+ # then additional fields
1509
+ if len(element_path) < len(source_path) + 2:
1510
+ return False
1511
+
1512
+ # Check source path prefix matches
1513
+ if element_path[: len(source_path)] != source_path:
1514
+ return False
1515
+
1516
+ # Check for current_item marker
1517
+ current_item_index = len(source_path)
1518
+ return (
1519
+ current_item_index < len(element_path)
1520
+ and isinstance(element_path[current_item_index], dict)
1521
+ and element_path[current_item_index].get("path_element_type")
1522
+ == "current_item"
1523
+ )
1524
+
1525
+ def _validate_block_structure(self, line: RecipeLine) -> list[ValidationError]:
1526
+ """Validate block numbering and keyword rules"""
1527
+ errors = []
1528
+
1529
+ # Block 0 must be trigger
1530
+ if line.number == 0 and line.keyword != Keyword.TRIGGER:
1531
+ errors.append(
1532
+ ValidationError(
1533
+ message="Block 0 must be a trigger",
1534
+ error_type=ErrorType.STRUCTURE_INVALID,
1535
+ line_number=line.number,
1536
+ field_label="keyword",
1537
+ )
1538
+ )
1539
+
1540
+ # Note: Removed the rule that blocks 1+ must be actions
1541
+ # This was incorrect - recipes can have various block structures
1542
+ # including conditional blocks, repeat blocks, etc.
1543
+
1544
+ # Recursively validate children
1545
+ if line.block:
1546
+ for child in line.block:
1547
+ child_errors = self._validate_block_structure(child)
1548
+ errors.extend(child_errors)
1549
+
1550
+ return errors
1551
+
1552
+ def _validate_input_modes(self, line: RecipeLine) -> list[ValidationError]:
1553
+ """Validate input mode consistency and formulas"""
1554
+ errors = []
1555
+
1556
+ if line.input:
1557
+ for input_field, value in line.input.items():
1558
+ if isinstance(value, str):
1559
+ # Check for mixed modes
1560
+ has_formula = "=_dp(" in value
1561
+ has_text = "#{_dp(" in value
1562
+
1563
+ if has_formula and has_text:
1564
+ errors.append(
1565
+ ValidationError(
1566
+ message=(
1567
+ f"Mixed input modes detected in field "
1568
+ f"'{input_field}'. Consider using one mode "
1569
+ "per field for clarity."
1570
+ ),
1571
+ error_type=ErrorType.INPUT_MODE_INCONSISTENT,
1572
+ line_number=line.number,
1573
+ field_path=[input_field],
1574
+ )
1575
+ )
1576
+
1577
+ # Validate formula syntax if in formula mode
1578
+ if has_formula:
1579
+ formula_errors = self._validate_formula_syntax(
1580
+ value, input_field, line.number
1581
+ )
1582
+ errors.extend(formula_errors)
1583
+
1584
+ # Recursively validate children
1585
+ if line.block:
1586
+ for child in line.block:
1587
+ child_errors = self._validate_input_modes(child)
1588
+ errors.extend(child_errors)
1589
+
1590
+ return errors
1591
+
1592
+ def _validate_formula_syntax(
1593
+ self, value: str, field: str, line_number: int
1594
+ ) -> list[ValidationError]:
1595
+ """Validate formula syntax and allowed methods"""
1596
+ errors = []
1597
+
1598
+ # Check if formula starts with =_dp(
1599
+ if not value.strip().startswith("=_dp("):
1600
+ errors.append(
1601
+ ValidationError(
1602
+ message=f"Formula in field '{field}' must start with '=_dp('",
1603
+ error_type=ErrorType.FORMULA_SYNTAX_INVALID,
1604
+ line_number=line_number,
1605
+ field_path=[field],
1606
+ )
1607
+ )
1608
+
1609
+ # Check for common formula syntax issues
1610
+ if "=_dp(" in value:
1611
+ # Extract the data pill part
1612
+ start = value.find("=_dp(")
1613
+ end = value.find(")", start)
1614
+ if end == -1:
1615
+ errors.append(
1616
+ ValidationError(
1617
+ message=f"Formula in field '{field}' has unmatched parentheses",
1618
+ error_type=ErrorType.FORMULA_SYNTAX_INVALID,
1619
+ line_number=line_number,
1620
+ field_path=[field],
1621
+ )
1622
+ )
1623
+ else:
1624
+ # Check for common Ruby method calls
1625
+ formula_part = value[end + 1 :]
1626
+ if formula_part:
1627
+ # Validate common Ruby methods
1628
+ allowed_methods = [
1629
+ "split",
1630
+ "first",
1631
+ "last",
1632
+ "length",
1633
+ "size",
1634
+ "count",
1635
+ "upcase",
1636
+ "downcase",
1637
+ "capitalize",
1638
+ "strip",
1639
+ "chomp",
1640
+ "gsub",
1641
+ "sub",
1642
+ "replace",
1643
+ "to_i",
1644
+ "to_f",
1645
+ "to_s",
1646
+ "present?",
1647
+ "blank?",
1648
+ "nil?",
1649
+ "empty?",
1650
+ "map",
1651
+ "select",
1652
+ "filter",
1653
+ "reject",
1654
+ "find",
1655
+ "detect",
1656
+ "any?",
1657
+ "all?",
1658
+ "none?",
1659
+ "one?",
1660
+ "sum",
1661
+ "min",
1662
+ "max",
1663
+ "average",
1664
+ "mean",
1665
+ ]
1666
+
1667
+ # Extract method calls (simple pattern matching)
1668
+ import re
1669
+
1670
+ method_calls = re.findall(r"\.(\w+)\s*\(?", formula_part)
1671
+ for method in method_calls:
1672
+ if method not in allowed_methods:
1673
+ errors.append(
1674
+ ValidationError(
1675
+ message=(
1676
+ f"Method '{method}' in field '{field}' "
1677
+ "may not be supported. Common methods: "
1678
+ f"{', '.join(allowed_methods[:10])}..."
1679
+ ),
1680
+ error_type=ErrorType.FORMULA_SYNTAX_INVALID,
1681
+ line_number=line_number,
1682
+ field_path=[field],
1683
+ )
1684
+ )
1685
+
1686
+ return errors
1687
+
1688
+ def _validate_array_mappings_enhanced(
1689
+ self, line: RecipeLine
1690
+ ) -> list[ValidationError]:
1691
+ """Enhanced array mapping validation"""
1692
+ errors = []
1693
+
1694
+ def check_array_mapping(obj: Any, field_path: list[str] | None = None) -> None:
1695
+ if field_path is None:
1696
+ field_path = []
1697
+ if isinstance(obj, dict):
1698
+ if "____source" in obj:
1699
+ # Check if there are current_item mappings in the same object
1700
+ has_current_item = False
1701
+ for key, value in obj.items():
1702
+ if (
1703
+ key != "____source"
1704
+ and isinstance(value, str)
1705
+ and "current_item" in value
1706
+ ):
1707
+ has_current_item = True
1708
+ break
1709
+
1710
+ if not has_current_item:
1711
+ errors.append(
1712
+ ValidationError(
1713
+ message=(
1714
+ f"Field '{'.'.join(field_path)}' uses ____source "
1715
+ "but no current_item mappings found. Array "
1716
+ "elements cannot be accessed without mappings."
1717
+ ),
1718
+ error_type=ErrorType.ARRAY_MAPPING_INVALID,
1719
+ line_number=line.number,
1720
+ field_path=field_path,
1721
+ )
1722
+ )
1723
+
1724
+ # Recursively check nested objects
1725
+ for key, value in obj.items():
1726
+ check_array_mapping(value, field_path + [key])
1727
+ elif isinstance(obj, list):
1728
+ for i, item in enumerate(obj):
1729
+ check_array_mapping(item, field_path + [str(i)])
1730
+
1731
+ if line.input:
1732
+ check_array_mapping(line.input)
1733
+
1734
+ # Recursively validate children
1735
+ if line.block:
1736
+ for child in line.block:
1737
+ child_errors = self._validate_array_mappings_enhanced(child)
1738
+ errors.extend(child_errors)
1739
+
1740
+ return errors