|
| 1 | +"""Provide common functions shared among different Java code generation modules.""" |
| 2 | +import re |
| 3 | +from typing import List, cast, Optional |
| 4 | + |
| 5 | +from icontract import ensure, require |
| 6 | + |
| 7 | +from aas_core_codegen import intermediate |
| 8 | +from aas_core_codegen.common import Stripped, assert_never |
| 9 | +from aas_core_codegen.java import naming as java_naming |
| 10 | + |
| 11 | + |
| 12 | +@ensure(lambda result: result.startswith('"')) |
| 13 | +@ensure(lambda result: result.endswith('"')) |
| 14 | +def string_literal(text: str) -> Stripped: |
| 15 | + """Generate a Java string literal from the ``text``.""" |
| 16 | + escaped = [] # type: List[str] |
| 17 | + |
| 18 | + for character in text: |
| 19 | + if character == "\t": |
| 20 | + escaped.append("\\t") |
| 21 | + elif character == "\b": |
| 22 | + escaped.append("\\b") |
| 23 | + elif character == "\n": |
| 24 | + escaped.append("\\n") |
| 25 | + elif character == "\r": |
| 26 | + escaped.append("\\r") |
| 27 | + elif character == "\f": |
| 28 | + escaped.append("\\f") |
| 29 | + elif character == "'": |
| 30 | + escaped.append("\\'") |
| 31 | + elif character == '"': |
| 32 | + escaped.append('\\"') |
| 33 | + elif character == "\\": |
| 34 | + escaped.append("\\\\") |
| 35 | + else: |
| 36 | + escaped.append(character) |
| 37 | + |
| 38 | + return Stripped('"{}"'.format("".join(escaped))) |
| 39 | + |
| 40 | + |
| 41 | +def needs_escaping(text: str) -> bool: |
| 42 | + """Check whether the ``text`` contains a character that needs escaping.""" |
| 43 | + for character in text: |
| 44 | + if character == "\t": |
| 45 | + return True |
| 46 | + elif character == "\b": |
| 47 | + return True |
| 48 | + elif character == "\n": |
| 49 | + return True |
| 50 | + elif character == "\r": |
| 51 | + return True |
| 52 | + elif character == "\f": |
| 53 | + return True |
| 54 | + elif character == "'": |
| 55 | + return True |
| 56 | + elif character == '"': |
| 57 | + return True |
| 58 | + elif character == "\\": |
| 59 | + return True |
| 60 | + else: |
| 61 | + pass |
| 62 | + |
| 63 | + return False |
| 64 | + |
| 65 | + |
| 66 | +PRIMITIVE_TYPE_MAP = { |
| 67 | + intermediate.PrimitiveType.BOOL: Stripped("Boolean"), |
| 68 | + intermediate.PrimitiveType.INT: Stripped("Long"), |
| 69 | + intermediate.PrimitiveType.FLOAT: Stripped("Float"), |
| 70 | + intermediate.PrimitiveType.STR: Stripped("String"), |
| 71 | + intermediate.PrimitiveType.BYTEARRAY: Stripped("byte[]"), |
| 72 | +} |
| 73 | + |
| 74 | + |
| 75 | +# fmt: off |
| 76 | +@require( |
| 77 | + lambda our_type_qualifier: |
| 78 | + not (our_type_qualifier is not None) |
| 79 | + or not our_type_qualifier.endswith('.') |
| 80 | +) |
| 81 | +# fmt: on |
| 82 | +def generate_type( |
| 83 | + type_annotation: intermediate.TypeAnnotationUnion, |
| 84 | + our_type_qualifier: Optional[Stripped] = None, |
| 85 | +) -> Stripped: |
| 86 | + """ |
| 87 | + Generate the Java type for the given type annotation. |
| 88 | +
|
| 89 | + ``our_type_prefix`` is appended to all our types, if specified. |
| 90 | + """ |
| 91 | + our_type_prefix = "" if our_type_qualifier is None else f"{our_type_qualifier}." |
| 92 | + # BEFORE-RELEASE (empwilli, 2023-12-14): test in isolation |
| 93 | + if isinstance(type_annotation, intermediate.PrimitiveTypeAnnotation): |
| 94 | + return PRIMITIVE_TYPE_MAP[type_annotation.a_type] |
| 95 | + |
| 96 | + elif isinstance(type_annotation, intermediate.OurTypeAnnotation): |
| 97 | + our_type = type_annotation.our_type |
| 98 | + |
| 99 | + if isinstance(our_type, intermediate.Enumeration): |
| 100 | + return Stripped( |
| 101 | + our_type_prefix + java_naming.enum_name(type_annotation.our_type.name) |
| 102 | + ) |
| 103 | + |
| 104 | + elif isinstance(our_type, intermediate.ConstrainedPrimitive): |
| 105 | + return PRIMITIVE_TYPE_MAP[our_type.constrainee] |
| 106 | + |
| 107 | + elif isinstance(our_type, intermediate.Class): |
| 108 | + # NOTE (empwilli, 2023-12-14): |
| 109 | + # We want to allow custom enhancements and wrappings around |
| 110 | + # our model classes. Therefore, we always operate over Java interfaces |
| 111 | + # instead of concrete classes, even if the class is a concrete one and |
| 112 | + # has no concrete descendants. |
| 113 | + |
| 114 | + return Stripped(our_type_prefix + java_naming.interface_name(our_type.name)) |
| 115 | + |
| 116 | + elif isinstance(type_annotation, intermediate.ListTypeAnnotation): |
| 117 | + item_type = generate_type( |
| 118 | + type_annotation=type_annotation.items, our_type_qualifier=our_type_qualifier |
| 119 | + ) |
| 120 | + |
| 121 | + return Stripped(f"List<{item_type}>") |
| 122 | + |
| 123 | + elif isinstance(type_annotation, intermediate.OptionalTypeAnnotation): |
| 124 | + value = generate_type( |
| 125 | + type_annotation=type_annotation.value, our_type_qualifier=our_type_qualifier |
| 126 | + ) |
| 127 | + return Stripped(f"Optional<{value}>") |
| 128 | + |
| 129 | + else: |
| 130 | + assert_never(type_annotation) |
| 131 | + |
| 132 | + raise AssertionError("Should not have gotten here") |
| 133 | + |
| 134 | + |
| 135 | +INDENT = " " |
| 136 | +INDENT2 = INDENT * 2 |
| 137 | +INDENT3 = INDENT * 3 |
| 138 | +INDENT4 = INDENT * 4 |
| 139 | +INDENT5 = INDENT * 5 |
| 140 | +INDENT6 = INDENT * 6 |
| 141 | + |
| 142 | + |
| 143 | +INTERFACE_PKG = "model" |
| 144 | +CLASS_PKG = "impl" |
| 145 | +ENUM_PKG = "enums" |
| 146 | + |
| 147 | + |
| 148 | +def interface_package_path(name: Stripped) -> Stripped: |
| 149 | + """Create the package path for an interface file.""" |
| 150 | + return Stripped(f"{INTERFACE_PKG}/{name}.java") |
| 151 | + |
| 152 | + |
| 153 | +def class_package_path(name: Stripped) -> Stripped: |
| 154 | + """Create the package path for an interface file.""" |
| 155 | + return Stripped(f"{CLASS_PKG}/{name}.java") |
| 156 | + |
| 157 | + |
| 158 | +def enum_package_path(name: Stripped) -> Stripped: |
| 159 | + """Create the package path for an interface file.""" |
| 160 | + return Stripped(f"{ENUM_PKG}/{name}.java") |
| 161 | + |
| 162 | + |
| 163 | +# noinspection RegExpSimplifiable |
| 164 | +PACKAGE_IDENTIFIER_RE = re.compile(r"[a-z][a-z_0-9]*(\.[a-z][a-z_0-9]*)*") |
| 165 | + |
| 166 | + |
| 167 | +class PackageIdentifier(str): |
| 168 | + """Capture a package identifier.""" |
| 169 | + |
| 170 | + @require(lambda identifier: PACKAGE_IDENTIFIER_RE.fullmatch(identifier)) |
| 171 | + def __new__(cls, identifier: str) -> "PackageIdentifier": |
| 172 | + return cast(PackageIdentifier, identifier) |
| 173 | + |
| 174 | + |
| 175 | +WARNING = Stripped( |
| 176 | + """\ |
| 177 | +/* |
| 178 | + * This code has been automatically generated by aas-core-codegen. |
| 179 | + * Do NOT edit or append. |
| 180 | + */""" |
| 181 | +) |
| 182 | + |
| 183 | + |
| 184 | +class JavaFile: |
| 185 | + """Representation of a Java source file.""" |
| 186 | + |
| 187 | + # fmt: off |
| 188 | + @require(lambda name, content: (len(name) > 0) and (len(content) > 0)) |
| 189 | + @require(lambda content: content.endswith('\n'), "Trailing newline mandatory for valid end-of-files") |
| 190 | + # fmt: on |
| 191 | + def __init__( |
| 192 | + self, |
| 193 | + name: str, |
| 194 | + content: str, |
| 195 | + ): |
| 196 | + self.name = name |
| 197 | + self.content = content |
0 commit comments