RegistryIndex.java
package network.ike.docs.plugin.diff;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* One side's view of the IKE topic registry: topic id → source file and
* assembly id → flattened topic-refs. This is what lets an assembly
* module's {@code idoc:diff} run <em>project</em> the corpus diff onto
* its own membership (ike-issues#649 scoping decision) — the registry,
* not include-line parsing, is the source of truth for which topics an
* assembly contains.
*
* <p>Also hosts the registry YAML shape helpers shared with
* {@link RegistryDelta}.
*/
public final class RegistryIndex {
private final Map<String, String> topicFilesById;
private final Map<String, List<String>> assemblyRefsById;
private RegistryIndex(Map<String, String> topicFilesById,
Map<String, List<String>> assemblyRefsById) {
this.topicFilesById = topicFilesById;
this.assemblyRefsById = assemblyRefsById;
}
/**
* Load the registry as it stands on one side of a comparison.
*
* @param git repository access
* @param ref the side to read ({@link GitSource#WORKTREE}
* or a committish) — projections use the to
* side, so membership reflects the state under
* review
* @param registryRoot repository-relative source root that contains
* {@code topic-registry.yaml} and
* {@code topic-registry/}
* @return the index (empty maps when no registry exists on that side)
* @throws IOException on repository access failure
*/
public static RegistryIndex load(GitSource git, String ref, String registryRoot)
throws IOException {
Map<String, String> topics = new LinkedHashMap<>();
for (String path : git.listYaml(ref, registryRoot + "/topic-registry")) {
if (path.endsWith("/assemblies.yaml")) {
continue;
}
topicsOf(parse(git.read(ref, path))).forEach((id, entry) ->
topics.put(id, String.valueOf(entry.get("file"))));
}
Map<String, List<String>> assemblies =
assemblyRefsOf(parse(git.read(ref, registryRoot + "/topic-registry/assemblies.yaml")));
return new RegistryIndex(topics, assemblies);
}
/**
* Resolve a topic id to its source file, relative to the registry
* root (e.g. {@code topics/arch/asg-substrate.adoc}).
*
* @param topicId the topic id
* @return the registry-root-relative file, or {@code null} when the
* id is unknown
*/
public String topicFile(String topicId) {
return topicFilesById.get(topicId);
}
/**
* An assembly's flattened topic-refs, in document order.
*
* @param assemblyId the assembly id (by IKE naming convention equal
* to the module's artifactId)
* @return the topic ids, or {@code null} when the assembly is not
* registered
*/
public List<String> assemblyRefs(String assemblyId) {
return assemblyRefsById.get(assemblyId);
}
/**
* Render one assembly's membership delta (topic-refs added and
* removed between two sides) as a small registry-delta partial for
* an assembly-projection packet.
*
* @param git repository access
* @param fromRef the from side
* @param toRef the to side (may be {@link GitSource#WORKTREE})
* @param registryRoot the registry source root
* @param assemblyId the assembly to report on
* @return the partial's text with a level-1 title, or an empty
* string when this assembly's membership is unchanged
* @throws IOException on repository access failure
*/
public static String membershipDelta(GitSource git, String fromRef, String toRef,
String registryRoot, String assemblyId)
throws IOException {
String path = registryRoot + "/topic-registry/assemblies.yaml";
List<String> oldRefs = assemblyRefsOf(parse(git.read(fromRef, path)))
.getOrDefault(assemblyId, List.of());
List<String> newRefs = assemblyRefsOf(parse(git.read(toRef, path)))
.getOrDefault(assemblyId, List.of());
List<String> plus = newRefs.stream().filter(r -> !oldRefs.contains(r)).toList();
List<String> minus = oldRefs.stream().filter(r -> !newRefs.contains(r)).toList();
if (plus.isEmpty() && minus.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder("[[registry-delta]]\n= Membership Delta — ")
.append(assemblyId).append("\n\nTopic-refs changed for this assembly between ")
.append(fromRef).append(" and ")
.append(GitSource.WORKTREE.equals(toRef) ? "the working tree" : toRef)
.append(".\n\n");
plus.forEach(r -> sb.append("* added `").append(r).append("`\n"));
minus.forEach(r -> sb.append("* removed `").append(r).append("`\n"));
sb.append('\n');
return sb.toString();
}
/**
* Parse one YAML document with safe construction (registry files
* contain dates).
*
* @param text the YAML text, possibly {@code null}
* @return the parsed root, or {@code null} for absent/blank input
*/
static Object parse(String text) {
if (text == null || text.isBlank()) {
return null;
}
return new Yaml(new SafeConstructor(new LoaderOptions())).load(text);
}
/**
* Extract a per-domain registry file's topics keyed by id. Accepts
* both the indented-fragment shape (a top-level sequence of one
* domain) and a root map with a {@code domains} list.
*
* @param root the parsed YAML root
* @return topic entries keyed by id (insertion-ordered)
*/
@SuppressWarnings("unchecked")
static Map<String, Map<String, Object>> topicsOf(Object root) {
Map<String, Map<String, Object>> out = new LinkedHashMap<>();
List<?> domains = root instanceof List<?> l ? l
: root instanceof Map<?, ?> m && m.get("domains") instanceof List<?> l2 ? l2
: List.of();
for (Object d : domains) {
if (d instanceof Map<?, ?> dm && dm.get("topics") instanceof List<?> ts) {
for (Object t : ts) {
if (t instanceof Map<?, ?> tm && tm.get("id") != null) {
out.put(String.valueOf(tm.get("id")), (Map<String, Object>) tm);
}
}
}
}
return out;
}
/**
* Extract every assembly's flattened topic-refs from a parsed
* {@code assemblies.yaml}. Accepts both a bare sequence and a root
* map with an {@code assemblies} list.
*
* @param root the parsed YAML root
* @return flattened refs keyed by assembly id
*/
static Map<String, List<String>> assemblyRefsOf(Object root) {
Map<String, List<String>> out = new LinkedHashMap<>();
Object assemblies = root instanceof Map<?, ?> m && m.get("assemblies") != null
? m.get("assemblies") : root;
if (assemblies instanceof List<?> list) {
for (Object a : list) {
if (a instanceof Map<?, ?> am && am.get("id") != null) {
List<String> refs = new ArrayList<>();
collectRefs(am.get("sections"), refs);
out.put(String.valueOf(am.get("id")), refs);
}
}
}
return out;
}
private static void collectRefs(Object sections, List<String> refs) {
if (sections instanceof List<?> list) {
for (Object s : list) {
if (s instanceof Map<?, ?> sm) {
if (sm.get("topic-refs") instanceof List<?> tr) {
tr.forEach(r -> refs.add(String.valueOf(r)));
}
collectRefs(sm.get("sections"), refs);
}
}
}
}
}