1212import java .util .List ;
1313import java .util .Map ;
1414import java .util .Objects ;
15+ import java .util .regex .Pattern ;
1516import java .util .stream .Collectors ;
1617
1718import org .apache .commons .cli .CommandLine ;
6869 * process</li>
6970 * <li><b>-t, --tgt <target-dir></b>: Target directory for generated
7071 * Python code (defaults to <code>./python</code> if not specified)</li>
72+ * <li><b>-v, --version <version></b>: Version number for the generated
73+ * package, in <code>#.#.#</code> format (defaults to
74+ * <code>0.0.0</code>)</li>
75+ * <li><b>-n, --project-name <projectName></b>: Override the
76+ * <code>pyproject.toml</code> project name (defaults to
77+ * <code>python-<first-namespace-segment></code>)</li>
78+ * <li><b>-e, --allow-errors</b>: Continue generation even if validation
79+ * errors are present</li>
80+ * <li><b>-w, --fail-on-warnings</b>: Treat validation warnings as
81+ * errors</li>
7182 * <li><b>-h</b>: Print usage/help</li>
7283 * </ul>
7384 *
@@ -96,6 +107,16 @@ public class PythonCodeGeneratorCLI {
96107 */
97108 private static final Logger LOGGER = LoggerFactory .getLogger (PythonCodeGeneratorCLI .class );
98109
110+ /**
111+ * Default version used when no {@code -v} option is provided.
112+ */
113+ static final String DEFAULT_VERSION = "0.0.0" ;
114+
115+ /**
116+ * Regex that a version string must fully match: three dot-separated integers.
117+ */
118+ private static final Pattern VALID_VERSION_PATTERN = Pattern .compile ("\\ d+\\ .\\ d+\\ .\\ d+" );
119+
99120 /**
100121 * Public constructor for the CLI tool.
101122 */
@@ -128,6 +149,12 @@ public final int run(String[] args) {
128149 Option projectNameOpt = Option .builder ("n" ).longOpt ("project-name" ).argName ("projectName" )
129150 .desc ("Override the pyproject.toml project name (default: python-<first-namespace-segment>)" )
130151 .hasArg ().build ();
152+ Option versionOpt = Option .builder ("v" ).longOpt ("version" ).argName ("version" )
153+ .desc ("Package version in #.#.# format (default: " + DEFAULT_VERSION + ")" )
154+ .hasArg ().build ();
155+ Option namespacePrefixOpt = Option .builder ("x" ).longOpt ("namespace-prefix" ).argName ("namespacePrefix" )
156+ .desc ("Prefix to prepend to every generated namespace (e.g. finos)" )
157+ .hasArg ().build ();
131158
132159 options .addOption (help );
133160 options .addOption (srcDirOpt );
@@ -136,6 +163,8 @@ public final int run(String[] args) {
136163 options .addOption (allowErrorsOpt );
137164 options .addOption (failOnWarningsOpt );
138165 options .addOption (projectNameOpt );
166+ options .addOption (versionOpt );
167+ options .addOption (namespacePrefixOpt );
139168
140169 CommandLineParser parser = new DefaultParser ();
141170 try {
@@ -148,13 +177,24 @@ public final int run(String[] args) {
148177 boolean allowErrors = cmd .hasOption ("e" );
149178 boolean failOnWarnings = cmd .hasOption ("w" );
150179 String projectName = cmd .getOptionValue ("n" );
180+ String namespacePrefix = cmd .getOptionValue ("x" );
181+
182+ String version = DEFAULT_VERSION ;
183+ if (cmd .hasOption ("v" )) {
184+ String rawVersion = cmd .getOptionValue ("v" );
185+ if (!VALID_VERSION_PATTERN .matcher (rawVersion ).matches ()) {
186+ LOGGER .error ("Invalid version format '{}'. Expected #.#.# (e.g. 1.2.3)." , rawVersion );
187+ return 1 ;
188+ }
189+ version = rawVersion ;
190+ }
151191
152192 if (cmd .hasOption ("s" )) {
153193 String srcDir = cmd .getOptionValue ("s" );
154- return translateFromSourceDir (srcDir , tgtDir , allowErrors , failOnWarnings , projectName );
194+ return translateFromSourceDir (srcDir , tgtDir , allowErrors , failOnWarnings , projectName , version , namespacePrefix );
155195 } else if (cmd .hasOption ("f" )) {
156196 String srcFile = cmd .getOptionValue ("f" );
157- return translateFromSourceFile (srcFile , tgtDir , allowErrors , failOnWarnings , projectName );
197+ return translateFromSourceFile (srcFile , tgtDir , allowErrors , failOnWarnings , projectName , version , namespacePrefix );
158198 } else {
159199 LOGGER .error ("Either a source directory (-s) or source file (-f) must be specified." );
160200 printUsage (options );
@@ -172,8 +212,15 @@ private static void printUsage(Options options) {
172212 formatter .printHelp ("PythonCodeGeneratorCLI" , options , true );
173213 }
174214
175- private int translateFromSourceDir (String srcDir , String tgtDir , boolean allowErrors , boolean failOnWarnings ,
176- String projectName ) {
215+ private int translateFromSourceDir (
216+ String srcDir ,
217+ String tgtDir ,
218+ boolean allowErrors ,
219+ boolean failOnWarnings ,
220+ String projectName ,
221+ String version ,
222+ String namespacePrefix
223+ ) {
177224 // Find all .rosetta files in a directory
178225 Path srcDirPath = Paths .get (srcDir );
179226 if (!Files .exists (srcDirPath )) {
@@ -189,15 +236,22 @@ private int translateFromSourceDir(String srcDir, String tgtDir, boolean allowEr
189236 .filter (Files ::isRegularFile )
190237 .filter (f -> f .getFileName ().toString ().endsWith (".rosetta" ))
191238 .collect (Collectors .toList ());
192- return processRosettaFiles (rosettaFiles , tgtDir , allowErrors , failOnWarnings , projectName );
239+ return processRosettaFiles (rosettaFiles , tgtDir , allowErrors , failOnWarnings , projectName , version , namespacePrefix );
193240 } catch (IOException e ) {
194241 LOGGER .error ("Failed to process source directory: {}" , srcDir , e );
195242 return 1 ;
196243 }
197244 }
198245
199- private int translateFromSourceFile (String srcFile , String tgtDir , boolean allowErrors , boolean failOnWarnings ,
200- String projectName ) {
246+ private int translateFromSourceFile (
247+ String srcFile ,
248+ String tgtDir ,
249+ boolean allowErrors ,
250+ boolean failOnWarnings ,
251+ String projectName ,
252+ String version ,
253+ String namespacePrefix
254+ ) {
201255 Path srcFilePath = Paths .get (srcFile );
202256 if (!Files .exists (srcFilePath )) {
203257 LOGGER .error ("Source file does not exist: {}" , srcFile );
@@ -212,12 +266,19 @@ private int translateFromSourceFile(String srcFile, String tgtDir, boolean allow
212266 return 1 ;
213267 }
214268 List <Path > rosettaFiles = List .of (srcFilePath );
215- return processRosettaFiles (rosettaFiles , tgtDir , allowErrors , failOnWarnings , projectName );
269+ return processRosettaFiles (rosettaFiles , tgtDir , allowErrors , failOnWarnings , projectName , version , namespacePrefix );
216270 }
217271
218272 // Common processing function
219- private int processRosettaFiles (List <Path > rosettaFiles , String tgtDir , boolean allowErrors , boolean failOnWarnings ,
220- String projectName ) {
273+ private int processRosettaFiles (
274+ List <Path > rosettaFiles ,
275+ String tgtDir ,
276+ boolean allowErrors ,
277+ boolean failOnWarnings ,
278+ String projectName ,
279+ String version ,
280+ String namespacePrefix
281+ ) {
221282 LOGGER .info ("Processing {} .rosetta files, writing to: {}" , rosettaFiles .size (), tgtDir );
222283
223284 if (rosettaFiles .isEmpty ()) {
@@ -237,14 +298,14 @@ private int processRosettaFiles(List<Path> rosettaFiles, String tgtDir, boolean
237298
238299 PythonCodeGenerator pythonCodeGenerator = injector .getInstance (PythonCodeGenerator .class );
239300 pythonCodeGenerator .setProjectName (projectName );
301+ pythonCodeGenerator .setNamespacePrefix (namespacePrefix );
240302 PythonModelLoader modelLoader = injector .getInstance (PythonModelLoader .class );
241303
242304 List <RosettaModel > models = modelLoader .getRosettaModels (resources );
243305 if (models .isEmpty ()) {
244306 LOGGER .error ("No valid Rosetta models found." );
245307 return 1 ;
246308 }
247- String version = models .getFirst ().getVersion ();
248309
249310 LOGGER .info ("Processing {} models, version: {}" , models .size (), version );
250311
0 commit comments