start off with an in-memory page hierarchy
This commit is contained in:
parent
0676da9e00
commit
51e27879c3
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
*~
|
85
model.go
Normal file
85
model.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrPageNotFound = fmt.Errorf("page not found")
|
||||||
|
|
||||||
|
// A Page is a single unit of content on the site.
|
||||||
|
type Page struct {
|
||||||
|
Title string
|
||||||
|
Contents []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index is a set of pages that exist at this level,
|
||||||
|
// as well as a set of "folders" that contain sub-indexes.
|
||||||
|
// The 'index' key in Pages is special, and will be returned
|
||||||
|
// if no key is provided.
|
||||||
|
type Index struct {
|
||||||
|
Children map[string]Index
|
||||||
|
Pages map[string]Page
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page returns the requested page from the index, recursively
|
||||||
|
// key is assumed to be a `/`-separated string; Page will split on the slashes,
|
||||||
|
// descending into an index to find the page, if possible. If no match is found,
|
||||||
|
// return an empty page and `ErrPageNotFound`.
|
||||||
|
// `foo/` will return a page named `foo` in the current index, if it exists.
|
||||||
|
// Otherwise, if a child index named "foo" exists, page will attempt to return its index page.
|
||||||
|
// Page strips leading / from keys.
|
||||||
|
func (i *Index) Page(key string) (*Page, error) {
|
||||||
|
if key == "" || key == "/" {
|
||||||
|
key = "index"
|
||||||
|
}
|
||||||
|
if key[0] == '/' { // strip leading slash
|
||||||
|
key = key[1:]
|
||||||
|
}
|
||||||
|
curr, rest, found := strings.Cut(key, "/")
|
||||||
|
page, pageok := i.Pages[curr]
|
||||||
|
child, childok := i.Children[curr]
|
||||||
|
if !found && !pageok {
|
||||||
|
return &Page{}, ErrPageNotFound
|
||||||
|
}
|
||||||
|
if rest == "" && pageok {
|
||||||
|
return &page, nil
|
||||||
|
}
|
||||||
|
if !childok {
|
||||||
|
return &Page{}, ErrPageNotFound
|
||||||
|
}
|
||||||
|
return (&child).Page(rest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save stores a page in the index, recursively,
|
||||||
|
// overwriting any that may have existed before.
|
||||||
|
// `foo/` is stored as a page named 'foo' in the current index;
|
||||||
|
// default 'index' files should be explicitly passed as such.
|
||||||
|
// The empty key or `/` are invalid and result in an error.
|
||||||
|
// Leading slashes are stripped.
|
||||||
|
func (i *Index) Save(key string, page *Page) error {
|
||||||
|
if key == "" || key == "/" {
|
||||||
|
return fmt.Errorf("invalid page key")
|
||||||
|
}
|
||||||
|
if key[0] == '/' { // strip leading slash
|
||||||
|
key = key[1:]
|
||||||
|
}
|
||||||
|
if i.Pages == nil {
|
||||||
|
i.Pages = map[string]Page{}
|
||||||
|
}
|
||||||
|
if i.Children == nil {
|
||||||
|
i.Children = map[string]Index{}
|
||||||
|
}
|
||||||
|
curr, rest, _ := strings.Cut(key, "/")
|
||||||
|
if rest == "" {
|
||||||
|
i.Pages[curr] = *page
|
||||||
|
} else {
|
||||||
|
children := i.Children[curr]
|
||||||
|
err := (&children).Save(rest, page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Children[curr] = children
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
200
model_test.go
Normal file
200
model_test.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPage(t *testing.T) {
|
||||||
|
i := &Index{
|
||||||
|
Children: map[string]Index{
|
||||||
|
"foo": {
|
||||||
|
Children: map[string]Index{},
|
||||||
|
Pages: map[string]Page{
|
||||||
|
"bar": {
|
||||||
|
Contents: []byte("bar"),
|
||||||
|
},
|
||||||
|
"index": {
|
||||||
|
Contents: []byte("quuz"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
Children: map[string]Index{},
|
||||||
|
Pages: map[string]Page{
|
||||||
|
"index": {
|
||||||
|
Contents: []byte("quuz"),
|
||||||
|
},
|
||||||
|
"goof": {
|
||||||
|
Contents: []byte("quuz2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Pages: map[string]Page{
|
||||||
|
"index": {
|
||||||
|
Contents: []byte("indextest"),
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
Contents: []byte("rootfoo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := i.Page("")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("indextest")) {
|
||||||
|
t.Logf("expected contents to be 'indextest', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("/")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("indextest")) {
|
||||||
|
t.Logf("expected contents to be 'indextest', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("foo/bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("bar")) {
|
||||||
|
t.Logf("expected contents to be 'bar', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("/foo/bar")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("bar")) {
|
||||||
|
t.Logf("expected contents to be 'bar', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("rootfoo")) {
|
||||||
|
t.Logf("expected contents to be 'rootfoo', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("foo/")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("rootfoo")) {
|
||||||
|
t.Logf("expected contents to be 'rootfoo', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("bar/")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("quuz")) {
|
||||||
|
t.Logf("expected contents to be 'quuz', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("bar/goof")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("quuz2")) {
|
||||||
|
t.Logf("expected contents to be 'quuz2', received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("foo/quuz")
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("expected err, received nil err")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("")) {
|
||||||
|
t.Logf("expected no content, received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("quuz/bar")
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("expected err, received nil err")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("")) {
|
||||||
|
t.Logf("expected no content, received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = i.Page("//////////")
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("expected err, received nil err")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if !bytes.Equal(p.Contents, []byte("")) {
|
||||||
|
t.Logf("expected no content, received '%v'", p.Contents)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSave(t *testing.T) {
|
||||||
|
i := &Index{}
|
||||||
|
|
||||||
|
err := i.Save("foo", &Page{Title: "fooroot"})
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received %v", err)
|
||||||
|
}
|
||||||
|
p, err := i.Page("foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if p.Title != "fooroot" {
|
||||||
|
t.Logf("expected title to be 'fooroot', received '%v'", p.Title)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.Save("foo/", &Page{Title: "fooroot2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received %v", err)
|
||||||
|
}
|
||||||
|
p, err = i.Page("foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if p.Title != "fooroot2" {
|
||||||
|
t.Logf("expected title to be 'fooroot2', received '%v'", p.Title)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.Save("bar/baz", &Page{Title: "quuz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received %v", err)
|
||||||
|
}
|
||||||
|
p, err = i.Page("bar/baz")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("expected no err, received err: %v", err)
|
||||||
|
t.Logf("%+v", i)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if p.Title != "quuz" {
|
||||||
|
t.Logf("expected title to be 'quuz', received '%v'", p.Title)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user