Skip to content

Commit

Permalink
feat: Implement Parse func and refactor DataSourcer interface (#4)
Browse files Browse the repository at this point in the history
* feat: Add Item properties

* refactor: DataSourcer returns Source interface

* feat: Extend Source interface with CreatedAt and ModifiedAt and FileSource implementation

* feat: Implement Parser
  • Loading branch information
MaikelVeen authored May 7, 2024
1 parent b331e19 commit 14a3b9f
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 25 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
module github.com/glass-cms/glasscms

go 1.22
go 1.22.0

require (
github.com/djherbis/times v1.6.0
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -12,6 +14,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
10 changes: 9 additions & 1 deletion item/item.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
package item

type Item struct{}
import "time"

type Item struct {
Title string
Content string
CreateTime time.Time
UpdateTime time.Time
Properties map[string]any
}
52 changes: 40 additions & 12 deletions parser/parser.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
package parser

import "github.com/glass-cms/glasscms/item"
import (
"bytes"
"errors"
"io"

type Parser struct {
Config Config
}
"github.com/glass-cms/glasscms/item"
"github.com/glass-cms/glasscms/sourcer"
"gopkg.in/yaml.v3"
)

const (
numParts = 3
)

func NewParser(config Config) *Parser {
return &Parser{
Config: config,
// Parse reads the content of a source and returns an item.
func Parse(src sourcer.Source) (*item.Item, error) {
c, err := io.ReadAll(src)
if err != nil {
return nil, err
}
}
defer src.Close()

type Config struct {
}
// Split the content into front matter and markdown
parts := bytes.SplitN(c, []byte("---\n"), numParts)
if len(parts) < numParts {
return nil, errors.New("invalid content")
}

// Parse the YAML front matter
var properties map[string]any
err = yaml.Unmarshal(parts[1], &properties)
if err != nil {
return nil, err
}

// Keep the markdown content as is
content := string(parts[2])

func (p *Parser) Parse(_ string) (*item.Item, error) {
return &item.Item{}, nil
return &item.Item{
Title: src.Name(),
Content: content,
CreateTime: src.CreatedAt(),
UpdateTime: src.ModifiedAt(),
Properties: properties,
}, nil
}
59 changes: 59 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package parser_test

import (
"testing"
"time"

"strings"

"github.com/glass-cms/glasscms/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type MockSource struct {
name string
reader *strings.Reader
}

func NewMockSource(name string, data string) *MockSource {
return &MockSource{
name: name,
reader: strings.NewReader(data),
}
}

func (m *MockSource) Read(p []byte) (int, error) {
return m.reader.Read(p)
}

func (m *MockSource) Close() error {
return nil
}

func (m *MockSource) Name() string {
return m.name
}

func (m *MockSource) CreatedAt() time.Time {
return time.Now()
}

func (m *MockSource) ModifiedAt() time.Time {
return time.Now()
}

func TestParse(t *testing.T) {
// Arrange
source := NewMockSource("test", "---\ntitle: Test\n---\n# Test\n")

// Act
item, err := parser.Parse(source)

// Assert
require.NoError(t, err)
assert.NotNil(t, item)
assert.Equal(t, "test", item.Title)
assert.Equal(t, "# Test\n", item.Content)
assert.Equal(t, map[string]interface{}{"title": "Test"}, item.Properties)
}
37 changes: 37 additions & 0 deletions sourcer/file_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package sourcer

import (
"os"
"time"

"github.com/djherbis/times"
)

var _ Source = (*FileSource)(nil)

type FileSource struct {
*os.File
birthtime time.Time
modtime time.Time
}

func NewFileSource(file *os.File) (*FileSource, error) {
stats, err := times.StatFile(file)
if err != nil {
return nil, err
}

return &FileSource{
File: file,
birthtime: stats.BirthTime(),
modtime: stats.ModTime(),
}, nil
}

func (f FileSource) CreatedAt() time.Time {
return f.birthtime
}

func (f FileSource) ModifiedAt() time.Time {
return f.modtime
}
45 changes: 45 additions & 0 deletions sourcer/file_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package sourcer_test

import (
"os"
"testing"

"github.com/djherbis/times"
"github.com/glass-cms/glasscms/sourcer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func createTempFile() (*os.File, error) {
tempFile, err := os.CreateTemp("", "source")
if err != nil {
return nil, err
}
return tempFile, nil
}

func TestNewFileSource(t *testing.T) {
t.Parallel()

// Arrange
tempFile, err := createTempFile()
if err != nil {
t.Fatal(err)
}
defer os.Remove(tempFile.Name())

// Act
fileSource, err := sourcer.NewFileSource(tempFile)

// Assert
require.NoError(t, err)
assert.NotNil(t, fileSource)

stats, err := times.StatFile(tempFile)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, stats.BirthTime(), fileSource.CreatedAt())
assert.Equal(t, stats.ModTime(), fileSource.ModifiedAt())
}
11 changes: 6 additions & 5 deletions sourcer/fs.go → sourcer/file_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,20 @@ func NewFileSystemSourcer(rootPath string) (*FileSystemSourcer, error) {
}, nil
}

func (s *FileSystemSourcer) Next() (string, error) {
func (s *FileSystemSourcer) Next() (Source, error) {
if s.cursor >= len(s.files) {
return "", ErrDone
return NilSource, ErrDone
}

data, err := os.ReadFile(s.files[s.cursor])
// Get a ReadCloser for the file
file, err := os.Open(s.files[s.cursor])
s.cursor++

if err != nil {
return "", err
return NilSource, err
}

return string(data), nil
return NewFileSource(file)
}

func (s *FileSystemSourcer) Remaining() int {
Expand Down
13 changes: 10 additions & 3 deletions sourcer/fs_test.go → sourcer/file_system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sourcer_test

import (
"fmt"
"io"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -60,7 +61,6 @@ func TestFileSystemSourcer(t *testing.T) {
{content: "file 3", depth: 0, pattern: "*.md"},
}

// 0 depth to ensure ordering needed for testing.
tempDir, err := createTempDirWithFiles(fileData)
if err != nil {
t.Fatal(err)
Expand All @@ -74,14 +74,21 @@ func TestFileSystemSourcer(t *testing.T) {

// Walk until the end of the files.
for i := range src.Size() {
var data string
var data sourcer.Source
data, err = src.Next()
if err != nil {
t.Fatal(err)
}

// Read the data from the file.
var fileContent []byte
fileContent, err = io.ReadAll(data)
if err != nil {
t.Fatal(err)
}

// Assert that data is the same as the fileData.
assert.Equal(t, fileData[i].content, data)
assert.Equal(t, fileData[i].content, string(fileContent))

// Assert that the remaining files is correct.
assert.Equal(t, src.Size()-i-1, src.Remaining())
Expand Down
27 changes: 27 additions & 0 deletions sourcer/nil_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package sourcer

import (
"io"
"time"
)

// NilSource is a no-op sentinel Source.
var NilSource = nilSource{
ReadCloser: io.NopCloser(nil),
}

type nilSource struct {
io.ReadCloser
}

func (nilSource) Name() string {
return ""
}

func (nilSource) CreatedAt() time.Time {
return time.Time{}
}

func (nilSource) ModifiedAt() time.Time {
return time.Time{}
}
15 changes: 15 additions & 0 deletions sourcer/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sourcer

import (
"io"
"time"
)

// Source is a data source that can be read from.
type Source interface {
io.ReadCloser
Name() string

CreatedAt() time.Time
ModifiedAt() time.Time
}
6 changes: 4 additions & 2 deletions sourcer/sourcer.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package sourcer

import "errors"
import (
"errors"
)

// ErrDone is returned when there are no items left in the data source.
var ErrDone = errors.New("no items left in the data source")

// DataSourcer is an iterator that provides data to be parsed.
type DataSourcer interface {
// Next returns the next piece of data to be parsed.
Next() (string, error)
Next() (Source, error)

// Remaining returns the number of pieces of data remaining to be parsed.
Remaining() int
Expand Down

0 comments on commit 14a3b9f

Please sign in to comment.