PersonValueObject.java

package fr.univtln.bruno.samples.java101.tp1.immutable;

import java.util.Objects;

/**
 * Immutable value object representing a person.
 *
 * <p>Instances of this record are immutable. Modification methods (withX/increment/merge)
 * return new instances instead of mutating the current one.</p>
 *
 * <p>Components:
 * <ul>
 *   <li>{@code id} - unique identifier, must be non-null</li>
 *   <li>{@code name} - person name, must be non-null</li>
 *   <li>{@code email} - contact email, must be non-null</li>
 *   <li>{@code age} - non-negative integer representing age</li>
 * </ul>
 *
 * <p>Invariants are validated in the compact constructor.</p>
 *
 * @param id non-null unique identifier
 * @param name non-null person name
 * @param email non-null contact email
 * @param age non-negative integer representing age
 */
public record PersonValueObject(String id, String name, String email, int age) {
    /**
     * Compact canonical constructor performing basic validation.
     *
     * @param id non-null unique identifier
     * @param name non-null person name
     * @param email non-null contact email
     * @param age non-negative integer age
     * @throws NullPointerException if {@code id}, {@code name} or {@code email} is null
     * @throws IllegalArgumentException if {@code age} is negative
     */
    public PersonValueObject {
        Objects.requireNonNull(id, "id must not be null");
        Objects.requireNonNull(name, "name must not be null");
        Objects.requireNonNull(email, "email must not be null");
        if (age < 0) throw new IllegalArgumentException("age must be >= 0");
    }

    /**
     * Factory method to create a new PersonRecord.
     *
     * @param id non-null unique identifier
     * @param name non-null person name
     * @param email non-null contact email
     * @param age non-negative age
     * @return a new {@code PersonRecord} instance
     * @throws NullPointerException if {@code id}, {@code name} or {@code email} is null
     * @throws IllegalArgumentException if {@code age} is negative
     */
    public static PersonValueObject of(String id, String name, String email, int age) {
        return new PersonValueObject(id, name, email, age);
    }

    /**
     * Return a new instance with the given name.
     *
     * @param newName non-null new name
     * @return a new {@code PersonRecord} with {@code name} set to {@code newName}
     * @throws NullPointerException if {@code newName} is null
     */
    public PersonValueObject withName(String newName) {
        return new PersonValueObject(this.id, Objects.requireNonNull(newName, "name must not be null"), this.email, this.age);
    }

    /**
     * Return a new instance with the given email.
     *
     * @param newEmail non-null new email
     * @return a new {@code PersonRecord} with {@code email} set to {@code newEmail}
     * @throws NullPointerException if {@code newEmail} is null
     */
    public PersonValueObject withEmail(String newEmail) {
        return new PersonValueObject(this.id, this.name, Objects.requireNonNull(newEmail, "email must not be null"), this.age);
    }

    /**
     * Return a new instance with the given age.
     *
     * @param newAge non-negative age
     * @return a new {@code PersonRecord} with {@code age} set to {@code newAge}
     * @throws IllegalArgumentException if {@code newAge} is negative
     */
    public PersonValueObject withAge(int newAge) {
        if (newAge < 0) throw new IllegalArgumentException("age must be >= 0");
        return new PersonValueObject(this.id, this.name, this.email, newAge);
    }

    /**
     * Return a new instance with the age incremented by one.
     *
     * @return a new {@code PersonRecord} with {@code age} = current age + 1
     */
    public PersonValueObject incrementAge() {
        return new PersonValueObject(this.id, this.name, this.email, this.age + 1);
    }

    /**
     * Merge non-null fields from another record into this one and return the result.
     *
     * <p>Behavior:
     * <ul>
     *   <li>If {@code other} is {@code null}, returns {@code this}.</li>
     *   <li>For {@code name} and {@code email}, non-null values from {@code other} replace current values.</li>
     *   <li>For {@code age}, {@code other.age} is used if it is >= 0; otherwise current age is retained.</li>
     * </ul>
     *
     * @param other another {@code PersonRecord} to merge from, may be null
     * @return a new {@code PersonRecord} representing the merged result
     */
    public PersonValueObject merge(PersonValueObject other) {
        if (other == null) return this;
        String mergedName = other.name != null ? other.name : this.name;
        String mergedEmail = other.email != null ? other.email : this.email;
        int mergedAge = other.age >= 0 ? other.age : this.age; // treat negative age as "absent"
        return new PersonValueObject(this.id, mergedName, mergedEmail, mergedAge);
    }
}