Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:41 +08:00
commit c2d0b101b0
22 changed files with 6446 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
---
# ==============================================================================
# Example: Framework Migration Recipe
# ==============================================================================
# This example demonstrates a realistic declarative recipe for migrating
# from one version of a framework to another, combining multiple recipes
# with configuration.
#
# Save this file in: src/main/resources/META-INF/rewrite/
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MigrateToFrameworkX
displayName: Migrate to Framework X 2.0
description: |
Migrates applications from Framework X 1.x to 2.0. This recipe performs the following steps:
- Updates dependency versions
- Migrates renamed packages and types
- Updates deprecated API usage
- Applies code formatting
tags:
- framework-x
- migration
- upgrade
estimatedEffortPerOccurrence: PT15M
recipeList:
# Step 1: Update dependencies
- org.openrewrite.java.dependencies.ChangeDependency:
oldGroupId: com.example.frameworkx
oldArtifactId: frameworkx-core
newGroupId: com.example.frameworkx
newArtifactId: frameworkx-core
newVersion: 2.0.x
- org.openrewrite.java.dependencies.AddDependency:
groupId: com.example.frameworkx
artifactId: frameworkx-new-module
version: 2.0.x
onlyIfUsing: com.example.frameworkx.NewFeature
configuration: implementation
# Step 2: Package and type migrations
- org.openrewrite.java.ChangePackage:
oldPackageName: com.example.frameworkx.old
newPackageName: com.example.frameworkx.v2
recursive: true
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: com.example.frameworkx.OldConfig
newFullyQualifiedTypeName: com.example.frameworkx.v2.NewConfig
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: com.example.frameworkx.OldClient
newFullyQualifiedTypeName: com.example.frameworkx.v2.Client
# Step 3: Method migrations
- org.openrewrite.java.ChangeMethodName:
methodPattern: com.example.frameworkx.v2.Client execute(..)
newMethodName: run
# Step 4: Apply custom recipes (if you have any)
# - com.yourorg.CustomFrameworkXMigration
# Step 5: Format the code
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Security Fixes Recipe
# ==============================================================================
# This recipe applies common security fixes across a codebase
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ApplySecurityFixes
displayName: Apply security best practices
description: Applies a collection of security-related fixes and improvements to the codebase.
tags:
- security
- SAST
recipeList:
# Use secure random number generation
- org.openrewrite.java.security.SecureRandom
# Fix SQL injection vulnerabilities
- org.openrewrite.java.security.UseFileCreateTempFile
# Apply static analysis security fixes
- org.openrewrite.staticanalysis.CommonStaticAnalysis
# Remove unused imports
- org.openrewrite.java.format.RemoveUnusedImports
---
# ==============================================================================
# Example: Testing Framework Migration
# ==============================================================================
# Migrates from JUnit 4 to JUnit 5
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MigrateToJUnit5
displayName: Migrate to JUnit 5
description: Migrates JUnit 4 tests to JUnit 5 (Jupiter).
tags:
- junit
- testing
- junit5
preconditions:
- org.openrewrite.java.search.UsesType:
fullyQualifiedTypeName: org.junit.Test
recipeList:
# Update dependencies
- org.openrewrite.java.dependencies.RemoveDependency:
groupId: junit
artifactId: junit
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.junit.jupiter
artifactId: junit-jupiter
version: 5.9.x
onlyIfUsing: org.junit.Test
configuration: testImplementation
# Migrate annotations and assertions
- org.openrewrite.java.testing.junit5.JUnit4to5Migration
# Format
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Code Quality Improvements
# ==============================================================================
# A collection of code quality improvements
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ImproveCodeQuality
displayName: Improve code quality
description: Applies a comprehensive set of code quality improvements.
tags:
- code-quality
- refactoring
- best-practices
recipeList:
# Static analysis
- org.openrewrite.staticanalysis.CommonStaticAnalysis
# Finalize local variables that aren't reassigned
- org.openrewrite.staticanalysis.FinalizeLocalVariables
# Add missing @Override annotations
- org.openrewrite.staticanalysis.MissingOverrideAnnotation
# Simplify boolean expressions
- org.openrewrite.staticanalysis.SimplifyBooleanExpression
# Remove unnecessary null checks
- org.openrewrite.staticanalysis.UnnecessaryNullCheckBeforeInstanceOf
# Format code
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Simple Type Replacement
# ==============================================================================
# A focused recipe that just replaces one type with another
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ReplaceStringUtils
displayName: Replace Apache Commons StringUtils with Spring StringUtils
description: Replaces usage of Apache Commons StringUtils with Spring Framework's StringUtils.
recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.apache.commons.lang3.StringUtils
newFullyQualifiedTypeName: org.springframework.util.StringUtils
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.springframework
artifactId: spring-core
version: 6.0.x
onlyIfUsing: org.springframework.util.StringUtils
configuration: implementation
---
# ==============================================================================
# TEST EXAMPLE
# ==============================================================================
# Here's how to test a declarative recipe:
#
# ```java
# package com.yourorg;
#
# import org.junit.jupiter.api.Test;
# import org.openrewrite.java.JavaParser;
# import org.openrewrite.test.RecipeSpec;
# import org.openrewrite.test.RewriteTest;
#
# import static org.openrewrite.java.Assertions.java;
#
# class MigrateToFrameworkXTest implements RewriteTest {
#
# @Override
# public void defaults(RecipeSpec spec) {
# spec
# // Load the recipe from resources
# .recipeFromResources("com.yourorg.MigrateToFrameworkX")
# // Add dependencies needed to compile the "before" code
# .parser(JavaParser.fromJavaVersion()
# .classpath("frameworkx-core-1.0"));
# }
#
# @Test
# void migratesFrameworkXCode() {
# rewriteRun(
# java(
# """
# package com.example;
#
# import com.example.frameworkx.old.OldConfig;
#
# class Example {
# OldConfig config;
# }
# """,
# """
# package com.example;
#
# import com.example.frameworkx.v2.NewConfig;
#
# class Example {
# NewConfig config;
# }
# """
# )
# );
# }
# }
# ```
# ==============================================================================
# TIPS FOR DECLARATIVE RECIPES
# ==============================================================================
# 1. Keep them focused - one migration or fix per recipe
# 2. Use meaningful names that describe what the recipe does
# 3. Document the purpose and steps in the description
# 4. Add tags for searchability
# 5. Use preconditions to avoid running on irrelevant files
# 6. Order matters - recipes run sequentially
# 7. Consider adding AutoFormat at the end
# 8. Test each recipe thoroughly
# 9. Wrap string values in quotes if they contain special characters
# 10. Use estimatedEffortPerOccurrence to help users understand the impact

View File

@@ -0,0 +1,186 @@
package com.yourorg;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.NonNull;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
/**
* A simple recipe that adds a hello() method to a specified class.
*
* This example demonstrates:
* - Basic recipe structure with @Value and @EqualsAndHashCode
* - Using @Option for configurable parameters
* - Using JavaTemplate for code generation
* - Checking preconditions before making changes
* - Following the "do no harm" principle
*
* Based on the official OpenRewrite tutorial:
* https://docs.openrewrite.org/authoring-recipes/writing-a-java-refactoring-recipe
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class SayHelloRecipe extends Recipe {
@Option(
displayName = "Fully Qualified Class Name",
description = "A fully qualified class name indicating which class to add a hello() method to.",
example = "com.yourorg.FooBar"
)
@NonNull
String fullyQualifiedClassName;
@JsonCreator
public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
this.fullyQualifiedClassName = fullyQualifiedClassName;
}
@Override
public String getDisplayName() {
return "Say 'Hello'";
}
@Override
public String getDescription() {
return "Adds a \"hello\" method to the specified class.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// Always return a new instance - never cache visitors
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
// Step 1: Traverse the subtree first
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Step 2: Check if this is the class we're looking for
// Do no harm: return unchanged if this isn't the target class
if (cd.getType() == null || !cd.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
return cd;
}
// Step 3: Check if the class already has a hello() method
// Do no harm: don't add if it already exists
boolean helloMethodExists = cd.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.MethodDeclaration)
.map(J.MethodDeclaration.class::cast)
.anyMatch(methodDeclaration -> "hello".equals(methodDeclaration.getName().getSimpleName()));
if (helloMethodExists) {
return cd;
}
// Step 4: Add the hello() method using JavaTemplate
// The template uses #{} for parameter substitution
J.Block body = JavaTemplate.apply(
"public String hello() { return \"Hello from #{}!\"; }",
new Cursor(getCursor(), cd.getBody()),
cd.getBody().getCoordinates().lastStatement(),
fullyQualifiedClassName
);
return cd.withBody(body);
}
};
}
}
// ============================================================
// TEST CLASS
// ============================================================
/*
package com.yourorg;
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import static org.openrewrite.java.Assertions.java;
class SayHelloRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new SayHelloRecipe("com.yourorg.FooBar"));
}
@DocumentExample
@Test
void addsHelloToFooBar() {
rewriteRun(
java(
"""
package com.yourorg;
class FooBar {
}
""",
"""
package com.yourorg;
class FooBar {
public String hello() {
return "Hello from com.yourorg.FooBar!";
}
}
"""
)
);
}
@Test
void doesNotChangeExistingHello() {
rewriteRun(
java(
"""
package com.yourorg;
class FooBar {
public String hello() { return ""; }
}
"""
)
);
}
@Test
void doesNotChangeOtherClasses() {
rewriteRun(
java(
"""
package com.yourorg;
class OtherClass {
}
"""
)
);
}
}
*/
// ============================================================
// YAML USAGE
// ============================================================
/*
Save this in src/main/resources/META-INF/rewrite/say-hello.yml:
---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.SayHelloToFooBar
displayName: Say Hello to FooBar
description: Adds a hello() method to the FooBar class.
recipeList:
- com.yourorg.SayHelloRecipe:
fullyQualifiedClassName: com.yourorg.FooBar
*/

View File

@@ -0,0 +1,294 @@
package com.yourorg;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.marker.SearchResult;
import java.util.HashMap;
import java.util.Map;
/**
* A ScanningRecipe that marks classes only if the project uses a specific type.
*
* This example demonstrates:
* - ScanningRecipe with accumulator pattern
* - Three-phase execution: scan, generate (optional), edit
* - Tracking per-project information with Map<JavaProject, T>
* - Using accumulator data to inform editing decisions
* - Proper handling of multi-module projects
*
* Use ScanningRecipe when you need to:
* - See all files before making changes
* - Generate new files based on analysis
* - Share data across multiple files
* - Make decisions based on project-wide information
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class ScanningRecipeExample extends ScanningRecipe<ScanningRecipeExample.Accumulator> {
/**
* The accumulator stores data collected during the scanning phase.
* This data is then available during the editing phase.
*
* Important: For multi-module projects, track data per JavaProject.
*/
public static class Accumulator {
// Track which projects use the target type
Map<JavaProject, Boolean> projectUsesTargetType = new HashMap<>();
// You can store any data structure you need
// Map<JavaProject, Set<String>> projectClasses = new HashMap<>();
// Map<JavaProject, List<MethodInfo>> projectMethods = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Mark classes in projects using target type";
}
@Override
public String getDescription() {
return "Marks classes with SearchResult only if the project uses a specific type.";
}
/**
* Initialize the accumulator before scanning begins.
*/
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
/**
* Phase 1: SCANNING
*
* The scanner visits all source files and collects information
* into the accumulator. No changes are made in this phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the JavaProject marker to track per-project data
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Initialize project tracking if needed
acc.projectUsesTargetType.putIfAbsent(project, false);
// Scan for the target type in this file
// In this example, we're looking for usage of java.util.Optional
cu.getImports().stream()
.filter(imp -> imp.getTypeName().equals("java.util.Optional"))
.findFirst()
.ifPresent(imp -> acc.projectUsesTargetType.put(project, true));
// Continue scanning subtree
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.Import visitImport(J.Import import_, ExecutionContext ctx) {
// You can also scan at finer granularity
// Example: track specific imports, method calls, etc.
return import_;
}
};
}
/**
* Phase 2: GENERATING (optional)
*
* This phase is used to generate new source files based on the
* accumulated data. Return null if no new files are needed.
*
* Uncomment to implement:
*/
/*
@Override
public Collection<SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
List<SourceFile> newFiles = new ArrayList<>();
// Example: Generate a file for each project that uses the target type
for (Map.Entry<JavaProject, Boolean> entry : acc.projectUsesTargetType.entrySet()) {
if (entry.getValue()) {
JavaProject project = entry.getKey();
// Create a new source file
// newFiles.add(createNewFile(project));
}
}
return newFiles;
}
*/
/**
* Phase 3: EDITING
*
* The editor visits all source files again and makes changes
* based on the data collected in the scanning phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the project for this file
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Check the accumulator to see if this project uses the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType == null || !usesTargetType) {
// Don't make changes in projects that don't use the target type
return cu;
}
// This project uses the target type, continue with editing
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Get the project
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project == null) {
return cd;
}
// Only mark classes in projects that use the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType != null && usesTargetType) {
// Mark this class with a SearchResult
return SearchResult.found(cd, "Found in project using Optional");
}
return cd;
}
};
}
}
// ============================================================
// SIMPLIFIED EXAMPLE: COUNT CLASSES
// ============================================================
/*
Here's a simpler ScanningRecipe that counts classes per project:
@Value
@EqualsAndHashCode(callSuper = false)
public class CountClasses extends ScanningRecipe<CountClasses.Accumulator> {
public static class Accumulator {
Map<JavaProject, Integer> classCounts = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Count classes per project";
}
@Override
public String getDescription() {
return "Counts the number of classes in each project.";
}
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) {
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project != null) {
acc.classCounts.merge(project, 1, Integer::sum);
}
return cd;
}
};
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
// Print the results
return new JavaIsoVisitor<ExecutionContext>() {
private boolean printed = false;
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
if (!printed) {
System.out.println("Class counts by project:");
acc.classCounts.forEach((project, count) ->
System.out.println(project.getProjectName() + ": " + count + " classes")
);
printed = true;
}
return cu;
}
};
}
}
*/
// ============================================================
// KEY TAKEAWAYS
// ============================================================
/*
1. ScanningRecipe has THREE phases:
- Scan: Collect data (no changes)
- Generate: Create new files (optional)
- Edit: Make changes based on collected data
2. Always track per-project data with Map<JavaProject, T>
- Don't assume single project per repository
- Get JavaProject from markers
3. Accumulator is shared across all phases
- Use it to pass data from scan to edit
- Keep it simple and focused
4. Scanner makes NO changes
- Only collect information
- Mark files or store data
5. Editor uses accumulator data
- Make informed decisions
- Can access all collected information
6. When multiple ScanningRecipes are in a recipe list:
- All scanners run first
- Then all generators run
- Then all editors run
- Scanners see state before ANY edits
- But scanners DO see generated files
*/

View File

@@ -0,0 +1,282 @@
package com.example.rewrite;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.yaml.JsonPathMatcher;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
/**
* Example recipe demonstrating YAML manipulation for GitHub Actions workflows.
*
* This recipe updates GitHub Actions checkout action from v2/v3 to v4.
*
* Before:
* ```yaml
* jobs:
* build:
* steps:
* - uses: actions/checkout@v2
* ```
*
* After:
* ```yaml
* jobs:
* build:
* steps:
* - uses: actions/checkout@v4
* ```
*
* Key Concepts Demonstrated:
* 1. YamlIsoVisitor for YAML LST manipulation
* 2. JsonPathMatcher for targeted YAML element matching
* 3. Safe value access with type checking
* 4. Preserving formatting and comments
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class UpdateGitHubActionsCheckout extends Recipe {
@Override
public String getDisplayName() {
return "Update GitHub Actions to `actions/checkout@v4`.";
}
@Override
public String getDescription() {
return "Updates all uses of `actions/checkout@v2` and `actions/checkout@v3` to `actions/checkout@v4`.\n\n" +
"**Before:**\n```yaml\n- uses: actions/checkout@v2\n```\n\n" +
"**After:**\n```yaml\n- uses: actions/checkout@v4\n```";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
// JsonPath to match the 'uses' field in GitHub Actions steps
private final JsonPathMatcher matcher =
new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
// Always call super to traverse the tree
scalar = super.visitScalar(scalar, ctx);
// Check if this scalar matches our JsonPath
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
// Safe null check
if (value != null) {
// Update v2 to v4
if (value.startsWith("actions/checkout@v2")) {
return scalar.withValue(value.replace("@v2", "@v4"));
}
// Update v3 to v4
if (value.startsWith("actions/checkout@v3")) {
return scalar.withValue(value.replace("@v3", "@v4"));
}
}
}
return scalar;
}
};
}
}
/**
* Additional YAML recipe examples demonstrating other common patterns:
*/
/**
* Example: Update Kubernetes container image tags
*
* Before:
* ```yaml
* spec:
* containers:
* - image: myapp:1.0.0
* ```
*
* After:
* ```yaml
* spec:
* containers:
* - image: myapp:2.0.0
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class UpdateKubernetesImageTag extends Recipe {
String oldTag;
String newTag;
@Override
public String getDisplayName() {
return "Update Kubernetes image tag.";
}
@Override
public String getDescription() {
return "Updates Kubernetes container image tags.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
private final JsonPathMatcher matcher =
new JsonPathMatcher("$.spec.template.spec.containers[*].image");
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
scalar = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
if (value != null && value.endsWith(":" + oldTag)) {
return scalar.withValue(
value.substring(0, value.lastIndexOf(":")) + ":" + newTag
);
}
}
return scalar;
}
};
}
}
/**
* Example: Change key name in YAML
*
* Before:
* ```yaml
* oldKey: value
* ```
*
* After:
* ```yaml
* newKey: value
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class ChangeYamlKey extends Recipe {
String oldKey;
String newKey;
@Override
public String getDisplayName() {
return "Change YAML key name.";
}
@Override
public String getDescription() {
return "Renames a YAML key while preserving its value.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
entry = super.visitMappingEntry(entry, ctx);
// Check if this entry has the old key
if (oldKey.equals(entry.getKey().getValue())) {
// Replace the key while preserving everything else
return entry.withKey(
((Yaml.Scalar) entry.getKey()).withValue(newKey)
);
}
return entry;
}
};
}
}
/**
* Example: Update value based on key match
*
* Before:
* ```yaml
* database:
* host: localhost
* ```
*
* After:
* ```yaml
* database:
* host: prod-db.example.com
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class UpdateYamlValue extends Recipe {
String keyPath;
String oldValue;
String newValue;
@Override
public String getDisplayName() {
return "Update YAML value.";
}
@Override
public String getDescription() {
return "Updates a YAML value at a specific key path.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
private final JsonPathMatcher matcher = new JsonPathMatcher(keyPath);
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
scalar = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
if (oldValue.equals(value)) {
return scalar.withValue(newValue);
}
}
return scalar;
}
};
}
}
/**
* Common JsonPath Patterns for YAML Recipes:
*
* GitHub Actions:
* - $.jobs.*.steps[*].uses - All 'uses' in steps
* - $.on.push.branches - Push trigger branches
* - $.jobs.*.runs-on - Runner configuration
* - $.jobs.*.strategy.matrix - Matrix strategy
*
* Kubernetes:
* - $.spec.template.spec.containers[*].image - Container images
* - $.metadata.labels - Labels
* - $.spec.replicas - Replica count
* - $.spec.template.spec.containers[*].env[*] - Environment variables
*
* Generic YAML:
* - $.databases.*.connection.host - Nested configuration
* - $[?(@.enabled == true)] - Conditional matching
* - $..*[?(@.type == 'service')] - Deep search with condition
*
* See references/jsonpath-patterns.md for comprehensive examples.
*/