SourceContent.java

package org.egothor.methodatlas.api;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Lazy provider of the source text for a test class.
 *
 * <p>
 * Each {@link DiscoveredMethod} carries one instance that is shared by all
 * methods of the same class. The content is accessed on demand by the
 * orchestration layer (e.g. for AI analysis or content hashing) and is never
 * fetched when those features are disabled.
 * </p>
 *
 * <p>
 * Platform-specific {@link TestDiscovery} implementations supply the content
 * in whatever way is natural for that platform: a file read, an in-memory
 * string captured during AST traversal, etc.
 * </p>
 *
 * @since 3.0.0
 */
@FunctionalInterface
public interface SourceContent {

    /**
     * Returns the source text of the enclosing test class.
     *
     * @return source text, or {@link Optional#empty()} when the source is
     *         unavailable (e.g. the scanner operates on compiled artifacts)
     */
    Optional<String> get();

    /**
     * Returns a {@code SourceContent} that lazily reads {@code file} as UTF-8 on
     * first access and caches the outcome for every subsequent call.
     *
     * <p>
     * The first {@link #get()} reads the file; the result — the text on success,
     * or {@link Optional#empty()} if the file cannot be read — is memoised, so
     * later calls neither re-read the file nor observe changes made to it after
     * the first read. This gives a stable, read-once view to all consumers
     * (content hashing, AI prompt assembly, …) of the same class, which is the
     * behaviour every bundled discovery plugin relies on. The returned instance
     * is safe to call from multiple threads; the file is read at most once.
     * </p>
     *
     * @param file path of the source file to read; must not be {@code null}
     * @return a caching, lazy {@code SourceContent} for {@code file}; never
     *         {@code null}
     * @throws NullPointerException if {@code file} is {@code null}
     * @since 4.0.0
     */
    static SourceContent ofFile(final Path file) {
        Objects.requireNonNull(file, "file");
        final AtomicReference<Optional<String>> cache = new AtomicReference<>();
        return () -> {
            Optional<String> value = cache.get();
            if (value == null) {
                Optional<String> read;
                try {
                    read = Optional.of(Files.readString(file));
                } catch (IOException e) {
                    read = Optional.empty();
                }
                cache.compareAndSet(null, read);
                value = cache.get();
            }
            return value;
        };
    }
}