11import abc
2+ import enum
23import json
34import logging
45import re
56import shutil # noqa: F401
7+ from functools import reduce
8+ from itertools import chain
69from pathlib import Path
710from typing import ClassVar
811
@@ -26,6 +29,15 @@ def _snake_case(s: str) -> str:
2629 return re .sub (r"[-\s]" , "_" , s ).lower ()
2730
2831
32+ class AppSpecType (enum .Enum ):
33+ ARC32 = "arc32"
34+ ARC56 = "arc56"
35+
36+
37+ class AppSpecsNotFoundError (Exception ):
38+ pass
39+
40+
2941class ClientGenerator (abc .ABC ):
3042 language : ClassVar [str ]
3143 extension : ClassVar [str ]
@@ -55,13 +67,15 @@ def create_for_language(cls, language: str, version: str | None) -> "ClientGener
5567 def create_for_extension (cls , extension : str , version : str | None ) -> "ClientGenerator" :
5668 return cls ._by_extension [extension ](version )
5769
58- def resolve_output_path (self , app_spec : Path , output_path_pattern : str | None ) -> Path | None :
70+ def resolve_output_path (self , app_spec : Path , output_path_pattern : str | None ) -> tuple [ Path , AppSpecType ] | None :
5971 try :
6072 application_json = json .loads (app_spec .read_text ())
6173 try :
6274 contract_name : str = application_json ["name" ] # ARC-56
75+ app_spec_type : AppSpecType = AppSpecType .ARC56
6376 except KeyError :
6477 contract_name = application_json ["contract" ]["name" ] # ARC-32
78+ app_spec_type = AppSpecType .ARC32
6579 except Exception :
6680 logger .error (f"Couldn't parse contract name from { app_spec } " , exc_info = True )
6781 return None
@@ -73,7 +87,7 @@ def resolve_output_path(self, app_spec: Path, output_path_pattern: str | None) -
7387 if output_path .exists () and not output_path .is_file ():
7488 logger .error (f"Could not output to { output_path } as it already exists and is a directory" )
7589 return None
76- return output_path
90+ return ( output_path , app_spec_type )
7791
7892 @abc .abstractmethod
7993 def generate (self , app_spec : Path , output : Path ) -> None : ...
@@ -88,6 +102,43 @@ def find_generate_command(self, version: str | None) -> list[str]: ...
88102 def format_contract_name (self , contract_name : str ) -> str :
89103 return contract_name
90104
105+ def generate_all (
106+ self ,
107+ app_spec_path_or_dir : Path ,
108+ output_path_pattern : str | None ,
109+ * ,
110+ raise_on_path_resolution_failure : bool ,
111+ ) -> None :
112+ if not app_spec_path_or_dir .is_dir ():
113+ app_specs = [app_spec_path_or_dir ]
114+ else :
115+ file_patterns = ["application.json" , "*.arc32.json" , "*.arc56.json" ]
116+ app_specs = list (set (chain .from_iterable (app_spec_path_or_dir .rglob (pattern ) for pattern in file_patterns )))
117+ app_specs .sort ()
118+ if not app_specs :
119+ raise AppSpecsNotFoundError
120+
121+ def accumulate_items_to_generate (
122+ acc : dict [Path , tuple [Path , AppSpecType ]], app_spec : Path
123+ ) -> dict [Path , tuple [Path , AppSpecType ]]:
124+ output_path_result = self .resolve_output_path (app_spec , output_path_pattern )
125+ if output_path_result is None :
126+ if raise_on_path_resolution_failure :
127+ raise click .ClickException (f"Error generating client for { app_spec } " )
128+ return acc
129+ (output_path , app_spec_type ) = output_path_result
130+ if output_path in acc :
131+ # ARC-56 app specs take precedence over ARC-32 app specs
132+ if acc [output_path ][1 ] == AppSpecType .ARC32 and app_spec_type == AppSpecType .ARC56 :
133+ acc [output_path ] = (app_spec , app_spec_type )
134+ else :
135+ acc [output_path ] = (app_spec , app_spec_type )
136+ return acc
137+
138+ items_to_generate : dict [Path , tuple [Path , AppSpecType ]] = reduce (accumulate_items_to_generate , app_specs , {})
139+ for output_path , (app_spec , _ ) in items_to_generate .items ():
140+ self .generate (app_spec , output_path )
141+
91142
92143class PythonClientGenerator (ClientGenerator , language = "python" , extension = ".py" ):
93144 def generate (self , app_spec : Path , output : Path ) -> None :
0 commit comments