Skip to content

Commit c697e85

Browse files
authored
chore: make constraint metadata more flexible (#2234)
1 parent bd1be4b commit c697e85

70 files changed

Lines changed: 919 additions & 811 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/src/main/java/ai/timefold/solver/core/api/domain/solution/ConstraintWeightOverrides.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import ai.timefold.solver.core.api.score.Score;
88
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
9+
import ai.timefold.solver.core.api.score.stream.ConstraintRef;
910
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream;
1011
import ai.timefold.solver.core.api.solver.change.ProblemChange;
1112
import ai.timefold.solver.core.impl.domain.solution.DefaultConstraintWeightOverrides;
@@ -44,16 +45,24 @@ static <Score_ extends Score<Score_>> ConstraintWeightOverrides<Score_> of(Map<S
4445
/**
4546
* Return a constraint weight for a particular constraint.
4647
*
47-
* @return null if the constraint name is not known
48+
* @return null if the constraint id is not known
4849
*/
4950
@Nullable
50-
Score_ getConstraintWeight(String constraintName);
51+
Score_ getConstraintWeight(String constraintId);
52+
53+
/**
54+
* As defined by {@link #getConstraintWeight(String)},
55+
* but accepts {@link ConstraintRef} instead of the ID directly.
56+
*/
57+
default @Nullable Score_ getConstraintWeight(ConstraintRef constraintRef) {
58+
return getConstraintWeight(constraintRef.id());
59+
}
5160

5261
/**
5362
* Returns all known constraints.
5463
*
55-
* @return All constraint names for which {@link #getConstraintWeight(String)} returns a non-null value.
64+
* @return All constraint IDs for which {@link #getConstraintWeight(String)} returns a non-null value.
5665
*/
57-
Set<String> getKnownConstraintNames();
66+
Set<String> getKnownConstraintIds();
5867

5968
}

core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ public interface ConstraintAnalysis<Score_ extends Score<Score_>> {
5252
int matchCount();
5353

5454
/**
55-
* Return name of the constraint that this analysis is for.
55+
* Return id of the constraint that this analysis is for.
5656
*
57-
* @return equal to {@code constraintRef.constraintName()}
57+
* @return equal to {@code constraintRef.id()}
5858
*/
59-
String constraintName();
59+
default String constraintId() {
60+
return constraintRef().id();
61+
}
6062

6163
/**
6264
* Returns a diagnostic text that explains part of the score quality through the {@link ConstraintAnalysis} API.

core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public interface ScoreAnalysis<Score_ extends Score<Score_>> {
101101
* @return null if no constraint matches of such constraint are present
102102
*/
103103
@Nullable
104-
ConstraintAnalysis<Score_> getConstraintAnalysis(String constraintName);
104+
ConstraintAnalysis<Score_> getConstraintAnalysis(String constraintId);
105105

106106
/**
107107
* Compare this {@link ScoreAnalysis} to another {@link ScoreAnalysis}

core/src/main/java/ai/timefold/solver/core/api/score/stream/Constraint.java

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import ai.timefold.solver.core.api.score.Score;
44

5-
import org.jspecify.annotations.NonNull;
5+
import org.jspecify.annotations.NullMarked;
66
import org.jspecify.annotations.Nullable;
77

88
/**
@@ -11,34 +11,27 @@
1111
* It is defined in {@link ConstraintProvider#defineConstraints(ConstraintFactory)}
1212
* by calling {@link ConstraintFactory#forEach(Class)}.
1313
*/
14+
@NullMarked
1415
public interface Constraint {
1516

16-
String DEFAULT_CONSTRAINT_GROUP = "default";
17-
1817
ConstraintRef getConstraintRef();
1918

2019
/**
21-
* Returns a human-friendly description of the constraint.
22-
* The format of the description is left unspecified and will not be parsed in any way.
20+
* Returns the metadata for this constraint, as provided to
21+
* {@link ConstraintBuilder#asConstraint(ConstraintMetadata)}.
22+
* The constraint's identity ({@link ConstraintMetadata#id()}) is fixed at build time;
23+
* any later mutation of the returned object does not affect the constraint's identity.
2324
*
24-
* @return may be left empty
25+
* @return never null
2526
*/
26-
default @NonNull String getDescription() {
27-
return "";
28-
}
29-
30-
default @NonNull String getConstraintGroup() {
31-
return DEFAULT_CONSTRAINT_GROUP;
32-
}
27+
ConstraintMetadata getConstraintMetadata();
3328

3429
/**
3530
* Returns the weight of the constraint as defined in the {@link ConstraintProvider},
3631
* without any overrides.
3732
*
3833
* @return null if the constraint does not have a weight defined
3934
*/
40-
default <Score_ extends Score<Score_>> @Nullable Score_ getConstraintWeight() {
41-
return null;
42-
}
35+
<Score_ extends Score<Score_>> @Nullable Score_ getConstraintWeight();
4336

4437
}
Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.solver.core.api.score.stream;
22

33
import ai.timefold.solver.core.api.score.analysis.ScoreAnalysis;
4+
import ai.timefold.solver.core.impl.score.stream.common.DefaultConstraintMetadata;
45

56
import org.jspecify.annotations.NullMarked;
67

@@ -9,40 +10,23 @@ public interface ConstraintBuilder {
910

1011
/**
1112
* Builds a {@link Constraint} from the constraint stream.
12-
* The constraint will be placed in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}.
13+
* Shorthand for {@link #asConstraint(ConstraintMetadata)}.
1314
*
14-
* @param constraintName shows up in {@link ScoreAnalysis}
15+
* @param id shows up in {@link ScoreAnalysis}
1516
*/
16-
default Constraint asConstraint(String constraintName) {
17-
return asConstraintDescribed(constraintName, "");
18-
}
19-
20-
/**
21-
* As defined by {@link #asConstraintDescribed(String, String, String)},
22-
* placing the constraint in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}.
23-
*
24-
* @param constraintName shows up in {@link ScoreAnalysis}
25-
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
26-
*/
27-
default Constraint asConstraintDescribed(String constraintName, String constraintDescription) {
28-
return asConstraintDescribed(constraintName, constraintDescription, Constraint.DEFAULT_CONSTRAINT_GROUP);
17+
default Constraint asConstraint(String id) {
18+
return asConstraint(new DefaultConstraintMetadata(id));
2919
}
3020

3121
/**
3222
* Builds a {@link Constraint} from the constraint stream.
33-
* Both the constraint name and the constraint group are only allowed
34-
* to contain alphanumeric characters, " ", "-" or "_".
35-
* The constraint description can contain any character, but it is recommended to keep it short and concise.
36-
* <p>
37-
* Unlike the constraint name and group,
38-
* the constraint description is unlikely to be used externally as an identifier,
39-
* and therefore doesn't need to be URL-friendly, or protected against injection attacks.
23+
* {@link ConstraintMetadata#id()} is called exactly once at this point;
24+
* the returned value is validated and snapshotted as the constraint's permanent identity.
25+
* Subsequent changes to the description's {@link ConstraintMetadata#id()} return value are ignored.
4026
*
41-
* @param constraintName shows up in {@link ScoreAnalysis}
42-
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
43-
* @param constraintGroup not used by the solver directly, but may be used by external tools to group constraints together,
44-
* such as by their source or by their purpose
27+
* @param metadata identifies and describes the constraint;
28+
* {@link ConstraintMetadata#id()} shows up in {@link ScoreAnalysis}
4529
*/
46-
Constraint asConstraintDescribed(String constraintName, String constraintDescription, String constraintGroup);
30+
Constraint asConstraint(ConstraintMetadata metadata);
4731

4832
}
Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package ai.timefold.solver.core.api.score.stream;
22

33
import java.util.Collection;
4-
import java.util.Set;
54

6-
import org.jspecify.annotations.NonNull;
5+
import org.jspecify.annotations.NullMarked;
76
import org.jspecify.annotations.Nullable;
87

98
/**
109
* Provides information about the known constraints.
1110
* Works in combination with {@link ConstraintProvider}.
1211
*/
12+
@NullMarked
1313
public interface ConstraintMetaModel {
1414

1515
/**
@@ -18,30 +18,23 @@ public interface ConstraintMetaModel {
1818
* @return null if such constraint does not exist
1919
*/
2020
@Nullable
21-
Constraint getConstraint(@NonNull ConstraintRef constraintRef);
21+
Constraint getConstraint(ConstraintRef constraintRef);
2222

2323
/**
24-
* Returns all constraints defined in the {@link ConstraintProvider}.
24+
* Returns the constraint with the given id.
25+
* Convenience shorthand for {@link #getConstraint(ConstraintRef)}.
2526
*
26-
* @return iteration order is undefined
27-
*/
28-
@NonNull
29-
Collection<Constraint> getConstraints();
30-
31-
/**
32-
* Returns all constraints from {@link #getConstraints()} that belong to the given group.
33-
*
34-
* @return iteration order is undefined
27+
* @return null if such constraint does not exist
3528
*/
36-
@NonNull
37-
Collection<Constraint> getConstraintsPerGroup(@NonNull String constraintGroup);
29+
default @Nullable Constraint getConstraint(String id) {
30+
return getConstraint(ConstraintRef.of(id));
31+
}
3832

3933
/**
40-
* Returns constraint groups with at least one constraint in it.
34+
* Returns all constraints defined in the {@link ConstraintProvider}.
4135
*
4236
* @return iteration order is undefined
4337
*/
44-
@NonNull
45-
Set<String> getConstraintGroups();
38+
Collection<Constraint> getConstraints();
4639

4740
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package ai.timefold.solver.core.api.score.stream;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
/**
6+
* Identifies a {@link Constraint} and optionally carries metadata about it.
7+
* Users implement this interface, adding any fields they require.
8+
* <p>
9+
* <strong>Immutability contract:</strong> {@link #id()} must return the same value
10+
* for the lifetime of the object.
11+
* The first value returned is snapshotted
12+
* when the constraint is built via {@link ConstraintBuilder#asConstraint(ConstraintMetadata)};
13+
* any later change to the return value of {@link #id()} will be silently ignored
14+
* and will NOT affect the constraint's identity.
15+
*/
16+
@NullMarked
17+
public interface ConstraintMetadata {
18+
19+
/**
20+
* Returns the unique identifier of the constraint.
21+
* Must be non-null, non-empty, and stable (see class Javadoc).
22+
*
23+
* @return the constraint's unique identifier
24+
*/
25+
String id();
26+
27+
}

core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintRef.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@
99
* <p>
1010
* If you need an instance created, use {@link ConstraintRef#of(String)} and not the record's constructors.
1111
*
12-
* @param constraintName The constraint name. It must be unique.
12+
* @param id The constraint id. It must be unique.
1313
*/
1414
@NullMarked
15-
public record ConstraintRef(String constraintName)
15+
public record ConstraintRef(String id)
1616
implements
1717
Comparable<ConstraintRef> {
1818

19-
public static ConstraintRef of(String constraintName) {
20-
return new ConstraintRef(constraintName);
19+
public static ConstraintRef of(String id) {
20+
return new ConstraintRef(id);
2121
}
2222

2323
public ConstraintRef {
24-
constraintName = AbstractConstraintBuilder.sanitize("constraintName", constraintName);
24+
id = AbstractConstraintBuilder.sanitize("id", id);
2525
}
2626

2727
@Override
2828
public int compareTo(ConstraintRef other) {
29-
return constraintName.compareTo(other.constraintName);
29+
return id.compareTo(other.id);
3030
}
3131

3232
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/BavetAbstractConstraintStream.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import ai.timefold.solver.core.api.score.Score;
1515
import ai.timefold.solver.core.api.score.stream.Constraint;
1616
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
17-
import ai.timefold.solver.core.api.score.stream.ConstraintRef;
17+
import ai.timefold.solver.core.api.score.stream.ConstraintMetadata;
1818
import ai.timefold.solver.core.api.score.stream.ConstraintStream;
1919
import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream;
2020
import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintStream;
@@ -104,14 +104,13 @@ public boolean guaranteesDistinct() {
104104
}
105105
}
106106

107-
protected <Score_ extends Score<Score_>> Constraint buildConstraint(String constraintName, String description,
108-
String constraintGroup, Score_ constraintWeight, ScoreImpactType impactType, Object justificationFunction,
107+
protected <Score_ extends Score<Score_>> Constraint buildConstraint(ConstraintMetadata description,
108+
Score_ constraintWeight, ScoreImpactType impactType, Object justificationFunction,
109109
BavetScoringConstraintStream<Solution_> stream) {
110110
var resolvedJustificationMapping =
111111
Objects.requireNonNullElseGet(justificationFunction, this::getDefaultJustificationMapping);
112112
var isConstraintWeightConfigurable = constraintWeight == null;
113-
var constraintRef = ConstraintRef.of(constraintName);
114-
var constraint = new BavetConstraint<>(constraintFactory, constraintRef, description, constraintGroup,
113+
var constraint = new BavetConstraint<>(constraintFactory, description,
115114
isConstraintWeightConfigurable ? null : constraintWeight, impactType, resolvedJustificationMapping,
116115
stream);
117116
stream.setConstraint(constraint);

core/src/main/java/ai/timefold/solver/core/impl/bavet/visual/NodeGraph.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ private static <Solution_> String getMetadata(GraphSink<Solution_> sink, Solutio
145145
var constraint = sink.constraint();
146146
var metadata = getBaseDOTProperties("#3423a6", true);
147147
metadata.put("label", "<B>%s</B><BR />(Weight: %s)"
148-
.formatted(constraint.getConstraintRef().constraintName(), constraint.extractConstraintWeight(solution)));
148+
.formatted(constraint.getConstraintRef().id(), constraint.extractConstraintWeight(solution)));
149149
return mergeMetadata(metadata);
150150
}
151151

0 commit comments

Comments
 (0)