300 lines
8.1 KiB
Python
Executable File
300 lines
8.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
OpenRewrite Recipe Initialization Script
|
|
|
|
Generates boilerplate code for new OpenRewrite YAML recipes including:
|
|
- Recipe class file
|
|
- Test file
|
|
- Optional declarative YAML recipe
|
|
|
|
Usage:
|
|
python init_recipe.py --name MyRecipe --package com.example.rewrite --description "Recipe description"
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
def read_license_header():
|
|
"""Read license header from gradle/licenseHeader.txt if it exists."""
|
|
license_path = Path.cwd()
|
|
while license_path != license_path.parent:
|
|
license_file = license_path / "gradle" / "licenseHeader.txt"
|
|
if license_file.exists():
|
|
with open(license_file, 'r') as f:
|
|
content = f.read()
|
|
# Substitute ${year} with current year
|
|
content = content.replace("${year}", str(datetime.now().year))
|
|
return content + "\n"
|
|
license_path = license_path.parent
|
|
return ""
|
|
|
|
|
|
def to_snake_case(name):
|
|
"""Convert PascalCase to snake_case."""
|
|
result = []
|
|
for i, char in enumerate(name):
|
|
if char.isupper() and i > 0:
|
|
result.append('_')
|
|
result.append(char.lower())
|
|
return ''.join(result)
|
|
|
|
|
|
def generate_recipe_class(name, package, description, license_header):
|
|
"""Generate the recipe class file content."""
|
|
return f"""{license_header}package {package};
|
|
|
|
import org.openrewrite.*;
|
|
import org.openrewrite.yaml.YamlIsoVisitor;
|
|
import org.openrewrite.yaml.tree.Yaml;
|
|
|
|
public class {name} extends Recipe {{
|
|
|
|
@Override
|
|
public String getDisplayName() {{
|
|
return "{name}";
|
|
}}
|
|
|
|
@Override
|
|
public String getDescription() {{
|
|
return "{description}";
|
|
}}
|
|
|
|
@Override
|
|
public TreeVisitor<?, ExecutionContext> getVisitor() {{
|
|
return new YamlIsoVisitor<ExecutionContext>() {{
|
|
@Override
|
|
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
|
|
// TODO: Implement recipe logic
|
|
return super.visitMappingEntry(entry, ctx);
|
|
}}
|
|
}};
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
|
|
def generate_parameterized_recipe_class(name, package, description, license_header):
|
|
"""Generate a parameterized recipe class file content."""
|
|
return f"""{license_header}package {package};
|
|
|
|
import lombok.EqualsAndHashCode;
|
|
import lombok.Value;
|
|
import org.openrewrite.*;
|
|
import org.openrewrite.yaml.YamlIsoVisitor;
|
|
import org.openrewrite.yaml.tree.Yaml;
|
|
|
|
@Value
|
|
@EqualsAndHashCode(callSuper = false)
|
|
public class {name} extends Recipe {{
|
|
|
|
@Option(
|
|
displayName = "Parameter name",
|
|
description = "Description of the parameter",
|
|
example = "example-value"
|
|
)
|
|
String parameterName;
|
|
|
|
@Override
|
|
public String getDisplayName() {{
|
|
return "{name}";
|
|
}}
|
|
|
|
@Override
|
|
public String getDescription() {{
|
|
return "{description}";
|
|
}}
|
|
|
|
@Override
|
|
public TreeVisitor<?, ExecutionContext> getVisitor() {{
|
|
return new YamlIsoVisitor<ExecutionContext>() {{
|
|
@Override
|
|
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
|
|
// TODO: Implement recipe logic using parameterName
|
|
return super.visitMappingEntry(entry, ctx);
|
|
}}
|
|
}};
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
|
|
def generate_test_class(name, package, license_header):
|
|
"""Generate the test class file content."""
|
|
return f"""{license_header}package {package};
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.openrewrite.test.RecipeSpec;
|
|
import org.openrewrite.test.RewriteTest;
|
|
|
|
import static org.openrewrite.yaml.Assertions.yaml;
|
|
|
|
class {name}Test implements RewriteTest {{
|
|
|
|
@Override
|
|
public void defaults(RecipeSpec spec) {{
|
|
spec.recipe(new {name}());
|
|
}}
|
|
|
|
@Test
|
|
void basicTransformation() {{
|
|
rewriteRun(
|
|
yaml(
|
|
\"\"\"
|
|
# Before YAML
|
|
key: old-value
|
|
\"\"\",
|
|
\"\"\"
|
|
# After YAML
|
|
key: new-value
|
|
\"\"\"
|
|
)
|
|
);
|
|
}}
|
|
|
|
@Test
|
|
void doesNotChangeUnrelatedYaml() {{
|
|
rewriteRun(
|
|
yaml(
|
|
\"\"\"
|
|
unrelated: value
|
|
\"\"\"
|
|
)
|
|
);
|
|
}}
|
|
|
|
@Test
|
|
void handlesEdgeCases() {{
|
|
rewriteRun(
|
|
yaml(
|
|
\"\"\"
|
|
# Empty value
|
|
key:
|
|
# Null value
|
|
key2: null
|
|
\"\"\"
|
|
)
|
|
);
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
|
|
def generate_declarative_recipe(name, package, description):
|
|
"""Generate declarative YAML recipe content."""
|
|
return f"""---
|
|
type: specs.openrewrite.org/v1beta/recipe
|
|
name: {package}.{name}
|
|
displayName: {name}
|
|
description: {description}
|
|
recipeList:
|
|
- org.openrewrite.yaml.search.FindKey:
|
|
keyPath: $.some.path
|
|
- org.openrewrite.yaml.ChangeValue:
|
|
keyPath: $.some.path
|
|
value: newValue
|
|
"""
|
|
|
|
|
|
def create_file(path, content):
|
|
"""Create a file with the given content."""
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, 'w') as f:
|
|
f.write(content)
|
|
print(f"Created: {path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Initialize a new OpenRewrite YAML recipe with boilerplate code"
|
|
)
|
|
parser.add_argument(
|
|
"--name",
|
|
required=True,
|
|
help="Recipe class name (PascalCase, e.g., UpdateGitHubActions)"
|
|
)
|
|
parser.add_argument(
|
|
"--package",
|
|
required=True,
|
|
help="Package name (e.g., com.example.rewrite)"
|
|
)
|
|
parser.add_argument(
|
|
"--description",
|
|
required=True,
|
|
help="Recipe description"
|
|
)
|
|
parser.add_argument(
|
|
"--parameterized",
|
|
action="store_true",
|
|
help="Generate parameterized recipe with @Option annotation"
|
|
)
|
|
parser.add_argument(
|
|
"--declarative",
|
|
action="store_true",
|
|
help="Also generate declarative YAML recipe template"
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
default=".",
|
|
help="Output directory (default: current directory)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate recipe name
|
|
if not args.name[0].isupper():
|
|
print("Error: Recipe name must start with uppercase letter (PascalCase)")
|
|
sys.exit(1)
|
|
|
|
# Read license header
|
|
license_header = read_license_header()
|
|
if license_header:
|
|
print(f"Found license header (will use year {datetime.now().year})")
|
|
|
|
# Convert package to path
|
|
package_path = args.package.replace('.', '/')
|
|
base_path = Path(args.output_dir)
|
|
|
|
# Generate file paths
|
|
recipe_path = base_path / "src" / "main" / "java" / package_path / f"{args.name}.java"
|
|
test_path = base_path / "src" / "test" / "java" / package_path / f"{args.name}Test.java"
|
|
|
|
# Generate recipe class
|
|
if args.parameterized:
|
|
recipe_content = generate_parameterized_recipe_class(
|
|
args.name, args.package, args.description, license_header
|
|
)
|
|
else:
|
|
recipe_content = generate_recipe_class(
|
|
args.name, args.package, args.description, license_header
|
|
)
|
|
|
|
# Generate test class
|
|
test_content = generate_test_class(args.name, args.package, license_header)
|
|
|
|
# Create files
|
|
create_file(recipe_path, recipe_content)
|
|
create_file(test_path, test_content)
|
|
|
|
# Generate declarative recipe if requested
|
|
if args.declarative:
|
|
yaml_name = to_snake_case(args.name)
|
|
yaml_path = base_path / "src" / "main" / "resources" / "META-INF" / "rewrite" / f"{yaml_name}.yml"
|
|
yaml_content = generate_declarative_recipe(args.name, args.package, args.description)
|
|
create_file(yaml_path, yaml_content)
|
|
|
|
print("\n✓ Recipe initialization complete!")
|
|
print("\nNext steps:")
|
|
print("1. Write failing tests in the test file")
|
|
print("2. Implement the recipe logic")
|
|
print("3. Run tests to verify: ./gradlew test")
|
|
if args.declarative:
|
|
print("4. Choose between imperative (Java) or declarative (YAML) approach")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|