diff --git a/api/src/main/groovy/com/jessebrault/ssg/di/SsgNamedRegistryExtension.java b/api/src/main/groovy/com/jessebrault/ssg/di/SsgNamedRegistryExtension.java new file mode 100644 index 0000000..2175188 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/di/SsgNamedRegistryExtension.java @@ -0,0 +1,229 @@ +package com.jessebrault.ssg.di; + +import groowt.util.di.*; +import jakarta.inject.Named; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class SsgNamedRegistryExtension implements NamedRegistryExtension { + + protected static void checkName(String name) { + if (name.startsWith(":") || name.endsWith(":")) { + throw new IllegalArgumentException( + "Illegal ssg @Named format: cannot start or end with colon (':'); given: " + name + ); + } + } + + protected static @Nullable String getPrefix(String fullName) { + final var firstColon = fullName.indexOf(":"); + if (firstColon == -1) { + return null; + } else { + return fullName.substring(0, firstColon); + } + } + + protected static String getAfterPrefix(String fullName) { + final int firstColon = fullName.indexOf(":"); + if (firstColon == -1) { + return fullName; + } else { + return fullName.substring(firstColon + 1); + } + } + + protected static boolean hasPrefix(String fullName) { + return fullName.contains(":"); + } + + protected static String getSimplePrefix(Class dependencyClass) { + final String simpleTypeName = dependencyClass.getSimpleName(); + final String simpleTypeNameStart = simpleTypeName.substring(0, 1).toLowerCase(); + final String uncapitalizedSimpleTypeName; + if (simpleTypeName.length() > 1) { + uncapitalizedSimpleTypeName = simpleTypeNameStart + simpleTypeName.substring(1); + } else { + uncapitalizedSimpleTypeName = simpleTypeNameStart; + } + return uncapitalizedSimpleTypeName; + } + + protected static String getCanonicalPrefix(Class dependencyClass) { + return dependencyClass.getName(); + } + + public static class SsgNamedQualifierHandler implements QualifierHandler { + + private final SsgNamedRegistryExtension extension; + + public SsgNamedQualifierHandler(SsgNamedRegistryExtension extension) { + this.extension = extension; + } + + @Override + public @Nullable Binding handle(Named annotation, Class dependencyClass) { + return this.extension.getBinding(new SimpleKeyHolder<>( + SsgNamedRegistryExtension.class, + dependencyClass, + annotation.value() + )); + } + + } + + public static class SsgNamedWithPrefixKeyHolder implements KeyHolder { + + private final Class dependencyType; + private final @Nullable String prefix; + private final String afterPrefix; + + public SsgNamedWithPrefixKeyHolder( + Class dependencyType, + @Nullable String prefix, + String afterPrefix + ) { + this.dependencyType = dependencyType; + this.afterPrefix = afterPrefix; + this.prefix = prefix; + } + + @Override + public Class binderType() { + return NamedRegistryExtension.class; + } + + @Override + public Class type() { + return this.dependencyType; + } + + @Override + public String key() { + return this.afterPrefix; + } + + public @Nullable String prefix() { + return this.prefix; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj instanceof SsgNamedWithPrefixKeyHolder other) { + return this.dependencyType.equals(other.type()) + && this.key().equals(other.key()) + && Objects.equals(this.prefix(), other.prefix()); + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = dependencyType.hashCode(); + result = 31 * result + afterPrefix.hashCode(); + result = 31 * result + Objects.hashCode(prefix); + return result; + } + + } + + private final Map, Binding> bindings = new HashMap<>(); + private final QualifierHandler qualifierHandler = this.getNamedQualifierHandler(); + + protected QualifierHandler getNamedQualifierHandler() { + return new SsgNamedQualifierHandler(this); + } + + @Override + public Class getKeyClass() { + return String.class; + } + + @Override + public , T> void bind( + KeyHolder keyHolder, + Consumer> configure + ) { + final var configurator = new SimpleBindingConfigurator<>(keyHolder.type()); + configure.accept(configurator); + final String fullName = keyHolder.key(); + checkName(fullName); + if (hasPrefix(fullName)) { + this.bindings.put(new SsgNamedWithPrefixKeyHolder<>( + keyHolder.type(), + getPrefix(fullName), + getAfterPrefix(fullName) + ), configurator.getBinding()); + } else { + this.bindings.put(new SimpleKeyHolder<>( + NamedRegistryExtension.class, + keyHolder.type(), + keyHolder.key() + ), configurator.getBinding()); + } + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable , T> Binding getBinding( + KeyHolder keyHolder + ) { + final String fullName = keyHolder.key(); + checkName(fullName); + if (hasPrefix(fullName)) { + if (keyHolder instanceof SsgNamedWithPrefixKeyHolder && this.bindings.containsKey(keyHolder)) { + return (Binding) this.bindings.get(keyHolder); + } else { + final String afterPrefix = getAfterPrefix(fullName); + final Class type = keyHolder.type(); + + final @Nullable Binding withSimple = (Binding) this.bindings.get( + new SsgNamedWithPrefixKeyHolder<>(type, afterPrefix, getSimplePrefix(type)) + ); + + if (withSimple != null) { + return withSimple; + } + + return (Binding) this.bindings.get(new SsgNamedWithPrefixKeyHolder<>( + type, afterPrefix, getCanonicalPrefix(type) + )); + } + } else { + return (Binding) this.bindings.getOrDefault(keyHolder, null); + } + } + + @Override + public , T> void removeBinding(KeyHolder keyHolder) { + throw new UnsupportedOperationException(); + } + + @Override + public , T> void removeBindingIf( + KeyHolder keyHolder, + Predicate> filter + ) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearAllBindings() { + this.bindings.clear(); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + return (QualifierHandler) this.qualifierHandler; + } + +}