Refactoring for more flexible class hierarchy.

This commit is contained in:
JesseBrault0709 2024-05-05 13:11:19 +02:00
parent 6af7e4fd82
commit 6788b10068
40 changed files with 793 additions and 591 deletions

View File

@ -0,0 +1,49 @@
package groowt.view.component;
import groowt.view.component.TemplateSource.*;
import java.io.*;
import java.net.URI;
import java.net.URL;
public abstract class AbstractComponentTemplateCompiler implements ComponentTemplateCompiler {
protected abstract ComponentTemplate compile(
TemplateSource templateSource,
Class<? extends ViewComponent> forClass,
Reader actualSource
);
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, TemplateSource source) {
return switch (source) {
case FileSource(File file) -> {
try {
yield this.compile(source, forClass, new FileReader(file));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
case StringSource(String rawSource) ->
this.compile(source, forClass, new StringReader(rawSource));
case InputStreamSource(InputStream inputStream) ->
this.compile(source, forClass, new InputStreamReader(inputStream));
case URISource(URI uri) -> {
try {
yield this.compile(source, forClass, new InputStreamReader(uri.toURL().openStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
case URLSource(URL url) -> {
try {
yield this.compile(source, forClass, new InputStreamReader(url.openStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
case ReaderSource(Reader reader) -> this.compile(source, forClass, reader);
};
}
}

View File

@ -5,9 +5,11 @@ import groovy.lang.Closure;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
public abstract class AbstractViewComponent implements ViewComponent { public abstract class AbstractViewComponent implements ViewComponent {
private ComponentContext context;
private ComponentTemplate template; private ComponentTemplate template;
public AbstractViewComponent() {} public AbstractViewComponent() {}
@ -24,6 +26,27 @@ public abstract class AbstractViewComponent implements ViewComponent {
} }
} }
protected AbstractViewComponent(TemplateSource source, Function<String, ComponentTemplateCompiler> getCompiler) {
final Class<? extends AbstractViewComponent> selfClass = this.getSelfClass();
this.template = getCompiler.apply(selfClass.getPackageName()).compile(selfClass, source);
}
protected AbstractViewComponent(TemplateSource source, ComponentTemplateCompiler compiler) {
this.template = compiler.compile(this.getSelfClass(), source);
}
protected abstract Class<? extends AbstractViewComponent> getSelfClass();
@Override
public void setContext(ComponentContext context) {
this.context = context;
}
@Override
public ComponentContext getContext() {
return Objects.requireNonNull(this.context);
}
protected ComponentTemplate getTemplate() { protected ComponentTemplate getTemplate() {
return Objects.requireNonNull(template); return Objects.requireNonNull(template);
} }
@ -40,6 +63,11 @@ public abstract class AbstractViewComponent implements ViewComponent {
this.getContext().afterComponentRender(this); this.getContext().afterComponentRender(this);
} }
/**
* @implSpec If overriding, <strong>please</strong> call
* {@link #beforeRender()}and {@link #afterRender()} before
* and after the actual rendering is done, respectively.
*/
@Override @Override
public void renderTo(Writer out) throws IOException { public void renderTo(Writer out) throws IOException {
final Closure<?> closure = this.template.getRenderer(); final Closure<?> closure = this.template.getRenderer();

View File

@ -1,27 +1,26 @@
package groowt.view.component; package groowt.view.component;
import java.io.Reader;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
public abstract class CachingComponentTemplateCompiler implements ComponentTemplateCompiler { public abstract class CachingComponentTemplateCompiler extends AbstractComponentTemplateCompiler {
private final Map<Class<? extends ViewComponent>, ComponentTemplate> cache = new HashMap<>(); private final Map<Class<? extends ViewComponent>, ComponentTemplate> cache = new HashMap<>();
protected final void putInCache(Class<? extends ViewComponent> forClass, ComponentTemplate template) { @Override
this.cache.put(forClass, template); protected final ComponentTemplate compile(
} TemplateSource source,
protected final ComponentTemplate getFromCache(Class<? extends ViewComponent> forClass) {
return Objects.requireNonNull(this.cache.get(forClass));
}
protected final ComponentTemplate getFromCacheOrElse(
Class<? extends ViewComponent> forClass, Class<? extends ViewComponent> forClass,
Supplier<? extends ComponentTemplate> onEmpty Reader sourceReader
) { ) {
return this.cache.computeIfAbsent(forClass, ignored -> onEmpty.get()); return this.cache.computeIfAbsent(forClass, ignored -> this.doCompile(source, forClass, sourceReader));
} }
protected abstract ComponentTemplate doCompile(
TemplateSource source,
Class<? extends ViewComponent> forClass,
Reader sourceReader
);
} }

View File

@ -4,50 +4,79 @@ import groovy.lang.Closure;
import static groowt.view.component.ComponentFactoryUtil.flatten; import static groowt.view.component.ComponentFactoryUtil.flatten;
final class ClosureComponentFactory<T extends ViewComponent> extends ComponentFactoryBase<T> { final class ClosureComponentFactory<T extends ViewComponent> implements ComponentFactory<T> {
private enum Type {
ALL,
NAME_AND_CONTEXT, NAME_AND_ARGS, CONTEXT_AND_ARGS,
NAME_ONLY, CONTEXT_ONLY, ARGS_ONLY,
NONE
}
private final Closure<T> closure; private final Closure<T> closure;
private final Class<?> firstParamType; private final Type type;
public ClosureComponentFactory(Closure<T> closure) { @SuppressWarnings("unchecked")
this.closure = closure; public ClosureComponentFactory(Closure<? extends T> closure) {
if (this.closure.getParameterTypes().length < 2) { this.closure = (Closure<T>) closure;
throw new IllegalArgumentException( final var paramTypes = this.closure.getParameterTypes();
"Closures for " + getClass().getName() + " require at least two parameters" if (paramTypes.length == 0) {
); this.type = Type.NONE;
} else if (paramTypes.length == 1) {
final var paramType = paramTypes[0];
if (paramType == String.class || paramType == Class.class) {
this.type = Type.NAME_ONLY;
} else if (ComponentContext.class.isAssignableFrom(paramType)) {
this.type = Type.CONTEXT_ONLY;
} else {
this.type = Type.ARGS_ONLY;
} }
this.firstParamType = this.closure.getParameterTypes()[0]; } else {
if (this.firstParamType != Object.class final var firstParamType = paramTypes[0];
&& !(this.firstParamType == String.class || this.firstParamType == Class.class)) { final var secondParamType = paramTypes[1];
throw new IllegalArgumentException( if (firstParamType == String.class || firstParamType == Class.class) {
"The first closure parameter must be any of type Object (i.e, dynamic), String, or Class" if (ComponentContext.class.isAssignableFrom(secondParamType)) {
); if (paramTypes.length > 2) {
this.type = Type.ALL;
} else {
this.type = Type.NAME_AND_CONTEXT;
} }
final var secondParamType = this.closure.getParameterTypes()[1]; } else {
if (secondParamType != Object.class && !ComponentContext.class.isAssignableFrom(secondParamType)) { this.type = Type.NAME_AND_ARGS;
throw new IllegalArgumentException(
"The second closure parameter must be of type Object (i.e., dynamic) or " +
"ComponentContext or a subclass thereof."
);
} }
} else if (ComponentContext.class.isAssignableFrom(firstParamType)) {
this.type = Type.CONTEXT_AND_ARGS;
} else {
this.type = Type.ARGS_ONLY;
}
}
}
private T flatCall(Object... args) {
return this.closure.call(flatten(args));
}
private T objTypeCreate(Object type, ComponentContext componentContext, Object... args) {
return switch (this.type) {
case ALL -> this.flatCall(type, componentContext, args);
case NAME_AND_CONTEXT -> this.closure.call(type, componentContext);
case NAME_AND_ARGS -> this.flatCall(type, args);
case CONTEXT_AND_ARGS -> this.flatCall(componentContext, args);
case NAME_ONLY -> this.closure.call(type);
case CONTEXT_ONLY -> this.closure.call(componentContext);
case ARGS_ONLY -> this.closure.call(args);
case NONE -> this.closure.call();
};
} }
@Override @Override
public T create(String type, ComponentContext componentContext, Object... args) { public T create(String type, ComponentContext componentContext, Object... args) {
if (this.firstParamType != Object.class && this.firstParamType != String.class) { return this.objTypeCreate(type, componentContext, args);
throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " +
"with a String component type argument.");
}
return this.closure.call(flatten(type, componentContext, args));
} }
@Override @Override
public T create(Class<?> type, ComponentContext componentContext, Object... args) { public T create(Class<?> type, ComponentContext componentContext, Object... args) {
if (this.firstParamType != Object.class && this.firstParamType != Class.class) { return this.objTypeCreate(type, componentContext, args);
throw new IllegalArgumentException("Cannot call this ClosureComponentFactory " +
"with a Class component type argument.");
}
return this.closure.call(flatten(type, componentContext, args));
} }
} }

View File

@ -27,8 +27,9 @@ public class ComponentCreateException extends RuntimeException {
@Override @Override
public String getMessage() { public String getMessage() {
return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() + return "Exception in " + this.template.getClass().getName() + " while creating "
" at line " + this.line + ", column " + this.column + "."; + this.componentType.getClass().getName() + " at line " + this.line
+ ", column " + this.column + ".";
} }
} }

View File

@ -1,28 +1,13 @@
package groowt.view.component; package groowt.view.component;
import groovy.lang.Closure; import groovy.lang.Closure;
import groovy.lang.GroovyObject;
import java.util.function.Supplier; import java.util.function.Supplier;
@FunctionalInterface @FunctionalInterface
public interface ComponentFactory<T extends ViewComponent> { public interface ComponentFactory<T extends ViewComponent> {
/** static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<? extends T> closure) {
* @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); return new ClosureComponentFactory<>(closure);
} }

View File

@ -1,5 +1,7 @@
package groowt.view.component; package groowt.view.component;
import groovy.lang.Closure;
public interface ComponentScope { public interface ComponentScope {
void add(String name, ComponentFactory<?> factory); void add(String name, ComponentFactory<?> factory);
@ -24,11 +26,6 @@ public interface ComponentScope {
return (ComponentFactory<T>) this.get(clazz.getName()); return (ComponentFactory<T>) this.get(clazz.getName());
} }
@SuppressWarnings("unchecked")
default <T extends ViewComponent> ComponentFactory<T> getAs(String name, Class<T> viewComponentType) {
return (ComponentFactory<T>) this.get(name);
}
default void remove(Class<? extends ViewComponent> clazz) { default void remove(Class<? extends ViewComponent> clazz) {
this.remove(clazz.getName()); this.remove(clazz.getName());
} }
@ -38,4 +35,12 @@ public interface ComponentScope {
return (ComponentFactory<T>) this.factoryMissing(clazz.getName()); return (ComponentFactory<T>) this.factoryMissing(clazz.getName());
} }
default void add(String name, Closure<? extends ViewComponent> closure) {
this.add(name, ComponentFactory.ofClosure(closure));
}
default <T extends ViewComponent> void add(Class<T> type, Closure<? extends T> closure) {
this.add(type, ComponentFactory.ofClosure(closure));
}
} }

View File

@ -1,16 +1,5 @@
package groowt.view.component; package groowt.view.component;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
public interface ComponentTemplateCompiler { public interface ComponentTemplateCompiler {
ComponentTemplate compile(Class<? extends ViewComponent> forClass, File templateFile); ComponentTemplate compile(Class<? extends ViewComponent> forClass, TemplateSource source);
ComponentTemplate compile(Class<? extends ViewComponent> forClass, String template);
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URI templateURI);
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URL templateURL);
ComponentTemplate compile(Class<? extends ViewComponent> forClass, InputStream inputStream);
ComponentTemplate compile(Class<? extends ViewComponent> forClass, Reader reader);
} }

View File

@ -0,0 +1,55 @@
package groowt.view.component;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
public sealed interface TemplateSource {
static TemplateSource of(String template) {
return new StringSource(template);
}
static TemplateSource of(File templateFile) {
return new FileSource(templateFile);
}
static TemplateSource of(URI templateURI) {
return new URISource(templateURI);
}
static TemplateSource of(URL url) {
return new URLSource(url);
}
static TemplateSource of(InputStream templateInputStream) {
return new InputStreamSource(templateInputStream);
}
static TemplateSource of(Reader templateReader) {
return new ReaderSource(templateReader);
}
/**
* @param resourceName An <strong>absolute</strong> path resource name.
* @return A template source
*/
static TemplateSource fromResource(String resourceName) {
return of(TemplateSource.class.getClassLoader().getResource(resourceName));
}
record StringSource(String template) implements TemplateSource {}
record FileSource(File templateFile) implements TemplateSource {}
record URISource(URI templateURI) implements TemplateSource {}
record URLSource(URL templateURL) implements TemplateSource {}
record InputStreamSource(InputStream templateInputStream) implements TemplateSource {}
record ReaderSource(Reader templateReader) implements TemplateSource {}
}

View File

@ -7,6 +7,7 @@ import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
@FunctionalInterface
public interface View { public interface View {
/** /**

View File

@ -0,0 +1,46 @@
package groowt.view.web
import groowt.view.component.AbstractViewComponent
import groowt.view.component.ComponentTemplate
import groowt.view.component.TemplateSource
class DefaultWebViewComponent extends AbstractWebViewComponent {
DefaultWebViewComponent() {}
DefaultWebViewComponent(ComponentTemplate template) {
super(template)
}
DefaultWebViewComponent(Class<? extends ComponentTemplate> templateType) {
super(templateType)
}
DefaultWebViewComponent(TemplateSource source) {
super(source)
}
DefaultWebViewComponent(TemplateSource source, WebViewComponentTemplateCompiler compiler) {
super(source, compiler)
}
/**
* A convenience constructor which creates a {@link TemplateSource}
* from the given {@code source} parameter and passes it to super. See
* {@link TemplateSource} for possible types.
*
* @param source the object passed to {@link TemplateSource#of}
*
* @see TemplateSource
*/
@SuppressWarnings('GroovyAssignabilityCheck')
DefaultWebViewComponent(Object source) {
super(TemplateSource.of(source))
}
@Override
protected final Class<? extends AbstractViewComponent> getSelfClass() {
this.class
}
}

View File

@ -1,122 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader;
import groowt.view.component.AbstractViewComponent;
import groowt.view.component.ComponentContext;
import groowt.view.component.ComponentTemplate;
import groowt.view.web.WebViewTemplateComponentSource.*;
import groowt.view.web.runtime.WebViewComponentWriter;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class DefaultWebViewComponent extends AbstractViewComponent implements WebViewComponent {
private static ComponentTemplate getComponentTemplate(
Class<? extends DefaultWebViewComponent> selfType,
WebViewTemplateComponentSource source,
@Nullable GroovyClassLoader groovyClassLoader
) {
final var compiler = new DefaultWebComponentTemplateCompiler(
CompilerConfiguration.DEFAULT,
selfType.getPackageName()
);
if (groovyClassLoader != null) {
compiler.setGroovyClassLoader(groovyClassLoader);
}
return switch (source) {
case FileSource(File f) -> compiler.compile(selfType, f);
case InputStreamSource(InputStream inputStream) -> compiler.compile(selfType, inputStream);
case ReaderSource(Reader r) -> compiler.compile(selfType, r);
case StringSource(String s) -> compiler.compile(selfType, s);
case URISource(URI uri) -> compiler.compile(selfType, uri);
case URLSource(URL url) -> compiler.compile(selfType, url);
};
}
private ComponentContext context;
private List<WebViewChildRenderer> children;
public DefaultWebViewComponent() {}
public DefaultWebViewComponent(ComponentTemplate template) {
super(template);
}
public DefaultWebViewComponent(Class<? extends ComponentTemplate> templateType) {
super(templateType);
}
public DefaultWebViewComponent(WebViewTemplateComponentSource source) {
this.setTemplate(getComponentTemplate(this.getClass(), source, null));
}
public DefaultWebViewComponent(WebViewTemplateComponentSource source, GroovyClassLoader groovyClassLoader) {
this.setTemplate(getComponentTemplate(this.getClass(), source, groovyClassLoader));
}
@Override
public void setContext(ComponentContext context) {
this.context = context;
}
@Override
public ComponentContext getContext() {
return Objects.requireNonNull(this.context);
}
@Override
public List<WebViewChildRenderer> getChildRenderers() {
if (this.children == null) {
this.children = new ArrayList<>();
}
return this.children;
}
@Override
public boolean hasChildren() {
return !this.getChildRenderers().isEmpty();
}
@Override
public void setChildRenderers(List<WebViewChildRenderer> children) {
this.children = children;
}
@Override
public void renderChildren() {
for (final var childRenderer : this.getChildRenderers()) {
try {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
}
childRenderer.render(this);
} catch (Exception e) {
throw new ChildRenderException(e);
} finally {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().afterComponentRender(childComponentRenderer.getComponent());
}
}
}
}
@Override
public void renderTo(Writer out) throws IOException {
final var webWriter = new WebViewComponentWriter(out);
final Closure<?> renderer = this.getTemplate().getRenderer();
renderer.setDelegate(this);
renderer.setResolveStrategy(Closure.DELEGATE_FIRST);
renderer.call(this.getContext(), webWriter);
}
}

View File

@ -4,22 +4,23 @@ import groowt.view.component.ComponentScope
import groowt.view.component.DefaultComponentContext import groowt.view.component.DefaultComponentContext
import groowt.view.component.ViewComponent import groowt.view.component.ViewComponent
import groowt.view.web.lib.Fragment import groowt.view.web.lib.Fragment
import groowt.view.web.runtime.WebViewComponentChildCollector import groowt.view.web.runtime.DefaultWebViewComponentChildCollection
import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.ApiStatus
class DefaultWebViewComponentContext extends DefaultComponentContext { class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext {
@Override @Override
protected ComponentScope getNewDefaultScope() { protected ComponentScope getNewDefaultScope() {
new WebViewScope() new WebViewScope()
} }
@Override
@ApiStatus.Internal @ApiStatus.Internal
ViewComponent createFragment(Closure<?> childCollector) { ViewComponent createFragment(Closure<?> childCollector) {
def collector = new WebViewComponentChildCollector() def childCollection = new DefaultWebViewComponentChildCollection()
childCollector.call(collector) childCollector.call(childCollection)
def fragment = new Fragment() def fragment = new Fragment()
fragment.childRenderers = collector.children fragment.childRenderers = childCollection.children
fragment fragment
} }

View File

@ -0,0 +1,24 @@
package groowt.view.web
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import groowt.view.component.ComponentFactory
import java.util.function.Function
final class WebViewComponentFactories {
static <T extends WebViewComponent> ComponentFactory<T> withAttr(
@ClosureParams(value = FromString, options = 'java.util.Map<String, Object>')
Closure<T> closure
) {
ComponentFactory.ofClosure { Map<String, Object> attr -> closure(attr) }
}
static <T extends WebViewComponent> ComponentFactory<T> withAttr(Function<Map<String, Object>, T> tFunction) {
ComponentFactory.ofClosure { Map<String, Object> attr -> tFunction.apply(attr) }
}
private WebViewComponentFactories() {}
}

View File

@ -2,15 +2,14 @@ package groowt.view.web
import groowt.view.component.ComponentFactory import groowt.view.component.ComponentFactory
import groowt.view.component.DefaultComponentScope import groowt.view.component.DefaultComponentScope
import groowt.view.web.lib.Echo
import groowt.view.web.lib.Echo.EchoFactory import groowt.view.web.lib.Echo.EchoFactory
class WebViewScope extends DefaultComponentScope { class WebViewScope extends DefaultComponentScope {
private final EchoFactory echoFactory = new EchoFactory()
@Override @Override
ComponentFactory factoryMissing(String typeName) { ComponentFactory factoryMissing(String typeName) {
echoFactory Echo.FACTORY
} }
} }

View File

@ -22,8 +22,10 @@ public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent
protected abstract View getDelegate(); protected abstract View getDelegate();
@Override @Override
public void renderTo(Writer out) throws IOException { public final void renderTo(Writer out) throws IOException {
this.beforeRender();
this.getDelegate().renderTo(out); this.getDelegate().renderTo(out);
this.afterRender();
} }
} }

View File

@ -1,14 +1,20 @@
package groowt.view.web.lib package groowt.view.web.lib
import groowt.view.StandardGStringTemplateView
import groowt.view.View import groowt.view.View
import groowt.view.component.ComponentContext import groowt.view.component.ComponentContext
import groowt.view.component.ComponentFactory import groowt.view.component.ComponentFactory
import groowt.view.component.ComponentRenderException
import groowt.view.web.WebViewChildComponentRenderer import groowt.view.web.WebViewChildComponentRenderer
class Echo extends DelegatingWebViewComponent { class Echo extends DelegatingWebViewComponent {
static final class EchoFactory implements ComponentFactory<Echo> { static final ComponentFactory<Echo> FACTORY = new EchoFactory()
private static final class EchoFactory implements ComponentFactory<Echo> {
Echo doCreate(String typeName) {
doCreate(typeName, [:], true)
}
Echo doCreate(String typeName, boolean selfClose) { Echo doCreate(String typeName, boolean selfClose) {
doCreate(typeName, [:], selfClose) doCreate(typeName, [:], selfClose)
@ -19,8 +25,7 @@ class Echo extends DelegatingWebViewComponent {
} }
Echo doCreate(String typeName, Map<String, Object> attr, boolean selfClose) { Echo doCreate(String typeName, Map<String, Object> attr, boolean selfClose) {
def echo = new Echo(attr, typeName, selfClose) new Echo(attr, typeName, selfClose)
echo
} }
Echo doCreate( Echo doCreate(
@ -56,26 +61,43 @@ class Echo extends DelegatingWebViewComponent {
@Override @Override
protected View getDelegate() { protected View getDelegate() {
return new StandardGStringTemplateView( if (this.selfClose && this.hasChildren()) {
src: Echo.getResource('EchoTemplate.gst'), throw new ComponentRenderException('Cannot have selfClose set to true and have children.')
parent: this }
) return {
it << '<'
it << this.name
if (!this.attr.isEmpty()) {
it << ' '
formatAttr(it)
}
if (this.selfClose) {
it << ' /'
}
it << '>'
if (this.hasChildren()) {
this.renderChildren() // TODO: fix this
}
if (this.hasChildren() || !this.selfClose) {
it << '</'
it << this.name
it << '>'
}
}
} }
String formatAttr() { protected void formatAttr(Writer writer) {
def sb = new StringBuilder()
def iter = this.attr.iterator() def iter = this.attr.iterator()
while (iter.hasNext()) { while (iter.hasNext()) {
def entry = iter.next() def entry = iter.next()
sb << entry.key writer << entry.key
sb << '="' writer << '="'
sb << entry.value writer << entry.value
sb << '"' writer << '"'
if (iter.hasNext()) { if (iter.hasNext()) {
sb << ' ' writer << ' '
} }
} }
sb.toString()
} }
} }

View File

@ -2,7 +2,7 @@ package groowt.view.web.lib
import groowt.view.web.DefaultWebViewComponent import groowt.view.web.DefaultWebViewComponent
class Fragment extends DefaultWebViewComponent { final class Fragment extends DefaultWebViewComponent {
@Override @Override
void renderTo(Writer out) throws IOException { void renderTo(Writer out) throws IOException {

View File

@ -0,0 +1,88 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.AbstractViewComponent;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.TemplateSource;
import groowt.view.web.runtime.DefaultWebViewComponentWriter;
import groowt.view.web.runtime.WebViewComponentWriter;
import org.codehaus.groovy.control.CompilerConfiguration;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractWebViewComponent extends AbstractViewComponent implements WebViewComponent {
private List<WebViewChildRenderer> childRenderers;
public AbstractWebViewComponent() {}
public AbstractWebViewComponent(ComponentTemplate template) {
super(template);
}
public AbstractWebViewComponent(Class<? extends ComponentTemplate> templateClass) {
super(templateClass);
}
protected AbstractWebViewComponent(TemplateSource source) {
super(source, packageName -> new DefaultWebViewComponentTemplateCompiler(
CompilerConfiguration.DEFAULT,
packageName
));
}
protected AbstractWebViewComponent(TemplateSource source, WebViewComponentTemplateCompiler compiler) {
super(source, compiler);
}
@Override
public List<WebViewChildRenderer> getChildRenderers() {
if (this.childRenderers == null) {
this.childRenderers = new ArrayList<>();
}
return this.childRenderers;
}
@Override
public boolean hasChildren() {
return !this.getChildRenderers().isEmpty();
}
@Override
public void setChildRenderers(List<WebViewChildRenderer> children) {
this.childRenderers = children;
}
@Override
public void renderChildren() {
for (final var childRenderer : this.getChildRenderers()) {
try {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
}
childRenderer.render(this);
} catch (Exception e) {
throw new ChildRenderException(e);
} finally {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().afterComponentRender(childComponentRenderer.getComponent());
}
}
}
}
@Override
public void renderTo(Writer out) throws IOException {
final WebViewComponentWriter webWriter = new DefaultWebViewComponentWriter(out);
final Closure<?> renderer = this.getTemplate().getRenderer();
renderer.setDelegate(this);
renderer.setResolveStrategy(Closure.DELEGATE_FIRST);
this.beforeRender();
renderer.call(this.getContext(), webWriter);
this.afterRender();
}
}

View File

@ -1,10 +1,7 @@
package groowt.view.web; package groowt.view.web;
import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyClassLoader;
import groowt.view.component.CachingComponentTemplateCompiler; import groowt.view.component.*;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ComponentTemplateCreateException;
import groowt.view.component.ViewComponent;
import groowt.view.web.antlr.CompilationUnitParseResult; import groowt.view.web.antlr.CompilationUnitParseResult;
import groowt.view.web.antlr.ParserUtil; import groowt.view.web.antlr.ParserUtil;
import groowt.view.web.antlr.TokenList; import groowt.view.web.antlr.TokenList;
@ -21,15 +18,14 @@ import org.codehaus.groovy.tools.GroovyClass;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.*; import java.io.IOException;
import java.io.Reader;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects; import java.util.Objects;
public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler { public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler
implements WebViewComponentTemplateCompiler {
private final CompilerConfiguration configuration; private final CompilerConfiguration configuration;
private final String defaultPackageName; private final String defaultPackageName;
@ -37,15 +33,12 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
private GroovyClassLoader groovyClassLoader; private GroovyClassLoader groovyClassLoader;
public DefaultWebComponentTemplateCompiler( public DefaultWebViewComponentTemplateCompiler(CompilerConfiguration configuration, String defaultPackageName) {
CompilerConfiguration configuration,
String defaultPackageName
) {
this(configuration, defaultPackageName, Phases.CLASS_GENERATION); this(configuration, defaultPackageName, Phases.CLASS_GENERATION);
} }
@ApiStatus.Internal @ApiStatus.Internal
public DefaultWebComponentTemplateCompiler( public DefaultWebViewComponentTemplateCompiler(
CompilerConfiguration configuration, CompilerConfiguration configuration,
String defaultPackageName, String defaultPackageName,
int phase int phase
@ -70,11 +63,30 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
this.groovyClassLoader = null; this.groovyClassLoader = null;
} }
protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader) { @Override
return this.doCompile(forClass, reader, null); protected ComponentTemplate doCompile(
@Nullable TemplateSource source,
@Nullable Class<? extends ViewComponent> forClass,
Reader sourceReader
) {
if (source instanceof TemplateSource.URISource uriSource) {
return this.doCompile(forClass, sourceReader, uriSource.templateURI());
} else if (source instanceof TemplateSource.URLSource urlSource) {
try {
return this.doCompile(forClass, sourceReader, urlSource.templateURL().toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
} else {
return this.doCompile(forClass, sourceReader, null);
}
} }
protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader, @Nullable URI uri) { protected ComponentTemplate doCompile(
@Nullable Class<? extends ViewComponent> forClass,
Reader reader,
@Nullable URI uri
) {
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader);
// TODO: analysis // TODO: analysis
@ -153,56 +165,8 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
} }
@Override @Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, File templateFile) {
return this.getFromCacheOrElse(forClass, () -> {
try {
return this.doCompile(forClass, new FileReader(templateFile));
} catch (FileNotFoundException e) {
throw new ComponentTemplateCreateException(e, forClass, templateFile);
}
});
}
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, String template) {
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new StringReader(template)));
}
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, URI templateURI) {
return this.getFromCacheOrElse(forClass, () -> {
final Path path = Paths.get(templateURI);
try {
return this.doCompile(forClass, Files.newBufferedReader(path), templateURI);
} catch (IOException e) {
throw new ComponentTemplateCreateException(e, forClass, templateURI);
}
});
}
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, URL templateURL) {
return this.getFromCacheOrElse(forClass, () -> {
try {
return this.doCompile(forClass, new InputStreamReader(templateURL.openStream()), templateURL.toURI());
} catch (Exception e) {
throw new ComponentTemplateCreateException(e, forClass, templateURL);
}
});
}
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, InputStream inputStream) {
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new InputStreamReader(inputStream)));
}
@Override
public ComponentTemplate compile(Class<? extends ViewComponent> forClass, Reader reader) {
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader));
}
public ComponentTemplate compileAnonymous(Reader reader) { public ComponentTemplate compileAnonymous(Reader reader) {
return this.doCompile(null, reader); return this.doCompile(null, null, reader);
} }
} }

View File

@ -0,0 +1,16 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.ComponentContext;
import groowt.view.component.ViewComponent;
import org.jetbrains.annotations.ApiStatus;
public interface WebViewComponentContext extends ComponentContext {
/**
* For use only by compiled web view component templates.
*/
@ApiStatus.Internal
ViewComponent createFragment(Closure<?> childCollector);
}

View File

@ -0,0 +1,10 @@
package groowt.view.web;
import groowt.view.component.ComponentTemplate;
import groowt.view.component.ComponentTemplateCompiler;
import java.io.Reader;
public interface WebViewComponentTemplateCompiler extends ComponentTemplateCompiler {
ComponentTemplate compileAnonymous(Reader reader);
}

View File

@ -1,55 +0,0 @@
package groowt.view.web;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
public sealed interface WebViewTemplateComponentSource {
static WebViewTemplateComponentSource of(String template) {
return new StringSource(template);
}
static WebViewTemplateComponentSource of(File templateFile) {
return new FileSource(templateFile);
}
static WebViewTemplateComponentSource of(URI templateURI) {
return new URISource(templateURI);
}
static WebViewTemplateComponentSource of(URL url) {
return new URLSource(url);
}
static WebViewTemplateComponentSource of(InputStream templateInputStream) {
return new InputStreamSource(templateInputStream);
}
static WebViewTemplateComponentSource of(Reader templateReader) {
return new ReaderSource(templateReader);
}
/**
* @param resourceName An <strong>absolute</strong> path resource name.
* @return A template source
*/
static WebViewTemplateComponentSource fromResource(String resourceName) {
return of(WebViewTemplateComponentSource.class.getClassLoader().getResource(resourceName));
}
record StringSource(String template) implements WebViewTemplateComponentSource {}
record FileSource(File templateFile) implements WebViewTemplateComponentSource {}
record URISource(URI templateURI) implements WebViewTemplateComponentSource {}
record URLSource(URL templateURL) implements WebViewTemplateComponentSource {}
record InputStreamSource(InputStream templateInputStream) implements WebViewTemplateComponentSource {}
record ReaderSource(Reader templateReader) implements WebViewTemplateComponentSource {}
}

View File

@ -11,22 +11,26 @@ import groowt.view.web.WebViewChildRenderer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class WebViewComponentChildCollector { public class DefaultWebViewComponentChildCollection implements WebViewComponentChildCollection {
private final List<WebViewChildRenderer> children = new ArrayList<>(); private final List<WebViewChildRenderer> children = new ArrayList<>();
@Override
public void add(String jString, Closure<Void> renderer) { public void add(String jString, Closure<Void> renderer) {
this.children.add(new WebViewChildJStringRenderer(jString, renderer)); this.children.add(new WebViewChildJStringRenderer(jString, renderer));
} }
@Override
public void add(GString gString, Closure<Void> renderer) { public void add(GString gString, Closure<Void> renderer) {
this.children.add(new WebViewChildGStringRenderer(gString, renderer)); this.children.add(new WebViewChildGStringRenderer(gString, renderer));
} }
@Override
public void add(ViewComponent component, Closure<Void> renderer) { public void add(ViewComponent component, Closure<Void> renderer) {
this.children.add(new WebViewChildComponentRenderer(component, renderer)); this.children.add(new WebViewChildComponentRenderer(component, renderer));
} }
@Override
public List<WebViewChildRenderer> getChildren() { public List<WebViewChildRenderer> getChildren() {
return this.children; return this.children;
} }

View File

@ -0,0 +1,94 @@
package groowt.view.web.runtime;
import groovy.lang.GString;
import groowt.view.component.ComponentRenderException;
import groowt.view.component.ViewComponent;
import java.io.IOException;
import java.io.Writer;
public class DefaultWebViewComponentWriter implements WebViewComponentWriter {
private final Writer delegate;
public DefaultWebViewComponentWriter(Writer delegate) {
this.delegate = delegate;
}
@Override
public void append(String string) {
try {
this.delegate.append(string);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
@Override
public void append(GString gString) {
try {
gString.writeTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(exception);
}
}
@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);
}
}
@Override
public void append(ViewComponent viewComponent) {
try {
viewComponent.renderTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, exception);
}
}
@Override
public void append(ViewComponent viewComponent, int line, int column) {
try {
viewComponent.renderTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, line, column, exception);
}
}
@Override
public void append(Object object) {
try {
this.delegate.append(object.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void leftShift(Object object) {
switch (object) {
case String s -> this.append(s);
case GString gs -> this.append(gs);
case ViewComponent viewComponent -> this.append(viewComponent);
default -> this.append(object);
}
}
}

View File

@ -0,0 +1,20 @@
package groowt.view.web.runtime;
import groovy.lang.Closure;
import groovy.lang.GString;
import groowt.view.component.ViewComponent;
import groowt.view.web.WebViewChildRenderer;
import java.util.List;
public interface WebViewComponentChildCollection {
void add(String jString, Closure<Void> renderer);
void add(GString gString, Closure<Void> renderer);
void add(ViewComponent component, Closure<Void> renderer);
List<WebViewChildRenderer> getChildren();
}

View File

@ -1,87 +1,14 @@
package groowt.view.web.runtime; package groowt.view.web.runtime;
import groovy.lang.GString; import groovy.lang.GString;
import groowt.view.component.ComponentRenderException;
import groowt.view.component.ViewComponent; import groowt.view.component.ViewComponent;
import java.io.IOException; public interface WebViewComponentWriter {
import java.io.Writer; void append(String string);
void append(GString gString);
public class WebViewComponentWriter { void append(GString gString, int line, int column);
void append(ViewComponent viewComponent);
private final Writer delegate; void append(ViewComponent viewComponent, int line, int column);
void append(Object object);
public WebViewComponentWriter(Writer delegate) { void leftShift(Object object);
this.delegate = delegate;
}
public void append(String string) {
try {
this.delegate.append(string);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
public void append(GString gString) {
try {
gString.writeTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(exception);
}
}
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);
}
}
public void append(ViewComponent viewComponent) {
try {
viewComponent.renderTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, exception);
}
}
public void append(ViewComponent viewComponent, int line, int column) {
try {
viewComponent.renderTo(this.delegate);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (Exception exception) {
throw new ComponentRenderException(viewComponent, line, column, exception);
}
}
public void append(Object object) {
try {
this.delegate.append(object.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void leftShift(Object object) {
switch (object) {
case String s -> this.append(s);
case GString gs -> this.append(gs);
case ViewComponent viewComponent -> this.append(viewComponent);
default -> this.append(object);
}
}
} }

View File

@ -3,7 +3,7 @@ package groowt.view.web.transpile;
import groovy.lang.Tuple2; import groovy.lang.Tuple2;
import groowt.view.component.*; import groowt.view.component.*;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.runtime.WebViewComponentChildCollector; import groowt.view.web.runtime.WebViewComponentChildCollection;
import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil;
import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
@ -22,7 +22,7 @@ import static groowt.view.web.transpile.TranspilerUtil.*;
public class DefaultComponentTranspiler implements ComponentTranspiler { public class DefaultComponentTranspiler implements ComponentTranspiler {
private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class); private static final ClassNode VIEW_COMPONENT = ClassHelper.make(ViewComponent.class);
private static final ClassNode CHILD_COLLECTOR = ClassHelper.make(WebViewComponentChildCollector.class); private static final ClassNode CHILD_COLLECTION = ClassHelper.make(WebViewComponentChildCollection.class);
private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class); private static final ClassNode EXCEPTION = ClassHelper.make(Exception.class);
private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class); private static final ClassNode COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class);
@ -182,7 +182,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
String componentVariableName String componentVariableName
) { ) {
final Parameter childCollectorParam = new Parameter( final Parameter childCollectorParam = new Parameter(
CHILD_COLLECTOR, CHILD_COLLECTION,
componentVariableName + "_childCollector" componentVariableName + "_childCollector"
); );

View File

@ -1,98 +0,0 @@
package groowt.view.web;
import groovy.lang.Closure;
import groowt.view.component.*;
import groowt.view.web.runtime.WebViewComponentWriter;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.junit.jupiter.api.Test;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*;
public class DefaultWebComponentTemplateCompilerTests {
private static ComponentTemplate doCompile(Class<? extends ViewComponent> componentClass, Reader source) {
final var compiler = new DefaultWebComponentTemplateCompiler(
CompilerConfiguration.DEFAULT,
componentClass.getPackageName()
);
return compiler.compile(componentClass, source);
}
private static ComponentTemplate doCompile(Class<? extends ViewComponent> componentClass, String source) {
return doCompile(componentClass, new StringReader(source));
}
private static final class Greeter extends DefaultWebViewComponent {
private final String target;
public Greeter(Map<String, Object> attr) {
super(doCompile(Greeter.class, "Hello, $target!"));
this.target = (String) Objects.requireNonNull(attr.get("target"));
}
public String getTarget() {
return this.target;
}
}
private static final class GreeterFactory extends ComponentFactoryBase<Greeter> {
public Greeter doCreate(Map<String, Object> attr) {
return new Greeter(attr);
}
}
private static final class UsingGreeter extends DefaultWebViewComponent {
public UsingGreeter(ComponentContext context) {
super(doCompile(UsingGreeter.class, "<Greeter target='World' />"));
this.setContext(context);
}
}
@Test
public void usingGreeter() {
final var context = new DefaultComponentContext();
final var scope = new DefaultComponentScope();
scope.add("Greeter", new GreeterFactory());
context.pushScope(scope);
final UsingGreeter usingGreeter = new UsingGreeter(context);
assertEquals("Hello, World!", usingGreeter.render());
}
@Test
public void withPreambleImport() {
final ComponentTemplate template = doCompile(DefaultWebViewComponent.class,
"""
---
import groovy.transform.Field
@Field
String greeting = 'Hello, World!'
---
$greeting
""".stripIndent()
);
final var context = new DefaultComponentContext();
context.pushDefaultScope();
final var sw = new StringWriter();
final var out = new WebViewComponentWriter(sw);
final Closure<?> renderer = template.getRenderer();
renderer.call(context, out);
assertEquals("Hello, World!", sw.toString());
}
}

View File

@ -0,0 +1,82 @@
package groowt.view.web
import groowt.view.component.ComponentFactoryBase
import groowt.view.web.lib.WithContext
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class DefaultWebViewComponentTests implements WithContext {
private static final class Greeter extends DefaultWebViewComponent {
private final String target
Greeter(Map<String, Object> attr) {
super('Hello, $target!')
this.target = Objects.requireNonNull(attr.get("target"))
}
String getTarget() {
return this.target
}
}
private static final class GreeterFactory extends ComponentFactoryBase<Greeter> {
Greeter doCreate(Map<String, Object> attr) {
return new Greeter(attr)
}
}
private static final class UsingGreeter extends DefaultWebViewComponent {
UsingGreeter() {
super("<Greeter target='World' />")
}
}
@Test
void withPreambleImport() {
def c = new DefaultWebViewComponent(
'''
---
import groovy.transform.Field
@Field
String greeting = 'Hello, World!'
---
$greeting
'''.stripIndent().trim()
)
c.context = this.context()
assertEquals("Hello, World!", c.render())
}
@Test
void nestedGreeter() {
def context = this.context {
this.configureContext(it)
currentScope.add('Greeter', new GreeterFactory())
}
def c = new DefaultWebViewComponent('<Greeter target="World" />')
c.context = context
assertEquals('Hello, World!', c.render())
}
@Test
void doubleNested() {
def context = this.context {
this.configureContext(it)
currentScope.add('UsingGreeter') { new UsingGreeter() }
currentScope.add('Greeter', new GreeterFactory())
}
def c = new DefaultWebViewComponent('<UsingGreeter />')
c.context = context
assertEquals('Hello, World!', c.render())
}
}

View File

@ -1,16 +1,31 @@
package groowt.view.web.lib package groowt.view.web.lib
import groowt.view.web.DefaultWebViewComponentContext import groowt.view.web.WebViewComponentContext
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class EchoTests extends AbstractWebViewComponentTests { class EchoTests extends AbstractWebViewComponentTests {
@Override
void configureContext(WebViewComponentContext context) {
super.configureContext(context)
context.currentScope.add('Echo', Echo.FACTORY)
}
@Test @Test
void typeOnlySelfClose() { void selfClose() {
def context = new DefaultWebViewComponentContext() this.doTest('<Echo />', '<Echo />')
context.pushDefaultScope() }
context.currentScope.add('Echo', new Echo.EchoFactory())
this.doTest('<Echo(true) />', '<Echo />', context) @Test
void noSelfClose() {
this.doTest('<Echo(false) />', '<Echo></Echo>')
}
@Test
@Disabled("Not possible to render children directly to a writer yet.")
void withChildren() {
this.doTest('<Echo>Hello, World!</Echo>', '<Echo>Hello, World!</Echo>')
} }
} }

View File

@ -4,9 +4,11 @@ import groowt.view.component.ComponentContext
import groowt.view.component.ComponentFactory import groowt.view.component.ComponentFactory
import groowt.view.web.DefaultWebViewComponent import groowt.view.web.DefaultWebViewComponent
import groowt.view.web.DefaultWebViewComponentContext import groowt.view.web.DefaultWebViewComponentContext
import groowt.view.web.WebViewTemplateComponentSource import groowt.view.component.TemplateSource
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import static groowt.view.web.WebViewComponentFactories.withAttr
class FragmentTests extends AbstractWebViewComponentTests { class FragmentTests extends AbstractWebViewComponentTests {
static class Greeter extends DefaultWebViewComponent { static class Greeter extends DefaultWebViewComponent {
@ -14,7 +16,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
String greeting String greeting
Greeter(Map<String, Object> attr) { Greeter(Map<String, Object> attr) {
super(WebViewTemplateComponentSource.of('$greeting')) super(TemplateSource.of('$greeting'))
greeting = attr.greeting greeting = attr.greeting
} }
@ -22,9 +24,7 @@ class FragmentTests extends AbstractWebViewComponentTests {
private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap { private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap {
pushDefaultScope() pushDefaultScope()
def greeterFactory = ComponentFactory.ofClosure { type, componentContext, attr -> def greeterFactory = withAttr(Greeter.&new)
new Greeter(attr)
}
currentScope.add('Greeter', greeterFactory) currentScope.add('Greeter', greeterFactory)
} }

View File

@ -0,0 +1,34 @@
package groowt.view.web.lib
import groowt.view.component.ComponentContext
import groowt.view.web.DefaultWebViewComponentTemplateCompiler
import groowt.view.web.WebViewComponentTemplateCompiler
import groowt.view.web.runtime.DefaultWebViewComponentWriter
import org.codehaus.groovy.control.CompilerConfiguration
import static org.junit.jupiter.api.Assertions.assertEquals
abstract class AbstractWebViewComponentTests implements WithContext {
protected WebViewComponentTemplateCompiler compiler() {
new DefaultWebViewComponentTemplateCompiler(
CompilerConfiguration.DEFAULT,
this.class.packageName
)
}
protected void doTest(Reader source, String expected, ComponentContext context) {
def template = this.compiler().compileAnonymous(source)
def renderer = template.getRenderer()
def sw = new StringWriter()
def out = new DefaultWebViewComponentWriter(sw)
renderer.call(context, out)
assertEquals(expected, sw.toString())
}
protected void doTest(String source, String expected, ComponentContext context = this.context()) {
this.doTest(new StringReader(source), expected, context)
}
}

View File

@ -0,0 +1,32 @@
package groowt.view.web.lib
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import groowt.view.web.DefaultWebViewComponentContext
import groowt.view.web.WebViewComponentContext
trait WithContext {
WebViewComponentContext context(
@ClosureParams(value = SimpleType, options = 'groowt.view.web.WebViewComponentContext')
@DelegatesTo(value = WebViewComponentContext)
Closure configure
) {
new DefaultWebViewComponentContext().tap {
pushDefaultScope()
configure.delegate = it
configure(it)
}
}
WebViewComponentContext context() {
new DefaultWebViewComponentContext().tap {
configureContext(it)
}
}
void configureContext(WebViewComponentContext context) {
context.pushDefaultScope()
}
}

View File

@ -1,41 +0,0 @@
package groowt.view.web.lib;
import groovy.lang.Closure;
import groowt.view.component.ComponentContext;
import groowt.view.component.ComponentTemplate;
import groowt.view.web.DefaultWebComponentTemplateCompiler;
import groowt.view.web.DefaultWebViewComponentContext;
import groowt.view.web.runtime.WebViewComponentWriter;
import org.codehaus.groovy.control.CompilerConfiguration;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import static org.junit.jupiter.api.Assertions.*;
public abstract class AbstractWebViewComponentTests {
protected void doTest(Reader source, String expected, ComponentContext context) {
final var compiler = new DefaultWebComponentTemplateCompiler(
CompilerConfiguration.DEFAULT, this.getClass().getPackageName()
);
final ComponentTemplate template = compiler.compileAnonymous(source);
final Closure<?> renderer = template.getRenderer();
final StringWriter sw = new StringWriter();
final WebViewComponentWriter out = new WebViewComponentWriter(sw);
renderer.call(context, out);
assertEquals(expected, sw.toString().trim());
}
protected void doTest(String source, String expected, ComponentContext context) {
this.doTest(new StringReader(source), expected, context);
}
protected void doTest(String source, String expected) {
final var context = new DefaultWebViewComponentContext();
context.pushDefaultScope();
this.doTest(source, expected, context);
}
}

View File

@ -1,11 +1,7 @@
package groowt.view.web.tools package groowt.view.web.tools
import groowt.view.component.DefaultComponentContext import groowt.view.component.DefaultComponentContext
import groowt.view.web.DefaultWebComponentTemplateCompiler
import groowt.view.web.DefaultWebViewComponent import groowt.view.web.DefaultWebViewComponent
import groowt.view.web.WebViewTemplateComponentSource
import groowt.view.web.runtime.WebViewComponentWriter
import org.codehaus.groovy.control.CompilerConfiguration
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
import picocli.CommandLine.Option import picocli.CommandLine.Option
@ -30,7 +26,7 @@ class RunTemplate implements Callable<Integer> {
@Override @Override
Integer call() throws Exception { Integer call() throws Exception {
def component = new DefaultWebViewComponent(WebViewTemplateComponentSource.of(this.template)) def component = new DefaultWebViewComponent(this.template)
def context = new DefaultComponentContext() def context = new DefaultComponentContext()
context.pushDefaultScope() context.pushDefaultScope()