Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions src/enum_generator.py
Original file line number Diff line number Diff line change
@@ -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<String, {enum_class.name}> CONSTANTS = new HashMap<String, {enum_class.name}>();")
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
4 changes: 2 additions & 2 deletions src/header_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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};"
160 changes: 160 additions & 0 deletions src/java_method_generator.py
Original file line number Diff line number Diff line change
@@ -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}'")
38 changes: 38 additions & 0 deletions src/java_model.py
Original file line number Diff line number Diff line change
@@ -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
Loading