Skip to content

Commit

Permalink
maint: refactor and add build test
Browse files Browse the repository at this point in the history
  • Loading branch information
gamebox committed Dec 25, 2023
1 parent 7e0b898 commit 50c1cf2
Show file tree
Hide file tree
Showing 19 changed files with 560 additions and 172 deletions.
8 changes: 8 additions & 0 deletions gwirl-example/model/participant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package model

type Participant struct {
FirstName string
LastName string
Email string
Id string
}
8 changes: 3 additions & 5 deletions gwirl-example/templates/base.html.gwirl
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
@(flash *flash.Flash, title string, path string, embed string)

@import "github.com/gamebox/gwirl/gwirl-example/flash"
@import "fmt"

<!DOCTYPE html>
<html lang="en" class="min-h-full">
<head>
<meta charset="UTF-8" />
Expand All @@ -17,7 +15,7 @@
html {
--accent: 22, 163, 74;
--tb-logo-stroke-color: rgb(var(--accent));
@}
}
</style>
</head>
<body class="dark:bg-black min-h-full">
Expand All @@ -29,8 +27,8 @@
const flash = document.body.querySelector("#flash");
setTimeout(() => {
flash.close();
@}, 10 * 1000);
@});
}, 10 * 1000);
});
</script>
}

Expand Down
6 changes: 4 additions & 2 deletions gwirl-example/templates/manageParticipants.html.gwirl
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@(participants []Participant)
@(participants []model.Participant)

@import "github.com/gamebox/gwirl/gwirl-example/model"

<div class="title-container">
<h1>Participant Manager</h1>
<a class="action" target="_blank" href="/cpc/">New</a>
Expand Down Expand Up @@ -32,4 +35,3 @@
}
</tbody>
</table>
<link href="/assets/table.css" rel="stylesheet" />
108 changes: 108 additions & 0 deletions gwirl/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"unicode"

"github.com/gamebox/gwirl/internal/gen"
"github.com/gamebox/gwirl/internal/parser"
)

type Builder struct {
flags *Flags
accessor FSAccessor
logger io.Writer
parser *parser.Parser2
generator *gen.Generator
}

func NewBuilder(flags *Flags, accessor FSAccessor, logger io.Writer) *Builder {
b := Builder{
flags: flags,
accessor: accessor,
logger: logger,
}
p := parser.NewParser2("")
if logger != nil {
p.SetLogger(logger)
}
g := gen.NewGenerator(false)
b.parser = &p
b.generator = &g

return &b
}

func capitalize(str string) string {
runes := []rune(str)
if len(runes) == 0 {
return ""
}
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}

func (b *Builder) Printf(format string, vals ...any) {
if b.logger != nil {
b.logger.Write([]byte(fmt.Sprintf(format, vals...)))
}
}

func (b *Builder) parse(f *File) (*parser.ParseResult2, error) {
b.Printf("Parsing %s\n", f.name)

result := b.parser.Parse(f.content, capitalize(f.name))
if len(result.Errors) > 0 {
err := strings.Builder{}
err.WriteString(fmt.Sprintf("Could not parse file %s:\n", f.name+f.filetype+".gwirl"))
for _, e := range result.Errors {
err.WriteString(fmt.Sprintf("%v\n", e))
}
return nil, errors.New(err.String())
}
return &result, nil
}

func (b *Builder) generate(result *parser.ParseResult2, f *File) error {
b.Printf("Generating %s\n", f.name)

fileWriter, fileName, err := b.accessor.CreateGwirlFile(f)
if err != nil {
e := errors.New(fmt.Sprintf("Failed to open go file for template: %s\nERROR: %v", f.name, err))
return errors.Join(e, err)
}

err = b.generator.Generate(result.Template, f.filetype, fileWriter)
if err != nil {
b.accessor.Remove(fileName)
e := errors.New(fmt.Sprintf("Could not generate a file for template: %s", f.name))
return errors.Join(e, err)
}

return nil
}

func (b *Builder) build() error {
if b.flags.clean {
clean()
}
fs := b.accessor.TemplateFiles("templates", b.flags.filter.filters)
for _, f := range fs {
b.accessor.EnsureDirectoryExists(filepath.Join("views", f.filetype))
result, err := b.parse(&f)
if err != nil {
return err
}
err = b.generate(result, &f)
if err != nil {
return err
}
}

b.Printf("Completed generating %d templates", len(fs))
return nil
}
56 changes: 56 additions & 0 deletions gwirl/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"os"
"path/filepath"
"testing"
)

var testTemplates = []string{"base", "layout", "manageParticipants", "nav", "testAll", "transcluded", "useOther"}

func TestBuildSuccess(t *testing.T) {
cwd, _ := os.Getwd()
accessor := NewRealFSAccessor(filepath.Join(cwd, "testdata"))
file, _ := os.Create(os.DevNull)
b := NewBuilder(&Flags{}, accessor, file)
b.build()
defer os.RemoveAll(filepath.Join(cwd, "testdata", "views"))

entries, err := os.ReadDir(filepath.Join(cwd, "testdata", "views", "html"))
if err != nil {
t.Fatalf("No views directory found")
}
if len(entries) != len(testTemplates) {
t.Fatalf("Not all templates were generated, got %d generated, expected %d", len(entries), len(testTemplates))
}
expectedEntries, err := os.ReadDir(filepath.Join(cwd, "testdata", "expected"))
if err != nil {
t.Fatalf("Unexpected error: could not find expected directory")
}
if len(expectedEntries) != len(testTemplates) {
t.Fatalf("Unexpected error: did not find the expected templates, got %d generated, expected %d", len(expectedEntries), len(testTemplates))

}
var expected map[string]string = make(map[string]string)
for _, entry := range expectedEntries {
contents, err := os.ReadFile(filepath.Join(cwd, "testdata", "expected", entry.Name()))
if err != nil {
t.Fatalf("Unexpected error: could not load expected template: %s", err.Error())
}
expected[entry.Name()] = string(contents)
}
for _, entry := range entries {
contents, err := os.ReadFile(filepath.Join(cwd, "testdata", "views", "html", entry.Name()))
if err != nil {
t.Fatalf("Unexpected error: could not load generated template")
}
expectedContents := expected[entry.Name()]
if expectedContents == "" {
t.Fatalf("No file contents for \"%s\"", entry.Name())
}
actualContents := string(contents)
if actualContents != expectedContents {
t.Fatalf("Template \"%s\" did not match", entry.Name())
}
}
}
30 changes: 30 additions & 0 deletions gwirl/clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"
)

func cleanDir(filetype string, d []os.DirEntry) {
for _, entry := range d {
if strings.HasSuffix(entry.Name(), "_gwirl.go") {
_ = os.Remove(filepath.Join("views", filetype, entry.Name()))
}
}
}

func clean() {
fmt.Println("Cleaning views directories of Gwirl files...")
fileTypes := [4]string{"html","xml","md","txt"}
for _, ft := range fileTypes {
d, err := os.ReadDir(filepath.Join("views", ft))
if err != nil {
continue
}
cleanDir(ft, d)
}
fmt.Println("Clean!")
}

119 changes: 119 additions & 0 deletions gwirl/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
)

type File struct {
name string
filetype string
content string
}

type FSAccessor interface {
// Collects a slice of all template files found within the directory specified.
// The directory should be relative to a root directory, which may be the
// current working directory or a configurable root directory depending on
// the implementation.
TemplateFiles(dir string, filters []string) []File
// Creates the "_gwirl.go" file that is executable in the Go program. Where
// it will be created is based on the name and the filetype. The base
// directory will be determined by joining the root directory with the
// segment "views".
CreateGwirlFile(f *File) (io.Writer, string, error)
// Will create the given subdirectory in the root directory if it does not
// exist.
EnsureDirectoryExists(name string)
// Will remove the file at the path determined by joining the root directory
// with the path given.
Remove(name string) error
}

type RealFSAccessor struct {
rootDir string
}

func NewRealFSAccessor(rootDir string) *RealFSAccessor {
a := RealFSAccessor{ rootDir: rootDir }
return &a
}

func (a *RealFSAccessor) Remove(name string) error {
return os.Remove(filepath.Join(a.rootDir, name))
}

func (a *RealFSAccessor) EnsureDirectoryExists(path string) {
_, err := os.Stat(filepath.Join(a.rootDir, path))
if err != nil {
err := os.MkdirAll(filepath.Join(a.rootDir,path), 0755)
if err != nil {
log.Fatalf("Error creating views directory: %v", err)
}
}
}

func (a *RealFSAccessor) TemplateFiles(templateDir string, filters []string) []File {
return templateFiles(filepath.Join(a.rootDir, templateDir), filters)
}

func templateFiles(templateDir string, filters []string) []File {
entries, err := os.ReadDir(templateDir)
files := make([]File, 0, len(entries))
if err != nil {
return files
}
for _, dir := range entries {
if dir.IsDir() {
subEntries := templateFiles(filepath.Join(templateDir, dir.Name()), filters)
files = append(files, subEntries...)
} else if strings.HasSuffix(dir.Name(), ".gwirl") && matchesFilter(dir.Name(), filters) {
fileContent, err := os.ReadFile(filepath.Join(templateDir, dir.Name()))
if err != nil {
continue
}
filenameSegments := strings.Split(filepath.Base(dir.Name()), ".")
fileType := filenameSegments[len(filenameSegments) - 2]
if fileType != "html" && fileType != "xml" && fileType != "md" && fileType != "txt" {
fileType = "txt"
}
files = append(files, File{
name: strings.Split(dir.Name(), ".")[0],
content: string(fileContent),
filetype: fileType,
})
}
}
return files
}

func (a *RealFSAccessor) CreateGwirlFile(f *File) (io.Writer, string, error) {
fileName := filepath.Join(a.rootDir, "views", f.filetype, f.name+"_gwirl.go")
fileWriter, err := os.Create(fileName)
if err != nil {
e := errors.New(fmt.Sprintf("Failed to open go file for template: %s\nERROR: %v", f.name, err))
return nil, fileName, errors.Join(e, err)
}
return fileWriter, fileName, nil
}

func matchesFilter(name string, filters []string) bool {
if len(filters) == 0 {
return true
}
for _, filter := range filters {
log.Printf("\"%s\" has prefix \"%s\"?", name, filter)
if strings.HasPrefix(name, filter) {
log.Println("YUP")
return true
}
}
return false
}


Loading

0 comments on commit 50c1cf2

Please sign in to comment.