11package com .regnosys .rosetta .generator .python .util ;
22
3- import java .util .HashSet ;
3+ import java .util .Map ;
44import java .util .Set ;
55
66import com .regnosys .rosetta .types .RAttribute ;
77import com .regnosys .rosetta .types .REnumType ;
88import com .regnosys .rosetta .types .RType ;
99
10- public class RuneToPythonMapper {
11- private static final Set <String > PYTHON_KEYWORDS = new HashSet <>();
12-
13- static {
14- // Initialize the set with Python keywords and soft keywords
15- PYTHON_KEYWORDS .add ("False" );
16- PYTHON_KEYWORDS .add ("await" );
17- PYTHON_KEYWORDS .add ("else" );
18- PYTHON_KEYWORDS .add ("import" );
19- PYTHON_KEYWORDS .add ("pass" );
20- PYTHON_KEYWORDS .add ("None" );
21- PYTHON_KEYWORDS .add ("break" );
22- PYTHON_KEYWORDS .add ("except" );
23- PYTHON_KEYWORDS .add ("in" );
24- PYTHON_KEYWORDS .add ("raise" );
25- PYTHON_KEYWORDS .add ("True" );
26- PYTHON_KEYWORDS .add ("class" );
27- PYTHON_KEYWORDS .add ("finally" );
28- PYTHON_KEYWORDS .add ("is" );
29- PYTHON_KEYWORDS .add ("return" );
30- PYTHON_KEYWORDS .add ("and" );
31- PYTHON_KEYWORDS .add ("continue" );
32- PYTHON_KEYWORDS .add ("for" );
33- PYTHON_KEYWORDS .add ("lambda" );
34- PYTHON_KEYWORDS .add ("try" );
35- PYTHON_KEYWORDS .add ("as" );
36- PYTHON_KEYWORDS .add ("def" );
37- PYTHON_KEYWORDS .add ("from" );
38- PYTHON_KEYWORDS .add ("nonlocal" );
39- PYTHON_KEYWORDS .add ("while" );
40- PYTHON_KEYWORDS .add ("assert" );
41- PYTHON_KEYWORDS .add ("del" );
42- PYTHON_KEYWORDS .add ("global" );
43- PYTHON_KEYWORDS .add ("not" );
44- PYTHON_KEYWORDS .add ("with" );
45- PYTHON_KEYWORDS .add ("async" );
46- PYTHON_KEYWORDS .add ("elif" );
47- PYTHON_KEYWORDS .add ("if" );
48- PYTHON_KEYWORDS .add ("or" );
49- PYTHON_KEYWORDS .add ("yield" );
50- PYTHON_KEYWORDS .add ("match" );
51- PYTHON_KEYWORDS .add ("case" );
52- PYTHON_KEYWORDS .add ("type" );
53- PYTHON_KEYWORDS .add ("_" );
10+ /**
11+ * Utility for mapping Rune/Rosetta types and identifiers to Python-friendly names and types.
12+ *
13+ * Note: Keep the Python keyword/soft-keyword list aligned with the Python version you target.
14+ * You can regenerate a canonical list via:
15+ * ```python
16+ * import keyword; print(sorted(keyword.kwlist))
17+ * ```
18+ * and add any soft keywords you wish to avoid (e.g., "match", "case") for your generator context.
19+ */
20+
21+ public final class RuneToPythonMapper {
22+
23+ private RuneToPythonMapper () {
24+ // Prevent instantiation (utility class)
5425 }
5526
56- // Define the set of Python types as a static final field
27+ // Prefix used to avoid collisions with Python reserved words or leading underscores.
28+ private static final String GENERATED_ATTR_PREFIX = "rune_attr_" ;
29+
30+ // Python keywords and soft keywords to avoid as identifiers (case-sensitive, as in Python).
31+ // Sorted alphabetically for maintainability.
32+ private static final Set <String > PYTHON_KEYWORDS = Set .of (
33+ "False" , "None" , "True" , "_" ,
34+ "and" , "as" , "assert" , "async" , "await" ,
35+ "break" ,
36+ "case" , "class" , "continue" ,
37+ "def" , "del" ,
38+ "elif" , "else" , "except" ,
39+ "finally" , "for" , "from" ,
40+ "global" ,
41+ "if" , "import" , "in" , "is" ,
42+ "lambda" ,
43+ "match" ,
44+ "nonlocal" , "not" ,
45+ "or" ,
46+ "pass" ,
47+ "raise" , "return" ,
48+ "try" , "type" ,
49+ "while" , "with" ,
50+ "yield"
51+ );
52+
53+ // Python basic types recognized by the generator. Includes both qualified and unqualified forms
54+ // as they may appear in generated output or user-provided annotations.
5755 private static final Set <String > PYTHON_TYPES = Set .of (
58- "int " , "str " , "Decimal" , " date" , "datetime" , "datetime.datetime " ,
59- "datetime.date " , "datetime.time" , "time " , "bool "
56+ "Decimal " , "bool " , "date" , "datetime" , "datetime.date " ,
57+ "datetime.datetime " , "datetime.time" , "int " , "str" , "time "
6058 );
6159
62- public static String mangleName (String attrib ) {
63- // Check if the attribute is a Python keyword or starts with an underscore
64- if (PYTHON_KEYWORDS .contains (attrib ) || attrib .charAt (0 ) == '_' ) {
65- return "rune_attr_" + attrib ;
66- }
67- return attrib ;
68- }
69-
70- private static String toPythonBasicTypeInnerFunction (String rosettaType ) {
71- // inner private function to convert from Rosetta type to Python type
72- // returns null if no matching type
73- switch (rosettaType ) {
74- case "string" :
75- case "eventType" :
76- case "calculation" :
77- case "productType" :
78- return "str" ;
79- case "time" :
80- return "datetime.time" ;
81- case "date" :
82- return "datetime.date" ;
83- case "dateTime" :
84- case "zonedDateTime" :
85- return "datetime.datetime" ;
86- case "number" :
87- return "Decimal" ;
88- case "boolean" :
89- return "bool" ;
90- case "int" :
91- return "int" ;
92- default :
93- return null ;
60+ // Mapping from Rosetta scalar types to Python basic types used by the generator.
61+ // Keys are the Rosetta/Rune type names.
62+ private static final Map <String , String > ROSETTA_TO_PY_BASIC = Map .ofEntries (
63+ Map .entry ("boolean" , "bool" ),
64+ Map .entry ("calculation" , "str" ),
65+ Map .entry ("date" , "datetime.date" ),
66+ Map .entry ("dateTime" , "datetime.datetime" ),
67+ Map .entry ("eventType" , "str" ),
68+ Map .entry ("int" , "int" ),
69+ Map .entry ("number" , "Decimal" ),
70+ Map .entry ("productType" , "str" ),
71+ Map .entry ("string" , "str" ),
72+ Map .entry ("time" , "datetime.time" ),
73+ Map .entry ("zonedDateTime" , "datetime.datetime" )
74+ );
75+
76+ // Mapping from Python basic types to their "WithMeta" counterparts used by the generator.
77+ private static final Map <String , String > PY_BASIC_TO_META = Map .ofEntries (
78+ Map .entry ("Decimal" , "NumberWithMeta" ),
79+ Map .entry ("bool" , "BoolWithMeta" ),
80+ Map .entry ("datetime.date" , "DateWithMeta" ),
81+ Map .entry ("datetime.datetime" , "DateTimeWithMeta" ),
82+ Map .entry ("datetime.time" , "TimeWithMeta" ),
83+ Map .entry ("int" , "IntWithMeta" ),
84+ Map .entry ("str" , "StrWithMeta" )
85+ );
86+
87+ /**
88+ * Mangles an identifier to avoid clashes with Python reserved words and names starting with "_".
89+ *
90+ * Null handling:
91+ * - Returns null if the input is null (caller-friendly for streaming/pipelines).
92+ *
93+ * @param name original identifier (may be null)
94+ * @return either the original name or the prefixed name if mangling is required
95+ */
96+ public static String mangleName (String name ) {
97+ if (name == null ) return null ;
98+ if (PYTHON_KEYWORDS .contains (name ) || name .startsWith ("_" )) {
99+ return GENERATED_ATTR_PREFIX + name ;
94100 }
101+ return name ;
95102 }
96103
97- public static String getAttributeTypeWithMeta (String attributeType ) {
98- // inner private function to convert from Rosetta type to Python type
99- // returns null if no matching type
100- switch (attributeType ) {
101- case "str" :
102- return "StrWithMeta" ;
103- case "datetime.time" :
104- return "TimeWithMeta" ;
105- case "datetime.date" :
106- return "DateWithMeta" ;
107- case "datetime.datetime" :
108- return "DateTimeWithMeta" ;
109- case "Decimal" :
110- return "NumberWithMeta" ;
111- case "bool" :
112- return "BoolWithMeta" ;
113- case "int" :
114- return "IntWithMeta" ;
115- default :
116- return attributeType ;
117- }
104+ /**
105+ * Maps a Python basic type to its WithMeta wrapper type used by the generator.
106+ *
107+ * @param pythonType Python basic type name (may be null)
108+ * @return the WithMeta type if known; otherwise returns the input unchanged; returns null if input is null
109+ */
110+ public static String getAttributeTypeWithMeta (String pythonType ) {
111+ if (pythonType == null ) return null ;
112+ return PY_BASIC_TO_META .getOrDefault (pythonType , pythonType );
118113 }
114+
115+ /**
116+ * Maps a Rosetta/Rune scalar type name to a Python basic type.
117+ *
118+ * @param rosettaType Rosetta type name (may be null)
119+ * @return Python basic type if mapped; otherwise the original input; null if input is null
120+ */
119121 public static String toPythonBasicType (String rosettaType ) {
120- String pythonType = toPythonBasicTypeInnerFunction ( rosettaType ) ;
121- return ( pythonType == null ) ? rosettaType : pythonType ;
122+ if ( rosettaType == null ) return null ;
123+ return ROSETTA_TO_PY_BASIC . getOrDefault ( rosettaType , rosettaType ) ;
122124 }
123125
126+ /**
127+ * Maps an RType to its Python type representation.
128+ * - If it's a Rosetta basic type, returns the mapped Python basic type.
129+ * - Otherwise returns a qualified name "namespace.TypeName".
130+ * - For enums, appends ".TypeName" to reference the enum class within its module/package.
131+ *
132+ * @param rt Rosetta type (may be null)
133+ * @return Python type name (possibly qualified), or null if input is null
134+ */
124135 public static String toPythonType (RType rt ) {
125- if (rt == null )
126- return null ;
127- var pythonType = toPythonBasicTypeInnerFunction (rt .getName ());
128- if (pythonType == null ) {
129- String rtName = rt .getName ();
130- pythonType = rt .getNamespace ().toString () + "." + rtName ;
131- pythonType = (rt instanceof REnumType ) ? pythonType + "." + rtName : pythonType ;
132- }
133- return pythonType ;
136+ if (rt == null ) return null ;
137+
138+ // Prefer basic mapping when available
139+ String basic = ROSETTA_TO_PY_BASIC .get (rt .getName ());
140+ if (basic != null ) return basic ;
141+
142+ String rtName = rt .getName ();
143+ String namespace = rt .getNamespace () != null ? rt .getNamespace ().toString () : "" ;
144+ String qualified = namespace .isEmpty () ? rtName : namespace + "." + rtName ;
145+
146+ // For enums, retain original behavior: append ".<TypeName>"
147+ return (rt instanceof REnumType ) ? qualified + "." + rtName : qualified ;
134148 }
149+
150+ /**
151+ * Checks whether a Rosetta type name corresponds to a Python basic type.
152+ *
153+ * @param rtName Rosetta type name (may be null)
154+ * @return true if the name maps to a Python basic type; false otherwise
155+ */
135156 public static boolean isRosettaBasicType (String rtName ) {
136- return ( toPythonBasicTypeInnerFunction ( rtName ) != null );
157+ return rtName != null && ROSETTA_TO_PY_BASIC . containsKey ( rtName );
137158 }
159+
160+ /**
161+ * Checks whether a Rosetta RType corresponds to a Python basic type.
162+ *
163+ * @param rt Rosetta type (may be null)
164+ * @return true if the type maps to a Python basic type; false otherwise
165+ */
138166 public static boolean isRosettaBasicType (RType rt ) {
139- return ( toPythonBasicTypeInnerFunction ( rt . getName ()) != null );
167+ return rt != null && isRosettaBasicType ( rt . getName () );
140168 }
169+
170+ /**
171+ * Safely checks if an attribute's type maps to a Python basic type.
172+ * Defensive null checks avoid NullPointerExceptions on partially-populated models.
173+ *
174+ * @param ra Rosetta attribute (may be null)
175+ * @return true if the attribute's type maps to a Python basic type; false otherwise
176+ */
141177 public static boolean isRosettaBasicType (RAttribute ra ) {
142- if (ra == null ) {
143- return false ;
144- }
178+ if (ra == null || ra .getRMetaAnnotatedType () == null ) return false ;
145179 RType rt = ra .getRMetaAnnotatedType ().getRType ();
146- return ( rt != null ) ? isRosettaBasicType (rt .getName ()) : false ;
180+ return rt != null && isRosettaBasicType (rt .getName ());
147181 }
148- public static boolean isPythonBasicType (final String pythonType ) {
149- // Check if the given type is in the set of Python types
150- return PYTHON_TYPES .contains (pythonType );
182+
183+ /**
184+ * Checks whether a Python type name is one of the generator's recognized basic types.
185+ *
186+ * @param pythonType Python type name (may be null)
187+ * @return true if the type is recognized as a basic Python type by the generator; false otherwise
188+ */
189+ public static boolean isPythonBasicType (String pythonType ) {
190+ return pythonType != null && PYTHON_TYPES .contains (pythonType );
151191 }
152- }
192+ }
0 commit comments