diff --git a/view-components/src/main/java/groowt/view/component/AbstractComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/AbstractComponentTemplateCompiler.java new file mode 100644 index 0000000..f9c7ba5 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/AbstractComponentTemplateCompiler.java @@ -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 forClass, + Reader actualSource + ); + + @Override + public ComponentTemplate compile(Class 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); + }; + } + +} diff --git a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java index 2cc7012..9d8344d 100644 --- a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java @@ -5,9 +5,11 @@ import groovy.lang.Closure; import java.io.IOException; import java.io.Writer; import java.util.Objects; +import java.util.function.Function; public abstract class AbstractViewComponent implements ViewComponent { + private ComponentContext context; private ComponentTemplate template; public AbstractViewComponent() {} @@ -24,6 +26,27 @@ public abstract class AbstractViewComponent implements ViewComponent { } } + protected AbstractViewComponent(TemplateSource source, Function getCompiler) { + final Class 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 getSelfClass(); + + @Override + public void setContext(ComponentContext context) { + this.context = context; + } + + @Override + public ComponentContext getContext() { + return Objects.requireNonNull(this.context); + } + protected ComponentTemplate getTemplate() { return Objects.requireNonNull(template); } @@ -40,6 +63,11 @@ public abstract class AbstractViewComponent implements ViewComponent { this.getContext().afterComponentRender(this); } + /** + * @implSpec If overriding, please call + * {@link #beforeRender()}and {@link #afterRender()} before + * and after the actual rendering is done, respectively. + */ @Override public void renderTo(Writer out) throws IOException { final Closure closure = this.template.getRenderer(); diff --git a/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java index 9c03d55..3f183ce 100644 --- a/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java +++ b/view-components/src/main/java/groowt/view/component/CachingComponentTemplateCompiler.java @@ -1,27 +1,26 @@ package groowt.view.component; +import java.io.Reader; import java.util.HashMap; 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, ComponentTemplate> cache = new HashMap<>(); - protected final void putInCache(Class forClass, ComponentTemplate template) { - this.cache.put(forClass, template); - } - - protected final ComponentTemplate getFromCache(Class forClass) { - return Objects.requireNonNull(this.cache.get(forClass)); - } - - protected final ComponentTemplate getFromCacheOrElse( + @Override + protected final ComponentTemplate compile( + TemplateSource source, Class forClass, - Supplier 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 forClass, + Reader sourceReader + ); + } diff --git a/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java index 2428788..abbd8ab 100644 --- a/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java @@ -4,50 +4,79 @@ import groovy.lang.Closure; import static groowt.view.component.ComponentFactoryUtil.flatten; -final class ClosureComponentFactory extends ComponentFactoryBase { +final class ClosureComponentFactory implements ComponentFactory { + + private enum Type { + ALL, + NAME_AND_CONTEXT, NAME_AND_ARGS, CONTEXT_AND_ARGS, + NAME_ONLY, CONTEXT_ONLY, ARGS_ONLY, + NONE + } private final Closure closure; - private final Class firstParamType; + private final Type type; - public ClosureComponentFactory(Closure closure) { - this.closure = closure; - if (this.closure.getParameterTypes().length < 2) { - throw new IllegalArgumentException( - "Closures for " + getClass().getName() + " require at least two parameters" - ); - } - this.firstParamType = this.closure.getParameterTypes()[0]; - if (this.firstParamType != Object.class - && !(this.firstParamType == String.class || this.firstParamType == Class.class)) { - throw new IllegalArgumentException( - "The first closure parameter must be any of type Object (i.e, dynamic), String, or Class" - ); - } - final var secondParamType = this.closure.getParameterTypes()[1]; - if (secondParamType != Object.class && !ComponentContext.class.isAssignableFrom(secondParamType)) { - throw new IllegalArgumentException( - "The second closure parameter must be of type Object (i.e., dynamic) or " + - "ComponentContext or a subclass thereof." - ); + @SuppressWarnings("unchecked") + public ClosureComponentFactory(Closure closure) { + this.closure = (Closure) closure; + final var paramTypes = this.closure.getParameterTypes(); + 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; + } + } else { + final var firstParamType = paramTypes[0]; + final var secondParamType = paramTypes[1]; + if (firstParamType == String.class || firstParamType == Class.class) { + if (ComponentContext.class.isAssignableFrom(secondParamType)) { + if (paramTypes.length > 2) { + this.type = Type.ALL; + } else { + this.type = Type.NAME_AND_CONTEXT; + } + } else { + this.type = Type.NAME_AND_ARGS; + } + } 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 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)); + return this.objTypeCreate(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)); + return this.objTypeCreate(type, componentContext, args); } } diff --git a/view-components/src/main/java/groowt/view/component/ComponentCreateException.java b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java index 3b8bfc2..c4508d5 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentCreateException.java +++ b/view-components/src/main/java/groowt/view/component/ComponentCreateException.java @@ -27,8 +27,9 @@ public class ComponentCreateException extends RuntimeException { @Override public String getMessage() { - return "Exception in " + this.template.getClass() + " while creating " + this.componentType.getClass() + - " at line " + this.line + ", column " + this.column + "."; + return "Exception in " + this.template.getClass().getName() + " while creating " + + this.componentType.getClass().getName() + " at line " + this.line + + ", column " + this.column + "."; } } diff --git a/view-components/src/main/java/groowt/view/component/ComponentFactory.java b/view-components/src/main/java/groowt/view/component/ComponentFactory.java index 99fd2f7..1478d91 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ComponentFactory.java @@ -1,28 +1,13 @@ package groowt.view.component; import groovy.lang.Closure; -import groovy.lang.GroovyObject; import java.util.function.Supplier; @FunctionalInterface public interface ComponentFactory { - /** - * @param closure A closure with the following signature: - *

- * {@code Object componentType, ComponentContext context, ... -> T } - *

- * where '{@code ...}' represents any additional parameters (or none). - *

- * 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 The desired {@link ViewComponent} type. - */ - static ComponentFactory ofClosure(Closure closure) { + static ComponentFactory ofClosure(Closure closure) { return new ClosureComponentFactory<>(closure); } diff --git a/view-components/src/main/java/groowt/view/component/ComponentScope.java b/view-components/src/main/java/groowt/view/component/ComponentScope.java index e6b3428..0209c9a 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentScope.java +++ b/view-components/src/main/java/groowt/view/component/ComponentScope.java @@ -1,5 +1,7 @@ package groowt.view.component; +import groovy.lang.Closure; + public interface ComponentScope { void add(String name, ComponentFactory factory); @@ -24,11 +26,6 @@ public interface ComponentScope { return (ComponentFactory) this.get(clazz.getName()); } - @SuppressWarnings("unchecked") - default ComponentFactory getAs(String name, Class viewComponentType) { - return (ComponentFactory) this.get(name); - } - default void remove(Class clazz) { this.remove(clazz.getName()); } @@ -38,4 +35,12 @@ public interface ComponentScope { return (ComponentFactory) this.factoryMissing(clazz.getName()); } + default void add(String name, Closure closure) { + this.add(name, ComponentFactory.ofClosure(closure)); + } + + default void add(Class type, Closure closure) { + this.add(type, ComponentFactory.ofClosure(closure)); + } + } diff --git a/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java b/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java index 3f1ee74..2363bff 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java +++ b/view-components/src/main/java/groowt/view/component/ComponentTemplateCompiler.java @@ -1,16 +1,5 @@ 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 { - ComponentTemplate compile(Class forClass, File templateFile); - ComponentTemplate compile(Class forClass, String template); - ComponentTemplate compile(Class forClass, URI templateURI); - ComponentTemplate compile(Class forClass, URL templateURL); - ComponentTemplate compile(Class forClass, InputStream inputStream); - ComponentTemplate compile(Class forClass, Reader reader); + ComponentTemplate compile(Class forClass, TemplateSource source); } diff --git a/view-components/src/main/java/groowt/view/component/TemplateSource.java b/view-components/src/main/java/groowt/view/component/TemplateSource.java new file mode 100644 index 0000000..8dae0c4 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/TemplateSource.java @@ -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 absolute 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 {} + +} diff --git a/views/src/main/groovy/groowt/view/View.java b/views/src/main/groovy/groowt/view/View.java index 2b19815..c3162d2 100644 --- a/views/src/main/groovy/groowt/view/View.java +++ b/views/src/main/groovy/groowt/view/View.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +@FunctionalInterface public interface View { /** diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy new file mode 100644 index 0000000..905cd50 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.groovy @@ -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 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 getSelfClass() { + this.class + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java deleted file mode 100644 index c053340..0000000 --- a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java +++ /dev/null @@ -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 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 children; - - public DefaultWebViewComponent() {} - - public DefaultWebViewComponent(ComponentTemplate template) { - super(template); - } - - public DefaultWebViewComponent(Class 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 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 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); - } - -} diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy index c1f3a74..a823102 100644 --- a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy +++ b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy @@ -4,22 +4,23 @@ import groowt.view.component.ComponentScope import groowt.view.component.DefaultComponentContext import groowt.view.component.ViewComponent import groowt.view.web.lib.Fragment -import groowt.view.web.runtime.WebViewComponentChildCollector +import groowt.view.web.runtime.DefaultWebViewComponentChildCollection import org.jetbrains.annotations.ApiStatus -class DefaultWebViewComponentContext extends DefaultComponentContext { +class DefaultWebViewComponentContext extends DefaultComponentContext implements WebViewComponentContext { @Override protected ComponentScope getNewDefaultScope() { new WebViewScope() } + @Override @ApiStatus.Internal ViewComponent createFragment(Closure childCollector) { - def collector = new WebViewComponentChildCollector() - childCollector.call(collector) + def childCollection = new DefaultWebViewComponentChildCollection() + childCollector.call(childCollection) def fragment = new Fragment() - fragment.childRenderers = collector.children + fragment.childRenderers = childCollection.children fragment } diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy new file mode 100644 index 0000000..62ca2c1 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/WebViewComponentFactories.groovy @@ -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 ComponentFactory withAttr( + @ClosureParams(value = FromString, options = 'java.util.Map') + Closure closure + ) { + ComponentFactory.ofClosure { Map attr -> closure(attr) } + } + + static ComponentFactory withAttr(Function, T> tFunction) { + ComponentFactory.ofClosure { Map attr -> tFunction.apply(attr) } + } + + private WebViewComponentFactories() {} + +} diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy index 4f5b981..fb4b36d 100644 --- a/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy +++ b/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy @@ -2,15 +2,14 @@ package groowt.view.web import groowt.view.component.ComponentFactory import groowt.view.component.DefaultComponentScope +import groowt.view.web.lib.Echo import groowt.view.web.lib.Echo.EchoFactory class WebViewScope extends DefaultComponentScope { - private final EchoFactory echoFactory = new EchoFactory() - @Override ComponentFactory factoryMissing(String typeName) { - echoFactory + Echo.FACTORY } } diff --git a/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java index 34a8a30..c48f94b 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java +++ b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java @@ -22,8 +22,10 @@ public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent protected abstract View getDelegate(); @Override - public void renderTo(Writer out) throws IOException { + public final void renderTo(Writer out) throws IOException { + this.beforeRender(); this.getDelegate().renderTo(out); + this.afterRender(); } } diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy index 12713cb..5c93ee2 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy @@ -1,14 +1,20 @@ package groowt.view.web.lib -import groowt.view.StandardGStringTemplateView import groowt.view.View import groowt.view.component.ComponentContext import groowt.view.component.ComponentFactory +import groowt.view.component.ComponentRenderException import groowt.view.web.WebViewChildComponentRenderer class Echo extends DelegatingWebViewComponent { - static final class EchoFactory implements ComponentFactory { + static final ComponentFactory FACTORY = new EchoFactory() + + private static final class EchoFactory implements ComponentFactory { + + Echo doCreate(String typeName) { + doCreate(typeName, [:], true) + } Echo doCreate(String typeName, boolean selfClose) { doCreate(typeName, [:], selfClose) @@ -19,8 +25,7 @@ class Echo extends DelegatingWebViewComponent { } Echo doCreate(String typeName, Map attr, boolean selfClose) { - def echo = new Echo(attr, typeName, selfClose) - echo + new Echo(attr, typeName, selfClose) } Echo doCreate( @@ -56,26 +61,43 @@ class Echo extends DelegatingWebViewComponent { @Override protected View getDelegate() { - return new StandardGStringTemplateView( - src: Echo.getResource('EchoTemplate.gst'), - parent: this - ) + if (this.selfClose && this.hasChildren()) { + throw new ComponentRenderException('Cannot have selfClose set to true and have children.') + } + 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 << '' + } + } } - String formatAttr() { - def sb = new StringBuilder() + protected void formatAttr(Writer writer) { def iter = this.attr.iterator() while (iter.hasNext()) { def entry = iter.next() - sb << entry.key - sb << '="' - sb << entry.value - sb << '"' + writer << entry.key + writer << '="' + writer << entry.value + writer << '"' if (iter.hasNext()) { - sb << ' ' + writer << ' ' } } - sb.toString() } } diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy index c51924b..b0d5d41 100644 --- a/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy +++ b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy @@ -2,7 +2,7 @@ package groowt.view.web.lib import groowt.view.web.DefaultWebViewComponent -class Fragment extends DefaultWebViewComponent { +final class Fragment extends DefaultWebViewComponent { @Override void renderTo(Writer out) throws IOException { diff --git a/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java b/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java new file mode 100644 index 0000000..6c23896 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/AbstractWebViewComponent.java @@ -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 childRenderers; + + public AbstractWebViewComponent() {} + + public AbstractWebViewComponent(ComponentTemplate template) { + super(template); + } + + public AbstractWebViewComponent(Class 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 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 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(); + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/ChildRenderException.java b/web-views/src/main/java/groowt/view/web/ChildRenderException.java similarity index 100% rename from web-views/src/main/groovy/groowt/view/web/ChildRenderException.java rename to web-views/src/main/java/groowt/view/web/ChildRenderException.java diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java similarity index 62% rename from web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java rename to web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java index 9f6b948..52697ac 100644 --- a/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java +++ b/web-views/src/main/java/groowt/view/web/DefaultWebViewComponentTemplateCompiler.java @@ -1,10 +1,7 @@ package groowt.view.web; import groovy.lang.GroovyClassLoader; -import groowt.view.component.CachingComponentTemplateCompiler; -import groowt.view.component.ComponentTemplate; -import groowt.view.component.ComponentTemplateCreateException; -import groowt.view.component.ViewComponent; +import groowt.view.component.*; import groowt.view.web.antlr.CompilationUnitParseResult; import groowt.view.web.antlr.ParserUtil; 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.Nullable; -import java.io.*; +import java.io.IOException; +import java.io.Reader; import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.net.URISyntaxException; import java.util.Objects; -public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler { +public class DefaultWebViewComponentTemplateCompiler extends CachingComponentTemplateCompiler + implements WebViewComponentTemplateCompiler { private final CompilerConfiguration configuration; private final String defaultPackageName; @@ -37,15 +33,12 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat private GroovyClassLoader groovyClassLoader; - public DefaultWebComponentTemplateCompiler( - CompilerConfiguration configuration, - String defaultPackageName - ) { + public DefaultWebViewComponentTemplateCompiler(CompilerConfiguration configuration, String defaultPackageName) { this(configuration, defaultPackageName, Phases.CLASS_GENERATION); } @ApiStatus.Internal - public DefaultWebComponentTemplateCompiler( + public DefaultWebViewComponentTemplateCompiler( CompilerConfiguration configuration, String defaultPackageName, int phase @@ -70,11 +63,30 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat this.groovyClassLoader = null; } - protected ComponentTemplate doCompile(@Nullable Class forClass, Reader reader) { - return this.doCompile(forClass, reader, null); + @Override + protected ComponentTemplate doCompile( + @Nullable TemplateSource source, + @Nullable Class 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 forClass, Reader reader, @Nullable URI uri) { + protected ComponentTemplate doCompile( + @Nullable Class forClass, + Reader reader, + @Nullable URI uri + ) { final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); // TODO: analysis @@ -153,56 +165,8 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat } @Override - public ComponentTemplate compile(Class 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 forClass, String template) { - return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new StringReader(template))); - } - - @Override - public ComponentTemplate compile(Class 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 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 forClass, InputStream inputStream) { - return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, new InputStreamReader(inputStream))); - } - - @Override - public ComponentTemplate compile(Class forClass, Reader reader) { - return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader)); - } - public ComponentTemplate compileAnonymous(Reader reader) { - return this.doCompile(null, reader); + return this.doCompile(null, null, reader); } } diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewComponent.java b/web-views/src/main/java/groowt/view/web/WebViewComponent.java similarity index 100% rename from web-views/src/main/groovy/groowt/view/web/WebViewComponent.java rename to web-views/src/main/java/groowt/view/web/WebViewComponent.java diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java b/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java new file mode 100644 index 0000000..b51324c --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentContext.java @@ -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); + +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompiler.java new file mode 100644 index 0000000..0e38806 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/WebViewComponentTemplateCompiler.java @@ -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); +} diff --git a/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java b/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java deleted file mode 100644 index 7ca4606..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewTemplateComponentSource.java +++ /dev/null @@ -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 absolute 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 {} - -} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java similarity index 86% rename from web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java rename to web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java index 9adec95..eaafb9d 100644 --- a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollector.java +++ b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentChildCollection.java @@ -11,22 +11,26 @@ import groowt.view.web.WebViewChildRenderer; import java.util.ArrayList; import java.util.List; -public class WebViewComponentChildCollector { +public class DefaultWebViewComponentChildCollection implements WebViewComponentChildCollection { private final List children = new ArrayList<>(); + @Override public void add(String jString, Closure renderer) { this.children.add(new WebViewChildJStringRenderer(jString, renderer)); } + @Override public void add(GString gString, Closure renderer) { this.children.add(new WebViewChildGStringRenderer(gString, renderer)); } + @Override public void add(ViewComponent component, Closure renderer) { this.children.add(new WebViewChildComponentRenderer(component, renderer)); } + @Override public List getChildren() { return this.children; } diff --git a/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java new file mode 100644 index 0000000..3ef07a8 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/DefaultWebViewComponentWriter.java @@ -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); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java new file mode 100644 index 0000000..84a5e05 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentChildCollection.java @@ -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 renderer); + + void add(GString gString, Closure renderer); + + void add(ViewComponent component, Closure renderer); + + List getChildren(); + +} diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java index 52edc1d..28a2d75 100644 --- a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java @@ -1,87 +1,14 @@ 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 WebViewComponentWriter { - - private final Writer delegate; - - public WebViewComponentWriter(Writer delegate) { - 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); - } - } - +public interface WebViewComponentWriter { + void append(String string); + void append(GString gString); + void append(GString gString, int line, int column); + void append(ViewComponent viewComponent); + void append(ViewComponent viewComponent, int line, int column); + void append(Object object); + void leftShift(Object object); } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java index 7d09c20..57128ba 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -3,7 +3,7 @@ package groowt.view.web.transpile; import groovy.lang.Tuple2; import groowt.view.component.*; 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.ConvertResult; import org.codehaus.groovy.ast.*; @@ -22,7 +22,7 @@ import static groowt.view.web.transpile.TranspilerUtil.*; public class DefaultComponentTranspiler implements ComponentTranspiler { 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 COMPONENT_CREATE = ClassHelper.make(ComponentCreateException.class); @@ -182,7 +182,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { String componentVariableName ) { final Parameter childCollectorParam = new Parameter( - CHILD_COLLECTOR, + CHILD_COLLECTION, componentVariableName + "_childCollector" ); diff --git a/web-views/src/main/resources/groowt/view/web/lib/EchoTemplateNoChildren.gst b/web-views/src/main/resources/groowt/view/web/lib/EchoTemplateNoChildren.gst new file mode 100644 index 0000000..2d74597 --- /dev/null +++ b/web-views/src/main/resources/groowt/view/web/lib/EchoTemplateNoChildren.gst @@ -0,0 +1 @@ +<$name diff --git a/web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst b/web-views/src/main/resources/groowt/view/web/lib/EchoTemplateWithChildren.gst similarity index 100% rename from web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst rename to web-views/src/main/resources/groowt/view/web/lib/EchoTemplateWithChildren.gst diff --git a/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java b/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java deleted file mode 100644 index e1f0635..0000000 --- a/web-views/src/test/groovy/groowt/view/web/DefaultWebComponentTemplateCompilerTests.java +++ /dev/null @@ -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 componentClass, Reader source) { - final var compiler = new DefaultWebComponentTemplateCompiler( - CompilerConfiguration.DEFAULT, - componentClass.getPackageName() - ); - return compiler.compile(componentClass, source); - } - - private static ComponentTemplate doCompile(Class componentClass, String source) { - return doCompile(componentClass, new StringReader(source)); - } - - private static final class Greeter extends DefaultWebViewComponent { - - private final String target; - - public Greeter(Map 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 { - - public Greeter doCreate(Map attr) { - return new Greeter(attr); - } - - } - - private static final class UsingGreeter extends DefaultWebViewComponent { - - public UsingGreeter(ComponentContext context) { - super(doCompile(UsingGreeter.class, "")); - 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()); - } - -} diff --git a/web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy b/web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy new file mode 100644 index 0000000..c033c08 --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/DefaultWebViewComponentTests.groovy @@ -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 attr) { + super('Hello, $target!') + this.target = Objects.requireNonNull(attr.get("target")) + } + + String getTarget() { + return this.target + } + + } + + private static final class GreeterFactory extends ComponentFactoryBase { + + Greeter doCreate(Map attr) { + return new Greeter(attr) + } + + } + + private static final class UsingGreeter extends DefaultWebViewComponent { + + UsingGreeter() { + super("") + } + + } + + @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('') + 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('') + c.context = context + assertEquals('Hello, World!', c.render()) + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy b/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy index d59fea1..25e92c0 100644 --- a/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/lib/EchoTests.groovy @@ -1,16 +1,31 @@ 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 class EchoTests extends AbstractWebViewComponentTests { + @Override + void configureContext(WebViewComponentContext context) { + super.configureContext(context) + context.currentScope.add('Echo', Echo.FACTORY) + } + @Test - void typeOnlySelfClose() { - def context = new DefaultWebViewComponentContext() - context.pushDefaultScope() - context.currentScope.add('Echo', new Echo.EchoFactory()) - this.doTest('', '', context) + void selfClose() { + this.doTest('', '') + } + + @Test + void noSelfClose() { + this.doTest('', '') + } + + @Test + @Disabled("Not possible to render children directly to a writer yet.") + void withChildren() { + this.doTest('Hello, World!', 'Hello, World!') } } diff --git a/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy index c7d65d6..ba78f9f 100644 --- a/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy +++ b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy @@ -4,9 +4,11 @@ import groowt.view.component.ComponentContext import groowt.view.component.ComponentFactory import groowt.view.web.DefaultWebViewComponent import groowt.view.web.DefaultWebViewComponentContext -import groowt.view.web.WebViewTemplateComponentSource +import groowt.view.component.TemplateSource import org.junit.jupiter.api.Test +import static groowt.view.web.WebViewComponentFactories.withAttr + class FragmentTests extends AbstractWebViewComponentTests { static class Greeter extends DefaultWebViewComponent { @@ -14,7 +16,7 @@ class FragmentTests extends AbstractWebViewComponentTests { String greeting Greeter(Map attr) { - super(WebViewTemplateComponentSource.of('$greeting')) + super(TemplateSource.of('$greeting')) greeting = attr.greeting } @@ -22,9 +24,7 @@ class FragmentTests extends AbstractWebViewComponentTests { private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap { pushDefaultScope() - def greeterFactory = ComponentFactory.ofClosure { type, componentContext, attr -> - new Greeter(attr) - } + def greeterFactory = withAttr(Greeter.&new) currentScope.add('Greeter', greeterFactory) } diff --git a/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy b/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy new file mode 100644 index 0000000..65defd5 --- /dev/null +++ b/web-views/src/testFixtures/groovy/groowt/view/web/lib/AbstractWebViewComponentTests.groovy @@ -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) + } + +} diff --git a/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy b/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy new file mode 100644 index 0000000..b55cdd6 --- /dev/null +++ b/web-views/src/testFixtures/groovy/groowt/view/web/lib/WithContext.groovy @@ -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() + } + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractWebViewComponentTests.java b/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractWebViewComponentTests.java deleted file mode 100644 index f614231..0000000 --- a/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractWebViewComponentTests.java +++ /dev/null @@ -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); - } - -} diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy index 646d1fa..a87eaed 100644 --- a/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy +++ b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy @@ -1,11 +1,7 @@ package groowt.view.web.tools import groowt.view.component.DefaultComponentContext -import groowt.view.web.DefaultWebComponentTemplateCompiler 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.Command import picocli.CommandLine.Option @@ -30,7 +26,7 @@ class RunTemplate implements Callable { @Override Integer call() throws Exception { - def component = new DefaultWebViewComponent(WebViewTemplateComponentSource.of(this.template)) + def component = new DefaultWebViewComponent(this.template) def context = new DefaultComponentContext() context.pushDefaultScope()