Module 4: Declarative YAML recipes
As a best practice, if your recipe can be declarative (meaning it can be built out of other recipes), then you should make it declarative. You can make some truly powerful migration recipes by combining many tiny recipes together (which have been vetted to handle specific tasks correctly, such as only adding dependencies as needed).
You've actually already built a declarative recipe in Module 2 when you used the Recipe Builder to combine existing recipes using Moderne. The YAML file you downloaded is a declarative recipe. Now you'll learn how to write or modify one yourself.
Exercise 4: Write a declarative YAML recipe
Let's have a look at a simple declarative YAML recipe, and expand that to cover an additional use case.
Goals for this exercise
- Write a declarative YAML recipe that ties together existing recipes.
- Learn how to configure a recipe with options.
- Gain an understanding of the order that recipes are executed and what that means for your recipe options.
Steps
If you don't have IntelliJ IDEA 2024.1 Ultimate, you'll lack bundled editor support for writing and running recipes. Some of the below steps will not work for you without this.
- With the
rewrite-recipe-starterstill open in IntelliJ, open theUseApacheStringUtilsrecipe which is defined in a YAML file: src/main/resources/META-INF/rewrite/stringutils.yml.- Notice how the file is structured, with a
type,name,displayName,description, andrecipeListfields. - Comment out the
type:and see how that disables the OpenRewrite support.
- Notice how the file is structured, with a
- Note how the
recipeListfield is a list of fully qualified class names of recipes along with their options (if any exist).- Click through on the
AddDependencyandChangeTyperecipes to open their definition. (You can click through in IntelliJ using a Ctrl + click combination, or Cmd + click on macOS.) - Have your IDE suggest additional options to include in the recipes by adding a new line between the
descriptionandrecipeListfields, then triggering auto-completion (Ctrl + Space by default). You should see that the recipe doesn't include every option by default (e.g., it's missingestimatedEffortPerOccurrenceand others).
- Click through on the
- The migration recipe is a great start, but far from complete. Let's add a recipe to change from Spring's
trimWhitepace(String)to Apache Common'sStringUtils.strip(String).- Begin by adding the org.openrewrite.java.ChangeMethodName recipe to the end of the
recipeListfield. - Make sure to pass in
methodPattern: org.apache.commons.lang3.StringUtils trimWhitespace(java.lang.String)andnewMethodName: stripsuch as in this example gist. - Please note that the method pattern refers to a method that does not exist. Apache Commons does not have a
trimWhitespacemethod, but Spring does. That's because recipes in therecipeListare executed in order. The ChangeType recipe comes before our newChangeMethodNamerecipe. That means that when ourChangeMethodNamerecipe is run, there will no longer be a SpringtrimWhitespacemethod. This is important to keep in mind when chaining recipes together.
- Begin by adding the org.openrewrite.java.ChangeMethodName recipe to the end of the
- Open the unit test src/test/java/com/yourorg/UseApacheStringUtilsTest.java.
- Notice how we implement
RewriteTest, overridedefaults(RecipeSpec)to run our recipe, and configure a classpath for the tests that hasspring-coreon it. (A comment in the code here explains why we only needspring-coreand notcommons-lang3in the classpath because only dependencies to compile thebeforecode are required, not theaftercode.) - Run the first test. You can use the green play icon to the left of the test method (
replacesStringEqualsin this case) to run the test. Note that we invokerewriteRun(SourceSpecs...)and pass in a singlejava(String, String)source specification, that takes in a before and after text block. - The
//language=javalanguage injection enables syntax highlighting and code completion in the text block. - All together, this asserts that when we run the recipe, code that matches the
beforeblock will be converted to code that matches theafterblock.
- Notice how we implement
- Add a unit test for the
ChangeMethodNamerecipe we added that convertstrimWhitespacetostrip.- Here is an example of what this trimWhitespace() test might look like
- Run the new unit test, and verify that the correct changes are indeed made.
- Feel free to test the recipe against the
Defaultrepositories. Just remember you'll need to let the CLI know the recipe has been updated by runningmod config recipes yaml install stringutils.ymlagain (from the/rewrite-recipe-starter/src/main/resources/META-INF/rewritedirectory).
Takeaways
- Declarative recipes are the simplest to write, and are the most common type of recipe.
- Common building blocks can be configured and combined to compose more complex migrations.
- Recipes can be chained together, to make multiple changes to your code in a single run.
- When changing types, keep in mind the order of recipes as subsequent recipes in the
recipeListwill need to use the new type. - Unit tests are a great way to ensure your recipe behaves as expected.