From 140dffefc6ad4b4a563827294933d16c4914bac2 Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Wed, 15 May 2024 08:54:37 +0200 Subject: [PATCH] Working on Build and ssg object factory. --- .../com/jessebrault/ssg/build/Build.groovy | 7 +- .../BuildDelegateToBuildSpecConverter.groovy | 5 +- .../delegates/BuildDelegate.groovy | 32 ++--- .../Page.groovy => objects/InjectPage.groovy} | 4 +- .../objects/InjectPageQualifierHandler.groovy | 33 +++++ .../InjectPages.groovy} | 4 +- .../InjectPagesQualifierHandler.groovy | 44 +++++++ .../Text.groovy => objects/InjectText.groovy} | 4 +- .../InjectTexts.groovy} | 4 +- .../ssg/objects/PagesExtension.groovy | 28 +++++ .../ssg/objects/SsgObjectFactory.groovy | 19 +++ .../ssg/provider/PageProvider.groovy | 27 ++++ .../com/jessebrault/ssg/util/Glob.groovy | 119 ++++++++++++++++++ 13 files changed, 304 insertions(+), 26 deletions(-) rename api/src/main/groovy/com/jessebrault/ssg/{build/Page.groovy => objects/InjectPage.groovy} (87%) create mode 100644 api/src/main/groovy/com/jessebrault/ssg/objects/InjectPageQualifierHandler.groovy rename api/src/main/groovy/com/jessebrault/ssg/{build/Pages.groovy => objects/InjectPages.groovy} (87%) create mode 100644 api/src/main/groovy/com/jessebrault/ssg/objects/InjectPagesQualifierHandler.groovy rename api/src/main/groovy/com/jessebrault/ssg/{build/Text.groovy => objects/InjectText.groovy} (87%) rename api/src/main/groovy/com/jessebrault/ssg/{build/Texts.groovy => objects/InjectTexts.groovy} (87%) create mode 100644 api/src/main/groovy/com/jessebrault/ssg/objects/PagesExtension.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/objects/SsgObjectFactory.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/provider/PageProvider.groovy create mode 100644 api/src/main/groovy/com/jessebrault/ssg/util/Glob.groovy diff --git a/api/src/main/groovy/com/jessebrault/ssg/build/Build.groovy b/api/src/main/groovy/com/jessebrault/ssg/build/Build.groovy index 7f0724e..0cc64d9 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/build/Build.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/build/Build.groovy @@ -2,6 +2,7 @@ package com.jessebrault.ssg.build import com.jessebrault.ssg.model.Model import com.jessebrault.ssg.page.Page +import groowt.util.di.RegistryObjectFactory import groowt.util.fp.provider.NamedSetProvider import static com.jessebrault.ssg.util.ObjectUtil.* @@ -14,8 +15,8 @@ class Build { final File outputDir final Map globals final Set textsDirs - final NamedSetProvider models final NamedSetProvider pages + final RegistryObjectFactory objectFactory Build(Map args) { this.name = requireString(args.name) @@ -24,14 +25,14 @@ class Build { this.outputDir = requireFile(args.outputDir) this.globals = requireMap(args.globals) this.textsDirs = requireSet(args.textsDirs) - this.models = requireType(NamedSetProvider, args.models) this.pages = requireType(NamedSetProvider, args.pages) + this.objectFactory = requireType(RegistryObjectFactory, args.objectFactory) } void doBuild() { // set up object factory for di // container should have: Build and all its properties - // container should also have @Text, @Texts, @Model, @Models, and @Page resolvers + // container should also have @Text, @Texts, @Page, and @Pages resolvers } } diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildDelegateToBuildSpecConverter.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildDelegateToBuildSpecConverter.groovy index c062fc2..7164e6d 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildDelegateToBuildSpecConverter.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/BuildDelegateToBuildSpecConverter.groovy @@ -4,11 +4,14 @@ import com.jessebrault.ssg.buildscript.delegates.BuildDelegate import groovy.transform.NullCheck import groovy.transform.TupleConstructor +import java.util.function.Supplier + @NullCheck @TupleConstructor(includeFields = true) class BuildDelegateToBuildSpecConverter { private final FileBuildScriptGetter buildScriptGetter + private final Supplier buildDelegateSupplier protected BuildSpec getFromDelegate(String name, BuildDelegate delegate) { new BuildSpec( @@ -32,7 +35,7 @@ class BuildDelegateToBuildSpecConverter { extending = from.extending } - def delegate = new BuildDelegate() + def delegate = this.buildDelegateSupplier.get() while (!buildHierarchy.isEmpty()) { def currentScript = buildHierarchy.pop() currentScript.buildClosure.delegate = delegate diff --git a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy index 979e3fb..b70a863 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/buildscript/delegates/BuildDelegate.groovy @@ -2,30 +2,34 @@ package com.jessebrault.ssg.buildscript.delegates import groovy.transform.EqualsAndHashCode import groovy.transform.NullCheck +import groowt.util.di.DefaultRegistryObjectFactory +import groowt.util.di.RegistryObjectFactory import groowt.util.fp.property.Property import groowt.util.fp.provider.DefaultSetProvider import groowt.util.fp.provider.Provider import groowt.util.fp.provider.SetProvider +import java.util.function.Supplier + @NullCheck(includeGenerated = true) @EqualsAndHashCode(includeFields = true) final class BuildDelegate { - final Property siteName = Property.empty().tap { - convention = 'An Ssg Site' + static Supplier withDefaults() { + return { + new BuildDelegate().tap { + outputDir.convention = 'dist' + globals.convention = [:] + objectFactory.convention = DefaultRegistryObjectFactory.Builder.withDefaults() + } + } } - final Property baseUrl = Property.empty().tap { - convention = '' - } - - final Property outputDir = Property.empty().tap { - convention = siteName.map { it.replace(' ', '-').toLowerCase() + '-build' } - } - - final Property> globals = Property.empty().tap { - convention = [:] - } + final Property siteName = Property.empty() + final Property baseUrl = Property.empty() + final Property outputDir = Property.empty() + final Property> globals = Property.empty() + final Property objectFactory = Property.empty() private final Set> textsDirs = [] @@ -53,7 +57,7 @@ final class BuildDelegate { this.outputDir.set(outputDirProvider) } - void globals(@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST) Closure globalsClosure) { + void globals(@DelegatesTo(value = GlobalsDelegate, strategy = Closure.DELEGATE_FIRST) Closure globalsClosure) { def globalsDelegate = new GlobalsDelegate() globalsClosure.delegate = globalsDelegate globalsClosure.resolveStrategy = Closure.DELEGATE_FIRST diff --git a/api/src/main/groovy/com/jessebrault/ssg/build/Page.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPage.groovy similarity index 87% rename from api/src/main/groovy/com/jessebrault/ssg/build/Page.groovy rename to api/src/main/groovy/com/jessebrault/ssg/objects/InjectPage.groovy index 69b77d5..bc6ddcf 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/build/Page.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPage.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.build +package com.jessebrault.ssg.objects import jakarta.inject.Qualifier @@ -10,7 +10,7 @@ import java.lang.annotation.Target @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) -@interface Page { +@interface InjectPage { /** * May be either a page name or a path starting with '/' diff --git a/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPageQualifierHandler.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPageQualifierHandler.groovy new file mode 100644 index 0000000..862562b --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPageQualifierHandler.groovy @@ -0,0 +1,33 @@ +package com.jessebrault.ssg.objects + +import com.jessebrault.ssg.page.Page +import groovy.transform.TupleConstructor +import groowt.util.di.Binding +import groowt.util.di.QualifierHandler +import groowt.util.di.SingletonBinding + +@TupleConstructor(includeFields = true) +class InjectPageQualifierHandler implements QualifierHandler { + + private final PagesExtension pagesExtension + + @Override + Binding handle(InjectPage injectPage, Class requestedClass) { + if (!Page.isAssignableFrom(requestedClass)) { + throw new IllegalArgumentException("Cannot inject a Page into a non-Page parameter/setter/field.") + } + def requested = injectPage.value() + def found = this.pagesExtension.pageProviders.find { + if (requested.startsWith('/')) { + it.path == requested + } else { + it.name == requested + } + } + if (found == null) { + throw new IllegalArgumentException("Cannot find a page with the following name or path: $requested") + } + new SingletonBinding(found.get() as T) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/build/Pages.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPages.groovy similarity index 87% rename from api/src/main/groovy/com/jessebrault/ssg/build/Pages.groovy rename to api/src/main/groovy/com/jessebrault/ssg/objects/InjectPages.groovy index 08e83cb..b74d09d 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/build/Pages.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPages.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.build +package com.jessebrault.ssg.objects import jakarta.inject.Qualifier @@ -10,7 +10,7 @@ import java.lang.annotation.Target @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) -@interface Pages { +@interface InjectPages { /** * Names of pages and/or globs (starting with '/') of pages diff --git a/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPagesQualifierHandler.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPagesQualifierHandler.groovy new file mode 100644 index 0000000..1c3a5c1 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectPagesQualifierHandler.groovy @@ -0,0 +1,44 @@ +package com.jessebrault.ssg.objects + +import com.jessebrault.ssg.page.Page +import com.jessebrault.ssg.util.Glob +import groovy.transform.TupleConstructor +import groowt.util.di.Binding +import groowt.util.di.QualifierHandler +import groowt.util.di.SingletonBinding + +@TupleConstructor(includeFields = true) +class InjectPagesQualifierHandler implements QualifierHandler { + + private final PagesExtension pagesExtension + + @Override + Binding handle(InjectPages injectPages, Class requestedClass) { + if (!(Set.isAssignableFrom(requestedClass))) { + throw new IllegalArgumentException( + 'Cannot inject a Collection of Pages into a non-Collection parameter/setter/field.' + ) + } + def foundPages = [] as Set + for (final String requested : injectPages.value()) { + if (requested.startsWith('/')) { + def glob = new Glob(requested) + def allFound = this.pagesExtension.pageProviders.inject([] as Set) { acc, pageProvider -> + if (glob.matches(pageProvider.path)) { + acc << pageProvider.get() + } + acc + } + allFound.each { foundPages << it } + } else { + def found = this.pagesExtension.pageProviders.find { it.name == requested } + if (found == null) { + throw new IllegalArgumentException("Cannot find page with the name: $requested") + } + foundPages << found.get() + } + } + new SingletonBinding(foundPages as T) + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/build/Text.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectText.groovy similarity index 87% rename from api/src/main/groovy/com/jessebrault/ssg/build/Text.groovy rename to api/src/main/groovy/com/jessebrault/ssg/objects/InjectText.groovy index 9e49dbb..c19c9b5 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/build/Text.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectText.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.build +package com.jessebrault.ssg.objects import jakarta.inject.Qualifier @@ -10,7 +10,7 @@ import java.lang.annotation.Target @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) -@interface Text { +@interface InjectText { /** * The name of the text, or the path of the text, starting with '/' diff --git a/api/src/main/groovy/com/jessebrault/ssg/build/Texts.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectTexts.groovy similarity index 87% rename from api/src/main/groovy/com/jessebrault/ssg/build/Texts.groovy rename to api/src/main/groovy/com/jessebrault/ssg/objects/InjectTexts.groovy index 9a1927f..b6bbf89 100644 --- a/api/src/main/groovy/com/jessebrault/ssg/build/Texts.groovy +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/InjectTexts.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.build +package com.jessebrault.ssg.objects import jakarta.inject.Qualifier @@ -10,7 +10,7 @@ import java.lang.annotation.Target @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) -@interface Texts { +@interface InjectTexts { /** * Names of texts and/or globs (starting with '/') of texts diff --git a/api/src/main/groovy/com/jessebrault/ssg/objects/PagesExtension.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/PagesExtension.groovy new file mode 100644 index 0000000..3833888 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/PagesExtension.groovy @@ -0,0 +1,28 @@ +package com.jessebrault.ssg.objects + + +import com.jessebrault.ssg.provider.PageProvider +import groowt.util.di.QualifierHandler +import groowt.util.di.QualifierHandlerContainer +import groowt.util.di.RegistryExtension + +import java.lang.annotation.Annotation + +class PagesExtension implements QualifierHandlerContainer, RegistryExtension { + + final Set pageProviders = [] + + private final QualifierHandler injectPage = new InjectPageQualifierHandler(this) + private final QualifierHandler injectPages = new InjectPagesQualifierHandler(this) + + @Override + QualifierHandler getQualifierHandler(Class annotationClass) { + if (annotationClass == InjectPage) { + return this.injectPage as QualifierHandler + } else if (annotationClass == InjectPages) { + return this.injectPages as QualifierHandler + } + return null + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/objects/SsgObjectFactory.groovy b/api/src/main/groovy/com/jessebrault/ssg/objects/SsgObjectFactory.groovy new file mode 100644 index 0000000..e6a690c --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/objects/SsgObjectFactory.groovy @@ -0,0 +1,19 @@ +package com.jessebrault.ssg.objects + +import groowt.util.di.DefaultRegistryObjectFactory +import groowt.util.di.RegistryObjectFactory + +final class SsgObjectFactory { + + static RegistryObjectFactory getDefault() { + DefaultRegistryObjectFactory.Builder.withDefaults().with { + it.configureRegistry { registry -> + registry.addExtension(new PagesExtension()) + } + build() + } + } + + private SsgObjectFactory() {} + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/provider/PageProvider.groovy b/api/src/main/groovy/com/jessebrault/ssg/provider/PageProvider.groovy new file mode 100644 index 0000000..48b91ef --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/provider/PageProvider.groovy @@ -0,0 +1,27 @@ +package com.jessebrault.ssg.provider + +import com.jessebrault.ssg.page.Page +import groovy.transform.TupleConstructor +import groowt.util.fp.provider.NamedProvider +import groowt.util.fp.provider.Provider + +@TupleConstructor(includeFields = true, force = true, defaults = false) +class PageProvider implements NamedProvider { + + final String name + final String path + + private final Provider lazyPage + + PageProvider(String name, String path, Page page) { + this.name = name + this.path = path + this.lazyPage = { page } + } + + @Override + Page get() { + this.lazyPage.get() + } + +} diff --git a/api/src/main/groovy/com/jessebrault/ssg/util/Glob.groovy b/api/src/main/groovy/com/jessebrault/ssg/util/Glob.groovy new file mode 100644 index 0000000..5b22870 --- /dev/null +++ b/api/src/main/groovy/com/jessebrault/ssg/util/Glob.groovy @@ -0,0 +1,119 @@ +package com.jessebrault.ssg.util + +import groovy.transform.TupleConstructor + +import java.util.regex.Pattern + +/** + * A very basic class for handling globs. Can handle one ** at most. + * In file/directory names, only '.' is escaped; any other special characters + * may cause an invalid regex pattern. + */ +final class Glob { + + private sealed interface GlobPart permits Literal, AnyDirectoryHierarchy, GlobFileOrDirectory {} + + @TupleConstructor + private static final class Literal implements GlobPart { + final String literal + } + + private static final class AnyDirectoryHierarchy implements GlobPart {} + + @TupleConstructor + private static final class GlobFileOrDirectory implements GlobPart { + final String original + final Pattern regexPattern + } + + private static List toParts(String glob) { + final List originalParts + if (glob.startsWith('/')) { + originalParts = glob.substring(1).split('/') as List + } else { + originalParts = glob.split('/') as List + } + + def result = originalParts.collect { + if (it == '**') { + new AnyDirectoryHierarchy() + } else if (it.contains('*')) { + def replaced = it.replace([ + '*': '.', + '.': '\\.' + ]) + new GlobFileOrDirectory(it, ~replaced) + } else { + new Literal(it) + } + } + + result + } + + private final List parts + + Glob(String glob) { + this.parts = toParts(glob) + } + + boolean matches(File file) { + this.matches(file.toString().replace(File.separator, '/')) + } + + /** + * @param subject Must contain only '/' as a separator. + * @return whether the subject String matches this glob. + */ + boolean matches(String subject) { + final List subjectParts + if (subject.startsWith('/')) { + subjectParts = subject.substring(1).split('/') as List + } else { + subjectParts = subject.split('/') as List + } + + def subjectPartIter = subjectParts.iterator() + def subjectPartStack = new LinkedList() + while (subjectPartIter.hasNext()) { + subjectPartStack.push(subjectPartIter.next()) + } + + boolean result = true + parts: + for (def part : this.parts) { + switch (part) { + case Literal -> { + if (subjectPartStack.isEmpty()) { + result = false + break + } + def subjectPart = subjectPartStack.pop() + if (part.literal != subjectPart) { + result = false + break + } + } + case AnyDirectoryHierarchy -> { + while (!subjectPartStack.isEmpty()) { + def current = subjectPartStack.pop() + if (subjectPartStack.isEmpty()) { + subjectPartStack.push(current) + continue parts + } + } + } + case GlobFileOrDirectory -> { + def subjectPart = subjectPartStack.pop() + def m = part.regexPattern.matcher(subjectPart) + if (!m.matches()) { + result = false + break + } + } + } + } + result + } + +}