Compare commits
12 Commits
eb331889bb
...
715445ee5d
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 715445ee5d | |
Maxim Lebedev | 7a0727aa8d | |
Maxim Lebedev | 25494915ba | |
Maxim Lebedev | 1718bf6f7e | |
Maxim Lebedev | 733a3e4e8b | |
Maxim Lebedev | 665af5ebfc | |
Maxim Lebedev | 5adcb66862 | |
Maxim Lebedev | ed87027846 | |
Maxim Lebedev | fa2178e597 | |
Maxim Lebedev | c1bae19013 | |
Maxim Lebedev | b23aad0791 | |
Maxim Lebedev | 9422f13e7c |
|
@ -0,0 +1,23 @@
|
|||
; https://EditorConfig.org/
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{css,js,html}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
|
||||
[*.go]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 8
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.golden]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
|
@ -57,7 +57,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
themer := themeucase.NewThemeUseCase(partialsDir, themes)
|
||||
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
||||
pager := pageucase.NewPageUseCase(pages, resources)
|
||||
server := servercase.NewServerUseCase()
|
||||
serverer := servercase.NewServerUseCase()
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
|
||||
// first before deep down to any page or it's resource which might be secured by middleware or
|
||||
|
@ -77,43 +77,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
siteServer, err := server.Do(r.Context(), *s)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i := range siteServer.Headers {
|
||||
if !siteServer.Headers[i].IsMatched(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
for name, value := range siteServer.Headers[i].Values {
|
||||
w.Header().Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
var redirect *domain.Redirect
|
||||
for i := range siteServer.Redirects {
|
||||
if !siteServer.Redirects[i].IsMatch(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if siteServer.Redirects[i].Force {
|
||||
http.Redirect(w, r, siteServer.Redirects[i].To, siteServer.Redirects[i].Status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirect = &siteServer.Redirects[i]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if s.IsMultiLingual() {
|
||||
head, tail := urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
if head == "" {
|
||||
supported := make([]language.Tag, len(s.Languages))
|
||||
for i := range s.Languages {
|
||||
|
@ -125,8 +90,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
supported...)
|
||||
}
|
||||
|
||||
requested, _, err := language.ParseAcceptLanguage(
|
||||
r.Header.Get(common.HeaderAcceptLanguage))
|
||||
requested, _, err := language.ParseAcceptLanguage(r.Header.Get(
|
||||
common.HeaderAcceptLanguage))
|
||||
if err != nil || len(requested) == 0 {
|
||||
requested = append(requested, language.English)
|
||||
}
|
||||
|
@ -139,14 +104,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
lang = domain.NewLanguage(head)
|
||||
r.URL.Path = tail
|
||||
}
|
||||
|
||||
if lang == domain.LanguageUnd && redirect != nil {
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
|
||||
return
|
||||
if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
|
||||
r.URL.Path = tail
|
||||
}
|
||||
}
|
||||
|
||||
if s, err = siter.Do(r.Context(), lang); err != nil {
|
||||
|
@ -155,38 +115,6 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
if siteServer, err = server.Do(r.Context(), *s); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for i := range siteServer.Headers {
|
||||
if !siteServer.Headers[i].IsMatched(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
for name, value := range siteServer.Headers[i].Values {
|
||||
w.Header().Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range siteServer.Redirects {
|
||||
if !siteServer.Redirects[i].IsMatch(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if siteServer.Redirects[i].Force {
|
||||
http.Redirect(w, r, siteServer.Redirects[i].To, siteServer.Redirects[i].Status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirect = &siteServer.Redirects[i]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
p, err := pager.Do(r.Context(), lang, r.URL.Path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, page.ErrNotExist) {
|
||||
|
@ -195,12 +123,6 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
if redirect != nil {
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
res, err := resourcer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
|
@ -255,7 +177,17 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
chain := middleware.Chain{middleware.LogFmt()}
|
||||
chain := middleware.Chain{
|
||||
middleware.LogFmt(),
|
||||
middleware.Redirect(middleware.RedirectConfig{
|
||||
Siter: siter,
|
||||
Serverer: serverer,
|
||||
}),
|
||||
middleware.Header(middleware.HeaderConfig{
|
||||
Siter: siter,
|
||||
Serverer: serverer,
|
||||
}),
|
||||
}
|
||||
|
||||
return &App{server: &http.Server{
|
||||
Addr: config.AddrPort().String(),
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package domain
|
||||
|
||||
import "source.toby3d.me/toby3d/home/internal/common"
|
||||
|
||||
type Direction struct{ direction string }
|
||||
|
||||
var (
|
||||
DirectionUnd Direction = Direction{} // "und"
|
||||
DirectionLeftToRight Direction = Direction{"ltr"} // "ltr"
|
||||
DirectionRightToLeft Direction = Direction{"rtl"} // "rtl"
|
||||
)
|
||||
|
||||
func (d Direction) String() string {
|
||||
if d.direction == "" {
|
||||
return common.Und
|
||||
}
|
||||
|
||||
return d.direction
|
||||
}
|
||||
|
||||
func (d Direction) GoString() string {
|
||||
return "domain.Direction(" + d.String() + ")"
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Language Language
|
||||
baseFileName string
|
||||
contentBaseName string
|
||||
dir string
|
||||
ext string
|
||||
filename string
|
||||
logicalName string
|
||||
path string
|
||||
translationBaseName string
|
||||
uniqueId string
|
||||
}
|
||||
|
||||
func NewFile(path string) File {
|
||||
out := File{
|
||||
Language: LanguageUnd,
|
||||
baseFileName: "",
|
||||
contentBaseName: "",
|
||||
dir: filepath.Dir(path) + string(filepath.Separator),
|
||||
ext: strings.TrimPrefix(filepath.Ext(path), "."),
|
||||
filename: path,
|
||||
logicalName: filepath.Base(path),
|
||||
path: path,
|
||||
translationBaseName: "",
|
||||
uniqueId: "",
|
||||
}
|
||||
out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName))
|
||||
|
||||
parts := strings.Split(out.baseFileName, ".")
|
||||
out.Language = NewLanguage(parts[len(parts)-1])
|
||||
out.translationBaseName = strings.Join(parts[:len(parts)-1], ".")
|
||||
out.contentBaseName = out.translationBaseName
|
||||
|
||||
switch out.translationBaseName {
|
||||
default:
|
||||
out.contentBaseName = out.translationBaseName
|
||||
case "_index", "index":
|
||||
out.contentBaseName = filepath.Base(out.dir)
|
||||
}
|
||||
|
||||
hash := md5.New()
|
||||
_, _ = hash.Write([]byte(out.path))
|
||||
out.uniqueId = string(hash.Sum(nil))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// BaseFileName returns file name without extention.
|
||||
func (f File) BaseFileName() string {
|
||||
return f.baseFileName
|
||||
}
|
||||
|
||||
func (f File) ContentBaseName() string {
|
||||
return f.contentBaseName
|
||||
}
|
||||
|
||||
// Dir returns directory path.
|
||||
func (f File) Dir() string {
|
||||
return f.dir
|
||||
}
|
||||
|
||||
// Ext returns file extention.
|
||||
func (f File) Ext() string {
|
||||
return f.ext
|
||||
}
|
||||
|
||||
func (f File) Filename() string {
|
||||
return f.filename
|
||||
}
|
||||
|
||||
func (f File) LogicalName() string {
|
||||
return f.logicalName
|
||||
}
|
||||
|
||||
func (f File) Path() string {
|
||||
return f.path
|
||||
}
|
||||
|
||||
func (f File) TranslationBaseName() string {
|
||||
return f.translationBaseName
|
||||
}
|
||||
|
||||
func (f File) UniqueID() string {
|
||||
return f.uniqueId
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package domain
|
||||
|
||||
type Headers []Header
|
||||
|
||||
func (h Headers) Match(p string) []Header {
|
||||
result := make(Headers, 0, len(h))
|
||||
|
||||
for i := range h {
|
||||
if !h[i].IsMatched(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, h[i])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -13,7 +13,7 @@ type Language struct {
|
|||
code string
|
||||
lang string
|
||||
name string
|
||||
dir string
|
||||
dir Direction
|
||||
}
|
||||
|
||||
var LanguageUnd Language = Language{} // "und"
|
||||
|
@ -26,13 +26,13 @@ func NewLanguage(raw string) Language {
|
|||
|
||||
out := Language{
|
||||
code: tag.String(),
|
||||
dir: "ltr",
|
||||
dir: DirectionLeftToRight,
|
||||
name: strings.ToLower(display.Self.Name(tag)),
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case language.Arabic, language.Persian, language.Hebrew, language.Urdu:
|
||||
out.dir = "rtl"
|
||||
out.dir = DirectionRightToLeft
|
||||
}
|
||||
|
||||
base, _ := tag.Base()
|
||||
|
@ -49,7 +49,7 @@ func (l Language) Code() string {
|
|||
return l.code
|
||||
}
|
||||
|
||||
func (l Language) Dir() string {
|
||||
func (l Language) Dir() Direction {
|
||||
return l.dir
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ func (l Language) Name() string {
|
|||
}
|
||||
|
||||
func (l Language) String() string {
|
||||
if l.code == "" {
|
||||
if l.code != "" {
|
||||
return l.code
|
||||
}
|
||||
|
||||
|
|
|
@ -60,13 +60,13 @@ func TestLanguage_Dir(t *testing.T) {
|
|||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
expect string
|
||||
expect domain.Direction
|
||||
}{
|
||||
"2letter": {"en", "ltr"},
|
||||
"rtl": {"ur", "rtl"},
|
||||
"3letter": {"eng", "ltr"},
|
||||
"region": {"en-US", "ltr"},
|
||||
common.Und: {"", ""},
|
||||
"2letter": {"en", domain.DirectionLeftToRight},
|
||||
"rtl": {"ur", domain.DirectionRightToLeft},
|
||||
"3letter": {"eng", domain.DirectionLeftToRight},
|
||||
"region": {"en-US", domain.DirectionLeftToRight},
|
||||
common.Und: {"", domain.DirectionUnd},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package domain
|
|||
type Page struct {
|
||||
Language Language
|
||||
Params map[string]any
|
||||
File File
|
||||
File Path
|
||||
Description string
|
||||
Title string
|
||||
Content []byte
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Path struct {
|
||||
Language Language
|
||||
baseFileName string
|
||||
contentBaseName string
|
||||
dir string
|
||||
ext string
|
||||
filename string
|
||||
logicalName string
|
||||
path string
|
||||
translationBaseName string
|
||||
uniqueId string
|
||||
}
|
||||
|
||||
func NewPath(path string) Path {
|
||||
out := Path{
|
||||
Language: LanguageUnd,
|
||||
baseFileName: "",
|
||||
contentBaseName: "",
|
||||
dir: filepath.Dir(path),
|
||||
ext: strings.TrimPrefix(filepath.Ext(path), "."),
|
||||
filename: path,
|
||||
logicalName: filepath.Base(path),
|
||||
path: path,
|
||||
translationBaseName: "",
|
||||
uniqueId: "",
|
||||
}
|
||||
out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName))
|
||||
|
||||
if out.dir[len(out.dir)-1] != '/' {
|
||||
out.dir += string(filepath.Separator)
|
||||
}
|
||||
|
||||
parts := strings.Split(out.baseFileName, ".")
|
||||
out.Language = NewLanguage(parts[len(parts)-1])
|
||||
out.translationBaseName = strings.Join(parts[:len(parts)-1], ".")
|
||||
out.contentBaseName = out.translationBaseName
|
||||
|
||||
switch out.translationBaseName {
|
||||
default:
|
||||
out.contentBaseName = out.translationBaseName
|
||||
case "_index", "index":
|
||||
out.contentBaseName = filepath.Base(out.dir)
|
||||
}
|
||||
|
||||
hash := md5.New()
|
||||
_, _ = hash.Write([]byte(out.path))
|
||||
out.uniqueId = string(hash.Sum(nil))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// BaseFileName returns file name without extention:
|
||||
//
|
||||
// /news/a.en.md => a.en
|
||||
// /news/b/index.en.md => index.en
|
||||
// /news/_index.en.md => _index.en
|
||||
func (p Path) BaseFileName() string {
|
||||
return p.baseFileName
|
||||
}
|
||||
|
||||
// ContentBaseName returns file or folder name based of index location:
|
||||
//
|
||||
// /news/a.en.md => a
|
||||
// /news/b/index.en.md => b
|
||||
// /news/_index.en.md => news
|
||||
func (p Path) ContentBaseName() string {
|
||||
return p.contentBaseName
|
||||
}
|
||||
|
||||
// Dir returns directory path:
|
||||
//
|
||||
// /news/a.en.md => news/
|
||||
// /news/b/index.en.md => news/b/
|
||||
// /news/_index.en.md => news/
|
||||
func (p Path) Dir() string {
|
||||
return p.dir
|
||||
}
|
||||
|
||||
// Ext returns file extention:
|
||||
//
|
||||
// /news/b/index.en.md => md
|
||||
func (p Path) Ext() string {
|
||||
return p.ext
|
||||
}
|
||||
|
||||
func (p Path) Filename() string {
|
||||
return p.filename
|
||||
}
|
||||
|
||||
// LogicalName returns fille file name in directory:
|
||||
//
|
||||
// /news/a.en.md => a.en.md
|
||||
// /news/b/index.en.md => index.en.md
|
||||
// /news/_index.en.md => _index.en.md
|
||||
func (p Path) LogicalName() string {
|
||||
return p.logicalName
|
||||
}
|
||||
|
||||
func (p Path) Path() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
// TranslationBaseName returns file name without language code and extention:
|
||||
//
|
||||
// /news/a.en.md => a
|
||||
// /news/b/index.en.md => index
|
||||
// /news/_index.en.md => _index
|
||||
func (p Path) TranslationBaseName() string {
|
||||
return p.translationBaseName
|
||||
}
|
||||
|
||||
func (p Path) UniqueID() string {
|
||||
return p.uniqueId
|
||||
}
|
||||
|
||||
func (p Path) GoString() string {
|
||||
return "domain.Path(" + p.path + ")"
|
||||
}
|
|
@ -8,159 +8,159 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
testRegularFile string = filepath.Join("news", "a.en.md")
|
||||
testLeafFile string = filepath.Join("news", "b", "index.en.md")
|
||||
testBranchFile string = filepath.Join("news", "_index.en.md")
|
||||
testRegularPath string = filepath.Join("news", "a.en.md")
|
||||
testLeafPath string = filepath.Join("news", "b", "index.en.md")
|
||||
testBranchPath string = filepath.Join("news", "_index.en.md")
|
||||
)
|
||||
|
||||
func TestFile_BaseFileName(t *testing.T) {
|
||||
func TestPath_BaseFileName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input, expect string
|
||||
}{
|
||||
"regular": {testRegularFile, "a.en"},
|
||||
"leaf": {testLeafFile, "index.en"},
|
||||
"branch": {testBranchFile, "_index.en"},
|
||||
"regular": {testRegularPath, "a.en"},
|
||||
"leaf": {testLeafPath, "index.en"},
|
||||
"branch": {testBranchPath, "_index.en"},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(tc.input).BaseFileName(); actual != tc.expect {
|
||||
if actual := domain.NewPath(tc.input).BaseFileName(); actual != tc.expect {
|
||||
t.Errorf("BaseFileName() = '%s', want '%s'", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_ContentBaseName(t *testing.T) {
|
||||
func TestPath_ContentBaseName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input, expect string
|
||||
}{
|
||||
"regular": {testRegularFile, "a"},
|
||||
"leaf": {testLeafFile, "b"},
|
||||
"branch": {testBranchFile, "news"},
|
||||
"regular": {testRegularPath, "a"},
|
||||
"leaf": {testLeafPath, "b"},
|
||||
"branch": {testBranchPath, "news"},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(tc.input).ContentBaseName(); actual != tc.expect {
|
||||
if actual := domain.NewPath(tc.input).ContentBaseName(); actual != tc.expect {
|
||||
t.Errorf("ContentBaseName() = '%s', want '%s'", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_Dir(t *testing.T) {
|
||||
func TestPath_Dir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input, expect string
|
||||
}{
|
||||
"regular": {testRegularFile, "news/"},
|
||||
"leaf": {testLeafFile, "news/b/"},
|
||||
"branch": {testBranchFile, "news/"},
|
||||
"regular": {testRegularPath, "news/"},
|
||||
"leaf": {testLeafPath, "news/b/"},
|
||||
"branch": {testBranchPath, "news/"},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(tc.input).Dir(); actual != tc.expect {
|
||||
if actual := domain.NewPath(tc.input).Dir(); actual != tc.expect {
|
||||
t.Errorf("Dir() = '%s', want '%s'", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_Ext(t *testing.T) {
|
||||
func TestPath_Ext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const expect string = "md"
|
||||
|
||||
for name, input := range map[string]string{
|
||||
"regular": testRegularFile,
|
||||
"leaf": testLeafFile,
|
||||
"branch": testBranchFile,
|
||||
"regular": testRegularPath,
|
||||
"leaf": testLeafPath,
|
||||
"branch": testBranchPath,
|
||||
} {
|
||||
name, input := name, input
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(input).Ext(); actual != expect {
|
||||
if actual := domain.NewPath(input).Ext(); actual != expect {
|
||||
t.Errorf("Ext() = '%s', want '%s'", actual, expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_Language(t *testing.T) {
|
||||
func TestPath_Language(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var expect domain.Language = domain.NewLanguage("en")
|
||||
|
||||
for name, input := range map[string]string{
|
||||
"regular": testRegularFile,
|
||||
"leaf": testLeafFile,
|
||||
"branch": testBranchFile,
|
||||
"regular": testRegularPath,
|
||||
"leaf": testLeafPath,
|
||||
"branch": testBranchPath,
|
||||
} {
|
||||
name, input := name, input
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(input).Language; actual != expect {
|
||||
if actual := domain.NewPath(input).Language; actual != expect {
|
||||
t.Errorf("Language() = '%s', want '%s'", actual, expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_LogicalName(t *testing.T) {
|
||||
func TestPath_LogicalName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input, expect string
|
||||
}{
|
||||
"regular": {testRegularFile, "a.en.md"},
|
||||
"leaf": {testLeafFile, "index.en.md"},
|
||||
"branch": {testBranchFile, "_index.en.md"},
|
||||
"regular": {testRegularPath, "a.en.md"},
|
||||
"leaf": {testLeafPath, "index.en.md"},
|
||||
"branch": {testBranchPath, "_index.en.md"},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(tc.input).LogicalName(); actual != tc.expect {
|
||||
if actual := domain.NewPath(tc.input).LogicalName(); actual != tc.expect {
|
||||
t.Errorf("LogicalName() = '%s', want '%s'", actual, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_TranslationBaseName(t *testing.T) {
|
||||
func TestPath_TranslationBaseName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input, expect string
|
||||
}{
|
||||
"regular": {testRegularFile, "a"},
|
||||
"leaf": {testLeafFile, "index"},
|
||||
"branch": {testBranchFile, "_index"},
|
||||
"regular": {testRegularPath, "a"},
|
||||
"leaf": {testLeafPath, "index"},
|
||||
"branch": {testBranchPath, "_index"},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if actual := domain.NewFile(tc.input).TranslationBaseName(); actual != tc.expect {
|
||||
if actual := domain.NewPath(tc.input).TranslationBaseName(); actual != tc.expect {
|
||||
t.Errorf("TranslationBaseName() = '%s', want '%s'", actual, tc.expect)
|
||||
}
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
package domain
|
||||
|
||||
type Redirects []Redirect
|
||||
|
||||
func (r Redirects) Match(p string) (*Redirect, bool) {
|
||||
for i := range r {
|
||||
if !r[i].IsMatch(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &r[i], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
|
@ -15,7 +15,7 @@ import (
|
|||
)
|
||||
|
||||
type Resource struct {
|
||||
File File
|
||||
File Path
|
||||
|
||||
modTime time.Time
|
||||
params map[string]any // TODO(toby3d): set from Page configuration
|
||||
|
@ -30,7 +30,7 @@ type Resource struct {
|
|||
func NewResource(modTime time.Time, r io.Reader, key string) *Resource {
|
||||
mediaType, _, _ := mime.ParseMediaType(mime.TypeByExtension(path.Ext(key)))
|
||||
out := &Resource{
|
||||
File: NewFile(key),
|
||||
File: NewPath(key),
|
||||
modTime: modTime,
|
||||
key: key,
|
||||
name: key, // TODO(toby3d): set from Page configuration
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package domain
|
||||
|
||||
type Server struct {
|
||||
Headers []Header
|
||||
Redirects []Redirect
|
||||
Headers Headers
|
||||
Redirects Redirects
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
Headers: make([]Header, 0),
|
||||
Redirects: make([]Redirect, 0),
|
||||
Headers: make(Headers, 0),
|
||||
Redirects: make(Redirects, 0),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ type Site struct {
|
|||
BaseURL *url.URL
|
||||
Params map[string]any
|
||||
TimeZone *time.Location
|
||||
File File
|
||||
File Path
|
||||
Title string
|
||||
Resources Resources
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func TestSite(tb testing.TB) *Site {
|
|||
Languages: []Language{en, ru},
|
||||
BaseURL: &url.URL{Scheme: "http", Host: "127.0.0.1:3000", Path: "/"},
|
||||
TimeZone: time.UTC,
|
||||
File: NewFile(filepath.Join("content", "index.en.md")),
|
||||
File: NewPath(filepath.Join("content", "index.en.md")),
|
||||
Title: "Testing",
|
||||
Resources: make([]*Resource, 0),
|
||||
Params: map[string]any{
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/server"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
)
|
||||
|
||||
type HeaderConfig struct {
|
||||
Skipper Skipper
|
||||
Siter site.UseCase
|
||||
Serverer server.UseCase
|
||||
}
|
||||
|
||||
func Header(config HeaderConfig) Interceptor {
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
|
||||
if config.Siter == nil {
|
||||
panic("middleware: header: Siter is nil")
|
||||
}
|
||||
|
||||
if config.Serverer == nil {
|
||||
panic("middleware: header: Serverer is nil")
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if config.Skipper(r) {
|
||||
next(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang, path := domain.LanguageUnd, r.URL.Path
|
||||
if head, tail := urlutil.ShiftPath(r.URL.Path); head != "" {
|
||||
if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
|
||||
path = tail
|
||||
}
|
||||
}
|
||||
|
||||
site, err := config.Siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
server, err := config.Serverer.Do(r.Context(), *site)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, header := range server.Headers.Match(path) {
|
||||
for k, v := range header.Values {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
|
@ -2,25 +2,38 @@ package middleware
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/server"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
)
|
||||
|
||||
type (
|
||||
RedirectConfig struct {
|
||||
Skipper Skipper
|
||||
Code int
|
||||
Skipper Skipper
|
||||
Siter site.UseCase
|
||||
Serverer server.UseCase
|
||||
}
|
||||
|
||||
redirectLogic func(u *url.URL) (url string, ok bool)
|
||||
redirectResponse struct {
|
||||
http.ResponseWriter
|
||||
error error
|
||||
statusCode int
|
||||
}
|
||||
)
|
||||
|
||||
func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor {
|
||||
func Redirect(config RedirectConfig) Interceptor {
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
|
||||
if config.Code == 0 {
|
||||
config.Code = http.StatusMovedPermanently
|
||||
if config.Siter == nil {
|
||||
panic("middleware: redirect: Siter is nil")
|
||||
}
|
||||
|
||||
if config.Serverer == nil {
|
||||
panic("middleware: redirect: Serverer is nil")
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
|
@ -30,20 +43,69 @@ func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor {
|
|||
return
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: r.Host,
|
||||
Path: r.RequestURI,
|
||||
lang, path := domain.LanguageUnd, r.URL.Path
|
||||
if head, tail := urlutil.ShiftPath(r.URL.Path); head != "" {
|
||||
if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
|
||||
path = tail
|
||||
}
|
||||
}
|
||||
|
||||
if r.TLS != nil {
|
||||
u.Scheme += "s"
|
||||
site, err := config.Siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if target, ok := redirect(u); ok {
|
||||
http.RedirectHandler(target, config.Code).ServeHTTP(w, r)
|
||||
} else {
|
||||
server, err := config.Serverer.Do(r.Context(), *site)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirect, ok := server.Redirects.Match(path)
|
||||
if !ok {
|
||||
next(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE(toby3d): always redirect no matter what exists on
|
||||
// requested URL.
|
||||
if redirect.Force {
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tx := &redirectResponse{
|
||||
error: nil,
|
||||
statusCode: http.StatusOK,
|
||||
ResponseWriter: w,
|
||||
}
|
||||
|
||||
next(tx, r)
|
||||
|
||||
// NOTE(toby3d): redirect only if something bad on requested
|
||||
// URL.
|
||||
if tx.error == nil && http.StatusOK < tx.statusCode && tx.statusCode < http.StatusBadRequest {
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *redirectResponse) WriteHeader(status int) {
|
||||
r.statusCode = status
|
||||
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (r *redirectResponse) Write(src []byte) (int, error) {
|
||||
var length int
|
||||
length, r.error = r.ResponseWriter.Write(src)
|
||||
|
||||
return length, r.error
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang domain.Langu
|
|||
}
|
||||
|
||||
return &domain.Page{
|
||||
File: domain.NewFile(target),
|
||||
File: domain.NewPath(target),
|
||||
Language: lang,
|
||||
Title: data.Title,
|
||||
Content: data.Content,
|
||||
|
|
|
@ -71,7 +71,7 @@ func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang domain.Langu
|
|||
}
|
||||
|
||||
return &domain.Site{
|
||||
File: domain.NewFile(target),
|
||||
File: domain.NewPath(target),
|
||||
DefaultLanguage: data.DefaultLanguage.Language,
|
||||
Language: lang,
|
||||
Title: data.Title,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
static static.UseCase
|
||||
}
|
||||
|
||||
func NewHandler(static static.UseCase) *Handler {
|
||||
return &Handler{static: static}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s, err := h.static.Do(r.Context(), strings.TrimPrefix(path.Clean(r.URL.Path), "/"))
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, s.Name(), s.ModTime(), s)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
delivery "source.toby3d.me/toby3d/home/internal/static/delivery/http"
|
||||
repository "source.toby3d.me/toby3d/home/internal/static/repository/stub"
|
||||
"source.toby3d.me/toby3d/home/internal/static/usecase"
|
||||
"source.toby3d.me/toby3d/home/internal/testutil"
|
||||
)
|
||||
|
||||
func TestHandler_ServeHTTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
testStatic := domain.NewStatic(strings.NewReader("User-agent: *\nAllow: /"), time.Now().UTC(), "robots.txt")
|
||||
delivery.NewHandler(usecase.NewStaticUseCase(repository.NewStubStaticRepository(nil, testStatic, false))).
|
||||
ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
if expect := http.StatusOK; resp.StatusCode != expect {
|
||||
t.Errorf("%s %s = %d, want %d", req.Method, req.RequestURI, resp.StatusCode, expect)
|
||||
}
|
||||
|
||||
testutil.GoldenEqual(t, resp.Body)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
|
@ -0,0 +1,7 @@
|
|||
// The static package is a module for working with static files.
|
||||
//
|
||||
// These files are placed in a separate directory $HOME_STATIC_DIR and are
|
||||
// available for embedding and reading as-is without access restrictions. The
|
||||
// implication is that these files are often necessary for machines: logos,
|
||||
// robots.txt, styles, public keys, sitemaps and so on.
|
||||
package static
|
|
@ -6,19 +6,13 @@ import (
|
|||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository interface {
|
||||
// Get returns Static on path if exists
|
||||
Get(ctx context.Context, path string) (*domain.Static, error)
|
||||
}
|
||||
type Repository interface {
|
||||
// Create copy static into store to path.
|
||||
Create(ctx context.Context, static domain.Static, path string) (bool, error)
|
||||
|
||||
dummyRepository struct{}
|
||||
)
|
||||
// Get returns static on path if exists.
|
||||
Get(ctx context.Context, path string) (*domain.Static, error)
|
||||
|
||||
func NewDummyRepository() dummyRepository {
|
||||
return dummyRepository{}
|
||||
}
|
||||
|
||||
func (dummyRepository) Get(ctx context.Context, path string) (*domain.Static, error) {
|
||||
return nil, nil
|
||||
// Delete remove static from store if exists.
|
||||
Delete(ctx context.Context, path string) (bool, error)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package dummy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type dummyStaticRepository struct{}
|
||||
|
||||
// NewDummyRepository creates a new dummy static repository which will be used
|
||||
// as argument to functions that you don’t care about.
|
||||
func NewDummyStaticRepository() dummyStaticRepository {
|
||||
return dummyStaticRepository{}
|
||||
}
|
||||
|
||||
func (dummyStaticRepository) Create(_ context.Context, _ domain.Static, _ string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (dummyStaticRepository) Get(_ context.Context, _ string) (*domain.Static, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (dummyStaticRepository) Delete(_ context.Context, _ string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
|
@ -3,12 +3,15 @@ package fs
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
@ -18,22 +21,30 @@ import (
|
|||
)
|
||||
|
||||
type fileServerStaticRepository struct {
|
||||
root fs.FS
|
||||
store fs.FS
|
||||
}
|
||||
|
||||
func NewFileServerStaticRepository(root fs.FS) static.Repository {
|
||||
return &fileServerStaticRepository{
|
||||
root: root,
|
||||
func (repo *fileServerStaticRepository) Create(_ context.Context, s domain.Static, p string) (bool, error) {
|
||||
f, err := os.OpenFile(filepath.Clean(p), os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot open static for writing: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = io.Copy(f, &s); err != nil {
|
||||
return false, fmt.Errorf("cannot copy static: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*domain.Static, error) {
|
||||
info, err := fs.Stat(repo.root, p)
|
||||
func (repo *fileServerStaticRepository) Get(_ context.Context, p string) (*domain.Static, error) {
|
||||
info, err := fs.Stat(repo.store, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat static on path '%s': %w", p, err)
|
||||
}
|
||||
|
||||
f, err := repo.root.Open(p)
|
||||
f, err := repo.store.Open(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open static on path '%s': %w", p, err)
|
||||
}
|
||||
|
@ -46,3 +57,30 @@ func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*dom
|
|||
|
||||
return domain.NewStatic(bytes.NewReader(content), info.ModTime(), info.Name()), nil
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Delete(_ context.Context, p string) (bool, error) {
|
||||
p = filepath.Clean(p)
|
||||
|
||||
_, err := fs.Stat(repo.store, p)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("cannot open static for writing: %w", err)
|
||||
}
|
||||
|
||||
if err = os.RemoveAll(p); err != nil {
|
||||
return false, fmt.Errorf("cannot remove static: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// NewFileServerStaticRepository creates a new FS repository for static files
|
||||
// which must be uploaded and used as is.
|
||||
func NewFileServerStaticRepository(store fs.FS) static.Repository {
|
||||
return &fileServerStaticRepository{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package stub
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
type stubStaticRepository struct {
|
||||
static *domain.Static
|
||||
error error
|
||||
status bool
|
||||
}
|
||||
|
||||
func (repo *stubStaticRepository) Create(_ context.Context, _ domain.Static, _ string) (bool, error) {
|
||||
return repo.status, repo.error
|
||||
}
|
||||
|
||||
func (repo *stubStaticRepository) Delete(_ context.Context, _ string) (bool, error) {
|
||||
return repo.status, repo.error
|
||||
}
|
||||
|
||||
func (repo *stubStaticRepository) Get(_ context.Context, _ string) (*domain.Static, error) {
|
||||
return repo.static, repo.error
|
||||
}
|
||||
|
||||
func NewStubStaticRepository(err error, s *domain.Static, ok bool) static.Repository {
|
||||
return &stubStaticRepository{
|
||||
static: s,
|
||||
error: err,
|
||||
status: ok,
|
||||
}
|
||||
}
|
|
@ -7,5 +7,6 @@ import (
|
|||
)
|
||||
|
||||
type UseCase interface {
|
||||
// Do search static on path and returns Static domain if exist.
|
||||
Do(ctx context.Context, path string) (*domain.Static, error)
|
||||
}
|
||||
|
|
|
@ -11,19 +11,19 @@ import (
|
|||
)
|
||||
|
||||
type staticUseCase struct {
|
||||
statics static.Repository
|
||||
store static.Repository
|
||||
}
|
||||
|
||||
func NewStaticUseCase(statics static.Repository) static.UseCase {
|
||||
func NewStaticUseCase(store static.Repository) static.UseCase {
|
||||
return &staticUseCase{
|
||||
statics: statics,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.Static, error) {
|
||||
p = strings.TrimPrefix(path.Clean(p), "/")
|
||||
|
||||
s, err := ucase.statics.Get(ctx, p)
|
||||
s, err := ucase.store.Get(ctx, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get static file: %w", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var update *bool = flag.Bool("update", false, "save current tests results as golden files")
|
||||
|
||||
// GoldenEqual compares the bytes of the provided r with the contents of the
|
||||
// golden file for a complete data match.
|
||||
//
|
||||
// When running tests with the -update flag, the contents of golden-files will
|
||||
// be overwritten with the provided contents of r, creating the testdata/
|
||||
// directory if it does not exist.
|
||||
//
|
||||
//nolint:gocognit,gocyclo
|
||||
func GoldenEqual(tb testing.TB, r io.Reader) {
|
||||
tb.Helper()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
tb.Fatal("cannot get current working directory path:", err)
|
||||
}
|
||||
|
||||
actual, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
tb.Fatal("cannot read provided data:", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(wd, "testdata")
|
||||
file := filepath.Join(dir, tb.Name()[4:]+".golden")
|
||||
|
||||
if *update {
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil && !errors.Is(err, os.ErrExist) && !errors.Is(err, os.ErrNotExist) {
|
||||
tb.Fatal("cannot create testdata folder for golden files:", err)
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err = os.Mkdir(dir, os.ModePerm); err != nil {
|
||||
tb.Fatal("cannot create testdata folder for golden files:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.WriteFile(file, actual, os.ModePerm); err != nil {
|
||||
tb.Fatal("cannot write data into golden file:", err)
|
||||
}
|
||||
|
||||
tb.Skip("skipped due force updating golden file")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
expected, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
tb.Fatal("cannot read golden file data:", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(string(actual), string(expected)); diff != "" {
|
||||
tb.Error(diff)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue