diff --git a/src/enum_generator.py b/src/enum_generator.py new file mode 100644 index 0000000..5dc43b2 --- /dev/null +++ b/src/enum_generator.py @@ -0,0 +1,129 @@ +import re +from typing import List + +from src.java_model import EnumClass, indent_lvl1, indent_lvl2, indent_lvl3 +from src.header_generator import set_package + + +def to_java_constant(value: str) -> str: + value = re.sub(r"[^A-Za-z0-9]", "_", value) # delimiters a-a -> A_A + value = re.sub(r"([a-z])[_]?([A-Z])([A-Z])([a-z])", r"\1_\2_\3\4", value) # aBCd / a_BCd-> a_B_CD + value = re.sub(r"([a-z])([A-Z])", r"\1_\2", value) # aA -> A_A + value = re.sub(r"([A-Za-z])([0-9])", r"\1_\2", value) # a9 / A9 -> a_9 + value = re.sub(r"([0-9])([A-Za-z])", r"\1_\2", value) # 9a / 9A -> 9_A + + return value.upper() + + +def generate_enum_class(enum_class: EnumClass, package: str) -> str: + enum_body = [ + set_package(package), + "", + f"import java.util.HashMap;", + f"import java.util.Map;", + f"" + ] + + enum_body.extend(_get_javadoc(enum_class.description)) + + enum_body.append(f"public enum {enum_class.name} {{") + enum_body.extend(_get_constants(enum_class.values)) + + enum_body.append(f"") + enum_body.append( + f"{indent_lvl1}private final static Map CONSTANTS = new HashMap();") + enum_body.append(f"") + enum_body.extend(_get_static_method(enum_class.name)) + + enum_body.append(f"") + enum_body.append(f"{indent_lvl1}private final String value;") + enum_body.append(f"") + enum_body.extend(_get_constructor(enum_class.name)) + + enum_body.append(f"") + enum_body.extend(_get_from_value_method(enum_class.name)) + + enum_body.append(f"") + enum_body.extend(_get_to_string_method()) + + enum_body.append(f"") + enum_body.extend(_get_value_method()) + enum_body.append("}") + enum_body.append(f"") + + return "\n".join(enum_body) + + +def _get_javadoc(description: str) -> List[str]: + javadoc = [""] + if description is not None: + javadoc = [ + "", + "/**", + f" * {description}", + " */" + ] + return javadoc + + +def _get_constants(constants: List[str]) -> List[str]: + values = [] + for i, value in enumerate(constants): + line_end = ";" if i == (len(constants) - 1) else "," + values.append( + f'{indent_lvl1}{to_java_constant(value)}("{value}"){line_end}' + ) + return values + + +def _get_static_method(class_name: str) -> List[str]: + body = [ + f"{indent_lvl1}static {{", + f"{indent_lvl2}for ({class_name} c : values()) {{", + f"{indent_lvl3}CONSTANTS.put(c.value, c);", + f"{indent_lvl2}}}", + f"{indent_lvl1}}}" + ] + return body + + +def _get_constructor(class_name: str) -> List[str]: + body = [ + f"{indent_lvl1}{class_name}(String value) {{", + f"{indent_lvl2}this.value = value;", + f"{indent_lvl1}}}" + ] + return body + + +def _get_from_value_method(class_name: str) -> List[str]: + body = [ + f"{indent_lvl1}public static {class_name} fromValue(String value) {{", + f"{indent_lvl2}{class_name} constant = CONSTANTS.get(value);", + f"{indent_lvl2}if (constant == null) {{", + f"{indent_lvl3}throw new IllegalArgumentException(value);", + f"{indent_lvl2}}} else {{", + f"{indent_lvl3}return constant;", + f"{indent_lvl2}}}", + f"{indent_lvl1}}}" + ] + return body + + +def _get_to_string_method() -> List[str]: + body = [ + f"{indent_lvl1}@Override", + f"{indent_lvl1}public String toString() {{", + f"{indent_lvl2}return this.value;", + f"{indent_lvl1}}}" + ] + return body + + +def _get_value_method() -> List[str]: + body = [ + f"{indent_lvl1}public String value() {{", + f"{indent_lvl2}return this.value;", + f"{indent_lvl1}}}" + ] + return body diff --git a/src/header_generator.py b/src/header_generator.py index 416b478..ba1487f 100644 --- a/src/header_generator.py +++ b/src/header_generator.py @@ -2,7 +2,7 @@ def set_package(package: str) -> str: - PACKAGE_REGEX = r"^(?:[a-z_][a-z0-9_]*)(?:\.(?:[a-z_][a-z0-9_]*))*$" + PACKAGE_REGEX = r"^(?:[A-Za-z_][A-Za-z0-9_]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_]*))*$" if not re.match(PACKAGE_REGEX, package): raise ValueError(f"Invalid package: '{package}'") - return f"package {package}" + return f"package {package};" diff --git a/src/java_method_generator.py b/src/java_method_generator.py new file mode 100644 index 0000000..5350fe3 --- /dev/null +++ b/src/java_method_generator.py @@ -0,0 +1,160 @@ +import re +from typing import List + +from src.java_model import JAVA_KEYWORDS, JAVA_BUILTIN_TYPES, JAVA_LITERALS, indent_lvl1, indent_lvl2, indent_lvl3, \ + return_indent, Field + + +def generate_fields_block(fields: List[Field]) -> str: + declaration = [] + for field in fields: + declaration.append(generate_field_declaration(field)) + + return "\n".join(declaration) + + +def generate_field_declaration(field: Field) -> str: + _validate_java_field_name(field.name) + + declaration = [ + _render_javadoc(field), + f"{indent_lvl1}private {field.type} {field.name};" + ] + return "\n".join(declaration) + + +def _render_javadoc(field: Field) -> str: + if field.description is None: + return "" + + return "\n".join([ + "", + f"{indent_lvl1}/**", + f"{indent_lvl1} * {field.description}", + f"{indent_lvl1} */" + ]) + + +def generate_getters_and_setters(fields: List[Field]) -> str: + methods = [] + for field in fields: + methods.append(generate_getter(field)) + methods.append(generate_setter(field)) + + return "\n\n".join(methods) + + +def generate_getter(field: Field) -> str: + getter_name = _build_getter_name(field.name) + + getter = [ + "", + f"{indent_lvl1}public {field.type} {getter_name}() {{", + f"{indent_lvl2}return {field.name};", + f"{indent_lvl1}}}" + ] + return "\n".join(getter) + + +def generate_setter(field: Field) -> str: + setter_name = _build_setter_name(field.name) + + setter = [ + "", + f"{indent_lvl1}public void {setter_name}({field.type} {field.name}) {{", + f"{indent_lvl2}this.{field.name} = {field.name};", + f"{indent_lvl1}}}" + ] + return "\n".join(setter) + + +def generate_equals(class_name: str, fields: List[Field]) -> str: + equals = [ + "", + f"{indent_lvl1}@Override", + f"{indent_lvl1}public boolean equals(Object obj) {{", + + f"{indent_lvl2}if (this == obj)", + f"{indent_lvl3}return true;", + + f"{indent_lvl2}if (!(obj instanceof {class_name}))", + f"{indent_lvl3}return false;", + + f"{indent_lvl2}{class_name} that = ({class_name}) obj;", + + _render_equals_return_statement(fields), + f"{indent_lvl1}}}" + ] + + return "\n".join(equals) + + +def _render_equals_return_statement(fields: List[Field]) -> str: + return_statement = [] + for i, field in enumerate(fields): + getter_name = _build_getter_name(field.name) + end_line = ";" if i == (len(fields) - 1) else "" + if i == 0: + return_statement.append(f"{indent_lvl2}return Objects.equals({getter_name}(), that.{getter_name}()){end_line}") + else: + return_statement.append( + f"{indent_lvl2}{return_indent}&& Objects.equals({getter_name}(), that.{getter_name}()){end_line}") + return "\n".join(return_statement) + + +def generate_hash_code(fields: List[Field]) -> str: + hash_code = [ + "", + f"{indent_lvl1}@Override", + f"{indent_lvl1}public int hashCode() {{", + _render_hashcode_return_statement(fields), + f"{indent_lvl1}}}" + ] + + return "\n".join(hash_code) + + +def _render_hashcode_return_statement(fields: List[Field]) -> str: + if len(fields) == 1: + return _render_hashcode_return_statement_single_field(fields) + + return _render_hashcode_return_statement_multiple_field(fields) + + +def _render_hashcode_return_statement_single_field(fields: List[Field]) -> str: + field_name = fields[0].name + getter_name = _build_getter_name(field_name) + return f"{indent_lvl2}return Objects.hash({getter_name}());" + + +def _render_hashcode_return_statement_multiple_field(fields: List[Field]) -> str: + return_statement = [f"{indent_lvl2}return Objects.hash("] + for index, field in enumerate(fields): + getter_name = _build_getter_name(field.name) + comma = "," if index < (len(fields) - 1) else "" + return_statement.append(f"{indent_lvl2}{return_indent}{getter_name}(){comma}") + + return_statement.append(f"{indent_lvl2});") + + return "\n".join(return_statement) + + +def _build_getter_name(field_name: str) -> str: + _validate_java_field_name(field_name) + return "get" + field_name[0].upper() + field_name[1:] + + +def _build_setter_name(field_name: str) -> str: + _validate_java_field_name(field_name) + return "set" + field_name[0].upper() + field_name[1:] + + +def _validate_java_field_name(name: str) -> None: + if not name: + raise ValueError("Field name cannot be empty") + + if name in JAVA_KEYWORDS | JAVA_BUILTIN_TYPES | JAVA_LITERALS: + raise ValueError(f"'{name}' is a Java reserved keyword") + + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): + raise ValueError(f"Invalid Java identifier: '{name}'") diff --git a/src/java_model.py b/src/java_model.py new file mode 100644 index 0000000..7991cd4 --- /dev/null +++ b/src/java_model.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from typing import List, Optional + +JAVA_KEYWORDS = { + "abstract", "assert", "boolean", "break", "byte", "case", "catch", + "char", "class", "const", "continue", "default", "do", "double", + "else", "enum", "extends", "final", "finally", "float", "for", + "goto", "if", "implements", "import", "instanceof", "int", + "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", + "super", "switch", "synchronized", "this", "throw", "throws", + "transient", "try", "void", "volatile", "while" +} +JAVA_BUILTIN_TYPES = { + "Boolean", "Byte", "Character", "Double", "Float", "Integer", "List", "Long", "Short", + "Class", "Object", "String", "Void" +} +JAVA_LITERALS = { + "null", "true", "false" +} +indent_lvl1 = " " * 4 +indent_lvl2 = indent_lvl1 * 2 +indent_lvl3 = indent_lvl1 * 3 +return_indent = " " + + +@dataclass +class EnumClass: + name: str + values: List[str] + description: Optional[str] = None + + +@dataclass +class Field: + name: str + type: str + description: Optional[str] = None diff --git a/src/method_generator.py b/src/method_generator.py deleted file mode 100644 index 6e68abb..0000000 --- a/src/method_generator.py +++ /dev/null @@ -1,189 +0,0 @@ -import re -from dataclasses import dataclass -from typing import List, Optional - -JAVA_KEYWORDS = { - "abstract", "assert", "boolean", "break", "byte", "case", "catch", - "char", "class", "const", "continue", "default", "do", "double", - "else", "enum", "extends", "final", "finally", "float", "for", - "goto", "if", "implements", "import", "instanceof", "int", - "interface", "long", "native", "new", "package", "private", - "protected", "public", "return", "short", "static", "strictfp", - "super", "switch", "synchronized", "this", "throw", "throws", - "transient", "try", "void", "volatile", "while" -} - -JAVA_BUILTIN_TYPES = { - "Boolean", "Byte", "Character", "Double", "Float", "Integer", "List", "Long", "Short", - "Class", "Object", "String", "Void" -} - -JAVA_LITERALS = { - "null", "true", "false" -} - -indent_lvl1 = " " -indent_lvl2 = indent_lvl1 * 2 -indent_lvl3 = indent_lvl1 * 3 -return_indent = " " - - -@dataclass -class Field: - name: str - type: str - description: Optional[str] = None - - -def generate_fields_block(fields: List[Field]) -> str: - declaration = [] - for field in fields: - declaration.append(generate_field_declaration(field)) - - return "\n".join(declaration) - - -def generate_field_declaration(field: Field) -> str: - _validate_java_identifier(field.name) - if field.description: - javadoc = "\n".join([ - "", - f"{indent_lvl1}/**", - f"{indent_lvl1} * {field.description}", - f"{indent_lvl1} */" - ]) - else: - javadoc = "" - declaration = [ - javadoc, - f"{indent_lvl1}private {field.type} {field.name};" - ] - return "\n".join(declaration) - - -def generate_getters_and_setters(fields: List[Field]) -> str: - methods = [] - for field in fields: - methods.append(generate_getter(field)) - methods.append(generate_setter(field)) - - return "\n\n".join(methods) - - -def generate_getter(field: Field) -> str: - field_name = field.name - field_type = field.type - - _validate_java_identifier(field_name) - - getter_name = _get_getter_name(field) - - getter = [ - "", - f"{indent_lvl1}public {field_type} {getter_name}() {{", - f"{indent_lvl2}return {field_name};", - f"{indent_lvl1}}}" - ] - getter = "\n".join(getter) - - return getter - - -def generate_setter(field: Field) -> str: - field_name = field.name - field_type = field.type - - _validate_java_identifier(field_name) - - setter_name = _get_setter_name(field_name) - - setter = [ - "", - f"{indent_lvl1}public void {setter_name}({field_type} {field_name}) {{", - f"{indent_lvl2}this.{field_name} = {field_name};", - f"{indent_lvl1}}}" - ] - setter = "\n".join(setter) - return setter - - -def generate_equals(class_name: str, fields: List[Field]) -> str: - equals = [ - "", - f"{indent_lvl1}@Override", - f"{indent_lvl1}public boolean equals(Object obj) {{", - - f"{indent_lvl2}if (this == obj)", - f"{indent_lvl3}return true;", - - f"{indent_lvl2}if (!(obj instanceof {class_name}))", - f"{indent_lvl3}return false;", - - f"{indent_lvl2}{class_name} that = ({class_name}) obj;" - ] - - for i, field in enumerate(fields): - _validate_java_identifier(field.name) - getter_name = _get_getter_name(field) - semicolon = ";" if i == (len(fields) - 1) else "" - if i == 0: - equals.append(f"{indent_lvl2}return Objects.equals({getter_name}(), that.{getter_name}()){semicolon}") - else: - equals.append(f"{indent_lvl2}{return_indent}&& Objects.equals({getter_name}(), that.{getter_name}()){semicolon}") - equals.append(f"{indent_lvl1}}}") - - return "\n".join(equals) - - -def generate_hash_code(fields: List[Field]) -> str: - hash_code = [ - "", - f"{indent_lvl1}@Override", - f"{indent_lvl1}public int hashCode() {{" - ] - - for i, field in enumerate(fields): - _validate_java_identifier(field.name) - hash_code.extend(_get_return_hash_lines(fields, index=i)) - - hash_code.append(f"{indent_lvl1}}}") - - return "\n".join(hash_code) - - -def _get_return_hash_lines(fields, index) -> List[str]: - field = fields[index] - getter_name = _get_getter_name(field) - comma = "," if index < (len(fields) - 1) else "" - another_hash_line = f"{indent_lvl2}{return_indent}{getter_name}(){comma}" - if len(fields) == 1: - return [f"{indent_lvl2}return Objects.hash({getter_name}());"] - if len(fields) > 1 and index == 0: - return [f"{indent_lvl2}return Objects.hash(", - f"{another_hash_line}"] - if len(fields) > 1 and index == (len(fields) - 1): - return [f"{another_hash_line}", - f"{indent_lvl2});"] - return [another_hash_line] - - -def _get_getter_name(field): - getter_name = "get" + field.name[0].upper() + field.name[1:] - return getter_name - - -def _get_setter_name(field_name): - setter_name = "set" + field_name[0].upper() + field_name[1:] - return setter_name - - -def _validate_java_identifier(name: str) -> None: - if not name: - raise ValueError("Field name cannot be empty") - - if name in JAVA_KEYWORDS | JAVA_BUILTIN_TYPES | JAVA_LITERALS: - raise ValueError(f"'{name}' is a Java reserved keyword") - - if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): - raise ValueError(f"Invalid Java identifier: '{name}'") - diff --git a/tests/enum_reference_data.py b/tests/enum_reference_data.py new file mode 100644 index 0000000..2a2d1e4 --- /dev/null +++ b/tests/enum_reference_data.py @@ -0,0 +1,102 @@ +from src.enum_generator import EnumClass + +enum_AttributeEnum = EnumClass( + name="AttributeEnum", + values=["Actual", "Target", "MinSet", "MaxSet"], + description="Type of attribute: Actual, Target, MinSet, MaxSet.") + +enum_CancelReservationStatusEnum = EnumClass( + name="CancelReservationStatusEnum", + values=["Accepted"]) + +expected_AttributeEnum = """package ocpp.msgDef.Enumerations; + +import java.util.HashMap; +import java.util.Map; + + +/** + * Type of attribute: Actual, Target, MinSet, MaxSet. + */ +public enum AttributeEnum { + ACTUAL("Actual"), + TARGET("Target"), + MIN_SET("MinSet"), + MAX_SET("MaxSet"); + + private final static Map CONSTANTS = new HashMap(); + + static { + for (AttributeEnum c : values()) { + CONSTANTS.put(c.value, c); + } + } + + private final String value; + + AttributeEnum(String value) { + this.value = value; + } + + public static AttributeEnum fromValue(String value) { + AttributeEnum constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + + @Override + public String toString() { + return this.value; + } + + public String value() { + return this.value; + } +} +""" + +expected_CancelReservationStatusEnum = """package ocpp.anotherDef.Enums; + +import java.util.HashMap; +import java.util.Map; + + +public enum CancelReservationStatusEnum { + ACCEPTED("Accepted"); + + private final static Map CONSTANTS = new HashMap(); + + static { + for (CancelReservationStatusEnum c : values()) { + CONSTANTS.put(c.value, c); + } + } + + private final String value; + + CancelReservationStatusEnum(String value) { + this.value = value; + } + + public static CancelReservationStatusEnum fromValue(String value) { + CancelReservationStatusEnum constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + + @Override + public String toString() { + return this.value; + } + + public String value() { + return this.value; + } +} +""" diff --git a/tests/reference_data.py b/tests/reference_data.py index c529088..c68668a 100644 --- a/tests/reference_data.py +++ b/tests/reference_data.py @@ -1,4 +1,4 @@ -from src.method_generator import Field +from src.java_model import Field field_exampleAttribute_int = Field(name="exampleAttribute", type="int", description="javadoc description") field_someName_String = Field(name="someName", type="String") @@ -32,4 +32,4 @@ expected_setCustomData_CustomObject = """ public void setCustomData(CustomObject customData) { this.customData = customData; - }""" \ No newline at end of file + }""" diff --git a/tests/test_enum_generator.py b/tests/test_enum_generator.py new file mode 100644 index 0000000..a6e6de4 --- /dev/null +++ b/tests/test_enum_generator.py @@ -0,0 +1,29 @@ +from src.enum_generator import to_java_constant, generate_enum_class +from tests.enum_reference_data import * + + +def test_to_java_constant(): + values_expected = { + "kit": "KIT", + "KIT": "KIT", + "kitKat": "KIT_KAT", + "KitKat": "KIT_KAT", + "kit-kat": "KIT_KAT", + "kit-Kat": "KIT_KAT", + "kit_kat": "KIT_KAT", + "kit_Kat": "KIT_KAT", + "kit9": "KIT_9", + "kit-9": "KIT_9", + "kit_9": "KIT_9", + "kit92": "KIT_92", + "KiT": "KI_T", + "KiTKat": "KI_T_KAT", + "KitKAT": "KIT_KAT" + } + for key in values_expected.keys(): + assert to_java_constant(key) == values_expected[key] + + +def test_generate_enum_class(): + assert generate_enum_class(enum_AttributeEnum, "ocpp.msgDef.Enumerations") == expected_AttributeEnum + assert generate_enum_class(enum_CancelReservationStatusEnum, "ocpp.anotherDef.Enums") == expected_CancelReservationStatusEnum diff --git a/tests/test_header_generator.py b/tests/test_header_generator.py index 5347846..4bba937 100644 --- a/tests/test_header_generator.py +++ b/tests/test_header_generator.py @@ -5,13 +5,17 @@ def test_set_package(): example = "org.example.hyphenated_name" - expected = f"package {example}" + expected = f"package {example};" assert set_package(example) == expected example_2 = "com.example._123name" - expected_2 = f"package {example_2}" + expected_2 = f"package {example_2};" assert set_package(example_2) == expected_2 + example_3 = "com.exAmple.Name" + expected_3 = f"package {example_3};" + assert set_package(example_3) == expected_3 + def test_set_package_illegal_name(): example = "org.example.hyphenated-name" diff --git a/tests/test_method_generator.py b/tests/test_java_method_generator.py similarity index 99% rename from tests/test_method_generator.py rename to tests/test_java_method_generator.py index bcac62b..d511bde 100644 --- a/tests/test_method_generator.py +++ b/tests/test_java_method_generator.py @@ -1,7 +1,7 @@ import pytest from tests.reference_data import * -from src.method_generator import * +from src.java_method_generator import * JAVA_KEYWORDS = { "abstract", "assert", "boolean", "break", "byte", "case", "catch",