Skip to content

Commit fea06b4

Browse files
authored
Add builder pattern to ConstraintViolation (#451)
This commit introduces a staged builder pattern for the ConstraintViolation class to provide a more fluent and type-safe API for creating constraint violations. Main changes: - Add StagedBuilders interface hierarchy to enforce required properties - Implement a fluent Builder class with various convenience methods - Make existing constructor deprecated in favor of the builder - Add null-safety with @nullable annotations and fallback defaults - Add comprehensive JavaDoc to all new methods - Create tests for all builder pattern functionality The new API makes it easier to create properly configured constraint violations while ensuring type safety and required properties.
1 parent d85727f commit fea06b4

10 files changed

+574
-81
lines changed

Diff for: src/main/java/am/ik/yavi/core/ConstraintViolation.java

+347-7
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
*/
1616
package am.ik.yavi.core;
1717

18+
import am.ik.yavi.jsr305.Nullable;
19+
import am.ik.yavi.message.MessageFormatter;
20+
import am.ik.yavi.message.SimpleMessageFormatter;
1821
import java.util.Arrays;
1922
import java.util.Locale;
2023
import java.util.function.Function;
2124

22-
import am.ik.yavi.message.MessageFormatter;
23-
2425
public class ConstraintViolation {
2526

2627
private final Object[] args;
@@ -35,14 +36,26 @@ public class ConstraintViolation {
3536

3637
private final String name;
3738

38-
public ConstraintViolation(String name, String messageKey, String defaultMessageFormat, Object[] args,
39-
MessageFormatter messageFormatter, Locale locale) {
39+
/**
40+
* Creates a new constraint violation.
41+
* @param name the field or property name that this constraint violation applies to
42+
* @param messageKey the key used to look up localized messages
43+
* @param defaultMessageFormat the default message format to use when no localized
44+
* message is found
45+
* @param args the arguments to be used when formatting the message
46+
* @param messageFormatter the message formatter to be used
47+
* @param locale the locale to be used for message localization
48+
* @deprecated Use {@link #builder()} instead for a more fluent and type-safe API
49+
*/
50+
@Deprecated
51+
public ConstraintViolation(String name, String messageKey, String defaultMessageFormat, @Nullable Object[] args,
52+
@Nullable MessageFormatter messageFormatter, @Nullable Locale locale) {
4053
this.name = name;
4154
this.messageKey = messageKey;
4255
this.defaultMessageFormat = defaultMessageFormat;
43-
this.args = args;
44-
this.messageFormatter = messageFormatter;
45-
this.locale = locale;
56+
this.args = args == null ? new Object[0] : args;
57+
this.messageFormatter = messageFormatter == null ? SimpleMessageFormatter.getInstance() : messageFormatter;
58+
this.locale = locale == null ? Locale.getDefault() : locale;
4659
}
4760

4861
public Object[] args() {
@@ -84,6 +97,11 @@ public Object violatedValue() {
8497
}
8598

8699
/**
100+
* Returns a new ConstraintViolation with the name transformed using the provided
101+
* function. If the arguments array is not empty, the first element will be updated
102+
* with the new name.
103+
* @param rename the function to transform the current name
104+
* @return a new ConstraintViolation with the renamed name
87105
* @since 0.7.0
88106
*/
89107
public ConstraintViolation rename(Function<? super String, String> rename) {
@@ -97,10 +115,332 @@ public ConstraintViolation rename(Function<? super String, String> rename) {
97115
}
98116

99117
/**
118+
* Returns a new ConstraintViolation with the name indexed by appending "[index]" to
119+
* the current name.
120+
* @param index the index to append to the name
121+
* @return a new ConstraintViolation with the indexed name
100122
* @since 0.7.0
101123
*/
102124
public ConstraintViolation indexed(int index) {
103125
return this.rename(name -> name + "[" + index + "]");
104126
}
105127

128+
/**
129+
* Creates a new builder for constructing ConstraintViolation instances using a
130+
* fluent, type-safe API. This builder implements a staged builder pattern to enforce
131+
* required properties while making optional properties truly optional.
132+
* @return the first stage of the builder which requires setting the name property
133+
* @since 0.15.0
134+
*/
135+
public static StagedBuilders.Name builder() {
136+
return new Builder();
137+
}
138+
139+
/**
140+
* Implementation of the staged builder pattern for creating ConstraintViolation
141+
* instances. This builder class implements all the staged interfaces to provide a
142+
* fluent and type-safe way to construct ConstraintViolation objects.
143+
*
144+
* @since 0.15.0
145+
*/
146+
public static class Builder implements StagedBuilders.Name, StagedBuilders.MessageKey,
147+
StagedBuilders.DefaultMessageFormat, StagedBuilders.Optionals {
148+
149+
/** The field name that the constraint violation applies to */
150+
private String name;
151+
152+
/** The message key for internationalization */
153+
private String messageKey;
154+
155+
/** The default message format if no localized message is found */
156+
private String defaultMessageFormat;
157+
158+
/** The arguments to be used when formatting the message */
159+
private Object[] args;
160+
161+
/** The message formatter to be used for formatting the message */
162+
private MessageFormatter messageFormatter;
163+
164+
/** The locale to be used for message localization */
165+
private Locale locale;
166+
167+
/**
168+
* Private constructor to prevent direct instantiation. Use
169+
* {@link ConstraintViolation#builder()} instead.
170+
*/
171+
private Builder() {
172+
}
173+
174+
/**
175+
* Sets the name of the field or property that this constraint violation applies
176+
* to.
177+
* @param name the field or property name
178+
* @return the next stage of the builder for method chaining
179+
*/
180+
public Builder name(String name) {
181+
this.name = name;
182+
return this;
183+
}
184+
185+
/**
186+
* Sets the message key for internationalization lookup.
187+
* @param messageKey the key used to look up localized messages
188+
* @return the next stage of the builder for method chaining
189+
*/
190+
public Builder messageKey(String messageKey) {
191+
this.messageKey = messageKey;
192+
return this;
193+
}
194+
195+
/**
196+
* Sets the default message format to use when no localized message is found.
197+
* @param defaultMessageFormat the default message format string
198+
* @return the next stage of the builder for method chaining
199+
*/
200+
public Builder defaultMessageFormat(String defaultMessageFormat) {
201+
this.defaultMessageFormat = defaultMessageFormat;
202+
return this;
203+
}
204+
205+
/**
206+
* Sets the arguments to be used when formatting the message.
207+
* @param args the arguments to be used in message formatting
208+
* @return this builder for method chaining
209+
*/
210+
public Builder args(Object... args) {
211+
this.args = args;
212+
return this;
213+
}
214+
215+
/**
216+
* Sets the arguments to be used when formatting the message, automatically
217+
* including the name as the first argument.
218+
*
219+
* This method is more convenient than {@link #args(Object...)} when the name
220+
* needs to be the first argument in the message, which is a common pattern for
221+
* constraint violations.
222+
* @param args the arguments to be used in message formatting (excluding the name)
223+
* @return this builder for method chaining
224+
*/
225+
public Builder argsWithPrependedName(Object... args) {
226+
Object[] argsWithName = new Object[args.length + 1];
227+
argsWithName[0] = this.name;
228+
System.arraycopy(args, 0, argsWithName, 1, args.length);
229+
this.args = argsWithName;
230+
return this;
231+
}
232+
233+
/**
234+
* Sets the arguments to be used when formatting the message, automatically
235+
* prepending the name as the first argument and appending the violatedValue as
236+
* the last argument.
237+
*
238+
* This method provides a complete solution for the most common constraint
239+
* violation formatting pattern by automatically handling both the name and
240+
* violated value positioning.
241+
* @param args optional arguments to be used in message formatting (excluding both
242+
* name and violated value)
243+
* @param violatedValue the value object that violated the constraint, actual
244+
* value is retrieved by calling value() method
245+
* @return this builder for method chaining
246+
*/
247+
public Builder argsWithPrependedNameAndAppendedViolatedValue(Object[] args, ViolatedValue violatedValue) {
248+
Object[] completeArgs = new Object[args.length + 2];
249+
completeArgs[0] = this.name;
250+
System.arraycopy(args, 0, completeArgs, 1, args.length);
251+
completeArgs[args.length + 1] = violatedValue.value();
252+
this.args = completeArgs;
253+
return this;
254+
}
255+
256+
/**
257+
* Sets the message formatter to be used for message formatting. If not specified,
258+
* a default {@link SimpleMessageFormatter} will be used.
259+
* @param messageFormatter the message formatter to use
260+
* @return this builder for method chaining
261+
*/
262+
public Builder messageFormatter(MessageFormatter messageFormatter) {
263+
this.messageFormatter = messageFormatter;
264+
return this;
265+
}
266+
267+
/**
268+
* Sets the locale to be used for message localization. If not specified, the
269+
* system default locale will be used.
270+
* @param locale the locale to use for message localization
271+
* @return this builder for method chaining
272+
*/
273+
public Builder locale(Locale locale) {
274+
this.locale = locale;
275+
return this;
276+
}
277+
278+
/**
279+
* Builds a new {@link ConstraintViolation} instance with the configured
280+
* properties.
281+
* @return a new {@link ConstraintViolation} instance
282+
*/
283+
public ConstraintViolation build() {
284+
return new ConstraintViolation(name, messageKey, defaultMessageFormat, args, messageFormatter, locale);
285+
}
286+
287+
}
288+
289+
/**
290+
* Container interface for the staged builder interfaces. This interface hierarchy
291+
* enables a type-safe builder pattern that enforces required properties to be set in
292+
* a specific order before optional properties.
293+
*
294+
* @since 0.15.0
295+
*/
296+
public interface StagedBuilders {
297+
298+
/**
299+
* First stage of the builder which requires setting the name property.
300+
*/
301+
interface Name {
302+
303+
/**
304+
* Sets the name of the field or property that this constraint violation
305+
* applies to.
306+
* @param name the field or property name
307+
* @return the next stage of the builder which requires setting the message
308+
* key
309+
*/
310+
MessageKey name(String name);
311+
312+
}
313+
314+
/**
315+
* Second stage of the builder which requires setting the message key property.
316+
*/
317+
interface MessageKey {
318+
319+
String DEFAULT_MESSAGE_KEY = "_";
320+
321+
/**
322+
* Sets the message key for internationalization lookup.
323+
* @param messageKey the key used to look up localized messages
324+
* @return the next stage of the builder which requires setting the default
325+
* message format
326+
*/
327+
DefaultMessageFormat messageKey(String messageKey);
328+
329+
/**
330+
* Convenient shortcut builder method that creates a complete constraint
331+
* violation in a single call. This method automatically:
332+
* <ul>
333+
* <li>Sets the message key to the default value
334+
* ({@link #DEFAULT_MESSAGE_KEY})</li>
335+
* <li>Uses the provided message as the default message format</li>
336+
* <li>Prepends the field name as the first argument in the argument list</li>
337+
* <li>Builds and returns the final ConstraintViolation object</li>
338+
* </ul>
339+
*
340+
* <p>
341+
* This method is particularly useful for simple constraint violations where
342+
* complex message formatting or internationalization is not required.
343+
* @param message the message text to be used as the default message format
344+
* @return a fully constructed {@link ConstraintViolation} instance with the
345+
* specified message
346+
* @since 0.15.0
347+
*/
348+
default ConstraintViolation message(String message) {
349+
return this.messageKey(DEFAULT_MESSAGE_KEY)
350+
.defaultMessageFormat(message)
351+
.argsWithPrependedName()
352+
.build();
353+
}
354+
355+
}
356+
357+
/**
358+
* Third stage of the builder which requires setting the default message format
359+
* property.
360+
*/
361+
interface DefaultMessageFormat {
362+
363+
/**
364+
* Sets the default message format to use when no localized message is found.
365+
* @param defaultMessageFormat the default message format string
366+
* @return the final stage of the builder where all remaining properties are
367+
* optional
368+
*/
369+
Optionals defaultMessageFormat(String defaultMessageFormat);
370+
371+
}
372+
373+
/**
374+
* Final stage of the builder where all remaining properties are optional. The
375+
* build() method can be called at any point from this stage.
376+
*/
377+
interface Optionals {
378+
379+
/**
380+
* Sets the arguments to be used when formatting the message.
381+
* @param args the arguments to be used in message formatting
382+
* @return this builder for method chaining
383+
*/
384+
Optionals args(Object... args);
385+
386+
/**
387+
* Sets the arguments to be used when formatting the message, automatically
388+
* including the name as the first argument.
389+
*
390+
* This method is more convenient than {@link #args(Object...)} when the name
391+
* needs to be the first argument in the message, which is a common pattern
392+
* for constraint violations.
393+
* @param args the arguments to be used in message formatting (excluding the
394+
* name)
395+
* @return this builder for method chaining
396+
*/
397+
Optionals argsWithPrependedName(Object... args);
398+
399+
/**
400+
* Sets the arguments to be used when formatting the message, automatically
401+
* prepending the name as the first argument and appending the violatedValue
402+
* as the last argument.
403+
*
404+
* This method provides a complete solution for the most common constraint
405+
* violation formatting pattern by automatically handling both the name and
406+
* violated value positioning.
407+
* @param args optional arguments to be used in message formatting (excluding
408+
* both name and violated value)
409+
* @param violatedValue the value object that violated the constraint, actual
410+
* value is retrieved by calling value() method
411+
* @return this builder for method chaining
412+
*/
413+
Optionals argsWithPrependedNameAndAppendedViolatedValue(Object[] args, ViolatedValue violatedValue);
414+
415+
/**
416+
* Sets the message formatter to be used for message formatting. If not
417+
* prepending the name as the first argument and appending the violatedValue
418+
* as the last argument.
419+
*
420+
* This method provides a complete solution for the most common constraint
421+
* violation formatting pattern by automatically handling both the name and
422+
* violated value positioning.
423+
* @return this builder for method chaining
424+
*/
425+
Optionals messageFormatter(MessageFormatter messageFormatter);
426+
427+
/**
428+
* Sets the locale to be used for message localization. If not specified, the
429+
* system default locale will be used.
430+
* @param locale the locale to use for message localization
431+
* @return this builder for method chaining
432+
*/
433+
Optionals locale(Locale locale);
434+
435+
/**
436+
* Builds a new {@link ConstraintViolation} instance with the configured
437+
* properties.
438+
* @return a new {@link ConstraintViolation} instance
439+
*/
440+
ConstraintViolation build();
441+
442+
}
443+
444+
}
445+
106446
}

0 commit comments

Comments
 (0)