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.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<File> textsDirs
final NamedSetProvider<Model> models
final NamedSetProvider<Page> 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
}
}

View File

@ -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<BuildDelegate> 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

View File

@ -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<String> siteName = Property.empty().tap {
convention = 'An Ssg Site'
static Supplier<BuildDelegate> withDefaults() {
return {
new BuildDelegate().tap {
outputDir.convention = 'dist'
globals.convention = [:]
objectFactory.convention = DefaultRegistryObjectFactory.Builder.withDefaults()
}
}
}
final Property<String> baseUrl = Property.empty().tap {
convention = ''
}
final Property<File> outputDir = Property.empty().tap {
convention = siteName.map { it.replace(' ', '-').toLowerCase() + '-build' }
}
final Property<Map<String, Object>> globals = Property.empty().tap {
convention = [:]
}
final Property<String> siteName = Property.empty()
final Property<String> baseUrl = Property.empty()
final Property<File> outputDir = Property.empty()
final Property<Map<String, Object>> globals = Property.empty()
final Property<RegistryObjectFactory> objectFactory = Property.empty()
private final Set<Provider<File>> 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

View File

@ -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 '/'

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
@ -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

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
@ -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 '/'

View File

@ -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

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
}
}