Refactored and added documentation for view-components.

This commit is contained in:
JesseBrault0709 2024-05-04 16:10:07 +02:00
parent ea4c29f1d7
commit d05b4f4c0f
14 changed files with 182 additions and 32 deletions

View File

@ -2,18 +2,52 @@ package groowt.view.component;
import groovy.lang.Closure; import groovy.lang.Closure;
final class ClosureComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> { import static groowt.view.component.ComponentFactoryUtil.flatten;
final class ClosureComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
private final Closure<T> closure; private final Closure<T> closure;
private final Class<?> firstParamType;
public ClosureComponentFactory(Closure<T> closure) { public ClosureComponentFactory(Closure<T> closure) {
this.closure = closure; this.closure = closure;
if (this.closure.getParameterTypes().length < 2) {
throw new IllegalArgumentException(
"Closures for " + getClass().getName() + " require at least two parameters"
);
}
this.firstParamType = this.closure.getParameterTypes()[0];
if (this.firstParamType != Object.class
&& !(this.firstParamType == String.class || this.firstParamType == Class.class)) {
throw new IllegalArgumentException(
"The first closure parameter must be any of type Object (i.e, dynamic), String, or Class"
);
}
final var secondParamType = this.closure.getParameterTypes()[1];
if (secondParamType != Object.class && !ComponentContext.class.isAssignableFrom(secondParamType)) {
throw new IllegalArgumentException(
"The second closure parameter must be of type Object (i.e., dynamic) or " +
"ComponentContext or a subclass thereof."
);
}
} }
@Override @Override
public T create(Object type, ComponentContext componentContext, Object... args) { public T create(String type, ComponentContext componentContext, Object... args) {
final Object[] flattened = ComponentFactoryUtil.flatten(type, componentContext, args); if (this.firstParamType != Object.class && this.firstParamType != String.class) {
return this.closure.call(flattened); throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " +
"with a String component type argument.");
}
return this.closure.call(flatten(type, componentContext, args));
}
@Override
public T create(Class<?> type, ComponentContext componentContext, Object... args) {
if (this.firstParamType != Object.class && this.firstParamType != Class.class) {
throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " +
"with a Class component type argument.");
}
return this.closure.call(flatten(type, componentContext, args));
} }
} }

View File

@ -10,20 +10,36 @@ import java.util.function.Predicate;
public interface ComponentContext { public interface ComponentContext {
/**
* For use only by compiled templates.
*/
@ApiStatus.Internal @ApiStatus.Internal
interface Resolved { interface Resolved {
String getTypeName(); String getTypeName();
ComponentFactory<?> getComponentFactory(); ComponentFactory<?> getComponentFactory();
} }
/**
* For use only by compiled templates.
*/
@ApiStatus.Internal @ApiStatus.Internal
Resolved resolve(String component); Resolved resolve(String component);
/**
* For use only by compiled templates.
*/
@ApiStatus.Internal @ApiStatus.Internal
ViewComponent create(Resolved resolved, Object... args); ViewComponent create(Resolved resolved, Object... args);
/**
* For use only by compiled templates.
*/
@ApiStatus.Internal
void beforeComponentRender(ViewComponent component); void beforeComponentRender(ViewComponent component);
/**
* For use only by compiled templates.
*/
@ApiStatus.Internal @ApiStatus.Internal
void afterComponentRender(ViewComponent component); void afterComponentRender(ViewComponent component);

View File

@ -1,13 +1,25 @@
package groowt.view.component; package groowt.view.component;
/**
* An exception which signals that a component of the given type
* could not be created in the given template.
*/
public class ComponentCreateException extends RuntimeException { public class ComponentCreateException extends RuntimeException {
private final Object componentType;
private final ComponentTemplate template; private final ComponentTemplate template;
private final int line; private final int line;
private final int column; private final int column;
public ComponentCreateException(ComponentTemplate template, int line, int column, Throwable cause) { public ComponentCreateException(
Object componentType,
ComponentTemplate template,
int line,
int column,
Throwable cause
) {
super(cause); super(cause);
this.componentType = componentType;
this.template = template; this.template = template;
this.line = line; this.line = line;
this.column = column; this.column = column;
@ -15,8 +27,8 @@ public class ComponentCreateException extends RuntimeException {
@Override @Override
public String getMessage() { public String getMessage() {
return "Exception while rendering " + this.template.getClass() return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() +
+ " at line " + this.line + ", column " + this.column + "."; " at line " + this.line + ", column " + this.column + ".";
} }
} }

View File

@ -5,8 +5,23 @@ import groovy.lang.GroovyObject;
import java.util.function.Supplier; import java.util.function.Supplier;
public interface ComponentFactory<T extends ViewComponent> extends GroovyObject { @FunctionalInterface
public interface ComponentFactory<T extends ViewComponent> {
/**
* @param closure A closure with the following signature:
* <p>
* {@code Object componentType, ComponentContext context, ... -> T }
* <p>
* where '{@code ...}' represents any additional parameters (or none).
* <p>
* The first two parameters are not optional and must be present
* or else this method will not work. The first parameter may be either
* a {@link String} or a {@link Class}.
*
* @return A factory which will create type {@code T}.
* @param <T> The desired {@link ViewComponent} type.
*/
static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) { static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) {
return new ClosureComponentFactory<>(closure); return new ClosureComponentFactory<>(closure);
} }
@ -15,6 +30,10 @@ public interface ComponentFactory<T extends ViewComponent> extends GroovyObject
return new SupplierComponentFactory<>(supplier); return new SupplierComponentFactory<>(supplier);
} }
T create(Object type, ComponentContext componentContext, Object... args); T create(String typeName, ComponentContext componentContext, Object... args);
default T create(Class<?> type, ComponentContext componentContext, Object... args) {
return this.create(type.getName(), componentContext, args);
}
} }

View File

@ -1,19 +1,53 @@
package groowt.view.component; package groowt.view.component;
import groovy.lang.GroovyObjectSupport; import groovy.lang.GroovyObjectSupport;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod; import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException; import groovy.lang.MissingMethodException;
import java.util.*; import java.util.HashMap;
import java.util.Map;
public abstract class AbstractComponentFactory<T extends ViewComponent> extends GroovyObjectSupport /**
* This class can be used to create custom implementations {@link ComponentFactory}.
*
* @implSpec All implementations must simply provide one or more {@code doCreate()} methods,
* which will be found by this class via the Groovy meta object protocol. The method(s) may
* have any of the following signatures:
* <ul>
* <li>{@code String | Class, ComponentContext, ... -> T}</li>
* <li>{@code ComponentContext, ... -> T}</li>
* <li>{@code String | Class, ... -> T}</li>
* <li>{@code ... -> }</li>
* </ul>
* where '{@code ...}' represents zero or more additional arguments.
*
* @implNote In most cases, the implementation does not need to consume the
* {@link ComponentContext} argument, as the compiled template is required to contain
* {@code component.setContext(context)} statements following the component
* creation call. However, if the component <strong>needs</strong> the context
* (for example, to do custom scope logic), then it is more than okay to consume it.
*
* @implNote In the case Web View Components, the first additional argument will be
* a {@link Map} containing the attributes of the component, followed by any additional
* component constructor args.
*
* @param <T> The type of the ViewComponent produced by this factory.
*/
public abstract class ComponentFactoryBase<T extends ViewComponent> extends GroovyObjectSupport
implements ComponentFactory<T> { implements ComponentFactory<T> {
protected static final String DO_CREATE = "doCreate";
protected static MetaMethod findDoCreateMethod(MetaClass metaClass, Class<?>[] types) {
return metaClass.getMetaMethod(DO_CREATE, types);
}
protected final Map<Class<?>[], MetaMethod> cache = new HashMap<>(); protected final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
protected MetaMethod findDoCreateMethod(Object[] allArgs) { protected MetaMethod findDoCreateMethod(Object[] allArgs) {
return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types -> return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types ->
ComponentFactoryUtil.findDoCreateMethod(this.getMetaClass(), types) findDoCreateMethod(this.getMetaClass(), types)
); );
} }
@ -54,15 +88,16 @@ public abstract class AbstractComponentFactory<T extends ViewComponent> extends
return (T) argsOnlyMethod.invoke(this, args); return (T) argsOnlyMethod.invoke(this, args);
} }
throw new MissingMethodException( throw new MissingMethodException(DO_CREATE, this.getClass(), args);
ComponentFactoryUtil.DO_CREATE,
this.getClass(),
args
);
} }
@Override @Override
public T create(Object type, ComponentContext componentContext, Object... args) { public T create(String type, ComponentContext componentContext, Object... args) {
return this.findAndDoCreate(type, componentContext, args);
}
@Override
public T create(Class<?> type, ComponentContext componentContext, Object... args) {
return this.findAndDoCreate(type, componentContext, args); return this.findAndDoCreate(type, componentContext, args);
} }

View File

@ -1,7 +1,9 @@
package groowt.view.component; package groowt.view.component;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass; import groovy.lang.MetaClass;
import groovy.lang.MetaMethod; import groovy.lang.MetaMethod;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -9,7 +11,6 @@ import java.util.List;
public final class ComponentFactoryUtil { public final class ComponentFactoryUtil {
public static final String DO_CREATE = "doCreate";
public static final Class<?>[] EMPTY_CLASSES = new Class[0]; public static final Class<?>[] EMPTY_CLASSES = new Class[0];
public static Object[] flatten(Object... args) { public static Object[] flatten(Object... args) {
@ -18,7 +19,7 @@ public final class ComponentFactoryUtil {
} else { } else {
final List<Object> result = new ArrayList<>(args.length); final List<Object> result = new ArrayList<>(args.length);
for (final var arg : args) { for (final var arg : args) {
if (arg instanceof Object[] arr) { if (arg instanceof Object[] arr && arr.length > 0) {
result.addAll(Arrays.asList(arr)); result.addAll(Arrays.asList(arr));
} else { } else {
result.add(arg); result.add(arg);
@ -44,10 +45,6 @@ public final class ComponentFactoryUtil {
return result; return result;
} }
public static MetaMethod findDoCreateMethod(MetaClass metaClass, Class<?>[] types) {
return metaClass.getMetaMethod(DO_CREATE, types);
}
private ComponentFactoryUtil() {} private ComponentFactoryUtil() {}
} }

View File

@ -1,5 +1,8 @@
package groowt.view.component; package groowt.view.component;
/**
* An exception which represents an error during rendering of a component.
*/
public class ComponentRenderException extends RuntimeException { public class ComponentRenderException extends RuntimeException {
private static String formatMessage(int line, int column) { private static String formatMessage(int line, int column) {

View File

@ -2,6 +2,9 @@ package groowt.view.component;
import java.io.Reader; import java.io.Reader;
/**
* Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation.
*/
public class ComponentTemplateCreateException extends RuntimeException { public class ComponentTemplateCreateException extends RuntimeException {
private final Class<? extends ViewComponent> forClass; private final Class<? extends ViewComponent> forClass;

View File

@ -1,5 +1,9 @@
package groowt.view.component; package groowt.view.component;
/**
* An exception which represents that a component type could not be
* found by the {@link ComponentContext}.
*/
public abstract class MissingComponentException extends RuntimeException { public abstract class MissingComponentException extends RuntimeException {
private final ComponentTemplate template; private final ComponentTemplate template;

View File

@ -2,7 +2,7 @@ package groowt.view.component;
import java.util.function.Supplier; import java.util.function.Supplier;
final class SupplierComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> { final class SupplierComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> {
private final Supplier<T> tSupplier; private final Supplier<T> tSupplier;

View File

@ -9,9 +9,24 @@ public interface ViewComponent extends View {
return this.getClass().getName(); return this.getClass().getName();
} }
@ApiStatus.Internal /**
* <em>Note:</em> compiled templates are required to automatically
* call this method after the component is constructed. One
* only needs to use this if doing custom rendering logic
* where the component is not rendered inside a compiled
* template.
*/
void setContext(ComponentContext context); void setContext(ComponentContext context);
/**
* <em>Note:</em> compiled templates call the
* related {@link #setContext} method <strong>after</strong>
* the component is instantiated. If one needs access to the
* {@link ComponentContext} in the component constructor,
* ask for it in the constructor parameters.
*
* @return the component context
*/
ComponentContext getContext(); ComponentContext getContext();
} }

View File

@ -2,7 +2,6 @@ package groowt.view.web.lib
import groowt.view.StandardGStringTemplateView import groowt.view.StandardGStringTemplateView
import groowt.view.View import groowt.view.View
import groowt.view.component.AbstractComponentFactory
import groowt.view.component.ComponentContext import groowt.view.component.ComponentContext
import groowt.view.component.ComponentFactory import groowt.view.component.ComponentFactory
import groowt.view.web.WebViewChildComponentRenderer import groowt.view.web.WebViewChildComponentRenderer
@ -34,10 +33,7 @@ class Echo extends DelegatingWebViewComponent {
} }
@Override @Override
Echo create(Object type, ComponentContext componentContext, Object... args) { Echo create(String type, ComponentContext componentContext, Object... args) {
if (!type instanceof String) {
throw new IllegalArgumentException('<Echo> can only be used with String types.')
}
if (args == null || args.length < 1) { if (args == null || args.length < 1) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
'<Echo> must have at least one attribute. ' + '<Echo> must have at least one attribute. ' +
@ -47,6 +43,11 @@ class Echo extends DelegatingWebViewComponent {
this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo
} }
@Override
Echo create(Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException('<Echo> can only be used with String types.')
}
} }
String typeName String typeName

View File

@ -341,11 +341,22 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName); final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName);
final VariableExpression exceptionVar = new VariableExpression(exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName);
final ConstantExpression componentTypeExpression = switch (componentNode) {
case TypedComponentNode typedComponentNode -> switch (typedComponentNode.getArgs().getType()) {
case StringComponentTypeNode stringComponentTypeNode ->
makeStringLiteral(stringComponentTypeNode.getIdentifier());
case ClassComponentTypeNode classComponentTypeNode ->
makeStringLiteral(classComponentTypeNode.getFullyQualifiedName());
};
case FragmentComponentNode ignored -> makeStringLiteral(FRAGMENT_FQN);
};
final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition()); final var lineAndColumn = lineAndColumn(componentNode.getTokenRange().getStartPosition());
final ConstructorCallExpression cce = new ConstructorCallExpression( final ConstructorCallExpression cce = new ConstructorCallExpression(
COMPONENT_CREATE, COMPONENT_CREATE,
new ArgumentListExpression(List.of( new ArgumentListExpression(List.of(
componentTypeExpression,
VariableExpression.THIS_EXPRESSION, VariableExpression.THIS_EXPRESSION,
lineAndColumn.getV1(), lineAndColumn.getV1(),
lineAndColumn.getV2(), lineAndColumn.getV2(),

View File

@ -43,7 +43,7 @@ public class DefaultWebComponentTemplateCompilerTests {
} }
private static final class GreeterFactory extends AbstractComponentFactory<Greeter> { private static final class GreeterFactory extends ComponentFactoryBase<Greeter> {
public Greeter doCreate(Map<String, Object> attr) { public Greeter doCreate(Map<String, Object> attr) {
return new Greeter(attr); return new Greeter(attr);