~chrisppy/go-barefeed

022d6ea6f0215c28fa4b4b69dd9d821c8766ded8 — Chris Palmer 4 months ago 6fdc622 v0.2.0
updates from barefeed fixes
19 files changed, 586 insertions(+), 208 deletions(-)

M barefeed.go
M barefeed_test.go
A entry.go
A entry_test.go
A feed.go
A feed_test.go
M go.mod
M go.sum
A link.go
A link_test.go
A message.go
A message_test.go
A person.go
A person_test.go
A text.go
A text_test.go
M time.go
A time_test.go
D version1.go
M barefeed.go => barefeed.go +2 -29
@@ 6,27 6,15 @@ package barefeed
import (
	"bytes"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

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

// Message it the root type of a barefeed.  It allows for future versions of the
// spec and allowing backward compatability
type Message interface {
	bare.Union
}

func init() {
	bare.RegisterUnion((*Message)(nil)).
		Member(*new(MessageV1), 0)

}

// FromFile will read barefeed from a file
func FromFile(path string) (*Message, error) {
	b, err := ioutil.ReadFile(filepath.Clean(path))
	b, err := os.ReadFile(filepath.Clean(path))
	if err != nil {
		return nil, err
	}


@@ 50,18 38,3 @@ func FromBytes(b []byte) (*Message, error) {
	}
	return &m, nil
}

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

// WriteFile will write barefeed to a file at the designated path
func WriteFile(t Message, path string) error {
	b, err := Bytes(t)
	if err != nil {
		return err
	}

	return ioutil.WriteFile(filepath.Clean(path), b, 0600)
}

M barefeed_test.go => barefeed_test.go +38 -64
@@ 3,94 3,68 @@ package barefeed
import (
	"bytes"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

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

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

	var m2 *Message
	m2, err = FromBytes(b)
	actual, err := FromBytes(b)
	if err != nil {
		assert.Fail(t, err.Error())
		t.Errorf("Message should not have error: %e", err)
	} else if actual == nil {
		t.Errorf("Message should not have been nil")
	}
	assert.NotNil(t, m2)

	msg1 := m.(MessageV1)
	msg2 := (*m2).(*MessageV1)

	assert.Equal(t, msg1.Feeds, msg2.Feeds)
	assert.Equal(t, msg1.Created, msg2.Created)
	assert.Equal(t, msg1.Generator, msg2.Generator)
	testMessage(t, expected, *actual)
}

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

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

	var m2 *Message
	m2, err = FromReader(bytes.NewReader(b))
	actual, err := FromReader(bytes.NewReader(b))
	if err != nil {
		assert.Fail(t, err.Error())
		t.Errorf("Message should not have error: %e", err)
	} else if actual == nil {
		t.Errorf("Message should not have been nil")
	}
	assert.NotNil(t, m2)

	msg1 := m.(MessageV1)
	msg2 := (*m2).(*MessageV1)

	assert.Equal(t, msg1.Feeds, msg2.Feeds)
	assert.Equal(t, msg1.Created, msg2.Created)
	assert.Equal(t, msg1.Generator, msg2.Generator)
}

func TestTimestamp(t *testing.T) {
	ts := Timestamp(time.Now().UTC().Unix())
	tm := ts.Time()
	assert.Equal(t, ts, ToTimestamp(tm))
	testMessage(t, expected, *actual)
}

func getTestMessage() Message {
	media := MediaV1{
		Length:   int64(2),
		Position: int64(1),
		Location: "d",
		Mimetype: "e",
func TestFromBytesError(t *testing.T) {
	m, err := FromBytes([]byte("{"))
	if m != nil {
		t.Errorf("Message should have been nil: %v", m)
	}

	i := ItemV1{
		Link:     "a",
		Title:    "b",
		Content:  "c",
		Read:     true,
		Favorite: true,
		Date:     ToTimestamp(time.Now()),
		Media:    &media,
	if err == nil {
		t.Errorf("error should not have been nil")
	}
}

	f := FeedV1{
		Feed:        "https://example.com/feed.xml",
		Title:       "Test file",
		Description: "This is a test",
		Link:        "example.com",
		Items:       []ItemV1{i},
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")
	}

	return Message(MessageV1{
		Created:   ToTimestamp(time.Now()),
		Generator: "generated",
		Feeds:     []FeedV1{f},
	})
}

A entry.go => entry.go +69 -0
@@ 0,0 1,69 @@
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"`
	Read      bool      `bare:"read"`
	Favorite  bool      `bare:"favorite"`
	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() {}

A entry_test.go => entry_test.go +83 -0
@@ 0,0 1,83 @@
package barefeed

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

func testEntries(t *testing.T, expected, actual Entries) {
	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].(*EntryV1)
		e := expected[i].(*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.Read != e.Read {
			t.Errorf("Expected %t, actual %t", e.Read, r.Read)
		}
		if r.Favorite != e.Favorite {
			t.Errorf("Expected %t, actual %t", e.Favorite, r.Favorite)
		}
		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 getEntry() Entry {
	e := EntryV1{
		FeedPath:  "https://example.com/feed.xml",
		ID:        "urn:uuid:603bb616-7ae6-4413-83ec-a8cffbdd148e",
		Read:      true,
		Favorite:  true,
		Title:     getText(),
		Content:   getText(),
		Published: ToTimestamp(time.Now()),
		Updated:   ToTimestamp(time.Now()),
		Links:     getLinks(),
		Authors:   getPersons(),
	}

	var en Entry = &e
	return en
}

A feed.go => feed.go +70 -0
@@ 0,0 1,70 @@
package barefeed

import (
	"fmt"
	"strings"

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

// 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"`
	IsAtom      bool      `bare:"isAtom"`
	Title       Text      `bare:"title"`
	Updated     Timestamp `bare:"updated"`
	Entries     Entries   `bare:"entries"`
	Authors     Persons   `bare:"authors"`
	Links       Links     `bare:"links"`
	Generator   *string   `bare:"generator,omitempty"`
	Description *Text     `bare:"description,omitempty"`
}

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

A feed_test.go => feed_test.go +98 -0
@@ 0,0 1,98 @@
package barefeed

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

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

	for i := 0; i < len(expected); i++ {
		r := actual[i].(*FeedV1)
		e := expected[i].(*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.IsAtom != e.IsAtom {
			t.Errorf("Expected %t, actual %t", e.IsAtom, r.IsAtom)
		}
		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)
		testEntries(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() Feeds {
	fd := getFeed("test title")
	return Feeds{fd}
}

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,
		IsAtom:      true,
		Title:       getText(),
		Updated:     ToTimestamp(time.Now()),
		Description: &desc,
		Links:       getLinks(),
		Authors:     getPersons(),
		Entries:     getEntries(),
	}
	f.Title.Value = title

	var fd Feed = &f
	return fd
}

M go.mod => go.mod +3 -5
@@ 1,11 1,9 @@
module git.sr.ht/~chrisppy/go-barefeed

go 1.15
go 1.16

require (
	git.sr.ht/~sircmpwn/go-bare v0.0.0-20210227202403-5dae5c48f917
	git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/kr/pretty v0.1.0 // indirect
	github.com/stretchr/testify v1.7.0
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	github.com/stretchr/testify v1.7.0 // indirect
)

M go.sum => go.sum +2 -9
@@ 1,14 1,9 @@
git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210227202403-5dae5c48f917 h1:/pfEvB399XDXksu4vyjfNTytWn/nbbKiNhvjtpgc4pY=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210227202403-5dae5c48f917/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE=
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=


@@ 17,7 12,5 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

A link.go => link.go +13 -0
@@ 0,0 1,13 @@
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"`
	Type     string `bare:"type"`
	Rel      string `bare:"rel"`
	Length   int    `bare:"length"`
	Position *int   `bare:"position,omitempty"`
}

A link_test.go => link_test.go +53 -0
@@ 0,0 1,53 @@
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].Type != actual[j].Type {
			t.Errorf("Expected %s, actual %s", expected[j].Type, actual[j].Type)
		}
		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)
		}
		if actual[j].Position != expected[j].Position {
			if expected[j].Position == nil {
				t.Errorf("Expected nil, actual %d", *actual[j].Position)
			} else if actual[j].Position == nil {
				t.Errorf("Expected %d, actual nil", *expected[j].Position)
			} else if *actual[j].Position != *expected[j].Position {
				t.Errorf("Expected %d, actual %d", *expected[j].Position, *actual[j].Position)
			}
		}
	}
}

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

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

	return Links{a, b}
}

A message.go => message.go +30 -0
@@ 0,0 1,30 @@
package barefeed

import (
	"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     Feeds     `bare:"feeds"`
}

// 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 {
	b, err := t.Bytes()
	if err != nil {
		return err
	}

	return os.WriteFile(filepath.Clean(path), b, 0600)
}

A message_test.go => message_test.go +24 -0
@@ 0,0 1,24 @@
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)
}

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

A person.go => person.go +11 -0
@@ 0,0 1,11 @@
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,omitempty"`
	URI   *string `bare:"uri,omitempty"`
}

A person_test.go => person_test.go +44 -0
@@ 0,0 1,44 @@
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 text.go => text.go +7 -0
@@ 0,0 1,7 @@
package barefeed

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

A text_test.go => text_test.go +21 -0
@@ 0,0 1,21 @@
package barefeed

import (
	"testing"
)

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

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

M time.go => time.go +4 -3
@@ 1,8 1,9 @@
package barefeed

import (
	"time"
)
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 {

A time_test.go => time_test.go +14 -0
@@ 0,0 1,14 @@
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)
	}
}

D version1.go => version1.go +0 -98
@@ 1,98 0,0 @@
package barefeed

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

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

// Decode will convert bytes into Timestamp
func (t *Timestamp) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

// Encode will convert Timestamp into bytes
func (t *Timestamp) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

// MessageV1 is the first version of the spec
type MessageV1 struct {
	Created   Timestamp `bare:"created"`
	Generator string    `bare:"generator"`
	Feeds     []FeedV1  `bare:"feeds"`
}

// Decode will convert bytes into MessageV1
func (t *MessageV1) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

// Encode will convert MessageV1 into bytes
func (t *MessageV1) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

// FeedV1 type contains elements from RSS Channel & Atom Feed
type FeedV1 struct {
	Feed        string     `bare:"feed"`
	Title       string     `bare:"title"`
	Description string     `bare:"description"`
	Link        string     `bare:"link"`
	Updated     *Timestamp `bare:"updated"`
	Items       []ItemV1   `bare:"items"`
}

// Decode will convert bytes into Feed
func (t *FeedV1) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

// Encode will convert Feed into bytes
func (t *FeedV1) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

// ItemV1 type contains elements from RSS Item & Atom Entry
type ItemV1 struct {
	Link     string    `bare:"link"`
	Title    string    `bare:"title"`
	Content  string    `bare:"content"`
	Read     bool      `bare:"read"`
	Favorite bool      `bare:"favorite"`
	Date     Timestamp `bare:"date"`
	Media    *MediaV1  `bare:"media"`
}

// Decode will convert bytes into Item
func (t *ItemV1) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

// Encode will convert Item into bytes
func (t *ItemV1) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

// MediaV1 contains info needed for a podcast
type MediaV1 struct {
	Location string `bare:"location"`
	Mimetype string `bare:"mimetype"`
	Length   int64  `bare:"length"`
	Position int64  `bare:"position"`
}

// Decode will convert bytes into Media
func (t *MediaV1) Decode(data []byte) error {
	return bare.Unmarshal(data, t)
}

// Encode will convert Media into bytes
func (t *MediaV1) Encode() ([]byte, error) {
	return bare.Marshal(t)
}

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