Compare commits

..

11 Commits

4 changed files with 194 additions and 0 deletions

39
blogposts/blogposts.go Normal file
View File

@ -0,0 +1,39 @@
package blogposts
import (
"errors"
"io/fs"
"strings"
)
var markdownSuffix = map[string]struct{}{
"md": {},
}
var ErrUnknownFileType = errors.New("unknown file type, must be markdown")
func NewPostsFromFS(fileSystem fs.FS) ([]Post, error) {
dir, err := fs.ReadDir(fileSystem, ".")
if err != nil {
return nil, err
}
var posts []Post
for _, f := range dir {
fileName := f.Name()
if !isMarkdownFile(fileName) {
return nil, ErrUnknownFileType
}
post, err := getPost(fileSystem, fileName)
if err != nil {
return nil, err
}
posts = append(posts, post)
}
return posts, nil
}
func isMarkdownFile(fileName string) bool {
splitted := strings.Split(fileName, ".")
_, ok := markdownSuffix[splitted[len(splitted)-1:][0]]
return ok
}

View File

@ -0,0 +1,75 @@
package blogposts
import (
"reflect"
"testing"
"testing/fstest"
)
// NOTE: This should be a black box test outside blogposts package.
// NOTE: If we want to test the return error
//
// type StubFailingFS struct{}
//
// func (s StubFailingFS) Open(name string) (fs.File, error) {
// return nil, errors.New("fail")
// }
func TestNewBlogPosts(t *testing.T) {
const (
firstBody = `Title: Post 1
Description: Description 1
Tags: tdd, go
---
Hello
World`
secondBody = `Title: Post 2
Description: Description 2
Tags: rust, borrow-checker
---
B
L
M`
)
fs := fstest.MapFS{
"hello world.md": {Data: []byte(firstBody)},
"hello-world2.md": {Data: []byte(secondBody)},
}
posts, err := NewPostsFromFS(fs)
if err != nil {
t.Fatal(err)
}
if len(posts) != len(fs) {
t.Errorf("got %d posts, wanted %d posts", len(posts), len(fs))
}
assertPost(t, posts[0], Post{
Title: "Post 1",
Description: "Description 1",
Tags: []string{"tdd", "go"},
Body: `Hello
World`,
})
}
func TestWrongFile(t *testing.T) {
fs := fstest.MapFS{
"hello world.txt": {Data: []byte("Yolo")},
}
_, err := NewPostsFromFS(fs)
if err == nil {
t.Errorf("should be an error but not")
}
}
func assertPost(t testing.TB, got Post, want Post) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}

7
blogposts/hello_world.md Normal file
View File

@ -0,0 +1,7 @@
Title: Hello, TDD world!
Description: First post on our wonderful blog
Tags: tdd, go
---
Hello world!
The body of posts starts after the `---`

73
blogposts/post.go Normal file
View File

@ -0,0 +1,73 @@
package blogposts
import (
"bufio"
"io"
"io/fs"
"strings"
)
type Post struct {
Title, Description, Body string
Tags []string
}
// TODO: Split a metadata line into 2 parts, one is the key, another is the
// value, check if the key exists in the key map (ex. Title, Description etc.)
// If not exist, then ignore this metadata line.
//
// WARN: What if a file doesn't have metadata, just pure Markdown ??
// We can control if we at least found title line. If at the end, it is not
// present, we consider that the post has an error.
const (
titleLine = "Title: "
descriptionLine = "Description: "
tagsLine = "Tags: "
tagSeparator = ", "
bodySeparator = "---"
)
func getPost(fileSystem fs.FS, fileName string) (Post, error) {
postFile, err := fileSystem.Open(fileName)
if err != nil {
return Post{}, err
}
defer postFile.Close()
return newPost(postFile)
}
// NOTE: Does newPost have to be coupled to an fs.File ?
// Do we use all the methods and data from this type? What do we really need?
func newPost(postFile io.Reader) (Post, error) {
scanner := bufio.NewScanner(postFile)
readMetaLine := func(tagName string) string {
scanner.Scan()
return strings.TrimPrefix(scanner.Text(), tagName)
}
post := Post{
Title: readMetaLine(titleLine),
Description: readMetaLine(descriptionLine),
Tags: strings.Split(readMetaLine(tagsLine), tagSeparator),
Body: readBody(scanner),
}
return post, nil
}
func readBody(scanner *bufio.Scanner) string {
for scanner.Scan() {
if strings.TrimSpace(scanner.Text()) == bodySeparator {
break
}
}
// The rest is the body
var buf strings.Builder
for scanner.Scan() {
buf.Write(scanner.Bytes())
buf.Write([]byte{'\n'})
}
return strings.TrimSuffix(buf.String(), "\n")
}