SemanticPlanRelation.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan.NodeKey;

/**
 * One ordered directed relation between two stable semantic-plan nodes.
 *
 * <p>Relations are stored in plan order and may occur more than once. This is
 * intentional: suite construction and runtime trees can contain duplicate test
 * occurrences, while relation attributes can record order, matrix coordinates
 * or other contract-specific data without adding project concepts to the core
 * model.</p>
 */
public record SemanticPlanRelation(String kind, NodeKey source, NodeKey target,
		Map<String, SemanticPlanValue> attributes) {

	/** Creates a defensive immutable relation. */
	public SemanticPlanRelation {
		kind= requireName(kind, "Relation kind"); //$NON-NLS-1$
		Objects.requireNonNull(source);
		Objects.requireNonNull(target);
		Map<String, SemanticPlanValue> copy= new LinkedHashMap<>();
		if (attributes != null) {
			attributes.forEach((name, value) -> copy.put(requireName(name, "Relation attribute"), //$NON-NLS-1$
					Objects.requireNonNull(value)));
		}
		attributes= Map.copyOf(copy);
	}

	/** Creates a relation without attributes. */
	public SemanticPlanRelation(String kind, NodeKey source, NodeKey target) {
		this(kind, source, target, Map.of());
	}

	/** Returns one typed relation attribute. */
	public Optional<SemanticPlanValue> attribute(String name) {
		return Optional.ofNullable(attributes.get(name));
	}

	/** Returns whether an attribute equals the supplied typed value. */
	public boolean hasAttribute(String name, SemanticPlanValue expected) {
		return expected != null && expected.equals(attributes.get(name));
	}

	private static String requireName(String value, String label) {
		if (value == null || value.isBlank()) {
			throw new IllegalArgumentException(label + " must not be blank"); //$NON-NLS-1$
		}
		return value.trim();
	}
}