Added some convenience methods to ComponentScope and related factories, etc. Fixed RunTemplate.

This commit is contained in:
JesseBrault0709 2024-05-12 11:50:54 +02:00
parent e4e972ea7b
commit e22fc1622e
14 changed files with 112 additions and 175 deletions

View File

@ -49,8 +49,6 @@ public abstract class AbstractViewComponent implements ViewComponent {
this.template = instantiateTemplate(templateClass); this.template = instantiateTemplate(templateClass);
} }
@Override @Override
public void setContext(ComponentContext context) { public void setContext(ComponentContext context) {
this.context = context; this.context = context;

View File

@ -1,5 +1,10 @@
package groowt.view.component.context; package groowt.view.component.context;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.FirstParam;
import groovy.transform.stc.SimpleType;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.runtime.RenderContext; import groowt.view.component.runtime.RenderContext;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -25,6 +30,27 @@ public interface ComponentContext {
return scopeClass.cast(this.getRootScope()); return scopeClass.cast(this.getRootScope());
} }
default void configureRootScope(
@ClosureParams(value = SimpleType.class, options = "groowt.view.component.context.ComponentScope")
@DelegatesTo(ComponentScope.class)
Closure<?> configure
) {
final var rootScope = this.getRootScope();
configure.setDelegate(rootScope);
configure.call(rootScope);
}
default <S extends ComponentScope> void configureRootScope(
Class<S> scopeClass,
@ClosureParams(value = FirstParam.FirstGenericType.class)
@DelegatesTo(type = "S")
Closure<?> configure
) {
final var rootScope = this.getRootScope(scopeClass);
configure.setDelegate(rootScope);
configure.call(rootScope);
}
default ComponentScope getCurrentScope() { default ComponentScope getCurrentScope() {
final List<ComponentScope> scopeStack = this.getScopeStack(); final List<ComponentScope> scopeStack = this.getScopeStack();
if (scopeStack.isEmpty()) { if (scopeStack.isEmpty()) {
@ -37,6 +63,27 @@ public interface ComponentContext {
return scopeClass.cast(this.getCurrentScope()); return scopeClass.cast(this.getCurrentScope());
} }
default void configureCurrentScope(
@ClosureParams(value = SimpleType.class, options = "groowt.view.component.context.ComponentScope")
@DelegatesTo(ComponentScope.class)
Closure<?> configure
) {
final var currentScope = this.getCurrentScope();
configure.setDelegate(currentScope);
configure.call(currentScope);
}
default <S extends ComponentScope> void configureCurrentScope(
Class<S> scopeClass,
@ClosureParams(value = FirstParam.FirstGenericType.class)
@DelegatesTo(type = "S")
Closure<?> configure
) {
final var currentScope = this.getCurrentScope(scopeClass);
configure.setDelegate(currentScope);
configure.call(currentScope);
}
@Nullable ViewComponent getParent(); @Nullable ViewComponent getParent();
default <T extends ViewComponent> @Nullable T getParent(Class<T> parentClass) { default <T extends ViewComponent> @Nullable T getParent(Class<T> parentClass) {

View File

@ -1,6 +1,7 @@
package groowt.view.component.context; package groowt.view.component.context;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import groowt.view.component.factory.ComponentFactories;
import groowt.view.component.factory.ComponentFactory; import groowt.view.component.factory.ComponentFactory;
public interface ComponentScope { public interface ComponentScope {
@ -11,6 +12,10 @@ public interface ComponentScope {
<T extends ViewComponent> void add(String typeName, Class<T> forClass, ComponentFactory<? extends T> factory); <T extends ViewComponent> void add(String typeName, Class<T> forClass, ComponentFactory<? extends T> factory);
default <T extends ViewComponent> void addWithNoArgConstructor(String typeName, Class<T> forClass) {
this.add(typeName, forClass, ComponentFactories.ofNoArgConstructor(forClass));
}
boolean contains(String typeName); boolean contains(String typeName);
void remove(String typeName); void remove(String typeName);
@ -34,6 +39,17 @@ public interface ComponentScope {
ComponentFactory<? extends T> factory ComponentFactory<? extends T> factory
); );
default <T extends ViewComponent> void addWithNoArgConstructor(Class<T> forClass) {
this.add(forClass, ComponentFactories.ofNoArgConstructor(forClass));
}
default <T extends ViewComponent> void addWithNoArgConstructor(
Class<T> publicType,
Class<? extends T> implementingClass
) {
this.add(publicType, implementingClass, ComponentFactories.ofNoArgConstructor(implementingClass));
}
boolean contains(Class<? extends ViewComponent> type); boolean contains(Class<? extends ViewComponent> type);
<T extends ViewComponent> TypeAndFactory<T> get(Class<T> type); <T extends ViewComponent> TypeAndFactory<T> get(Class<T> type);

View File

@ -22,6 +22,18 @@ public final class ComponentFactories {
return (typeName, componentContext, args) -> supplier.get(); return (typeName, componentContext, args) -> supplier.get();
} }
public static <T extends ViewComponent> ComponentFactory<T> ofNoArgConstructor(
Class<? extends T> viewComponentClass
) {
return (typeName, componentContext, args) -> {
try {
return viewComponentClass.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
private ComponentFactories() {} private ComponentFactories() {}
} }

View File

@ -1,107 +0,0 @@
package groowt.view.component.factory;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException;
import groowt.view.component.context.ComponentContext;
import groowt.view.component.ViewComponent;
import java.util.HashMap;
import java.util.Map;
/**
* 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 ->
findDoCreateMethod(this.getMetaClass(), types)
);
}
@SuppressWarnings("unchecked")
protected T findAndDoCreate(Object type, ComponentContext componentContext, Object[] args) {
final Object[] typeContextAndArgs = ComponentFactoryUtil.flatten(type, componentContext, args);
final MetaMethod typeContextAndArgsMethod = this.findDoCreateMethod(typeContextAndArgs);
if (typeContextAndArgsMethod != null) {
return (T) typeContextAndArgsMethod.invoke(this, typeContextAndArgs);
}
final Object[] typeAndContext = new Object[] { type, componentContext };
final MetaMethod typeAndContextMethod = this.findDoCreateMethod(typeAndContext);
if (typeAndContextMethod != null) {
return (T) typeAndContextMethod.invoke(this, typeAndContext);
}
final Object[] typeAndArgs = ComponentFactoryUtil.flatten(type, args);
final MetaMethod typeAndArgsMethod = this.findDoCreateMethod(typeAndArgs);
if (typeAndArgsMethod != null) {
return (T) typeAndArgsMethod.invoke(this, typeAndArgs);
}
final Object[] typeOnly = new Object[] { type };
final MetaMethod typeOnlyMethod = this.findDoCreateMethod(typeOnly);
if (typeOnlyMethod != null) {
return (T) typeOnlyMethod.invoke(this, typeOnly);
}
final Object[] contextOnly = new Object[] { componentContext };
final MetaMethod contextOnlyMethod = this.findDoCreateMethod(contextOnly);
if (contextOnlyMethod != null) {
return (T) contextOnlyMethod.invoke(this, contextOnly);
}
final MetaMethod argsOnlyMethod = this.findDoCreateMethod(args);
if (argsOnlyMethod != null) {
return (T) argsOnlyMethod.invoke(this, args);
}
throw new MissingMethodException(DO_CREATE, this.getClass(), args);
}
@Override
public T create(String typeName, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException();
}
// TODO: this needs to be updated.
@Override
public T create(String alias, Class<?> type, ComponentContext componentContext, Object... args) {
throw new UnsupportedOperationException();
}
}

View File

@ -20,7 +20,7 @@ public abstract class AbstractRenderContext implements RenderContext {
this.writer = writer; this.writer = writer;
} }
public ComponentContext getComponentContext() { protected ComponentContext getComponentContext() {
return this.componentContext; return this.componentContext;
} }

View File

@ -15,11 +15,10 @@ public interface ComponentWriter {
void append(String string); void append(String string);
void append(GString gString); void append(GString gString);
void append(GString gString, int line, int column);
void append(ViewComponent viewComponent); void append(ViewComponent viewComponent);
void append(ViewComponent viewComponent, int line, int column);
void append(Object object); void append(Object object);
@SuppressWarnings("unused")
default void leftShift(Object object) { default void leftShift(Object object) {
switch (object) { switch (object) {
case String s -> this.append(s); case String s -> this.append(s);

View File

@ -57,33 +57,14 @@ public class DefaultComponentWriter implements ComponentWriter {
} }
} }
@Override
public void append(GString gString, int line, int column) {
final String content;
try {
content = gString.toString();
} catch (Exception exception) {
throw new ComponentRenderException(line, column, exception);
}
try {
this.delegate.append(content);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
private void doComponentRender(ViewComponent viewComponent) throws IOException {
this.getRenderContext().pushComponent(viewComponent);
this.getComponentContext().pushDefaultScope();
viewComponent.renderTo(this.delegate);
this.getComponentContext().popScope();
this.getRenderContext().popComponent(viewComponent);
}
@Override @Override
public void append(ViewComponent viewComponent) { public void append(ViewComponent viewComponent) {
try { try {
this.doComponentRender(viewComponent); this.getRenderContext().pushComponent(viewComponent);
this.getComponentContext().pushDefaultScope();
viewComponent.renderTo(this.delegate);
this.getComponentContext().popScope();
this.getRenderContext().popComponent(viewComponent);
} catch (IOException ioException) { } catch (IOException ioException) {
throw new RuntimeException(ioException); throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) { } catch (ComponentRenderException componentRenderException) {
@ -93,19 +74,6 @@ public class DefaultComponentWriter implements ComponentWriter {
} }
} }
@Override
public void append(ViewComponent viewComponent, int line, int column) {
try {
this.doComponentRender(viewComponent);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (ComponentRenderException componentRenderException) {
throw componentRenderException;
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, line, column, exception);
}
}
@Override @Override
public void append(Object object) { public void append(Object object) {
switch (object) { switch (object) {

View File

@ -1,4 +1,4 @@
--- ---
import groowt.view.web.lib.Echo import groowt.view.web.lib.Echo
--- ---
<Echo greeting="Hello!"><p>${context}</p></Echo> <Echo greeting=${cliGreeting}><p>$greeting</p></Echo>

View File

@ -7,7 +7,7 @@ import org.codehaus.groovy.runtime.InvokerHelper
import static groowt.view.web.WebViewComponentFactories.withAttr import static groowt.view.web.WebViewComponentFactories.withAttr
class DefaultWebViewComponentScope extends DefaultComponentScope { class DefaultWebViewComponentScope extends DefaultComponentScope implements WebViewComponentScope {
static DefaultWebViewComponentScope getDefaultRootScope() { static DefaultWebViewComponentScope getDefaultRootScope() {
new DefaultWebViewComponentScope().tap { new DefaultWebViewComponentScope().tap {
@ -15,6 +15,7 @@ class DefaultWebViewComponentScope extends DefaultComponentScope {
} }
} }
@Override
<T extends WebViewComponent> void addWithAttr(Class<T> componentClass) { <T extends WebViewComponent> void addWithAttr(Class<T> componentClass) {
add(componentClass, withAttr(componentClass) { attr -> add(componentClass, withAttr(componentClass) { attr ->
InvokerHelper.invokeConstructorOf(componentClass, attr) as T InvokerHelper.invokeConstructorOf(componentClass, attr) as T

View File

@ -0,0 +1,7 @@
package groowt.view.web;
import groowt.view.component.context.ComponentScope;
public interface WebViewComponentScope extends ComponentScope {
<T extends WebViewComponent> void addWithAttr(Class<T> componentClass);
}

View File

@ -1,6 +1,5 @@
package groowt.view.web package groowt.view.web
import groowt.view.component.factory.ComponentFactories
import groowt.view.web.lib.AbstractWebViewComponentTests import groowt.view.web.lib.AbstractWebViewComponentTests
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -44,8 +43,8 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests {
@Test @Test
void nestedGreeter() { void nestedGreeter() {
def context = this.context { def context = this.context() {
getRootScope(DefaultWebViewComponentScope).with { configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter) addWithAttr(Greeter)
} }
} }
@ -55,9 +54,9 @@ class BaseWebViewComponentTests extends AbstractWebViewComponentTests {
@Test @Test
void doubleNested() { void doubleNested() {
def context = this.context { def context = this.context {
getRootScope(DefaultWebViewComponentScope).with { configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter) addWithAttr(Greeter)
add(UsingGreeter, ComponentFactories.ofSupplier { new UsingGreeter() }) addWithNoArgConstructor(UsingGreeter)
} }
} }
this.doTest('<BaseWebViewComponentTests.UsingGreeter />', 'Hello, World!', context) this.doTest('<BaseWebViewComponentTests.UsingGreeter />', 'Hello, World!', context)

View File

@ -1,8 +1,8 @@
package groowt.view.web.lib package groowt.view.web.lib
import groowt.view.web.BaseWebViewComponent import groowt.view.web.BaseWebViewComponent
import groowt.view.web.DefaultWebViewComponentScope
import groowt.view.web.WebViewComponentContext import groowt.view.web.WebViewComponentContext
import groowt.view.web.WebViewComponentScope
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class FragmentTests extends AbstractWebViewComponentTests { class FragmentTests extends AbstractWebViewComponentTests {
@ -20,7 +20,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
@Override @Override
void configureContext(WebViewComponentContext context) { void configureContext(WebViewComponentContext context) {
context.getRootScope(DefaultWebViewComponentScope).with { context.configureRootScope(WebViewComponentScope) {
addWithAttr(Greeter) addWithAttr(Greeter)
} }
} }

View File

@ -1,10 +1,10 @@
package groowt.view.web.tools package groowt.view.web.tools
import groowt.view.component.ComponentTemplate
import groowt.view.component.compiler.SimpleComponentTemplateClassFactory import groowt.view.component.compiler.SimpleComponentTemplateClassFactory
import groowt.view.component.compiler.source.ComponentTemplateSource import groowt.view.component.compiler.source.ComponentTemplateSource
import groowt.view.component.context.DefaultComponentContext
import groowt.view.component.runtime.DefaultComponentWriter
import groowt.view.web.BaseWebViewComponent import groowt.view.web.BaseWebViewComponent
import groowt.view.web.DefaultWebViewComponentContext
import groowt.view.web.compiler.AnonymousWebViewComponent import groowt.view.web.compiler.AnonymousWebViewComponent
import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit import groowt.view.web.compiler.WebViewComponentTemplateCompileUnit
import picocli.CommandLine import picocli.CommandLine
@ -27,16 +27,15 @@ class RunTemplate implements Callable<Integer> {
names = ['-A', '--attr', '--attribute'], names = ['-A', '--attr', '--attribute'],
description = 'Attribute(s) to pass to the template.' description = 'Attribute(s) to pass to the template.'
) )
Map<String, String> properties Map<String, String> attr
static class PropertiesComponent extends BaseWebViewComponent { static class RunnableTemplate extends BaseWebViewComponent {
private final Map<String, String> properties private final Map<String, String> cliAttr
@SuppressWarnings('GroovyAssignabilityCheck') RunnableTemplate(Class<? extends ComponentTemplate> templateClass, Map<String, String> cliAttr) {
PropertiesComponent(Map<String, Object> attr) { super(templateClass)
super('${renderChildren()}') this.cliAttr = cliAttr
this.properties = attr.properties ?: [:]
} }
@Override @Override
@ -44,7 +43,7 @@ class RunTemplate implements Callable<Integer> {
try { try {
return super.getProperty(propertyName) return super.getProperty(propertyName)
} catch (Exception ignored) { } catch (Exception ignored) {
return this.properties.get(propertyName) return this.cliAttr.get(propertyName)
} }
} }
@ -61,14 +60,12 @@ class RunTemplate implements Callable<Integer> {
def compileResult = compileUnit.compile() def compileResult = compileUnit.compile()
def templateLoader = new SimpleComponentTemplateClassFactory() def templateLoader = new SimpleComponentTemplateClassFactory()
def templateClass = templateLoader.getTemplateClass(compileResult) def templateClass = templateLoader.getTemplateClass(compileResult)
def template = templateClass.getConstructor().newInstance()
def context = new DefaultComponentContext() def runnableTemplate = new RunnableTemplate(templateClass, this.attr)
context.pushDefaultScope() def componentContext = new DefaultWebViewComponentContext()
runnableTemplate.context = componentContext
def componentWriter = new DefaultComponentWriter(new OutputStreamWriter(System.out)) println runnableTemplate.render()
template.renderer.call(context, componentWriter)
return 0 return 0
} }