Refactored and added documentation for view-components.
This commit is contained in:
parent
ea4c29f1d7
commit
d05b4f4c0f
@ -2,18 +2,52 @@ package groowt.view.component;
|
||||
|
||||
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 Class<?> firstParamType;
|
||||
|
||||
public ClosureComponentFactory(Closure<T> 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
|
||||
public T create(Object type, ComponentContext componentContext, Object... args) {
|
||||
final Object[] flattened = ComponentFactoryUtil.flatten(type, componentContext, args);
|
||||
return this.closure.call(flattened);
|
||||
public T create(String type, ComponentContext componentContext, Object... args) {
|
||||
if (this.firstParamType != Object.class && this.firstParamType != String.class) {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,20 +10,36 @@ import java.util.function.Predicate;
|
||||
|
||||
public interface ComponentContext {
|
||||
|
||||
/**
|
||||
* For use only by compiled templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
interface Resolved {
|
||||
String getTypeName();
|
||||
ComponentFactory<?> getComponentFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* For use only by compiled templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
Resolved resolve(String component);
|
||||
|
||||
/**
|
||||
* For use only by compiled templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
ViewComponent create(Resolved resolved, Object... args);
|
||||
|
||||
/**
|
||||
* For use only by compiled templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void beforeComponentRender(ViewComponent component);
|
||||
|
||||
/**
|
||||
* For use only by compiled templates.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void afterComponentRender(ViewComponent component);
|
||||
|
||||
|
@ -1,13 +1,25 @@
|
||||
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 {
|
||||
|
||||
private final Object componentType;
|
||||
private final ComponentTemplate template;
|
||||
private final int line;
|
||||
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);
|
||||
this.componentType = componentType;
|
||||
this.template = template;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
@ -15,8 +27,8 @@ public class ComponentCreateException extends RuntimeException {
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "Exception while rendering " + this.template.getClass()
|
||||
+ " at line " + this.line + ", column " + this.column + ".";
|
||||
return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() +
|
||||
" at line " + this.line + ", column " + this.column + ".";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,8 +5,23 @@ import groovy.lang.GroovyObject;
|
||||
|
||||
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) {
|
||||
return new ClosureComponentFactory<>(closure);
|
||||
}
|
||||
@ -15,6 +30,10 @@ public interface ComponentFactory<T extends ViewComponent> extends GroovyObject
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,53 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import groovy.lang.GroovyObjectSupport;
|
||||
import groovy.lang.MetaClass;
|
||||
import groovy.lang.MetaMethod;
|
||||
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> {
|
||||
|
||||
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 MetaMethod findDoCreateMethod(Object[] allArgs) {
|
||||
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);
|
||||
}
|
||||
|
||||
throw new MissingMethodException(
|
||||
ComponentFactoryUtil.DO_CREATE,
|
||||
this.getClass(),
|
||||
args
|
||||
);
|
||||
throw new MissingMethodException(DO_CREATE, this.getClass(), args);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package groowt.view.component;
|
||||
|
||||
import groovy.lang.GroovyObject;
|
||||
import groovy.lang.MetaClass;
|
||||
import groovy.lang.MetaMethod;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -9,7 +11,6 @@ import java.util.List;
|
||||
|
||||
public final class ComponentFactoryUtil {
|
||||
|
||||
public static final String DO_CREATE = "doCreate";
|
||||
public static final Class<?>[] EMPTY_CLASSES = new Class[0];
|
||||
|
||||
public static Object[] flatten(Object... args) {
|
||||
@ -18,7 +19,7 @@ public final class ComponentFactoryUtil {
|
||||
} else {
|
||||
final List<Object> result = new ArrayList<>(args.length);
|
||||
for (final var arg : args) {
|
||||
if (arg instanceof Object[] arr) {
|
||||
if (arg instanceof Object[] arr && arr.length > 0) {
|
||||
result.addAll(Arrays.asList(arr));
|
||||
} else {
|
||||
result.add(arg);
|
||||
@ -44,10 +45,6 @@ public final class ComponentFactoryUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static MetaMethod findDoCreateMethod(MetaClass metaClass, Class<?>[] types) {
|
||||
return metaClass.getMetaMethod(DO_CREATE, types);
|
||||
}
|
||||
|
||||
private ComponentFactoryUtil() {}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package groowt.view.component;
|
||||
|
||||
/**
|
||||
* An exception which represents an error during rendering of a component.
|
||||
*/
|
||||
public class ComponentRenderException extends RuntimeException {
|
||||
|
||||
private static String formatMessage(int line, int column) {
|
||||
|
@ -2,6 +2,9 @@ package groowt.view.component;
|
||||
|
||||
import java.io.Reader;
|
||||
|
||||
/**
|
||||
* Represents an exception thrown while attempting to instantiate a ComponentTemplate during compilation.
|
||||
*/
|
||||
public class ComponentTemplateCreateException extends RuntimeException {
|
||||
|
||||
private final Class<? extends ViewComponent> forClass;
|
||||
|
@ -1,5 +1,9 @@
|
||||
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 {
|
||||
|
||||
private final ComponentTemplate template;
|
||||
|
@ -2,7 +2,7 @@ package groowt.view.component;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -9,9 +9,24 @@ public interface ViewComponent extends View {
|
||||
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);
|
||||
|
||||
/**
|
||||
* <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();
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package groowt.view.web.lib
|
||||
|
||||
import groowt.view.StandardGStringTemplateView
|
||||
import groowt.view.View
|
||||
import groowt.view.component.AbstractComponentFactory
|
||||
import groowt.view.component.ComponentContext
|
||||
import groowt.view.component.ComponentFactory
|
||||
import groowt.view.web.WebViewChildComponentRenderer
|
||||
@ -34,10 +33,7 @@ class Echo extends DelegatingWebViewComponent {
|
||||
}
|
||||
|
||||
@Override
|
||||
Echo create(Object type, ComponentContext componentContext, Object... args) {
|
||||
if (!type instanceof String) {
|
||||
throw new IllegalArgumentException('<Echo> can only be used with String types.')
|
||||
}
|
||||
Echo create(String type, ComponentContext componentContext, Object... args) {
|
||||
if (args == null || args.length < 1) {
|
||||
throw new IllegalArgumentException(
|
||||
'<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
|
||||
}
|
||||
|
||||
@Override
|
||||
Echo create(Class<?> type, ComponentContext componentContext, Object... args) {
|
||||
throw new UnsupportedOperationException('<Echo> can only be used with String types.')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
String typeName
|
||||
|
@ -341,11 +341,22 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
|
||||
final Parameter exceptionParam = new Parameter(EXCEPTION, 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 ConstructorCallExpression cce = new ConstructorCallExpression(
|
||||
COMPONENT_CREATE,
|
||||
new ArgumentListExpression(List.of(
|
||||
componentTypeExpression,
|
||||
VariableExpression.THIS_EXPRESSION,
|
||||
lineAndColumn.getV1(),
|
||||
lineAndColumn.getV2(),
|
||||
|
@ -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) {
|
||||
return new Greeter(attr);
|
||||
|
Loading…
Reference in New Issue
Block a user