Skip to content

Commit fb73848

Browse files
ziekatimtebeek
andauthored
Add TOML recipes to change keys and values, find and delete keys, and create new TOML files (#6124)
* add some basic toml recipes * Apply best practices * Polish ChangeKey * Fix nullable warnings in TomlPathMatcher * Show difference between single and multiple wildcards * Override `visitKeyValue` in `DeleteKey` directly * Polish ChangeValue * Make CreateTomlFile complete in a single cycle * Organize imports * Remove unused import --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent e6b63a8 commit fb73848

11 files changed

Lines changed: 1957 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.toml;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Option;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.toml.tree.Toml;
25+
26+
@Value
27+
@EqualsAndHashCode(callSuper = false)
28+
public class ChangeKey extends Recipe {
29+
@Option(displayName = "Old key path",
30+
description = "A TOML path expression to locate a key.",
31+
example = "package.name")
32+
String oldKeyPath;
33+
34+
@Option(displayName = "New key",
35+
description = "The new name for the key.",
36+
example = "project-name")
37+
String newKey;
38+
39+
@Override
40+
public String getDisplayName() {
41+
return "Change TOML key";
42+
}
43+
44+
@Override
45+
public String getInstanceNameSuffix() {
46+
return String.format("`%s` to `%s`", oldKeyPath, newKey);
47+
}
48+
49+
@Override
50+
public String getDescription() {
51+
return "Change a TOML key, while leaving the value intact.";
52+
}
53+
54+
@Override
55+
public TreeVisitor<?, ExecutionContext> getVisitor() {
56+
TomlPathMatcher matcher = new TomlPathMatcher(oldKeyPath);
57+
return new TomlIsoVisitor<ExecutionContext>() {
58+
@Override
59+
public Toml.KeyValue visitKeyValue(Toml.KeyValue keyValue, ExecutionContext ctx) {
60+
Toml.KeyValue kv = super.visitKeyValue(keyValue, ctx);
61+
62+
if (matcher.matches(getCursor()) && kv.getKey() instanceof Toml.Identifier) {
63+
String newKeyName = newKey;
64+
if ((newKeyName.startsWith("\"") && newKeyName.endsWith("\"")) ||
65+
(newKeyName.startsWith("'") && newKeyName.endsWith("'"))) {
66+
newKeyName = newKeyName.substring(1, newKeyName.length() - 1);
67+
}
68+
69+
String formattedKey = newKeyName.matches("^[A-Za-z0-9_-]+$") ? newKeyName : "\"" + newKeyName + "\"";
70+
return kv.withKey(((Toml.Identifier) kv.getKey())
71+
.withName(newKeyName)
72+
.withSource(formattedKey));
73+
}
74+
75+
return kv;
76+
}
77+
};
78+
}
79+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.toml;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import lombok.With;
21+
import org.openrewrite.*;
22+
import org.openrewrite.marker.Marker;
23+
import org.openrewrite.marker.Markers;
24+
import org.openrewrite.toml.tree.Space;
25+
import org.openrewrite.toml.tree.Toml;
26+
import org.openrewrite.toml.tree.TomlType;
27+
28+
import java.util.UUID;
29+
30+
@Value
31+
@EqualsAndHashCode(callSuper = false)
32+
public class ChangeValue extends Recipe {
33+
@Option(displayName = "Key path",
34+
description = "A TOML path expression to locate a key.",
35+
example = "package.version")
36+
String keyPath;
37+
38+
@Option(displayName = "New value",
39+
description = "The new value for the key.",
40+
example = "\"2.0.0\"")
41+
String newValue;
42+
43+
@Override
44+
public String getDisplayName() {
45+
return "Change TOML value";
46+
}
47+
48+
@Override
49+
public String getInstanceNameSuffix() {
50+
return String.format("`%s` to `%s`", keyPath, newValue);
51+
}
52+
53+
@Override
54+
public String getDescription() {
55+
return "Change the value of a TOML key.";
56+
}
57+
58+
@Override
59+
public TreeVisitor<?, ExecutionContext> getVisitor() {
60+
TomlPathMatcher matcher = new TomlPathMatcher(keyPath);
61+
return new TomlIsoVisitor<ExecutionContext>() {
62+
@Override
63+
public Toml.KeyValue visitKeyValue(Toml.KeyValue keyValue, ExecutionContext ctx) {
64+
Toml.KeyValue kv = super.visitKeyValue(keyValue, ctx);
65+
66+
if (matcher.matches(getCursor()) && !kv.getMarkers().findFirst(Changed.class).isPresent()) {
67+
Toml newValueNode = parseValue(newValue, kv.getValue().getPrefix());
68+
kv = kv.withValue(newValueNode)
69+
.withMarkers(kv.getMarkers().add(new Changed(Tree.randomId())));
70+
}
71+
72+
return kv;
73+
}
74+
75+
private Toml parseValue(String value, Space prefix) {
76+
value = value.trim();
77+
78+
if (value.startsWith("[") && value.endsWith("]")) {
79+
// Array - parse as string literal for now
80+
return new Toml.Literal(
81+
Tree.randomId(),
82+
prefix,
83+
Markers.EMPTY,
84+
TomlType.Primitive.String,
85+
value,
86+
value
87+
);
88+
}
89+
90+
if (value.startsWith("{") && value.endsWith("}")) {
91+
// Inline table - parse as string literal for now
92+
return new Toml.Literal(
93+
Tree.randomId(),
94+
prefix,
95+
Markers.EMPTY,
96+
TomlType.Primitive.String,
97+
value,
98+
value
99+
);
100+
}
101+
102+
if ((value.startsWith("\"") && value.endsWith("\"")) ||
103+
(value.startsWith("'") && value.endsWith("'"))) {
104+
String unquoted = value.substring(1, value.length() - 1);
105+
return new Toml.Literal(
106+
Tree.randomId(),
107+
prefix,
108+
Markers.EMPTY,
109+
TomlType.Primitive.String,
110+
value,
111+
unquoted
112+
);
113+
}
114+
115+
if (value.startsWith("\"\"\"") && value.endsWith("\"\"\"")) {
116+
String unquoted = value.substring(3, value.length() - 3);
117+
return new Toml.Literal(
118+
Tree.randomId(),
119+
prefix,
120+
Markers.EMPTY,
121+
TomlType.Primitive.String,
122+
value,
123+
unquoted
124+
);
125+
}
126+
127+
if (value.startsWith("'''") && value.endsWith("'''")) {
128+
String unquoted = value.substring(3, value.length() - 3);
129+
return new Toml.Literal(
130+
Tree.randomId(),
131+
prefix,
132+
Markers.EMPTY,
133+
TomlType.Primitive.String,
134+
value,
135+
unquoted
136+
);
137+
}
138+
139+
if ("true".equals(value) || "false".equals(value)) {
140+
return new Toml.Literal(
141+
Tree.randomId(),
142+
prefix,
143+
Markers.EMPTY,
144+
TomlType.Primitive.Boolean,
145+
value,
146+
Boolean.parseBoolean(value)
147+
);
148+
}
149+
150+
if ("inf".equals(value) || "+inf".equals(value) || "-inf".equals(value) ||
151+
"nan".equals(value) || "+nan".equals(value) || "-nan".equals(value)) {
152+
Double doubleValue;
153+
switch (value) {
154+
case "inf":
155+
case "+inf":
156+
doubleValue = Double.POSITIVE_INFINITY;
157+
break;
158+
case "-inf":
159+
doubleValue = Double.NEGATIVE_INFINITY;
160+
break;
161+
default:
162+
doubleValue = Double.NaN;
163+
}
164+
return new Toml.Literal(
165+
Tree.randomId(),
166+
prefix,
167+
Markers.EMPTY,
168+
TomlType.Primitive.Float,
169+
value,
170+
doubleValue
171+
);
172+
}
173+
174+
if (value.contains("T") || value.contains(":")) {
175+
// Simplified datetime check
176+
return new Toml.Literal(
177+
Tree.randomId(),
178+
prefix,
179+
Markers.EMPTY,
180+
TomlType.Primitive.OffsetDateTime,
181+
value,
182+
value
183+
);
184+
}
185+
186+
try {
187+
if (value.contains(".") || value.contains("e") || value.contains("E")) {
188+
Double doubleValue = Double.parseDouble(value.replace("_", ""));
189+
return new Toml.Literal(
190+
Tree.randomId(),
191+
prefix,
192+
Markers.EMPTY,
193+
TomlType.Primitive.Float,
194+
value,
195+
doubleValue
196+
);
197+
} else {
198+
Long longValue;
199+
if (value.startsWith("0x") || value.startsWith("0X")) {
200+
longValue = Long.parseLong(value.substring(2).replace("_", ""), 16);
201+
} else if (value.startsWith("0o") || value.startsWith("0O")) {
202+
longValue = Long.parseLong(value.substring(2).replace("_", ""), 8);
203+
} else if (value.startsWith("0b") || value.startsWith("0B")) {
204+
longValue = Long.parseLong(value.substring(2).replace("_", ""), 2);
205+
} else {
206+
longValue = Long.parseLong(value.replace("_", ""));
207+
}
208+
return new Toml.Literal(
209+
Tree.randomId(),
210+
prefix,
211+
Markers.EMPTY,
212+
TomlType.Primitive.Integer,
213+
value,
214+
longValue
215+
);
216+
}
217+
} catch (NumberFormatException e) {
218+
// Fallback to string for unparseable values
219+
return new Toml.Literal(
220+
Tree.randomId(),
221+
prefix,
222+
Markers.EMPTY,
223+
TomlType.Primitive.String,
224+
"\"" + value + "\"",
225+
value
226+
);
227+
}
228+
}
229+
};
230+
}
231+
232+
@Value
233+
@With
234+
static class Changed implements Marker {
235+
UUID id;
236+
}
237+
}

0 commit comments

Comments
 (0)