Working on Build and ssg object factory.

This commit is contained in:
JesseBrault0709 2024-05-15 08:54:37 +02:00
parent 76c6280b5d
commit 140dffefc6
13 changed files with 304 additions and 26 deletions

View File

@ -2,6 +2,7 @@ package com.jessebrault.ssg.build
import com.jessebrault.ssg.model.Model import com.jessebrault.ssg.model.Model
import com.jessebrault.ssg.page.Page import com.jessebrault.ssg.page.Page
import groowt.util.di.RegistryObjectFactory
import groowt.util.fp.provider.NamedSetProvider import groowt.util.fp.provider.NamedSetProvider
import static com.jessebrault.ssg.util.ObjectUtil.* import static com.jessebrault.ssg.util.ObjectUtil.*
@ -14,8 +15,8 @@ class Build {
final File outputDir final File outputDir
final Map globals final Map globals
final Set<File> textsDirs final Set<File> textsDirs
final NamedSetProvider<Model> models
final NamedSetProvider<Page> pages final NamedSetProvider<Page> pages
final RegistryObjectFactory objectFactory
Build(Map args) { Build(Map args) {
this.name = requireString(args.name) this.name = requireString(args.name)
@ -24,14 +25,14 @@ class Build {
this.outputDir = requireFile(args.outputDir) this.outputDir = requireFile(args.outputDir)
this.globals = requireMap(args.globals) this.globals = requireMap(args.globals)
this.textsDirs = requireSet(args.textsDirs) this.textsDirs = requireSet(args.textsDirs)
this.models = requireType(NamedSetProvider, args.models)
this.pages = requireType(NamedSetProvider, args.pages) this.pages = requireType(NamedSetProvider, args.pages)
this.objectFactory = requireType(RegistryObjectFactory, args.objectFactory)
} }
void doBuild() { void doBuild() {
// set up object factory for di // set up object factory for di
// container should have: Build and all its properties // 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
} }
} }

View File

@ -4,11 +4,14 @@ import com.jessebrault.ssg.buildscript.delegates.BuildDelegate
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor import groovy.transform.TupleConstructor
import java.util.function.Supplier
@NullCheck @NullCheck
@TupleConstructor(includeFields = true) @TupleConstructor(includeFields = true)
class BuildDelegateToBuildSpecConverter { class BuildDelegateToBuildSpecConverter {
private final FileBuildScriptGetter buildScriptGetter private final FileBuildScriptGetter buildScriptGetter
private final Supplier<BuildDelegate> buildDelegateSupplier
protected BuildSpec getFromDelegate(String name, BuildDelegate delegate) { protected BuildSpec getFromDelegate(String name, BuildDelegate delegate) {
new BuildSpec( new BuildSpec(
@ -32,7 +35,7 @@ class BuildDelegateToBuildSpecConverter {
extending = from.extending extending = from.extending
} }
def delegate = new BuildDelegate() def delegate = this.buildDelegateSupplier.get()
while (!buildHierarchy.isEmpty()) { while (!buildHierarchy.isEmpty()) {
def currentScript = buildHierarchy.pop() def currentScript = buildHierarchy.pop()
currentScript.buildClosure.delegate = delegate currentScript.buildClosure.delegate = delegate

View File

@ -2,30 +2,34 @@ package com.jessebrault.ssg.buildscript.delegates
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groowt.util.di.DefaultRegistryObjectFactory
import groowt.util.di.RegistryObjectFactory
import groowt.util.fp.property.Property import groowt.util.fp.property.Property
import groowt.util.fp.provider.DefaultSetProvider import groowt.util.fp.provider.DefaultSetProvider
import groowt.util.fp.provider.Provider import groowt.util.fp.provider.Provider
import groowt.util.fp.provider.SetProvider import groowt.util.fp.provider.SetProvider
import java.util.function.Supplier
@NullCheck(includeGenerated = true) @NullCheck(includeGenerated = true)
@EqualsAndHashCode(includeFields = true) @EqualsAndHashCode(includeFields = true)
final class BuildDelegate { final class BuildDelegate {
final Property<String> siteName = Property.empty().tap { static Supplier<BuildDelegate> withDefaults() {
convention = 'An Ssg Site' return {
new BuildDelegate().tap {
outputDir.convention = 'dist'
globals.convention = [:]
objectFactory.convention = DefaultRegistryObjectFactory.Builder.withDefaults()
}
}
} }
final Property<String> baseUrl = Property.empty().tap { final Property<String> siteName = Property.empty()
convention = '' final Property<String> baseUrl = Property.empty()
} final Property<File> outputDir = Property.empty()
final Property<Map<String, Object>> globals = Property.empty()
final Property<File> outputDir = Property.empty().tap { final Property<RegistryObjectFactory> objectFactory = Property.empty()
convention = siteName.map { it.replace(' ', '-').toLowerCase() + '-build' }
}
final Property<Map<String, Object>> globals = Property.empty().tap {
convention = [:]
}
private final Set<Provider<File>> textsDirs = [] private final Set<Provider<File>> textsDirs = []
@ -53,7 +57,7 @@ final class BuildDelegate {
this.outputDir.set(outputDirProvider) 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() def globalsDelegate = new GlobalsDelegate()
globalsClosure.delegate = globalsDelegate globalsClosure.delegate = globalsDelegate
globalsClosure.resolveStrategy = Closure.DELEGATE_FIRST globalsClosure.resolveStrategy = Closure.DELEGATE_FIRST

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.build package com.jessebrault.ssg.objects
import jakarta.inject.Qualifier import jakarta.inject.Qualifier
@ -10,7 +10,7 @@ import java.lang.annotation.Target
@Qualifier @Qualifier
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface Page { @interface InjectPage {
/** /**
* May be either a page name or a path starting with '/' * May be either a page name or a path starting with '/'

View File

@ -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<InjectPage> {
private final PagesExtension pagesExtension
@Override
<T> Binding<T> handle(InjectPage injectPage, Class<T> 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<T>(found.get() as T)
}
}

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.build package com.jessebrault.ssg.objects
import jakarta.inject.Qualifier import jakarta.inject.Qualifier
@ -10,7 +10,7 @@ import java.lang.annotation.Target
@Qualifier @Qualifier
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface Pages { @interface InjectPages {
/** /**
* Names of pages and/or globs (starting with '/') of pages * Names of pages and/or globs (starting with '/') of pages

View File

@ -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<InjectPages> {
private final PagesExtension pagesExtension
@Override
<T> Binding<T> handle(InjectPages injectPages, Class<T> 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<Page>
for (final String requested : injectPages.value()) {
if (requested.startsWith('/')) {
def glob = new Glob(requested)
def allFound = this.pagesExtension.pageProviders.inject([] as Set<Page>) { 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<T>(foundPages as T)
}
}

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.build package com.jessebrault.ssg.objects
import jakarta.inject.Qualifier import jakarta.inject.Qualifier
@ -10,7 +10,7 @@ import java.lang.annotation.Target
@Qualifier @Qualifier
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface Text { @interface InjectText {
/** /**
* The name of the text, or the path of the text, starting with '/' * The name of the text, or the path of the text, starting with '/'

View File

@ -1,4 +1,4 @@
package com.jessebrault.ssg.build package com.jessebrault.ssg.objects
import jakarta.inject.Qualifier import jakarta.inject.Qualifier
@ -10,7 +10,7 @@ import java.lang.annotation.Target
@Qualifier @Qualifier
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD]) @Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
@interface Texts { @interface InjectTexts {
/** /**
* Names of texts and/or globs (starting with '/') of texts * Names of texts and/or globs (starting with '/') of texts

View File

@ -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<PageProvider> pageProviders = []
private final QualifierHandler<InjectPage> injectPage = new InjectPageQualifierHandler(this)
private final QualifierHandler<InjectPages> injectPages = new InjectPagesQualifierHandler(this)
@Override
<A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> annotationClass) {
if (annotationClass == InjectPage) {
return this.injectPage as QualifierHandler<A>
} else if (annotationClass == InjectPages) {
return this.injectPages as QualifierHandler<A>
}
return null
}
}

View File

@ -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() {}
}

View File

@ -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<Page> {
final String name
final String path
private final Provider<Page> lazyPage
PageProvider(String name, String path, Page page) {
this.name = name
this.path = path
this.lazyPage = { page }
}
@Override
Page get() {
this.lazyPage.get()
}
}

View File

@ -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<GlobPart> toParts(String glob) {
final List<String> originalParts
if (glob.startsWith('/')) {
originalParts = glob.substring(1).split('/') as List<String>
} else {
originalParts = glob.split('/') as List<String>
}
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<GlobPart> 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<String> subjectParts
if (subject.startsWith('/')) {
subjectParts = subject.substring(1).split('/') as List<String>
} else {
subjectParts = subject.split('/') as List<String>
}
def subjectPartIter = subjectParts.iterator()
def subjectPartStack = new LinkedList<String>()
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
}
}