Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some integer enum code generation for Rust. #166

Merged
merged 2 commits into from
Feb 16, 2023
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
10 changes: 10 additions & 0 deletions .github/actions/lint/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ runs:
- name: Check linting and formatting
run: python -m nox --session lint
shell: bash

- name: Rust Tool Chain setup
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt

- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@v1
with:
manifest-path: packages/rust/lsprotocol/Cargo.toml
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist
**/.pytest_cache
**/.vs
**/.mypy_cache
packages/rust/**/target/
19 changes: 18 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
"version": "0.2.0",
"configurations": [
{
"name": "Run Generator",
"name": "Run Generator (all)",
"type": "python",
"request": "launch",
"module": "generator",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Run Generator (select plugin)",
"type": "python",
"request": "launch",
"module": "generator",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["--plugin", "${input:plugin}"]
},
{
"name": "DON'T SELECT (test debug config)",
"type": "python",
Expand All @@ -25,5 +34,13 @@
"order": 4
}
}
],
"inputs": [
{
"id": "plugin",
"type": "pickString",
"description": "Select a plugin to debug",
"options": ["python", "rust"]
}
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"python.testing.pytestArgs": ["tests"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.extraPaths": ["packages/python", "tests/python/common"]
"python.analysis.extraPaths": ["packages/python", "tests/python/common"],
"rust-analyzer.linkedProjects": ["packages/rust/lsprotocol/Cargo.toml"]
}
2 changes: 2 additions & 0 deletions generator-plugins/rust/rust_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
37 changes: 37 additions & 0 deletions generator-plugins/rust/rust_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Dict, List

import generator.model as model

from .rust_lang_utils import to_upper_camel_case


def generate_int_enum(enum: model.Enum) -> List[str]:
is_int = all(isinstance(item.value, int) for item in enum.values)
if not is_int:
raise Exception("Enum is not an integer enum")

return [
f"#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]",
f"#[repr(i64)]",
f"pub enum {enum.name} " "{",
*[
f" {to_upper_camel_case(item.name)} = {item.value},"
for item in enum.values
],
"}",
]


def generate_enum(enum: model.Enum) -> List[str]:
is_int = all(isinstance(item.value, int) for item in enum.values)
lines = []
if is_int:
lines += generate_int_enum(enum)
return lines


def generate_enums(enums: List[model.Enum]) -> Dict[str, List[str]]:
return {enum.name: generate_enum(enum) for enum in enums}
15 changes: 15 additions & 0 deletions generator-plugins/rust/rust_file_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import List


def license_header() -> List[str]:
return [
"Copyright (c) Microsoft Corporation. All rights reserved.",
"Licensed under the MIT License.",
]


def package_description() -> List[str]:
return ["Language Server Protocol types for Rust generated from LSP specification."]
55 changes: 55 additions & 0 deletions generator-plugins/rust/rust_lang_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.


from typing import List


def lines_to_comments(lines: List[str]) -> List[str]:
return ["// " + line for line in lines]


def lines_to_doc_comments(lines: List[str]) -> List[str]:
return ["/// " + line for line in lines]


def lines_to_block_comment(lines: List[str]) -> List[str]:
return ["/*"] + lines + ["*/"]


def to_snake_case(name: str) -> str:
result = ""
for i, c in enumerate(name):
if i > 0 and c.isupper():
result += "_"
result += c.lower()
return result


def has_upper_case(name: str) -> bool:
return any(c.isupper() for c in name)


def is_snake_case(name: str) -> bool:
return (
not name.startswith("_")
and not name.endswith("_")
and ("_" in name)
and not has_upper_case(name)
)


def to_upper_camel_case(name: str) -> str:
if not is_snake_case(name):
name = to_snake_case(name)
return "".join([c.capitalize() for c in name.split("_")])


def to_camel_case(name: str) -> str:
if not is_snake_case(name):
name = to_snake_case(name)
parts = name.split("_")
if len(parts) > 1:
return parts[0] + "".join([c.capitalize() for c in parts[1:]])
else:
return parts[0]
34 changes: 33 additions & 1 deletion generator-plugins/rust/rust_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os
import pathlib
from typing import List

import generator.model as model

from .rust_enum import generate_enums
from .rust_file_header import license_header
from .rust_lang_utils import lines_to_comments

PACKAGE_DIR_NAME = "lsprotocol"


def generate_from_spec(spec: model.LSPModel, output_dir: str) -> None:
pass
code = generate_package_code(spec)
for file_name in code:
pathlib.Path(output_dir, PACKAGE_DIR_NAME, file_name).write_text(
code[file_name], encoding="utf-8"
)


def generate_package_code(spec: model.LSPModel) -> List[str]:
return {
"src/lib.rs": generate_lib_rs(spec),
}


def generate_lib_rs(spec: model.LSPModel) -> List[str]:
lines = lines_to_comments(license_header())
lines += ["use serde_repr::*;", ""]

enums = generate_enums(spec.enumerations)
for enum_name in enums:
lines += enums[enum_name]
lines += [""]

return "\n".join(lines)
34 changes: 22 additions & 12 deletions generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ def get_parser() -> argparse.ArgumentParser:
help="Path to a model JSON file. By default uses packaged model file.",
type=str,
)
parser.add_argument(
"--plugin",
"-p",
help="Name of a builtin plugin module. By default uses all plugins.",
type=str,
action="append",
)
return parser


Expand Down Expand Up @@ -71,18 +78,21 @@ def main(argv: Sequence[str]) -> None:
LOGGER.info("Validating model.")
jsonschema.validate(json_model, schema)

LOGGER.info("Finding plugins.")
plugin_root = pathlib.Path(__file__).parent.parent / "generator-plugins"
plugins = []
for item in plugin_root.iterdir():
if (
item.is_dir()
and (item / "__init__.py").exists()
and not item.name.startswith("_")
):
plugins.append(item.name)
LOGGER.info(f"Found plugins: {plugins}")
LOGGER.info("Starting code generation.")
plugins = args.plugin or []

if not plugins:
LOGGER.info("Finding plugins.")
plugin_root = pathlib.Path(__file__).parent.parent / "generator-plugins"

for item in plugin_root.iterdir():
if (
item.is_dir()
and (item / "__init__.py").exists()
and not item.name.startswith("_")
):
plugins.append(item.name)
LOGGER.info(f"Found plugins: {plugins}")
LOGGER.info("Starting code generation.")

for plugin in plugins:
LOGGER.info(f"Running plugin {plugin}.")
Expand Down
Loading