~chrisppy/go-barefeed

b95e22ccd760f5eda495b2436a36f62ebe9b3050 — Chris Palmer 1 year, 11 months ago 5cc7bbe
updated barefeed definition
19 files changed, 413 insertions(+), 820 deletions(-)

M Makefile
M barefeed.go
D barefeed_test.go
D entry.go
D entry_test.go
D feed.go
D feed_test.go
D link.go
D link_test.go
D message.go
D message_test.go
D person.go
D person_test.go
A schema.go
D test-files/test.barefeed
D text.go
D text_test.go
D time.go
D time_test.go
M Makefile => Makefile +14 -22
@@ 2,43 2,35 @@
.SUFFIXES:

GO = go
RM = rm
GOFLAGS =

gosrc = $(shell find . -name '*.go')
gosrc += go.mod go.sum
goflags = $(GOFLAGS)

all: barefeed
barefeed: $(gosrc)
barefeed:
	$(GO) build $(goflags)
clean:
	$(GO) mod tidy

check: clean tidy test format lint security dependencies 
	make clean
dependencies:
	$(GO) install github.com/psampaz/go-mod-outdated@latest
	$(GO) list -u -m -json all | go-mod-outdated -direct -ci
format:
	$(GO) fmt -x ./...
	$(GO) vet ./...
lint:
	$(GO) get -u golang.org/x/lint/golint
	$(GO) get -u honnef.co/go/tools/cmd/staticcheck
	$(GO) get -u gitlab.com/opennota/check/cmd/aligncheck
	$(GO) get -u gitlab.com/opennota/check/cmd/structcheck
	$(GO) get -u gitlab.com/opennota/check/cmd/varcheck
	$(GO) get -u github.com/kisielk/errcheck
	golint -set_exit_status ./...
	$(GO) install honnef.co/go/tools/cmd/staticcheck@latest
	$(GO) install github.com/kisielk/errcheck@latest
	staticcheck ./...
	aligncheck ./...
	structcheck ./...
	varcheck ./...
	errcheck ./...
security:
	$(GO) get -u github.com/securego/gosec/cmd/gosec
	gosec -exclude=G107,G204 ./... 
dependencies:
	$(GO) get -u github.com/psampaz/go-mod-outdated
	$(GO) list -u -m -json all | go-mod-outdated -direct -ci
	$(GO) install github.com/securego/gosec/cmd/gosec@latest
	gosec ./... 
test:
	$(GO) test ./...
check: clean format lint security dependencies test 
	make clean
	$(GO) test -v ./...
tidy:
	$(GO) mod tidy



M barefeed.go => barefeed.go +271 -20
@@ 4,48 4,299 @@
package barefeed

import (
	"bytes"
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/go-bare"
)

// FromFile will read barefeed from a file
func FromFile(path string) (*Message, error) {
	f, err := os.Open(filepath.Clean(path))
// FeedEntries map
type FeedEntries map[string]FeedEntry

// FeedEntry containing the linkage between a Feed and Entry
type FeedEntry struct {
	Feed    Feed
	Entries map[string]Entry
}

// Importer contains the source path to the barefeed file
type Importer struct {
	Path string
}

// Exporter containers the source path to the barefeed directory
type Exporter struct {
	Path string
}

// NewImporter creates the importer based on the source path to the barefeed
// file.
func NewImporter(source ...string) *Importer {
	return &Importer{Path: filepath.Join(source...)}
}

// Import a barefeed file to the target location
func (d *Importer) Import(target ...string) (err error) {
	var f *os.File
	f, err = os.Open(filepath.Clean(d.Path))
	if err != nil {
		return nil, err
		return
	}

	msg, err := FromReader(f)
	defer func() {
		err = f.Close()
	}()

	var gzf *gzip.Reader
	gzf, err = gzip.NewReader(f)
	if err != nil {
		if err := f.Close(); err != nil {
			return nil, err
		return
	}

	defer func() {
		err = gzf.Close()
	}()

	p := filepath.Join(target...)
	p = filepath.Join(p, "barefeed")

	tf := tar.NewReader(gzf)
	for {
		var exit bool
		if exit, err = writeFromArchive(tf, p); err != nil {
			return
		} else if exit {
			break
		}
		return nil, err
	}

	if err := f.Close(); err != nil {
	return
}

// NewExporter creates the exporter based on the directory path that contains
// the barefeed folder.
func NewExporter(directory ...string) *Exporter {
	d := filepath.Join(directory...)
	d = filepath.Join(d, "barefeed")
	return &Exporter{Path: d}
}

// Export a barefeed file to the target location
func (d *Exporter) Export(target ...string) (err error) {
	if _, err = os.Stat(d.Path); err != nil {
		return
	}

	date := time.Now().Format("2006-01-02")

	path := filepath.Join(target...)
	path = filepath.Join(path, fmt.Sprintf("%s.barefeed", date))

	var f *os.File
	f, err = os.Create(path)
	if err != nil {
		return
	}
	defer func() {
		err = f.Close()
	}()

	gz := gzip.NewWriter(f)
	defer func() {
		err = gz.Close()
	}()

	tw := tar.NewWriter(gz)
	defer func() {
		err = tw.Close()
	}()

	err = filepath.Walk(d.Path, func(path string, fi os.FileInfo, e error) error {
		if e != nil {
			return e
		} else if !fi.Mode().IsRegular() {
			return nil
		}

		header, e := tar.FileInfoHeader(fi, fi.Name())
		if e != nil {
			return e
		}

		header.Name = strings.TrimPrefix(strings.Replace(path, d.Path, "", -1), string(filepath.Separator))

		if e := tw.WriteHeader(header); e != nil {
			return e
		}

		file, e := os.Open(filepath.Clean(path))
		if e != nil {
			return e
		}

		if _, e := io.Copy(tw, file); e != nil {
			if e := file.Close(); e != nil {
				return e
			}
			return e
		}

		return file.Close()
	})

	return
}

// Read will parse the provided directory for a barefeed directory
func Read(directory ...string) (FeedEntries, error) {
	fem := make(FeedEntries)

	d := filepath.Join(directory...)
	d = filepath.Join(d, "barefeed")
	if err := filepath.Walk(filepath.Clean(d), fem.walk); err != nil {
		return nil, err
	}

	return msg, nil
	return fem, nil
}

// Write the Feed to a bin file
func (d Feed) Write(generator string, path ...string) error {
	bin := Bin{
		Generator: generator,
		Content:   d,
	}

	return writeBin(bin)
}

// Write the Entry to a bin file. Path should be where you want the barefeed
// directory to exist
func (d Entry) Write(generator string, path ...string) error {
	bin := Bin{
		Generator: generator,
		Content:   d,
	}

	return writeBin(bin)
}

func (d FeedEntries) walk(path string, f os.FileInfo, err error) error {
	if err != nil {
		return err
	} else if f.IsDir() {
		return nil
	} else if filepath.Ext(path) != ".bin" {
		return nil
	}

	fe, err := readFile(path)
	if err != nil {
		return err
	}

	switch content := fe.Content.(type) {
	case *Feed:
		if v, ok := d[content.FeedID]; ok {
			d[content.FeedID] = FeedEntry{
				Feed:    *content,
				Entries: v.Entries,
			}
		} else {
			d[content.FeedID] = FeedEntry{
				Feed:    *content,
				Entries: make(map[string]Entry),
			}
		}
	case *Entry:
		if _, ok := d[content.FeedID]; ok {
			d[content.FeedID].Entries[content.EntryID] = *content
		} else {
			d[content.FeedID] = FeedEntry{
				Entries: make(map[string]Entry),
			}
			d[content.FeedID].Entries[content.EntryID] = *content
		}
	default:
		return fmt.Errorf("unknown %+v for %s", content, path)
	}

	return nil
}

// FromReader will read barefeed from a Reader
func FromReader(reader io.Reader) (*Message, error) {
	buf := &bytes.Buffer{}
	if _, err := buf.ReadFrom(reader); err != nil {
		return nil, fmt.Errorf("error reading from buffer: %s", err.Error())
func writeBin(bin Bin, path ...string) (err error) {
	p := filepath.Join(path...)
	p = filepath.Join(p, "barefeed")

	var f *os.File
	f, err = os.Create(filepath.Clean(p))
	if err != nil {
		return
	}

	var m Message
	if err := bare.Unmarshal(buf.Bytes(), &m); err != nil {
		return nil, fmt.Errorf("error unmarshalling message: %s", err.Error())
	defer func() {
		err = f.Close()
	}()

	w := bare.NewWriter(f)
	err = bare.MarshalWriter(w, &bin)

	return
}

func readFile(path ...string) (b Bin, err error) {
	var f *os.File
	f, err = os.Open(filepath.Clean(filepath.Join(path...)))
	if err != nil {
		return
	}

	return &m, nil
	defer func() {
		err = f.Close()
	}()

	err = bare.UnmarshalReader(f, &b)

	return
}

func writeFromArchive(tf *tar.Reader, p string) (exit bool, err error) {
	var hdr *tar.Header
	hdr, err = tf.Next()
	if err == io.EOF {
		err = nil
		exit = true
		return
	} else if err != nil {
		return
	}

	path := filepath.Join(p, hdr.Name)
	info := hdr.FileInfo()
	if info.IsDir() {
		if err = os.MkdirAll(path, info.Mode()); err != nil {
			return
		}
		return
	}

	var file *os.File
	file, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
	if err != nil {
		return
	}

	defer func() {
		err = file.Close()
	}()

	_, err = io.Copy(file, tf)

	return
}

D barefeed_test.go => barefeed_test.go +0 -87
@@ 1,87 0,0 @@
package barefeed

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

func TestBytes(t *testing.T) {
	expected := getMessage()

	b, err := expected.Bytes()
	if err != nil {
		t.Errorf("Bytes should not have error: %s", err.Error())
	} else if b == nil {
		t.Errorf("Bytes should not be nil")
	} else if len(b) == 0 {
		t.Errorf("Bytes should not be empty")
	}

	actual, err := FromReader(bytes.NewReader(b))
	if err != nil {
		t.Errorf("Message should not have error: %s", err.Error())
	} else if actual == nil {
		t.Errorf("Message should not have been nil")
	}

	testMessage(t, expected, *actual)
}

func TestReader(t *testing.T) {
	expected := getMessage()

	b, err := expected.Bytes()
	if err != nil {
		t.Errorf("Bytes should not have error: %s", err.Error())
	} else if b == nil {
		t.Errorf("Bytes should not be nil")
	} else if len(b) == 0 {
		t.Errorf("Bytes should not be empty")
	}

	actual, err := FromReader(bytes.NewReader(b))
	if err != nil {
		t.Errorf("Message should not have error: %s", err.Error())
	} else if actual == nil {
		t.Errorf("Message should not have been nil")
	}

	testMessage(t, expected, *actual)
}

func TestFromBytesError(t *testing.T) {
	m, err := FromReader(bytes.NewReader([]byte("{")))
	if m != nil {
		t.Errorf("Message should have been nil: %v", m)
	}
	if err == nil {
		t.Errorf("error should not have been nil")
	}
}

func TestFromReaderError(t *testing.T) {
	m, err := FromReader(bytes.NewReader([]byte("{")))
	if m != nil {
		t.Errorf("Message should have been nil: %v", m)
	}
	if err == nil {
		t.Errorf("error should not have been nil")
	}
}

func TestWriteRead(t *testing.T) {
	path := filepath.Join("test-files", "test.barefeed")
	expected := getMessage()
	err := expected.WriteFile(filepath.Clean(path))
	if err != nil {
		t.Errorf("WriteFile should not have error: %s", err.Error())
	}

	actual, err := FromFile(path)
	if err != nil {
		t.Errorf("FromFile should not have error: %s", err.Error())
	} else {
		testMessage(t, expected, *actual)
	}
}

D entry.go => entry.go +0 -67
@@ 1,67 0,0 @@
package barefeed

import (
	"fmt"

	"git.sr.ht/~sircmpwn/go-bare"
)

// Entry type contains elements from RSS Item & Atom Entry
type Entry interface {
	bare.Union
}

func init() {
	bare.RegisterUnion((*Entry)(nil)).
		Member(*new(EntryV1), 0)

}

// Entries are a slice of Entry
type Entries []Entry

// Len is the length of the slice
func (e Entries) Len() int {
	return len(e)
}

// Less is how to compare the two Entry
func (e Entries) Less(i, j int) bool {
	var t1 Timestamp
	switch t := e[i].(type) {
	case *EntryV1:
		t1 = e[i].(*EntryV1).Published
	default:
		panic(fmt.Sprintf("invalid type: %v", t))
	}

	var t2 Timestamp
	switch t := e[j].(type) {
	case *EntryV1:
		t2 = e[j].(*EntryV1).Published
	default:
		panic(fmt.Sprintf("invalid type: %v", t))
	}
	return t1 > t2
}

// Swap is how to swap the two Entry
func (e Entries) Swap(i, j int) {
	e[i], e[j] = e[j], e[i]
}

// EntryV1 type contains elements from RSS Item & Atom Entry
type EntryV1 struct {
	FeedPath  string    `bare:"feedPath"`
	ID        string    `bare:"id"`
	Title     Text      `bare:"title"`
	Content   Text      `bare:"content"`
	Published Timestamp `bare:"published"`
	Updated   Timestamp `bare:"updated"`
	Authors   Persons   `bare:"authors"`
	Links     Links     `bare:"links"`
}

// IsUnion function is necessary to make the type compatible with the Union
// interface
func (t EntryV1) IsUnion() {}

D entry_test.go => entry_test.go +0 -105
@@ 1,105 0,0 @@
package barefeed

import (
	"sort"
	"testing"
	"time"
)

func testEntryList(t *testing.T, expected, actual []string) {
	if len(expected) != len(actual) {
		t.Errorf("Expected %d entries, actual %d entries", len(expected), len(actual))
	}

	for i := 0; i < len(expected); i++ {
		r := actual[i]
		e := expected[i]
		if r != e {
			t.Errorf("Expected %s, actual %s", e, r)
		}
	}
}

func testEntries(t *testing.T, expected, actual map[string]Entry) {
	if len(expected) != len(actual) {
		t.Errorf("Expected %d entries, actual %d entries", len(expected), len(actual))
	}

	for k, ev := range expected {
		rv, ok := actual[k]
		if !ok {
			t.Errorf("expected key %s not found", k)
		}

		r := rv.(*EntryV1)
		e := ev.(*EntryV1)
		if r.FeedPath != e.FeedPath {
			t.Errorf("Expected %s, actual %s", e.FeedPath, r.FeedPath)
		}
		if r.ID != e.ID {
			t.Errorf("Expected %s, actual %s", e.ID, r.ID)
		}
		if r.Published != e.Published {
			t.Errorf("Expected %v, actual %v", e.Published, r.Published)
		}
		if r.Updated != e.Updated {
			t.Errorf("Expected %v, actual %v", e.Updated, r.Updated)
		}

		testText(t, e.Title, r.Title)
		testText(t, e.Content, r.Content)
		testPersons(t, e.Authors, r.Authors)
		testLinks(t, e.Links, r.Links)
	}
}

func TestEntrySort(t *testing.T) {
	a := getEntry()
	b := getEntry()
	c := getEntry()

	en := Entries{a, c, b}
	sort.Sort(en)

	if en[0] == c {
		t.Errorf("Expected %v, actual %v", c, en[0])
	}
	if en[1] == b {
		t.Errorf("Expected %v, actual %v", b, en[1])
	}
	if en[2] == a {
		t.Errorf("Expected %v, actual %v", a, en[2])
	}
}

func getEntries() Entries {
	a := getEntry()
	return Entries{a}
}

func getEntryList() []string {
	e := getEntries()

	r := make([]string, len(e))
	for i, a := range e {
		r[i] = a.(*EntryV1).ID
	}

	return r
}

func getEntry() Entry {
	e := EntryV1{
		FeedPath:  "https://example.com/feed.xml",
		ID:        "urn:uuid:603bb616-7ae6-4413-83ec-a8cffbdd148e",
		Title:     getText(),
		Content:   getText(),
		Published: ToTimestamp(time.Now()),
		Updated:   ToTimestamp(time.Now()),
		Links:     getLinks(),
		Authors:   getPersons(),
	}

	var en Entry = &e
	return en
}

D feed.go => feed.go +0 -100
@@ 1,100 0,0 @@
package barefeed

import (
	"fmt"
	"strings"

	"git.sr.ht/~sircmpwn/go-bare"
)

// FeedType is an enumerated type for Feed
type FeedType uint

const (
	// RSS denotes the feed as RSS
	RSS FeedType = iota
	// ATOM denotes the feed as ATOM
	ATOM
)

// Feed type contains elements from RSS Channel & Atom Feed
type Feed interface {
	bare.Union
}

func init() {
	bare.RegisterUnion((*Feed)(nil)).
		Member(*new(FeedV1), 0)
}

// Feeds are a slice of Feed
type Feeds []Feed

// Len is the length of the slice
func (f Feeds) Len() int {
	return len(f)
}

// Less is how to compare the two Feed
func (f Feeds) Less(i, j int) bool {
	var t1 string
	switch t := f[i].(type) {
	case *FeedV1:
		t1 = f[i].(*FeedV1).Title.Value
	default:
		panic(fmt.Sprintf("invalid type: %v", t))
	}

	var t2 string
	switch t := f[j].(type) {
	case *FeedV1:
		t2 = f[j].(*FeedV1).Title.Value
	default:
		panic(fmt.Sprintf("invalid type: %v", t))
	}

	return strings.ToLower(t1) < strings.ToLower(t2)
}

// Swap is how to swap the two Feed
func (f Feeds) Swap(i, j int) {
	f[i], f[j] = f[j], f[i]
}

// FeedV1 type contains elements from RSS Channel & Atom Feed
type FeedV1 struct {
	Path        string    `bare:"path"`
	ID          string    `bare:"id"`
	FeedType    FeedType  `bare:"feedType"`
	Title       Text      `bare:"title"`
	Updated     Timestamp `bare:"updated"`
	Authors     Persons   `bare:"authors"`
	Links       Links     `bare:"links"`
	Entries     []string  `bare:"entries"`
	Generator   *string   `bare:"generator"`
	Description *Text     `bare:"description"`
}

// IsUnion function is necessary to make the type compatible with the Union
// interface
func (t FeedV1) IsUnion() {}

// String converts the enumerated value to a string.  "RSS" is default.
func (t FeedType) String() string {
	switch t {
	case ATOM:
		return "ATOM"
	default:
		return "RSS"
	}
}

// ToFeedType converts a string to the enumerated value.  RSS is default.
func ToFeedType(t string) FeedType {
	switch t {
	case "ATOM":
		return ATOM
	default:
		return RSS
	}
}

D feed_test.go => feed_test.go +0 -121
@@ 1,121 0,0 @@
package barefeed

import (
	"sort"
	"testing"
	"time"
)

func testFeeds(t *testing.T, expected, actual map[string]Feed) {
	if len(expected) != len(actual) {
		t.Errorf("Expected %d feeds, actual %d feeds", len(expected), len(actual))
	}

	for k, ev := range expected {
		rv, ok := actual[k]
		if !ok {
			t.Errorf("expected key %s not found", k)
		}

		r := rv.(*FeedV1)
		e := ev.(*FeedV1)
		if e.Path != r.Path {
			t.Errorf("Expected %s, actual %s", e.Path, r.Path)
		}
		if r.ID != e.ID {
			t.Errorf("Expected %s, actual %s", e.ID, r.ID)
		}
		if r.Generator != e.Generator {
			if e.Generator == nil {
				t.Errorf("Expected nil, actual %s", *r.Generator)
			} else if r.Generator == nil {
				t.Errorf("Expected %s, actual nil", *e.Generator)
			} else if *r.Generator != *e.Generator {
				t.Errorf("Expected %s, actual %s", *e.Generator, *r.Generator)
			}
		}
		if r.FeedType != e.FeedType {
			t.Errorf("Expected %d, actual %d", e.FeedType, r.FeedType)
		}
		if r.Updated != e.Updated {
			t.Errorf("Expected %v, actual %v", e.Updated, r.Updated)
		}
		if r.Description != e.Description {
			if e.Description == nil {
				t.Errorf("Expected nil, actual %v", *r.Description)
			} else if r.Description == nil {
				t.Errorf("Expected %v, actual nil", *e.Description)
			} else {
				testText(t, *e.Description, *r.Description)
			}
		}

		testText(t, e.Title, r.Title)
		testPersons(t, e.Authors, r.Authors)
		testLinks(t, e.Links, r.Links)
		testEntryList(t, e.Entries, r.Entries)
	}
}

func TestFeedSort(t *testing.T) {
	a := getFeed("Omega")
	b := getFeed("beta")
	c := getFeed("Alpha")

	f := Feeds{a, c, b}
	sort.Sort(f)

	if f[0].(*FeedV1).Title.Value != c.(*FeedV1).Title.Value {
		t.Errorf("Expected %v, actual %v", c.(*FeedV1).Title.Value, f[0].(*FeedV1).Title.Value)
	}
	if f[1].(*FeedV1).Title.Value != b.(*FeedV1).Title.Value {
		t.Errorf("Expected %v, actual %v", b.(*FeedV1).Title.Value, f[1].(*FeedV1).Title.Value)
	}
	if f[2].(*FeedV1).Title.Value != a.(*FeedV1).Title.Value {
		t.Errorf("Expected %v, actual %v", a.(*FeedV1).Title.Value, f[2].(*FeedV1).Title.Value)
	}
}

func getFeeds() map[string]Feed {
	m := make(map[string]Feed)

	fd := getFeed("test title")
	m[fd.(*FeedV1).Path] = fd

	return m
}

func getFeed(title string) Feed {
	gen := "test gen"
	desc := getText()
	f := FeedV1{
		Path:        "https://example.com/feed.xml",
		ID:          "urn:uuid:22fea9cf-095c-4634-95f5-d0b72f99944f",
		Generator:   &gen,
		FeedType:    ATOM,
		Title:       getText(),
		Updated:     ToTimestamp(time.Now()),
		Description: &desc,
		Links:       getLinks(),
		Authors:     getPersons(),
		Entries:     getEntryList(),
	}
	f.Title.Value = title

	var fd Feed = &f
	return fd
}

func TestFeedType(t *testing.T) {
	e := RSS
	a := ToFeedType(e.String())
	if e != a {
		t.Errorf("Expected %d, actual %d", e, a)
	}

	e = ATOM
	a = ToFeedType(e.String())
	if e != a {
		t.Errorf("Expected %d, actual %d", e, a)
	}
}

D link.go => link.go +0 -12
@@ 1,12 0,0 @@
package barefeed

// Links is a slice of Link
type Links []Link

// Link contains info needed for any link
type Link struct {
	URL      string `bare:"url"`
	LinkType string `bare:"linkType"`
	Rel      string `bare:"rel"`
	Length   int    `bare:"length"`
}

D link_test.go => link_test.go +0 -41
@@ 1,41 0,0 @@
package barefeed

import "testing"

func testLinks(t *testing.T, expected, actual Links) {
	if len(expected) != len(actual) {
		t.Errorf("Expected %d links, actual %d links", len(expected), len(actual))
	}
	for j := 0; j < len(expected); j++ {
		if expected[j].URL != actual[j].URL {
			t.Errorf("Expected %s, actual %s", expected[j].URL, actual[j].URL)
		}
		if expected[j].LinkType != actual[j].LinkType {
			t.Errorf("Expected %s, actual %s", expected[j].LinkType, actual[j].LinkType)
		}
		if expected[j].Rel != actual[j].Rel {
			t.Errorf("Expected %s, actual %s", expected[j].Rel, actual[j].Rel)
		}
		if expected[j].Length != actual[j].Length {
			t.Errorf("Expected %d, actual %d", expected[j].Length, actual[j].Length)
		}
	}
}

func getLinks() Links {
	a := Link{
		URL:      "https://example.com/entry.mp3",
		LinkType: "audio/mp3",
		Rel:      "enclosure",
		Length:   50416767,
	}

	b := Link{
		URL:      "https://example.com/feed.xml",
		LinkType: "plain/html",
		Rel:      "self",
		Length:   123,
	}

	return Links{a, b}
}

D message.go => message.go +0 -51
@@ 1,51 0,0 @@
package barefeed

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

	"git.sr.ht/~sircmpwn/go-bare"
)

// Message is the first version of the spec
type Message struct {
	Generator string               `bare:"generator"`
	Created   Timestamp            `bare:"created"`
	Feeds     map[string]Feed      `bare:"feeds"`
	Entries   map[string]Entry     `bare:"entries"`
	Unread    map[string]int64     `bare:"unread"`
	Favorite  map[string]Timestamp `bare:"favorite"`
}

// Bytes will return the barefeed as a slice of bytes
func (t Message) Bytes() ([]byte, error) {
	return bare.Marshal(&t)
}

// WriteFile will write barefeed to a file at the designated path
func (t Message) WriteFile(path string) error {
	f, err := os.OpenFile(filepath.Clean(path), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return err
	}

	w := bare.NewWriter(f)
	if w == nil {
		if err := f.Close(); err != nil {
			return err
		}

		return fmt.Errorf("writer is nil")
	}

	if err := bare.MarshalWriter(w, &t); err != nil {
		if err := f.Close(); err != nil {
			return err
		}

		return fmt.Errorf("unable to marshal message to file: %s", err.Error())
	}

	return f.Close()
}

D message_test.go => message_test.go +0 -25
@@ 1,25 0,0 @@
package barefeed

import (
	"testing"
	"time"
)

func testMessage(t *testing.T, expected, actual Message) {
	if actual.Created != expected.Created {
		t.Errorf("Expected %v, actual %v", expected.Created, actual.Created)
	}
	if actual.Generator != expected.Generator {
		t.Errorf("Expected %s, actual %s", expected.Generator, actual.Generator)
	}
	testFeeds(t, expected.Feeds, actual.Feeds)
	testEntries(t, expected.Entries, actual.Entries)
}

func getMessage() Message {
	return Message{
		Created:   ToTimestamp(time.Now()),
		Generator: "generated",
		Feeds:     getFeeds(),
	}
}

D person.go => person.go +0 -11
@@ 1,11 0,0 @@
package barefeed

// Persons is a slice of Person
type Persons []Person

// Person contains info needed for authors
type Person struct {
	Name  string  `bare:"name"`
	Email *string `bare:"email"`
	URI   *string `bare:"uri"`
}

D person_test.go => person_test.go +0 -44
@@ 1,44 0,0 @@
package barefeed

import "testing"

func testPersons(t *testing.T, expected, actual Persons) {
	if len(expected) != len(actual) {
		t.Errorf("Expected %d authors, actual %d authors", len(expected), len(actual))
	}
	for j := 0; j < len(expected); j++ {
		if expected[j].Name != actual[j].Name {
			t.Errorf("Expected %s, actual %s", expected[j].Name, actual[j].Name)
		}
		if actual[j].Email != expected[j].Email {
			if expected[j].Email == nil {
				t.Errorf("Expected nil, actual %s", *actual[j].Email)
			} else if actual[j].Email == nil {
				t.Errorf("Expected %s, actual nil", *expected[j].Email)
			} else if *actual[j].Email != *expected[j].Email {
				t.Errorf("Expected %s, actual %s", *expected[j].Email, *actual[j].Email)
			}
		}
		if actual[j].URI != expected[j].URI {
			if expected[j].URI == nil {
				t.Errorf("Expected nil, actual %s", *actual[j].URI)
			} else if actual[j].URI == nil {
				t.Errorf("Expected %s, actual nil", *expected[j].URI)
			} else if *actual[j].URI != *expected[j].URI {
				t.Errorf("Expected %s, actual %s", *expected[j].URI, *actual[j].URI)
			}
		}
	}
}

func getPersons() Persons {
	email := "johndoe@example.com"
	uri := "example.com"

	a := Person{
		Name:  "John Doe",
		Email: &email,
		URI:   &uri,
	}
	return Persons{a}
}

A schema.go => schema.go +128 -0
@@ 0,0 1,128 @@
package barefeed

// Code generated by go-bare/cmd/gen, DO NOT EDIT.

import (
	"git.sr.ht/~sircmpwn/go-bare"
)

type Time string

func (t *Time) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Time) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Text struct {
	TextType string `bare:"textType"`
	Value    string `bare:"value"`
}

func (t *Text) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Text) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Link struct {
	Href     string  `bare:"href"`
	Rel      *string `bare:"rel"`
	LinkType *string `bare:"linkType"`
	Lang     *string `bare:"lang"`
	Title    *string `bare:"title"`
	Length   *string `bare:"length"`
}

func (t *Link) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Link) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Person struct {
	Name  string  `bare:"name"`
	Email *string `bare:"email"`
	Uri   *string `bare:"uri"`
}

func (t *Person) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Person) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Entry struct {
	FeedID    string   `bare:"feedID"`
	EntryID   string   `bare:"entryID"`
	Read      bool     `bare:"read"`
	Liked     bool     `bare:"liked"`
	Position  uint     `bare:"position"`
	Title     Text     `bare:"title"`
	Content   Text     `bare:"content"`
	Updated   Time     `bare:"updated"`
	Published *Time    `bare:"published"`
	Authors   []Person `bare:"authors"`
	Links     []Link   `bare:"links"`
}

func (t *Entry) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Entry) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Feed struct {
	FeedPath string   `bare:"feedPath"`
	FeedID   string   `bare:"feedID"`
	Title    Text     `bare:"title"`
	Updated  Time     `bare:"updated"`
	Authors  []Person `bare:"authors"`
	Links    []Link   `bare:"links"`
}

func (t *Feed) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Feed) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Bin struct {
	Generator string  `bare:"generator"`
	Content   Content `bare:"content"`
}

func (t *Bin) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

func (t *Bin) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

type Content interface {
	bare.Union
}

func (_ Feed) IsUnion() {}

func (_ Entry) IsUnion() {}

func init() {
	bare.RegisterUnion((*Content)(nil)).
		Member(*new(Feed), 0).
		Member(*new(Entry), 1)

}

D test-files/test.barefeed => test-files/test.barefeed +0 -0
D text.go => text.go +0 -43
@@ 1,43 0,0 @@
package barefeed

// TextType is an enumerated type for Text
type TextType uint

const (
	// TEXT denotes the text as plain text
	TEXT TextType = iota
	// HTML denotes the text as entity escaped html
	HTML
	// XHTML denotes the text as inline xhtml, wrapped in a div element
	XHTML
)

// Text contains info needed for any text
type Text struct {
	Value    string   `bare:"value"`
	TextType TextType `bare:"textType"`
}

// String converts the enumerated value to a string.  "TEXT" is default.
func (t TextType) String() string {
	switch t {
	case HTML:
		return "html"
	case XHTML:
		return "xhtml"
	default:
		return "text"
	}
}

// ToTextType converts a string to the enumerated value.  TEXT is default.
func ToTextType(t string) TextType {
	switch t {
	case "html":
		return HTML
	case "xhtml":
		return XHTML
	default:
		return TEXT
	}
}

D text_test.go => text_test.go +0 -41
@@ 1,41 0,0 @@
package barefeed

import (
	"testing"
)

func testText(t *testing.T, expected, actual Text) {
	if expected.TextType != actual.TextType {
		t.Errorf("Expected %d, actual %d", expected.TextType, actual.TextType)
	}
	if expected.Value != actual.Value {
		t.Errorf("Expected %s, actual %s", expected.Value, actual.Value)
	}
}

func getText() Text {
	return Text{
		TextType: HTML,
		Value:    "<p>This is a test</p>",
	}
}

func TestTextType(t *testing.T) {
	e := TEXT
	a := ToTextType(e.String())
	if e != a {
		t.Errorf("Expected %d, actual %d", e, a)
	}

	e = HTML
	a = ToTextType(e.String())
	if e != a {
		t.Errorf("Expected %d, actual %d", e, a)
	}

	e = XHTML
	a = ToTextType(e.String())
	if e != a {
		t.Errorf("Expected %d, actual %d", e, a)
	}
}

D time.go => time.go +0 -16
@@ 1,16 0,0 @@
package barefeed

import "time"

// Timestamp type to hold the UTC Unix value of time
type Timestamp int64

// Time will convert the timestamp to time
func (t Timestamp) Time() time.Time {
	return time.Unix(int64(t), 0).UTC()
}

// ToTimestamp will convert the time to timestamp
func ToTimestamp(t time.Time) Timestamp {
	return Timestamp(t.UTC().Unix())
}

D time_test.go => time_test.go +0 -14
@@ 1,14 0,0 @@
package barefeed

import (
	"testing"
	"time"
)

func TestTimestamp(t *testing.T) {
	ts := Timestamp(time.Now().UTC().Unix())
	tm := ts.Time()
	if ts != ToTimestamp(tm) {
		t.Errorf("Expected %v, actual %v", ts, tm)
	}
}