diff options
Diffstat (limited to 'generator/plugins/rust/rust_commons.py')
-rw-r--r-- | generator/plugins/rust/rust_commons.py | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/generator/plugins/rust/rust_commons.py b/generator/plugins/rust/rust_commons.py new file mode 100644 index 0000000..4022e27 --- /dev/null +++ b/generator/plugins/rust/rust_commons.py @@ -0,0 +1,681 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Dict, List, Optional, Tuple, Union + +from generator import model + +from .rust_lang_utils import ( + get_parts, + indent_lines, + lines_to_doc_comments, + to_snake_case, + to_upper_camel_case, +) + +TypesWithId = Union[ + model.Request, + model.TypeAlias, + model.Enum, + model.Structure, + model.Notification, + model.LiteralType, + model.ReferenceType, + model.ReferenceMapKeyType, + model.Property, + model.EnumItem, +] + + +class TypeData: + def __init__(self) -> None: + self._id_data: Dict[ + str, + Tuple[ + str, + TypesWithId, + List[str], + ], + ] = {} + + def add_type_info( + self, + type_def: TypesWithId, + type_name: str, + impl: List[str], + ) -> None: + if type_def.id_ in self._id_data: + raise Exception(f"Duplicate id {type_def.id_} for type {type_name}") + self._id_data[type_def.id_] = (type_name, type_def, impl) + + def has_id( + self, + type_def: TypesWithId, + ) -> bool: + return type_def.id_ in self._id_data + + def has_name(self, type_name: str) -> bool: + return any(type_name == name for name, _, _ in self._id_data.values()) + + def get_by_name(self, type_name: str) -> List[TypesWithId]: + return [type_name == name for name, _, _ in self._id_data.values()] + + def get_lines(self): + lines = [] + for _, _, impl in self._id_data.values(): + lines += impl + ["", ""] + return lines + + +def generate_custom_enum(type_data: TypeData) -> None: + type_data.add_type_info( + model.ReferenceType(kind="reference", name="CustomStringEnum"), + "CustomStringEnum", + [ + "/// This type allows extending any string enum to support custom values.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum CustomStringEnum<T> {", + " /// The value is one of the known enum values.", + " Known(T),", + " /// The value is custom.", + " Custom(String),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="CustomIntEnum"), + "CustomIntEnum", + [ + "/// This type allows extending any integer enum to support custom values.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum CustomIntEnum<T> {", + " /// The value is one of the known enum values.", + " Known(T),", + " /// The value is custom.", + " Custom(i32),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR2"), + "OR2", + [ + "/// This allows a field to have two types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR2<T, U> {", + " T(T),", + " U(U),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR3"), + "OR3", + [ + "/// This allows a field to have three types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR3<T, U, V> {", + " T(T),", + " U(U),", + " V(V),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR4"), + "OR4", + [ + "/// This allows a field to have four types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR4<T, U, V, W> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR5"), + "OR5", + [ + "/// This allows a field to have five types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR5<T, U, V, W, X> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR6"), + "OR6", + [ + "/// This allows a field to have six types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR6<T, U, V, W, X, Y> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + " Y(Y),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="OR7"), + "OR7", + [ + "/// This allows a field to have seven types.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum OR7<T, U, V, W, X, Y, Z> {", + " T(T),", + " U(U),", + " V(V),", + " W(W),", + " X(X),", + " Y(Y),", + " Z(Z),", + "}", + "", + ], + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPNull"), + "LSPNull", + [ + "/// This allows a field to always have null or empty value.", + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum LSPNull {", + " None,", + "}", + "", + ], + ) + + +def get_definition( + name: str, spec: model.LSPModel +) -> Optional[Union[model.TypeAlias, model.Structure]]: + for type_def in spec.typeAliases + spec.structures: + if type_def.name == name: + return type_def + return None + + +def generate_special_types(model: model.LSPModel, types: TypeData) -> None: + special_types = [ + get_definition("LSPAny", model), + get_definition("LSPObject", model), + get_definition("LSPArray", model), + get_definition("SelectionRange", model), + ] + + for type_def in special_types: + if type_def: + doc = ( + type_def.documentation.splitlines(keepends=False) + if type_def.documentation + else [] + ) + lines = lines_to_doc_comments(doc) + lines += generate_extras(type_def) + + if type_def.name == "LSPAny": + lines += [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + "pub enum LSPAny {", + " String(String),", + " Integer(i32),", + " UInteger(u32),", + " Decimal(Decimal),", + " Boolean(bool),", + " Object(LSPObject),", + " Array(LSPArray),", + " Null,", + "}", + ] + elif type_def.name == "LSPObject": + lines += ["type LSPObject = serde_json::Value;"] + elif type_def.name == "LSPArray": + lines += ["type LSPArray = Vec<LSPAny>;"] + elif type_def.name == "SelectionRange": + lines += [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "pub struct SelectionRange {", + ] + for property in type_def.properties: + doc = ( + property.documentation.splitlines(keepends=False) + if property.documentation + else [] + ) + lines += lines_to_doc_comments(doc) + lines += generate_extras(property) + prop_name = to_snake_case(property.name) + prop_type = get_type_name( + property.type, types, model, property.optional + ) + if "SelectionRange" in prop_type: + prop_type = prop_type.replace( + "SelectionRange", "Box<SelectionRange>" + ) + lines += [f"pub {prop_name}: {prop_type},"] + lines += [""] + lines += ["}"] + lines += [""] + types.add_type_info(type_def, type_def.name, lines) + + +def fix_lsp_method_name(name: str) -> str: + if name.startswith("$/"): + name = name[2:] + return to_upper_camel_case(name.replace("/", "_")) + + +def generate_special_enum(enum_name: str, items: List[str]) -> Dict[str, List[str]]: + lines = [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + f"pub enum {enum_name}" "{", + ] + for item in items: + lines += indent_lines( + [ + f'#[serde(rename = "{item}")]', + f"{fix_lsp_method_name(item)},", + ] + ) + lines += ["}"] + return lines + + +def generate_extra_types(spec: model.LSPModel, type_data: TypeData) -> None: + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPRequestMethods"), + "LSPRequestMethods", + generate_special_enum("LSPRequestMethods", [m.method for m in spec.requests]), + ) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="LSPNotificationMethods"), + "LSPNotificationMethods", + generate_special_enum( + "LSPNotificationMethods", [m.method for m in spec.notifications] + ), + ) + + direction = set([m.messageDirection for m in (spec.requests + spec.notifications)]) + type_data.add_type_info( + model.ReferenceType(kind="reference", name="MessageDirection"), + "MessageDirection", + generate_special_enum("MessageDirection", direction), + ) + + +def generate_commons( + model: model.LSPModel, type_data: TypeData +) -> Dict[str, List[str]]: + generate_custom_enum(type_data) + generate_special_types(model, type_data) + generate_extra_types(model, type_data) + + +def lsp_to_base_types(lsp_type: model.BaseType): + if lsp_type.name in ["string", "DocumentUri", "URI", "RegExp"]: + return "String" + elif lsp_type.name in ["decimal"]: + return "Decimal" + elif lsp_type.name in ["integer"]: + return "i32" + elif lsp_type.name in ["uinteger"]: + return "u32" + elif lsp_type.name in ["boolean"]: + return "bool" + + # null should be handled by the caller as an Option<> type + raise ValueError(f"Unknown base type: {lsp_type.name}") + + +def _get_enum(name: str, spec: model.LSPModel) -> Optional[model.Enum]: + for enum in spec.enumerations: + if enum.name == name: + return enum + return None + + +def get_from_name( + name: str, spec: model.LSPModel +) -> Optional[Union[model.Structure, model.Enum, model.TypeAlias]]: + for some in spec.enumerations + spec.structures + spec.typeAliases: + if some.name == name: + return some + return None + + +def get_extended_properties( + struct_def: model.Structure, spec: model.LSPModel +) -> List[model.Property]: + properties = [p for p in struct_def.properties] + for t in struct_def.extends + struct_def.mixins: + if t.kind == "reference": + s = get_from_name(t.name, spec) + if s: + properties += [p for p in s.properties] + elif t.kind == "literal": + properties += [p for p in t.value.properties] + else: + raise ValueError(f"Unhandled extension type or mixin type: {t.kind}") + unique_props = [] + for p in properties: + if not any((p.name == u.name) for u in unique_props): + unique_props.append(p) + return sorted(unique_props, key=lambda p: p.name) + + +def _is_str_enum(enum_def: model.Enum) -> bool: + return all(isinstance(item.value, str) for item in enum_def.values) + + +def _is_int_enum(enum_def: model.Enum) -> bool: + return all(isinstance(item.value, int) for item in enum_def.values) + + +def generate_or_type( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + pass + + +def generate_and_type( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + pass + + +def get_type_name( + type_def: model.LSP_TYPE_SPEC, + types: TypeData, + spec: model.LSPModel, + optional: Optional[bool] = None, + name_context: Optional[str] = None, +) -> str: + if type_def.kind == "reference": + enum_def = _get_enum(type_def.name, spec) + if enum_def and enum_def.supportsCustomValues: + if _is_str_enum(enum_def): + name = f"CustomStringEnum<{enum_def.name}>" + elif _is_int_enum(enum_def): + name = f"CustomIntEnum<{enum_def.name}>" + else: + name = type_def.name + elif type_def.kind == "array": + name = f"Vec<{get_type_name(type_def.element, types, spec)}>" + elif type_def.kind == "map": + key_type = get_type_name(type_def.key, types, spec) + value_type = get_type_name(type_def.value, types, spec) + name = f"HashMap<{key_type}, {value_type}>" + elif type_def.kind == "base": + name = lsp_to_base_types(type_def) + elif type_def.kind == "or": + sub_set_items = [ + sub_spec + for sub_spec in type_def.items + if not (sub_spec.kind == "base" and sub_spec.name == "null") + ] + sub_types = [get_type_name(sub_spec, types, spec) for sub_spec in sub_set_items] + sub_types_str = ", ".join(sub_types) + if len(sub_types) >= 2: + name = f"OR{len(sub_types)}<{sub_types_str}>" + elif len(sub_types) == 1: + name = sub_types[0] + else: + raise ValueError( + f"OR type with more than out of range count of subtypes: {type_def}" + ) + optional = optional or is_special(type_def) + print(f"Option<{name}>" if optional else name) + elif type_def.kind == "literal": + name = generate_literal_struct_type(type_def, types, spec, name_context) + elif type_def.kind == "stringLiteral": + name = "String" + # This type in rust requires a custom deserializer that fails if the value is not + # one of the allowed values. This should be handled by the caller. This cannot be + # handled here because all this does is handle type names. + elif type_def.kind == "tuple": + optional = optional or is_special(type_def) + sub_set_items = [ + sub_spec + for sub_spec in type_def.items + if not (sub_spec.kind == "base" and sub_spec.name == "null") + ] + sub_types = [get_type_name(sub_spec, types, spec) for sub_spec in sub_set_items] + sub_types_str = ", ".join(sub_types) + if len(sub_types) >= 2: + name = f"({sub_types_str})" + elif len(sub_types) == 1: + name = sub_types[0] + else: + raise ValueError(f"Invalid number of items for tuple: {type_def}") + else: + raise ValueError(f"Unknown type kind: {type_def.kind}") + + return f"Option<{name}>" if optional else name + + +def is_special(type_def: model.LSP_TYPE_SPEC) -> bool: + if type_def.kind in ["or", "tuple"]: + for item in type_def.items: + if item.kind == "base" and item.name == "null": + return True + return False + + +def is_special_property(prop_def: model.Property) -> bool: + return is_special(prop_def.type) + + +def is_string_literal_property(prop_def: model.Property) -> bool: + return prop_def.type.kind == "stringLiteral" + + +def generate_literal_struct_name( + type_def: model.LiteralType, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> str: + ignore_list = ["Struct", "Type", "Kind", "Options", "Params", "Result", "Options"] + + initial_parts = ["Struct"] + if name_context: + initial_parts += get_parts(name_context) + + optional_props = [p for p in type_def.value.properties if p.optional] + required_props = [p for p in type_def.value.properties if not p.optional] + + required_parts = [] + for property in required_props: + for p in get_parts(property.name): + if p not in (ignore_list + required_parts): + required_parts.append(p) + + optional = ( + ["Options"] if len(optional_props) == len(type_def.value.properties) else [] + ) + + name_parts = initial_parts + name = to_upper_camel_case("_".join(name_parts)) + + all_ignore = all(n in ignore_list for n in name_parts) + if types.has_name(name) or all_ignore: + parts = [] + + for r in required_parts: + parts.append(r) + name = to_upper_camel_case("_".join(initial_parts + parts + optional)) + if not types.has_name(name): + return name + + for i in range(1, 100): + end = [f"{i}"] if i > 1 else [] + name = to_upper_camel_case( + "_".join(initial_parts + required_parts + optional + end) + ) + if not types.has_name(name): + return name + return name + + +def _get_doc(doc: Optional[str]) -> str: + if doc: + return lines_to_doc_comments(doc.splitlines(keepends=False)) + return [] + + +def generate_property( + prop_def: model.Property, types: TypeData, spec: model.LSPModel +) -> str: + prop_name = to_snake_case(prop_def.name) + prop_type = get_type_name( + prop_def.type, types, spec, prop_def.optional, prop_def.name + ) + optional = ( + [f'#[serde(skip_serializing_if = "Option::is_none")]'] + if is_special_property(prop_def) and not prop_def.optional + else [] + ) + + if prop_name in ["type"]: + prop_name = f"{prop_name}_" + if optional: + optional = [ + f'#[serde(rename = "{prop_def.name}", skip_serializing_if = "Option::is_none")]' + ] + else: + optional = [f'#[serde(rename = "{prop_def.name}")]'] + + return ( + _get_doc(prop_def.documentation) + + generate_extras(prop_def) + + optional + + [f"pub {prop_name}: {prop_type},"] + + [""] + ) + + +def get_message_type_name(type_def: Union[model.Notification, model.Request]) -> str: + name = fix_lsp_method_name(type_def.method) + if isinstance(type_def, model.Notification): + return f"{name}Notification" + return f"{name}Request" + + +def struct_wrapper( + type_def: Union[model.Structure, model.Notification, model.Request], + inner: List[str], +) -> List[str]: + if hasattr(type_def, "name"): + name = type_def.name + else: + name = get_message_type_name(type_def) + lines = ( + _get_doc(type_def.documentation) + + generate_extras(type_def) + + [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + '#[serde(rename_all = "camelCase")]', + f"pub struct {name}", + "{", + ] + ) + lines += indent_lines(inner) + lines += ["}", ""] + return lines + + +def type_alias_wrapper(type_def: model.TypeAlias, inner: List[str]) -> List[str]: + lines = ( + _get_doc(type_def.documentation) + + generate_extras(type_def) + + [ + "#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]", + "#[serde(untagged)]", + f"pub enum {type_def.name}", + "{", + ] + ) + lines += indent_lines(inner) + lines += ["}", ""] + return lines + + +def generate_literal_struct_type( + type_def: model.LiteralType, + types: TypeData, + spec: model.LSPModel, + name_context: Optional[str] = None, +) -> None: + if len(type_def.value.properties) == 0: + return "LSPObject" + + if types.has_id(type_def): + return type_def.name + + type_def.name = generate_literal_struct_name(type_def, types, spec, name_context) + + inner = [] + for prop_def in type_def.value.properties: + inner += generate_property(prop_def, types, spec) + + lines = struct_wrapper(type_def, inner) + types.add_type_info(type_def, type_def.name, lines) + return type_def.name + + +def generate_extras( + type_def: Union[ + model.Enum, model.EnumItem, model.Property, model.TypeAlias, model.Structure + ] +) -> List[str]: + extras = [] + if type_def.deprecated: + extras = ["#[deprecated]"] + elif type_def.proposed: + if type_def.since: + extras = [f'#[cfg(feature = "proposed", since = "{type_def.since}")]'] + else: + extras = [f'#[cfg(feature = "proposed")]'] + # else: + # if type_def.since: + # extras = [f'#[cfg(feature = "stable", since = "{type_def.since}")]'] + # else: + # extras = [f'#[cfg(feature = "stable")]'] + return extras |